-
+
+
+
-
+
+
+
+
+
+
+ Canvas Styles
+
diff --git a/frontend/src/AppBuilder/QueryManager/Components/ParameterDetails.jsx b/frontend/src/AppBuilder/QueryManager/Components/ParameterDetails.jsx
index 6ff93ee9b6..2b9d03dc28 100644
--- a/frontend/src/AppBuilder/QueryManager/Components/ParameterDetails.jsx
+++ b/frontend/src/AppBuilder/QueryManager/Components/ParameterDetails.jsx
@@ -4,6 +4,7 @@ import cx from 'classnames';
import PlusRectangle from '@/_ui/Icon/solidIcons/PlusRectangle';
import Remove from '@/_ui/Icon/bulkIcons/Remove';
import ParameterForm from './ParameterForm';
+import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRemove, otherParams }) => {
const [showModal, setShowModal] = useState(false);
@@ -47,6 +48,17 @@ const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRe
}
};
+ usePopoverObserver(
+ document.getElementsByClassName('query-details')[0],
+ isEdit
+ ? document.getElementById(`query-param-${String(name).toLowerCase()}`)
+ : document.getElementById('runjs-param-add-btn'),
+ document.getElementById('parameter-form-popover'),
+ showModal,
+ () => setShowModal(true),
+ closeMenu
+ );
+
return (
{
const moduleId = useModuleId();
@@ -166,16 +167,17 @@ const NameInput = ({ onInput, value, darkMode, isDiabled, selectedQuery }) => {
}
}, [isFocused]);
+ const debouncedHandleInput = useCallback(
+ debounce((newName) => {
+ onInput(newName);
+ }, 300),
+ [onInput]
+ );
+
const handleChange = (event) => {
const sanitizedValue = event.target.value.replace(/[ \t&]/g, '');
setName(sanitizedValue);
- };
-
- const handleInput = (newName) => {
- const result = onInput(newName);
- if (!result) {
- setName(value);
- }
+ debouncedHandleInput(sanitizedValue);
};
return (
@@ -200,12 +202,12 @@ const NameInput = ({ onInput, value, darkMode, isDiabled, selectedQuery }) => {
event.persist();
if (event.keyCode === 13) {
setIsFocused(false);
- handleInput(event.target.value);
+ debouncedHandleInput(event.target.value);
}
}}
onBlur={({ target }) => {
setIsFocused(false);
- handleInput(target.value);
+ debouncedHandleInput(target.value);
}}
/>
) : (
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
index 23d1c7f7cf..3ddf2da932 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
@@ -219,4 +219,11 @@
.react-datepicker__navigation{
overflow: visible !important;
height: inherit !important;
+}
+.tjdb-td-wrapper{
+ .react-datepicker-time__input{
+ input{
+ line-height: normal !important;
+ }
+ }
}
\ No newline at end of file
diff --git a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx
index 20f499fbe8..6ee3f6cf0f 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/DropDownSelect.jsx
@@ -9,6 +9,7 @@ import Remove from '@/_ui/Icon/bulkIcons/Remove';
import { v4 as uuidv4 } from 'uuid';
import { isEmpty } from 'lodash';
import { ToolTip } from '@/_components/ToolTip';
+import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
const DropDownSelect = ({
darkMode,
@@ -130,6 +131,15 @@ const DropDownSelect = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selected]);
+ usePopoverObserver(
+ document.getElementsByClassName('query-details')[0],
+ document.getElementById(popoverBtnId.current),
+ document.getElementById(popoverId.current),
+ showMenu,
+ () => setShowMenu(true),
+ () => setShowMenu(false)
+ );
+
function checkElementPosition() {
if (isForeignKeyInEditCell) {
return 'bottom-start';
diff --git a/frontend/src/AppBuilder/QueryManager/QueryManager.jsx b/frontend/src/AppBuilder/QueryManager/QueryManager.jsx
index 7dba16f49b..7227c86639 100644
--- a/frontend/src/AppBuilder/QueryManager/QueryManager.jsx
+++ b/frontend/src/AppBuilder/QueryManager/QueryManager.jsx
@@ -37,7 +37,7 @@ const QueryManager = ({ mode, darkMode }) => {
useEffect(() => {
if (selectedQuery) {
- const selectedDS = [...dataSources, ...globalDataSources, ...(sampleDataSource?.length ? sampleDataSource : [])]
+ const selectedDS = [...dataSources, ...globalDataSources, !!sampleDataSource && sampleDataSource]
.filter(Boolean)
.find((datasource) => datasource.id === selectedQuery?.data_source_id);
//TODO: currently type is not taken into account. May create issues in importing REST apis. to be revamped when import app is revamped
diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx
index 2ad7977496..ae5aadfac6 100644
--- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/ComponentsManagerTab.jsx
@@ -118,7 +118,10 @@ export const ComponentsManagerTab = ({ darkMode }) => {
'TextInput',
'NumberInput',
'PasswordInput',
- 'Textarea',
+ 'TextArea',
+ 'EmailInput',
+ 'PhoneInput',
+ 'CurrencyInput',
'ToggleSwitchV2',
'DropdownV2',
'MultiselectV2',
diff --git a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx
index 77274cd658..9589ac145b 100644
--- a/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/ComponentsManagerTab/DragLayer.jsx
@@ -42,12 +42,18 @@ const CustomDragLayer = ({ size }) => {
const canvasBounds = item?.canvasRef?.getBoundingClientRect();
const height = size.height;
- const width = (canvasWidth * size.width) / NO_OF_GRIDS;
+ const mainCanvasWidth = document.getElementById('real-canvas')?.offsetWidth || 0;
+ let width = (mainCanvasWidth * size.width) / NO_OF_GRIDS;
// Calculate position relative to the current canvas (parent or child)
const left = currentOffset.x - (canvasBounds?.left || 0);
const top = currentOffset.y - (canvasBounds?.top || 0);
+ // Adjust position and width if exceeding grid bounds
+ if (width >= canvasWidth) {
+ width = canvasWidth;
+ }
+
const [x, y] = snapToGrid(canvasWidth, left, top);
return (
{
+ const {
+ layoutPropertyChanged,
+ component,
+ paramUpdated,
+ dataQueries,
+ currentState,
+ eventsChanged,
+ apps,
+ allComponents,
+ } = restProps;
+
+ const properties = Object.keys(componentMeta.properties);
+ const events = Object.keys(componentMeta.events);
+ const validations = Object.keys(componentMeta.validation || {});
+ const resolvedProperties = useStore((state) => state.getResolvedComponent(component.id)?.properties);
+ const defaultCountry = resolvedProperties?.defaultCountry || 'US';
+ const isDefaultCountryFxOn = componentMeta?.definition?.properties?.dateFormat?.fxActive || false;
+
+ const options = useMemo(() => {
+ return Object.keys(CurrencyMap).map((country) => ({
+ label: `${CurrencyMap[country].prefix} (${CurrencyMap[country].currency})`,
+ value: country,
+ }));
+ }, []);
+
+ const renderCustomOption = ({ label, value: optionValue }) => {
+ const optionStyle = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'start',
+ height: '18px',
+ gap: '6px',
+ cursor: 'pointer',
+ fontFamily: 'IBM Plex Sans',
+ fontSize: '12px',
+ lineHeight: '18px',
+ fontWeight: '400',
+ color: darkMode ? '#fff' : '#1B1F24',
+ };
+ const FlagIcon = flags[optionValue];
+
+ return (
+
+
{FlagIcon ? : null}
+ {label}
+
+ );
+ };
+
+ const getCountryDropdown = () => {
+ return (
+
+
+
+
+ {
+ paramUpdated({ name: 'dateFormat' }, 'fxActive', !isDefaultCountryFxOn, 'properties');
+ }}
+ />
+
+
+ {isDefaultCountryFxOn ? (
+
paramUpdated({ name: 'defaultCountry' }, 'value', value, 'properties')}
+ />
+ ) : (
+
+ );
+ };
+
+ const filteredProperties = properties.filter(
+ (property) => componentMeta.properties[property].section !== 'additionalActions'
+ );
+
+ const additionalActions = properties.filter(
+ (property) => componentMeta.properties[property].section === 'additionalActions'
+ );
+
+ const accordionItems = baseComponentProperties(
+ filteredProperties,
+ events,
+ component,
+ componentMeta,
+ layoutPropertyChanged,
+ paramUpdated,
+ dataQueries,
+ currentState,
+ eventsChanged,
+ apps,
+ allComponents,
+ validations,
+ darkMode,
+ null,
+ additionalActions
+ );
+
+ accordionItems[0].children.splice(4, 0, getCountryDropdown());
+
+ return
;
+};
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx
index f5da78fc32..346d0bfc17 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/DefaultComponent.jsx
@@ -14,8 +14,12 @@ const SHOW_ADDITIONAL_ACTIONS = [
'Text',
'Container',
'TextInput',
+ 'TextArea',
'NumberInput',
'PasswordInput',
+ 'EmailInput',
+ 'PhoneInput',
+ 'CurrencyInput',
'ToggleSwitchV2',
'Checkbox',
'DropdownV2',
@@ -23,6 +27,8 @@ const SHOW_ADDITIONAL_ACTIONS = [
'Button',
'RichTextEditor',
'Image',
+ 'Divider',
+ 'VerticalDivider',
'ModalV2',
'Link',
];
@@ -33,9 +39,12 @@ const PROPERTIES_VS_ACCORDION_TITLE = {
NumberInput: 'Data',
ToggleSwitchV2: 'Data',
Checkbox: 'Data',
+ TextArea: 'Data',
Button: 'Data',
Image: 'Data',
Container: 'Data',
+ Divider: 'Data',
+ VerticalDivider: 'Data',
ModalV2: 'Data',
Link: 'Data',
};
@@ -122,6 +131,10 @@ export const baseComponentProperties = (
'Modal',
'TextInput',
'PasswordInput',
+ 'TextArea',
+ 'EmailInput',
+ 'PhoneInput',
+ 'CurrencyInput',
'NumberInput',
'Text',
'Table',
@@ -131,6 +144,8 @@ export const baseComponentProperties = (
'DropdownV2',
'MultiselectV2',
'Image',
+ 'Divider',
+ 'VerticalDivider',
'Link',
],
Layout: [],
@@ -271,7 +286,6 @@ export const baseComponentProperties = (
>
),
});
-
return items.filter(
(item) => !(item.title in accordionFilters && accordionFilters[item.title].includes(componentMeta.component))
);
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/PhoneInput/PhoneInput.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/PhoneInput/PhoneInput.jsx
new file mode 100644
index 0000000000..f246514202
--- /dev/null
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/PhoneInput/PhoneInput.jsx
@@ -0,0 +1,135 @@
+import React, { useMemo, useState } from 'react';
+import Accordion from '@/_ui/Accordion';
+import { baseComponentProperties } from '../DefaultComponent';
+import Select from '@/_ui/Select';
+import useStore from '@/AppBuilder/_stores/store';
+import { getCountries } from 'react-phone-number-input/input';
+import en from 'react-phone-number-input/locale/en';
+import flags from 'react-phone-number-input/flags';
+import FxButton from '@/AppBuilder/CodeBuilder/Elements/FxButton';
+import CodeHinter from '@/AppBuilder/CodeEditor';
+import cx from 'classnames';
+
+export const PhoneInput = ({ componentMeta, darkMode, ...restProps }) => {
+ const {
+ layoutPropertyChanged,
+ component,
+ paramUpdated,
+ dataQueries,
+ currentState,
+ eventsChanged,
+ apps,
+ allComponents,
+ } = restProps;
+
+ const properties = Object.keys(componentMeta.properties);
+ const events = Object.keys(componentMeta.events);
+ const validations = Object.keys(componentMeta.validation || {});
+ const resolvedProperties = useStore((state) => state.getResolvedComponent(component.id)?.properties);
+ const defaultCountry = resolvedProperties?.defaultCountry || 'US';
+ const isDefaultCountryFxOn = componentMeta?.definition?.properties?.dateFormat?.fxActive || false;
+
+ const options = useMemo(
+ () =>
+ getCountries().map((country) => ({
+ label: `${en[country]}`,
+ value: country,
+ })),
+ []
+ );
+
+ const renderCustomOption = ({ label, value: optionValue }) => {
+ const optionStyle = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'start',
+ height: '18px',
+ gap: '6px',
+ cursor: 'pointer',
+ fontFamily: 'IBM Plex Sans',
+ fontSize: '12px',
+ lineHeight: '18px',
+ fontWeight: '400',
+ color: darkMode ? '#fff' : '#1B1F24',
+ };
+ const FlagIcon = flags[optionValue];
+
+ return (
+
+
{FlagIcon ? : null}
+ {label}
+
+ );
+ };
+
+ const getCountryDropdown = () => {
+ return (
+
+
+
+
+ {
+ paramUpdated({ name: 'dateFormat' }, 'fxActive', !isDefaultCountryFxOn, 'properties');
+ }}
+ />
+
+
+ {isDefaultCountryFxOn ? (
+
paramUpdated({ name: 'defaultCountry' }, 'value', value, 'properties')}
+ />
+ ) : (
+
+ );
+ };
+
+ const filteredProperties = properties.filter(
+ (property) => componentMeta.properties[property].section !== 'additionalActions'
+ );
+
+ const additionalActions = properties.filter(
+ (property) => componentMeta.properties[property].section === 'additionalActions'
+ );
+
+ const accordionItems = baseComponentProperties(
+ filteredProperties,
+ events,
+ component,
+ componentMeta,
+ layoutPropertyChanged,
+ paramUpdated,
+ dataQueries,
+ currentState,
+ eventsChanged,
+ apps,
+ allComponents,
+ validations,
+ darkMode,
+ null,
+ additionalActions
+ );
+
+ accordionItems[0].children.splice(3, 0, getCountryDropdown());
+
+ return
;
+};
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx
index 12d4d68102..d250a4342e 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import Accordion from '@/_ui/Accordion';
import { EventManager } from '../EventManager';
import { renderElement } from '../Utils';
@@ -14,6 +14,7 @@ import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SortableList from '@/_components/SortableList';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import { shallow } from 'zustand/shallow';
+import { sortArray } from '@/Editor/Components/DropdownV2/utils';
export function Select({ componentMeta, darkMode, ...restProps }) {
const {
@@ -27,10 +28,13 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
allComponents,
pages,
} = restProps;
+
+ const isInitialRender = useRef(true);
const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
const isMultiSelect = component?.component?.component === 'MultiselectV2';
-
const isDynamicOptionsEnabled = getResolvedValue(component?.component?.definition?.properties?.advanced?.value);
+ const isSortingEnabled = componentMeta?.properties['sort'] ?? false;
+ const sort = component?.component?.definition?.properties?.sort?.value;
const constructOptions = () => {
let optionsValue = component?.component?.definition?.properties?.options?.value;
@@ -89,6 +93,15 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
paramUpdated({ name: 'options' }, 'value', options, 'properties', false, props);
};
+ const updateSortParam = (value) => {
+ paramUpdated({ name: 'sort' }, 'value', value, 'properties');
+ };
+
+ const updateOptions = (options) => {
+ setOptions(options);
+ updateAllOptionsParams(options);
+ };
+
const generateNewOptions = () => {
let found = false;
let label = '';
@@ -114,8 +127,8 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
const handleAddOption = () => {
let _option = generateNewOptions();
const _items = [...options, _option];
- setOptions(_items);
- updateAllOptionsParams(_items);
+ const sortedItems = sortArray(_items, sort);
+ updateOptions(sortedItems);
};
const handleDeleteOption = (index) => {
@@ -134,8 +147,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
return option;
});
- setOptions(_options);
- updateAllOptionsParams(_options);
+ updateOptions(_options);
};
const handleValueChange = (value, index) => {
@@ -148,16 +160,17 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
return option;
});
- setOptions(_options);
- updateAllOptionsParams(_options);
+ updateOptions(_options);
};
const reorderOptions = async (startIndex, endIndex) => {
const result = [...options];
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
- setOptions(result);
- updateAllOptionsParams(result);
+ updateOptions(result);
+ if (isSortingEnabled && sort !== 'none') {
+ updateSortParam('none');
+ }
};
const onDragEnd = ({ source, destination }) => {
@@ -200,9 +213,8 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
};
}
});
- setOptions(_options);
+ updateOptions(_options);
setMarkedAsDefault(_value);
- updateAllOptionsParams(_options);
}
};
@@ -219,8 +231,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
return option;
});
- setOptions(_options);
- updateAllOptionsParams(_options);
+ updateOptions(_options);
};
const handleDisableChange = (value, index) => {
@@ -236,8 +247,7 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
return option;
});
- setOptions(_options);
- updateAllOptionsParams(_options);
+ updateOptions(_options);
};
const handleOnFxPress = (active, index, key) => {
@@ -253,12 +263,20 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
}
return option;
});
- setOptions(_options);
- updateAllOptionsParams(_options);
+ updateOptions(_options);
};
useEffect(() => {
- setOptions(constructOptions());
+ if (!isInitialRender.current && isSortingEnabled) {
+ const sortedOptions = sortArray([...options], sort);
+ updateOptions(sortedOptions);
+ }
+ }, [sort]);
+
+ useEffect(() => {
+ const sortedOptions = sortArray(constructOptions(), sort);
+ updateOptions(sortedOptions);
+ isInitialRender.current = false;
}, [isMultiSelect, component?.id]);
const _renderOverlay = (item, index) => {
@@ -385,6 +403,12 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
trigger="click"
placement="left"
rootClose
+ onExited={() => {
+ if (isSortingEnabled && sort !== 'none') {
+ const sortedOptions = sortArray([...options], sort);
+ updateOptions(sortedOptions);
+ }
+ }}
overlay={_renderOverlay(item, index)}
onToggle={(isOpen) => {
if (!isOpen) {
@@ -515,6 +539,17 @@ export function Select({ componentMeta, darkMode, ...restProps }) {
currentState,
allComponents
)}
+ {isSortingEnabled &&
+ renderElement(
+ component,
+ componentMeta,
+ paramUpdated,
+ dataQueries,
+ 'sort',
+ 'properties',
+ currentState,
+ allComponents
+ )}
>
),
});
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx
index 2e975109d2..0568b85ccd 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx
@@ -53,6 +53,7 @@ export const PropertiesTabElements = ({
{ label: 'Image', value: 'image' },
{ label: 'Link', value: 'link' },
{ label: 'JSON', value: 'json' },
+ { label: 'Markdown', value: 'markdown' },
// Following column types are deprecated
{ label: 'Default', value: 'default' },
{ label: 'Dropdown', value: 'dropdown' },
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/StylesTabElements.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/StylesTabElements.jsx
index 5091e56c6c..2b91a8dd15 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/StylesTabElements.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/StylesTabElements.jsx
@@ -128,6 +128,7 @@ export const StylesTabElements = ({
undefined,
'number',
'json',
+ 'markdown',
'boolean',
'select',
'text',
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx
index 3aca83b046..bb6c1d94bb 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx
@@ -631,6 +631,8 @@ class TableComponent extends React.Component {
return 'Multiselect';
case 'json':
return 'JSON';
+ case 'markdown':
+ return 'Markdown';
default:
capitalize(text ?? '');
}
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx
index c3add3f1cc..6c8398724d 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/EventManager.jsx
@@ -30,6 +30,8 @@ import { appService } from '@/_services';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import useStore from '@/AppBuilder/_stores/store';
import { useEventActions, useEvents } from '@/AppBuilder/_stores/slices/eventsSlice';
+import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup';
+import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
export const EventManager = ({
sourceId,
@@ -503,7 +505,7 @@ export const EventManager = ({
)}
{event.actionId === 'open-webpage' && (
-
+
+
+
+ handlerChanged(index, 'windowTarget', _value)}
+ defaultValue={event?.windowTarget || 'newTab'}
+ style={{ width: '74%' }}
+ >
+ New tab
+ Current tab
+
+
)}
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx
index 785ee19f34..07b53c4454 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx
@@ -8,6 +8,8 @@ import { validateQueryName, convertToKebabCase, resolveReferences } from '@/_hel
import { useHotkeys } from 'react-hotkeys-hook';
import { DefaultComponent } from './Components/DefaultComponent';
import { FilePicker } from './Components/FilePicker';
+import { PhoneInput } from './Components/PhoneInput/PhoneInput.jsx';
+import { CurrencyInput } from './Components/CurrencyInput/CurrencyInput.jsx';
import { Modal } from './Components/Modal';
import { ModalV2 } from './Components/ModalV2';
import { CustomComponent } from './Components/CustomComponent';
@@ -37,6 +39,7 @@ import { Select } from './Components/Select';
import { Steps } from './Components/Steps.jsx';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import useStore from '@/AppBuilder/_stores/store';
+// import { componentTypes } from '@/Editor/WidgetManager/components';
import { componentTypes } from '@/AppBuilder/WidgetManager/componentTypes';
import { copyComponents } from '@/AppBuilder/AppCanvas/appCanvasUtils.js';
import DatetimePickerV2 from './Components/DatetimePickerV2.jsx';
@@ -67,7 +70,11 @@ const INSPECTOR_HEADER_OPTIONS = [
const NEW_REVAMPED_COMPONENTS = [
'Text',
'TextInput',
+ 'TextArea',
'PasswordInput',
+ 'EmailInput',
+ 'PhoneInput',
+ 'CurrencyInput',
'NumberInput',
'Table',
'ToggleSwitchV2',
@@ -80,6 +87,8 @@ const NEW_REVAMPED_COMPONENTS = [
'Icon',
'Image',
'Container',
+ 'Divider',
+ 'VerticalDivider',
'ModalV2',
'Link',
'Steps',
@@ -532,8 +541,8 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte
componentMeta.displayName === 'Toggle Switch (Legacy)'
? 'Toggle (Legacy)'
: componentMeta.displayName === 'Toggle Switch'
- ? 'Toggle Switch'
- : componentMeta.component,
+ ? 'Toggle Switch'
+ : componentMeta.component,
})}
@@ -735,6 +744,10 @@ const GetAccordion = React.memo(
return
;
case 'Steps':
return
;
+ case 'PhoneInput':
+ return
;
+ case 'CurrencyInput':
+ return
;
default: {
return
;
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js
index 62ee032172..7e47f7f6c0 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Utils.js
@@ -43,6 +43,9 @@ export function renderCustomStyles(
componentConfig.component == 'TextInput' ||
componentConfig.component == 'NumberInput' ||
componentConfig.component == 'PasswordInput' ||
+ componentConfig.component == 'EmailInput' ||
+ componentConfig.component == 'PhoneInput' ||
+ componentConfig.component == 'CurrencyInput' ||
componentConfig.component == 'ToggleSwitchV2' ||
componentConfig.component == 'Checkbox' ||
componentConfig.component == 'Table' ||
diff --git a/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx b/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx
index 69ded14971..9ce3bbd4ce 100644
--- a/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx
+++ b/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx
@@ -13,6 +13,7 @@ const NEW_WIDGETS = [
'DatePickerV2',
'TimePicker',
'ModalV2',
+ 'TextArea',
];
export const WidgetBox = ({ component, darkMode }) => {
diff --git a/frontend/src/AppBuilder/Viewer/MobileNavigationMenu.jsx b/frontend/src/AppBuilder/Viewer/MobileNavigationMenu.jsx
index e6c8179328..bf4b6b50b4 100644
--- a/frontend/src/AppBuilder/Viewer/MobileNavigationMenu.jsx
+++ b/frontend/src/AppBuilder/Viewer/MobileNavigationMenu.jsx
@@ -134,7 +134,7 @@ const MobileNavigationMenu = ({ pages, switchPage, currentPageId, darkMode, chan
version: selectedVersionName,
env: selectedEnvironmentName,
};
- switchPage(pageId, pages.find((page) => page.id === pageId)?.handle, Object.entries(queryParams), true);
+ switchPage(pageId, pages.find((page) => page.id === pageId)?.handle, Object.entries(queryParams));
};
var styles = {
bmBurgerButton: {
diff --git a/frontend/src/AppBuilder/Viewer/PreviewSettings.jsx b/frontend/src/AppBuilder/Viewer/PreviewSettings.jsx
index e17398afb2..633a6dcf0e 100644
--- a/frontend/src/AppBuilder/Viewer/PreviewSettings.jsx
+++ b/frontend/src/AppBuilder/Viewer/PreviewSettings.jsx
@@ -29,7 +29,9 @@ const PreviewSettings = ({ isMobileLayout, showHeader, darkMode }) => {
const renderOverlay = () => (
-
Preview settings
+
+ Preview settings
+
{editingVersion && (
<>
@@ -59,10 +61,14 @@ const PreviewSettings = ({ isMobileLayout, showHeader, darkMode }) => {
className="released-version-no-header-mbl-preview"
style={{ backgroundColor: 'var(--slate5)', top: '7px', left: showHeader ? '61%' : '41%' }}
>
-
+
Preview
-
+
@@ -108,11 +114,11 @@ const PreviewSettings = ({ isMobileLayout, showHeader, darkMode }) => {
className="released-version-no-header-mbl-preview"
style={{ backgroundColor: 'var(--slate5)', top: showHeader ? '' : '14px' }}
>
-
+
Preview
-
+
diff --git a/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx b/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx
index d913bffd91..a7111a48a0 100644
--- a/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx
+++ b/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx
@@ -95,7 +95,7 @@ export const ViewerSidebarNavigation = ({
version: selectedVersionName,
env: selectedEnvironmentName,
};
- switchPage(pageId, pages.find((page) => page.id === pageId)?.handle, Object.entries(queryParams), true);
+ switchPage(pageId, pages.find((page) => page.id === pageId)?.handle, Object.entries(queryParams));
};
const isLicensed =
diff --git a/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js b/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js
index be2855c476..136ef942c7 100644
--- a/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js
+++ b/frontend/src/AppBuilder/WidgetManager/configs/widgetConfig.js
@@ -58,6 +58,9 @@ import {
linkConfig,
iconConfig,
boundedBoxConfig,
+ emailinputConfig,
+ phoneinputConfig,
+ currencyinputConfig,
} from '../widgets';
export const widgets = [
@@ -70,6 +73,9 @@ export const widgets = [
textinputConfig,
numberinputConfig,
passinputConfig,
+ emailinputConfig,
+ phoneinputConfig,
+ currencyinputConfig,
datepickerConfig,
datetimePickerV2Config,
datePickerV2Config,
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/button.js b/frontend/src/AppBuilder/WidgetManager/widgets/button.js
index 67da105c55..d5b6a58214 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/button.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/button.js
@@ -61,7 +61,7 @@ export const buttonConfig = {
accordian: 'button',
},
backgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: {
schema: { type: 'string' },
@@ -74,7 +74,7 @@ export const buttonConfig = {
accordian: 'button',
},
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text color',
validation: {
schema: { type: 'string' },
@@ -83,7 +83,7 @@ export const buttonConfig = {
accordian: 'button',
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border color',
validation: {
schema: { type: 'string' },
@@ -92,7 +92,7 @@ export const buttonConfig = {
accordian: 'button',
},
loaderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Loader color',
validation: {
schema: { type: 'string' },
@@ -110,7 +110,7 @@ export const buttonConfig = {
visibility: false,
},
iconColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Icon color',
validation: { schema: { type: 'string' } },
accordian: 'button',
@@ -219,10 +219,10 @@ export const buttonConfig = {
events: [],
styles: {
textColor: { value: '#FFFFFF' },
- borderColor: { value: '#4368E3' },
+ borderColor: { value: 'var(--primary-brand)' },
loaderColor: { value: '#FFFFFF' },
borderRadius: { value: '{{6}}' },
- backgroundColor: { value: '#4368E3' },
+ backgroundColor: { value: 'var(--primary-brand)' },
iconColor: { value: '#FFFFFF' },
direction: { value: 'left' },
padding: { value: 'default' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js
index ab3eb40c2c..d4ab44b3ab 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/buttonGroup.js
@@ -68,7 +68,7 @@ export const buttonGroupConfig = {
},
styles: {
backgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background color',
validation: {
schema: { type: 'string' },
@@ -76,7 +76,7 @@ export const buttonGroupConfig = {
},
},
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text color',
validation: {
schema: { type: 'string' },
@@ -108,7 +108,7 @@ export const buttonGroupConfig = {
},
},
selectedTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Selected text colour',
validation: {
schema: { type: 'string' },
@@ -116,11 +116,19 @@ export const buttonGroupConfig = {
},
},
selectedBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Selected background color',
validation: {
schema: { type: 'string' },
- defaultValue: '#007bff',
+ defaultValue: 'var(--primary-brand)',
+ },
+ },
+ alignment: {
+ type: 'alignButtons',
+ displayName: 'Alignment',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: 'left',
},
},
},
@@ -154,7 +162,8 @@ export const buttonGroupConfig = {
borderRadius: { value: '{{4}}' },
disabledState: { value: '{{false}}' },
selectedTextColor: { value: '#FFFFFF' },
- selectedBackgroundColor: { value: '#4368E3' },
+ selectedBackgroundColor: { value: 'var(--primary-brand)' },
+ alignment: { value: 'left' },
},
},
};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/chart.js b/frontend/src/AppBuilder/WidgetManager/widgets/chart.js
index 72ab688056..81dbd2547a 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/chart.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/chart.js
@@ -39,13 +39,13 @@ export const chartConfig = {
},
},
markerColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Marker color',
validation: {
schema: {
type: 'string',
},
- defaultValue: '#CDE1F8',
+ defaultValue: 'var(--primary-brand)',
},
},
showAxes: {
@@ -134,7 +134,7 @@ export const chartConfig = {
},
styles: {
backgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background color',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
},
@@ -192,7 +192,7 @@ export const chartConfig = {
},
properties: {
title: { value: 'This title can be changed' },
- markerColor: { value: '#CDE1F8' },
+ markerColor: { value: 'var(--primary-brand)' },
showAxes: { value: '{{true}}' },
showGridLines: { value: '{{true}}' },
plotFromJson: { value: '{{false}}' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/checkbox.js b/frontend/src/AppBuilder/WidgetManager/widgets/checkbox.js
index c9b6424020..ca509979cb 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/checkbox.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/checkbox.js
@@ -70,7 +70,7 @@ export const checkboxConfig = {
},
styles: {
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text color',
validation: {
schema: { type: 'string' },
@@ -78,7 +78,7 @@ export const checkboxConfig = {
accordian: 'label',
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border color',
validation: {
schema: { type: 'string' },
@@ -86,7 +86,7 @@ export const checkboxConfig = {
accordian: 'switch',
},
checkboxColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Checked color',
validation: {
schema: { type: 'string' },
@@ -94,7 +94,7 @@ export const checkboxConfig = {
accordian: 'switch',
},
uncheckedColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Unchecked color',
validation: {
schema: { type: 'string' },
@@ -102,7 +102,7 @@ export const checkboxConfig = {
accordian: 'switch',
},
handleColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Handle color',
validation: {
schema: { type: 'string' },
@@ -183,7 +183,7 @@ export const checkboxConfig = {
styles: {
disabledState: { value: '{{false}}' },
textColor: { value: '#1B1F24' },
- checkboxColor: { value: '#4368E3' },
+ checkboxColor: { value: 'var(--primary-brand)' },
uncheckedColor: { value: '#E4E7EB' },
borderColor: { value: '#CCD1D5' },
handleColor: { value: '#FFFFFF' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/circularProgressbar.js b/frontend/src/AppBuilder/WidgetManager/widgets/circularProgressbar.js
index a7ffd551af..2babc27a12 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/circularProgressbar.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/circularProgressbar.js
@@ -32,15 +32,15 @@ export const circularProgressbarConfig = {
events: {},
styles: {
color: {
- type: 'color',
- displayName: 'Color',
+ type: 'colorSwatches',
+ displayName: 'colorSwatches',
validation: {
schema: { type: 'string' },
- defaultValue: '#375FCF',
+ defaultValue: 'var(--primary-brand)',
},
},
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text Color',
validation: {
schema: { type: 'string' },
@@ -104,7 +104,7 @@ export const circularProgressbarConfig = {
},
events: [],
styles: {
- color: { value: '' },
+ color: { value: 'var(--primary-brand)' },
textColor: { value: '' },
textSize: { value: '{{16}}' },
strokeWidth: { value: '{{8}}' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/colorPicker.js b/frontend/src/AppBuilder/WidgetManager/widgets/colorPicker.js
index b2fddd7e4c..6ecdede1a8 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/colorPicker.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/colorPicker.js
@@ -4,7 +4,7 @@ export const colorPickerConfig = {
description: 'Choose colors from a palette',
component: 'ColorPicker',
properties: {
- defaultColor: { type: 'color', displayName: 'Default color' },
+ defaultColor: { type: 'colorSwatches', displayName: 'Default color' },
},
defaultSize: {
width: 9,
@@ -14,7 +14,9 @@ export const colorPickerConfig = {
{
displayName: 'Set Color',
handle: 'setColor',
- params: [{ handle: 'color', displayName: 'color', defaultValue: '#ffffff', type: 'color' }],
+ params: [
+ { handle: 'colorSwatches', displayName: 'colorSwatches', defaultValue: '#ffffff', type: 'colorSwatches' },
+ ],
},
],
others: {
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/container.js b/frontend/src/AppBuilder/WidgetManager/widgets/container.js
index 37a895f553..424b9a801d 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/container.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/container.js
@@ -44,7 +44,7 @@ export const containerConfig = {
displayName: 'Show header',
validation: {
schema: { type: 'boolean' },
- defaultValue: true,
+ defaultValue: false,
},
},
},
@@ -58,6 +58,7 @@ export const containerConfig = {
},
displayName: 'ContainerText',
properties: ['text'],
+ slotName: 'header',
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
@@ -71,7 +72,7 @@ export const containerConfig = {
events: {},
styles: {
backgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: {
schema: { type: 'string' },
@@ -80,7 +81,7 @@ export const containerConfig = {
accordian: 'container',
},
headerBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: {
schema: { type: 'string' },
@@ -89,7 +90,7 @@ export const containerConfig = {
accordian: 'header',
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border color',
validation: {
schema: { type: 'string' },
@@ -153,7 +154,7 @@ export const containerConfig = {
showOnMobile: { value: '{{false}}' },
},
properties: {
- showHeader: { value: `{{true}}` },
+ showHeader: { value: `{{false}}` },
loadingState: { value: `{{false}}` },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
@@ -163,6 +164,7 @@ export const containerConfig = {
backgroundColor: { value: '#fff' },
headerBackgroundColor: { value: '#fff' },
borderRadius: { value: '4' },
+ headerHeight: { value: '{{80}}' },
borderColor: { value: '#fff' },
boxShadow: { value: '0px 0px 0px 0px #00000040' },
},
diff --git a/server/src/modules/apps/services/widget-config/timepickerV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/currencyinput.js
similarity index 59%
rename from server/src/modules/apps/services/widget-config/timepickerV2.js
rename to frontend/src/AppBuilder/WidgetManager/widgets/currencyinput.js
index 22f64ea366..64a8f2df57 100644
--- a/server/src/modules/apps/services/widget-config/timepickerV2.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/currencyinput.js
@@ -1,32 +1,12 @@
-export const timePickerConfig = {
- name: 'TimePicker',
- displayName: 'Time Picker',
- description: 'Choose date and time',
- component: 'TimePicker',
+export const currencyinputConfig = {
+ name: 'CurrencyInput',
+ displayName: 'Currency Input',
+ description: 'Currency input field',
+ component: 'CurrencyInput',
defaultSize: {
width: 10,
height: 40,
},
- validation: {
- minTime: {
- type: 'timepicker',
- placeholder: 'HH:mm',
- displayName: 'Min Time',
- },
- maxTime: {
- type: 'timepicker',
- placeholder: 'HH:mm',
- displayName: 'Max Time',
- },
- customRule: {
- type: 'code',
- displayName: 'Custom validation',
- },
- mandatory: {
- type: 'toggle',
- displayName: 'Make this field mandatory',
- },
- },
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
@@ -35,142 +15,88 @@ export const timePickerConfig = {
label: {
type: 'code',
displayName: 'Label',
+ validation: { schema: { type: 'string' }, defaultValue: 'Label' },
+ },
+ placeholder: {
+ type: 'code',
+ displayName: 'Placeholder',
validation: {
schema: { type: 'string' },
- defaultValue: 'Label',
+ defaultValue: 'Enter your number',
},
- accordian: 'Data',
},
- defaultValue: {
+ value: {
type: 'code',
displayName: 'Default value',
validation: {
- schema: { type: 'string' },
- defaultValue: '00:00',
+ schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
+ defaultValue: 0,
},
},
- isTimezoneEnabled: {
+ decimalPlaces: {
+ type: 'code',
+ displayName: 'Decimal places',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: '2' },
+ },
+ isCountryChangeEnabled: {
type: 'toggle',
- displayName: 'Manage time zones',
- validation: { schema: { type: 'boolean' }, defaultValue: false },
- section: 'formatting',
+ displayName: 'Enable currency change',
+ validation: { schema: { type: 'boolean' }, defaultValue: true },
},
loadingState: {
type: 'toggle',
displayName: 'Loading state',
- validation: { schema: { type: 'boolean' }, defaultValue: true },
+ validation: { schema: { type: 'boolean' }, defaultValue: false },
section: 'additionalActions',
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: { schema: { type: 'boolean' }, defaultValue: true },
-
section: 'additionalActions',
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
- validation: { schema: { type: 'boolean' }, defaultValue: true },
+ validation: { schema: { type: 'boolean' }, defaultValue: false },
section: 'additionalActions',
},
tooltip: {
type: 'code',
displayName: 'Tooltip',
- validation: {
- schema: { type: 'string' },
- defaultValue: 'Enter tooltip text',
- },
+ validation: { schema: { type: 'string' }, defaultValue: 'Tooltip text' },
section: 'additionalActions',
placeholder: 'Enter tooltip text',
},
},
+ validation: {
+ mandatory: { type: 'toggle', displayName: 'Make this field mandatory' },
+ regex: { type: 'code', displayName: 'Regex', placeholder: '^[a-zA-Z0-9_ -]{3,16}$' },
+ minValue: { type: 'code', displayName: 'Min value', placeholder: 'Enter min value' },
+ maxValue: { type: 'code', displayName: 'Max value', placeholder: 'Enter max value' },
+ customRule: {
+ type: 'code',
+ displayName: 'Custom validation',
+ placeholder: `{{components.text2.text=='yes'&&'valid'}}`,
+ },
+ },
events: {
- onSelect: { displayName: 'On select' },
+ onChange: { displayName: 'On change' },
+ onEnterPressed: { displayName: 'On enter pressed' },
onFocus: { displayName: 'On focus' },
onBlur: { displayName: 'On blur' },
},
- actions: [
- {
- handle: 'setValue',
- displayName: 'Set value',
- params: [
- { handle: 'value', displayName: 'Value' },
- { handle: 'format', displayName: 'Format' },
- ],
- },
- {
- handle: 'clearValue',
- displayName: 'Clear value',
- },
- {
- handle: 'setTime',
- displayName: 'Set time',
- params: [
- { handle: 'value', displayName: 'Value' },
- { handle: 'format', displayName: 'Format' },
- ],
- },
- {
- handle: 'setValueInTimestamp',
- displayName: 'Set value in timestamp',
- params: [{ handle: 'value', displayName: 'Value' }],
- },
- {
- handle: 'setMinTime',
- displayName: 'Set min time',
- params: [{ handle: 'value', displayName: 'Value' }],
- },
- {
- handle: 'setMaxTime',
- displayName: 'Set max time',
- params: [{ handle: 'value', displayName: 'Value' }],
- },
- {
- handle: 'setDisplayTimezone',
- displayName: 'Set display timezone',
- params: [{ handle: 'value', displayName: 'Value' }],
- },
- {
- handle: 'setStoreTimezone',
- displayName: 'Set store timezone',
- params: [{ handle: 'value', displayName: 'Value' }],
- },
- {
- handle: 'setVisibility',
- displayName: 'Set visibility',
- params: [{ handle: 'value', displayName: 'Value', defaultValue: `{{true}}`, type: 'toggle' }],
- },
- {
- handle: 'setLoading',
- displayName: 'Set loading',
- params: [{ handle: 'value', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
- },
- {
- handle: 'setDisable',
- displayName: 'Set disable',
- params: [{ handle: 'value', displayName: 'Value', defaultValue: `{{false}}`, type: 'toggle' }],
- },
- {
- handle: 'setFocus',
- displayName: 'Set focus',
- },
- {
- handle: 'setBlur',
- displayName: 'Set blur',
- },
- ],
styles: {
- labelColor: {
+ color: {
type: 'color',
- displayName: 'Color',
+ displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
},
alignment: {
type: 'switch',
displayName: 'Alignment',
- validation: { schema: { type: 'string' }, defaultValue: 'top' },
+ validation: { schema: { type: 'string' }, defaultValue: 'side' },
options: [
{ displayName: 'Side', value: 'side' },
{ displayName: 'Top', value: 'top' },
@@ -179,7 +105,7 @@ export const timePickerConfig = {
},
direction: {
type: 'switch',
- displayName: 'Direction',
+ displayName: '',
validation: { schema: { type: 'string' }, defaultValue: 'left' },
showLabel: false,
isIcon: true,
@@ -190,7 +116,7 @@ export const timePickerConfig = {
accordian: 'label',
isFxNotRequired: true,
},
- labelWidth: {
+ width: {
type: 'slider',
displayName: 'Width',
accordian: 'label',
@@ -204,7 +130,7 @@ export const timePickerConfig = {
type: 'checkbox',
displayName: 'auto',
showLabel: false,
- validation: { schema: { type: 'boolean' } },
+ validation: { schema: { type: 'boolean' }, defaultValue: true },
accordian: 'label',
conditionallyRender: {
key: 'alignment',
@@ -212,13 +138,14 @@ export const timePickerConfig = {
},
isFxNotRequired: true,
},
- fieldBackgroundColor: {
+
+ backgroundColor: {
type: 'color',
displayName: 'Background',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
accordian: 'field',
},
- fieldBorderColor: {
+ borderColor: {
type: 'color',
displayName: 'Border',
validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
@@ -230,7 +157,7 @@ export const timePickerConfig = {
validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
accordian: 'field',
},
- selectedTextColor: {
+ textColor: {
type: 'color',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
@@ -239,7 +166,7 @@ export const timePickerConfig = {
errTextColor: {
type: 'color',
displayName: 'Error text',
- validation: { schema: { type: 'string' }, defaultValue: '#E54D2E' },
+ validation: { schema: { type: 'string' }, defaultValue: '#D72D39' },
accordian: 'field',
},
icon: {
@@ -251,38 +178,24 @@ export const timePickerConfig = {
},
iconColor: {
type: 'color',
- displayName: '',
- showLabel: false,
- validation: {
- schema: { type: 'string' },
- defaultValue: '#6A727C',
- },
+ displayName: 'Icon color',
+ validation: { schema: { type: 'string' }, defaultValue: '#CFD3D859' },
accordian: 'field',
- },
- iconDirection: {
- type: 'switch',
- displayName: '',
- validation: { schema: { type: 'string' } },
+ visibility: false,
showLabel: false,
- isIcon: true,
- options: [
- { displayName: 'alignleftinspector', value: 'left', iconName: 'alignleftinspector' },
- { displayName: 'alignrightinspector', value: 'right', iconName: 'alignrightinspector' },
- ],
- accordian: 'field',
},
- fieldBorderRadius: {
- type: 'input',
+ borderRadius: {
+ type: 'numberInput',
displayName: 'Border radius',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 6 },
accordian: 'field',
},
boxShadow: {
type: 'boxShadow',
- displayName: 'Box shadow',
+ displayName: 'Box Shadow',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
- defaultValue: '0px 0px 0px 0px #121212',
+ defaultValue: '0px 0px 0px 0px #00000040',
},
accordian: 'field',
},
@@ -293,6 +206,7 @@ export const timePickerConfig = {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
defaultValue: 'default',
},
+ isFxNotRequired: true,
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
@@ -300,52 +214,92 @@ export const timePickerConfig = {
accordian: 'container',
},
},
-
exposedVariables: {
value: '',
+ isMandatory: false,
+ isVisible: true,
+ isDisabled: false,
+ isLoading: false,
},
+ actions: [
+ {
+ handle: 'setValue',
+ displayName: 'Set Value',
+ params: [
+ { handle: 'value', displayName: 'value', defaultValue: '' },
+ { handle: 'country', displayName: 'country', defaultValue: '' },
+ ],
+ },
+ {
+ handle: 'clear',
+ displayName: 'Clear',
+ },
+ {
+ handle: 'setFocus',
+ displayName: 'Set focus',
+ },
+ {
+ handle: 'setBlur',
+ displayName: 'Set blur',
+ },
+ {
+ handle: 'setVisibility',
+ displayName: 'Set visibility',
+ params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setDisable',
+ displayName: 'Set disable',
+ params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setLoading',
+ displayName: 'Set loading',
+ params: [{ handle: 'loading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ ],
definition: {
+ validation: {
+ mandatory: { value: '{{false}}' },
+ regex: { value: '' },
+ minValue: { value: '' },
+ maxValue: { value: '' },
+ customRule: { value: '' },
+ },
+
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
- validation: {
- minTime: { value: '' },
- maxTime: { value: '' },
- customRule: { value: '' },
- mandatory: { value: '{{false}}' },
- },
properties: {
+ value: { value: '0' },
label: { value: 'Label' },
- defaultValue: { value: '00:00' },
- timeFormat: { value: 'HH:mm' },
- isTimezoneEnabled: { value: '{{false}}' },
- displayTimezone: { value: 'UTC' },
- storeTimezone: { value: 'UTC' },
- loadingState: { value: '{{false}}' },
+ placeholder: { value: 'Enter amount' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
+ loadingState: { value: '{{false}}' },
tooltip: { value: '' },
+ isCountryChangeEnabled: { value: '{{true}}' },
+ decimalPlaces: { value: '2' },
},
events: [],
styles: {
- labelColor: { value: '#1B1F24' },
- alignment: { value: 'side' },
- direction: { value: 'left' },
- labelWidth: { value: '20' },
- auto: { value: '{{true}}' },
- fieldBackgroundColor: { value: '#fff' },
- fieldBorderColor: { value: '#CCD1D5' },
+ textColor: { value: '#1B1F24' },
+ borderColor: { value: '#CCD1D5' },
accentColor: { value: '#4368E3' },
- selectedTextColor: { value: '#1B1F24' },
- errTextColor: { value: '#E54D2E' },
- icon: { value: 'IconClock' },
- iconVisibility: { value: true },
- iconDirection: { value: 'left' },
- fieldBorderRadius: { value: '{{6}}' },
- boxShadow: { value: '0px 0px 0px 0px #121212' },
+ errTextColor: { value: '#D72D39' },
+ borderRadius: { value: '{{6}}' },
+ backgroundColor: { value: '#fff' },
+ iconColor: { value: '#CFD3D859' },
+ direction: { value: 'left' },
+ width: { value: '{{33}}' },
+ alignment: { value: 'side' },
+ color: { value: '#1B1F24' },
+ auto: { value: '{{true}}' },
padding: { value: 'default' },
- iconColor: { value: '#6A727C' },
+ boxShadow: { value: '0px 0px 0px 0px #00000040' },
+ icon: { value: 'IconHome2' },
+ iconVisibility: { value: false },
},
},
};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/datepickerV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/datepickerV2.js
index 87de5d137b..ea56598d2e 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/datepickerV2.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/datepickerV2.js
@@ -169,8 +169,8 @@ export const datePickerV2Config = {
],
styles: {
labelColor: {
- type: 'color',
- displayName: 'Color',
+ type: 'colorSwatches',
+ displayName: 'colorSwatches',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
},
@@ -220,31 +220,31 @@ export const datePickerV2Config = {
isFxNotRequired: true,
},
fieldBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
accordian: 'field',
},
fieldBorderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
accordian: 'field',
},
accentColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Accent',
validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
accordian: 'field',
},
selectedTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'field',
},
errTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Error text',
validation: { schema: { type: 'string' }, defaultValue: '#E54D2E' },
accordian: 'field',
@@ -257,7 +257,7 @@ export const datePickerV2Config = {
visibility: false,
},
iconColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: '',
showLabel: false,
validation: {
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/daterangepicker.js b/frontend/src/AppBuilder/WidgetManager/widgets/daterangepicker.js
index 1f38a8df6d..2fcd5bfb42 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/daterangepicker.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/daterangepicker.js
@@ -199,8 +199,8 @@ export const daterangepickerConfig = {
],
styles: {
labelColor: {
- type: 'color',
- displayName: 'Color',
+ type: 'colorSwatches',
+ displayName: 'colorSwatches',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
},
@@ -250,31 +250,31 @@ export const daterangepickerConfig = {
isFxNotRequired: true,
},
fieldBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
accordian: 'field',
},
fieldBorderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
accordian: 'field',
},
accentColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Accent',
validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
accordian: 'field',
},
selectedTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'field',
},
errTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Error text',
validation: { schema: { type: 'string' }, defaultValue: '#E54D2E' },
accordian: 'field',
@@ -287,7 +287,7 @@ export const daterangepickerConfig = {
visibility: false,
},
iconColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: '',
showLabel: false,
validation: {
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/datetimepickerV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/datetimepickerV2.js
index 388eb9786c..658cc31c01 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/datetimepickerV2.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/datetimepickerV2.js
@@ -214,8 +214,8 @@ export const datetimePickerV2Config = {
],
styles: {
labelColor: {
- type: 'color',
- displayName: 'Color',
+ type: 'colorSwatches',
+ displayName: 'colorSwatches',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
},
@@ -265,31 +265,31 @@ export const datetimePickerV2Config = {
isFxNotRequired: true,
},
fieldBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
accordian: 'field',
},
fieldBorderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
accordian: 'field',
},
accentColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Accent',
validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
accordian: 'field',
},
selectedTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'field',
},
errTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Error text',
validation: { schema: { type: 'string' }, defaultValue: '#E54D2E' },
accordian: 'field',
@@ -302,7 +302,7 @@ export const datetimePickerV2Config = {
visibility: false,
},
iconColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: '',
showLabel: false,
validation: {
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/divider.js b/frontend/src/AppBuilder/WidgetManager/widgets/divider.js
index 045f894816..33b64d9f56 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/divider.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/divider.js
@@ -1,6 +1,6 @@
export const dividerConfig = {
- name: 'Divider',
- displayName: 'Divider',
+ name: 'HorizontalDivider',
+ displayName: 'Horizontal divider',
description: 'Separator between components',
component: 'Divider',
defaultSize: {
@@ -11,15 +11,12 @@ export const dividerConfig = {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
- properties: {},
- events: {},
- styles: {
- dividerColor: {
- type: 'color',
- displayName: 'Divider color',
+ properties: {
+ label: {
+ type: 'code',
+ displayName: 'Label',
validation: {
schema: { type: 'string' },
- defaultValue: '#000000',
},
},
visibility: {
@@ -29,6 +26,109 @@ export const dividerConfig = {
schema: { type: 'boolean' },
defaultValue: true,
},
+ section: 'additionalActions',
+ },
+ tooltip: {
+ type: 'code',
+ displayName: 'Tooltip',
+ validation: { schema: { type: 'string' }, defaultValue: 'Tooltip text' },
+ section: 'additionalActions',
+ placeholder: 'Enter tooltip text',
+ },
+ },
+ events: {},
+ styles: {
+ dividerColor: {
+ type: 'colorSwatches',
+ displayName: 'Divider color',
+ validation: {
+ schema: { type: 'string' },
+ },
+ },
+ visibility: {
+ type: 'toggle',
+ displayName: 'Visibility',
+ validation: {
+ schema: { type: 'boolean' },
+ defaultValue: true,
+ },
+ section: 'additionalActions',
+ },
+ tooltip: {
+ type: 'code',
+ displayName: 'Tooltip',
+ validation: { schema: { type: 'string' }, defaultValue: 'Tooltip text' },
+ section: 'additionalActions',
+ placeholder: 'Enter tooltip text',
+ },
+ },
+ events: {},
+ styles: {
+ dividerColor: {
+ type: 'color',
+ displayName: 'Divider color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: '#000000',
+ },
+ accordian: 'Divider',
+ },
+ dividerStyle: {
+ type: 'switch',
+ displayName: 'Style',
+ validation: {
+ schema: { type: 'string' },
+ },
+ options: [
+ { displayName: 'Solid', value: 'solid' },
+ { displayName: 'Dashed', value: 'dashed' },
+ ],
+ accordian: 'Divider',
+ },
+ labelAlignment: {
+ type: 'switch',
+ displayName: 'Label alignment',
+ validation: { schema: { type: 'string' }, defaultValue: 'left' },
+ showLabel: true,
+ isIcon: true,
+ options: [
+ { displayName: 'alignleftinspector', value: 'left', iconName: 'alignleftinspector' },
+ { displayName: 'alignhorizontalcenter', value: 'center', iconName: 'alignhorizontalcenter' },
+ { displayName: 'alignrightinspector', value: 'right', iconName: 'alignrightinspector' },
+ ],
+ accordian: 'Divider',
+ isFxNotRequired: true,
+ },
+ labelColor: {
+ type: 'color',
+ displayName: 'Label Color',
+ validation: {
+ schema: { type: 'string' },
+ },
+ accordian: 'Divider',
+ },
+ boxShadow: {
+ type: 'boxShadow',
+ displayName: 'Box Shadow',
+ validation: {
+ schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
+ defaultValue: '0px 0px 0px 0px #00000040',
+ },
+ accordian: 'Divider',
+ },
+ padding: {
+ type: 'switch',
+ displayName: 'Padding',
+ validation: {
+ schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
+ defaultValue: 'default',
+ },
+ isFxNotRequired: true,
+ options: [
+ { displayName: 'Default', value: 'default' },
+ { displayName: 'None', value: 'none' },
+ ],
+ accordian: 'container',
},
},
exposedVariables: {
@@ -39,11 +139,19 @@ export const dividerConfig = {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
- properties: {},
+ properties: {
+ label: { value: '' },
+ visibility: { value: '{{true}}' },
+ tooltip: { value: '' },
+ },
events: [],
styles: {
- visibility: { value: '{{true}}' },
- dividerColor: { value: '#000000' },
+ dividerColor: { value: '#CCD1D5' },
+ labelAlignment: { value: 'center' },
+ dividerStyle: { value: 'solid' },
+ labelColor: { value: '#6A727C' },
+ padding: { value: 'default' },
+ boxShadow: { value: '0px 0px 0px 0px #00000040' },
},
},
};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/dropdown.js b/frontend/src/AppBuilder/WidgetManager/widgets/dropdown.js
index b2c58acc7a..b8b090af24 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/dropdown.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/dropdown.js
@@ -128,7 +128,7 @@ export const dropdownConfig = {
defaultValue: true,
},
selectedTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Selected text color',
validation: {
schema: {
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js
index de90dbd0bf..cb90554e6b 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/dropdownV2.js
@@ -63,6 +63,18 @@ export const dropdownV2Config = {
},
accordian: 'Options',
},
+ sort: {
+ type: 'switch',
+ displayName: 'Sort options',
+ validation: { schema: { type: 'string' }, defaultValue: 'asc' },
+ options: [
+ { displayName: 'None', value: 'none' },
+ { displayName: 'a-z', value: 'asc' },
+ { displayName: 'z-a', value: 'desc' },
+ ],
+ accordian: 'Options',
+ isFxNotRequired: true,
+ },
loadingState: {
type: 'toggle',
displayName: 'Loading state',
@@ -101,7 +113,7 @@ export const dropdownV2Config = {
},
styles: {
labelColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Color',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
@@ -153,31 +165,31 @@ export const dropdownV2Config = {
},
fieldBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
accordian: 'field',
},
fieldBorderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
accordian: 'field',
},
accentColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Accent',
validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
accordian: 'field',
},
selectedTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'field',
},
errTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Error text',
validation: { schema: { type: 'string' }, defaultValue: '#D72D39' },
accordian: 'field',
@@ -190,7 +202,7 @@ export const dropdownV2Config = {
visibility: false,
},
iconColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: '',
showLabel: false,
validation: {
@@ -300,6 +312,7 @@ export const dropdownV2Config = {
},
label: { value: 'Select' },
optionsLoadingState: { value: '{{false}}' },
+ sort: { value: 'asc' },
placeholder: { value: 'Select an option' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/emailinput.js b/frontend/src/AppBuilder/WidgetManager/widgets/emailinput.js
new file mode 100644
index 0000000000..825340616f
--- /dev/null
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/emailinput.js
@@ -0,0 +1,292 @@
+export const emailinputConfig = {
+ name: 'EmailInput',
+ displayName: 'Email Input',
+ description: 'Email input field',
+ component: 'EmailInput',
+ defaultSize: {
+ width: 10,
+ height: 40,
+ },
+ others: {
+ showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
+ showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
+ },
+ properties: {
+ label: {
+ type: 'code',
+ displayName: 'Label',
+ validation: { schema: { type: 'string' }, defaultValue: 'Label' },
+ },
+ placeholder: {
+ type: 'code',
+ displayName: 'Placeholder',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: 'Enter email',
+ },
+ },
+ value: {
+ type: 'code',
+ displayName: 'Default value',
+ validation: {
+ schema: {
+ type: 'string',
+ },
+ defaultValue: 'Default value',
+ },
+ },
+ loadingState: {
+ type: 'toggle',
+ displayName: 'Loading state',
+ validation: { schema: { type: 'boolean' }, defaultValue: false },
+ section: 'additionalActions',
+ },
+ visibility: {
+ type: 'toggle',
+ displayName: 'Visibility',
+ validation: { schema: { type: 'boolean' }, defaultValue: true },
+ section: 'additionalActions',
+ },
+ disabledState: {
+ type: 'toggle',
+ displayName: 'Disable',
+ validation: { schema: { type: 'boolean' }, defaultValue: false },
+ section: 'additionalActions',
+ },
+ tooltip: {
+ type: 'code',
+ displayName: 'Tooltip',
+ validation: { schema: { type: 'string' }, defaultValue: 'Tooltip text' },
+ section: 'additionalActions',
+ placeholder: 'Enter tooltip text',
+ },
+ },
+ validation: {
+ mandatory: { type: 'toggle', displayName: 'Make this field mandatory' },
+ regex: { type: 'code', displayName: 'Regex', placeholder: '^[a-zA-Z0-9_ -]{3,16}$' },
+ minLength: { type: 'code', displayName: 'Min length', placeholder: 'Enter min length' },
+ maxLength: { type: 'code', displayName: 'Max length', placeholder: 'Enter max length' },
+ customRule: {
+ type: 'code',
+ displayName: 'Custom validation',
+ placeholder: `{{components.text2.text=='yes'&&'valid'}}`,
+ },
+ },
+ events: {
+ onChange: { displayName: 'On change' },
+ onEnterPressed: { displayName: 'On enter pressed' },
+ onFocus: { displayName: 'On focus' },
+ onBlur: { displayName: 'On blur' },
+ },
+ styles: {
+ color: {
+ type: 'color',
+ displayName: 'Text',
+ validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
+ accordian: 'label',
+ },
+ alignment: {
+ type: 'switch',
+ displayName: 'Alignment',
+ validation: { schema: { type: 'string' }, defaultValue: 'side' },
+ options: [
+ { displayName: 'Side', value: 'side' },
+ { displayName: 'Top', value: 'top' },
+ ],
+ accordian: 'label',
+ },
+ direction: {
+ type: 'switch',
+ displayName: '',
+ validation: { schema: { type: 'string' }, defaultValue: 'left' },
+ showLabel: false,
+ isIcon: true,
+ options: [
+ { displayName: 'alignleftinspector', value: 'left', iconName: 'alignleftinspector' },
+ { displayName: 'alignrightinspector', value: 'right', iconName: 'alignrightinspector' },
+ ],
+ accordian: 'label',
+ isFxNotRequired: true,
+ },
+ width: {
+ type: 'slider',
+ displayName: 'Width',
+ accordian: 'label',
+ conditionallyRender: {
+ key: 'alignment',
+ value: 'side',
+ },
+ isFxNotRequired: true,
+ },
+ auto: {
+ type: 'checkbox',
+ displayName: 'auto',
+ showLabel: false,
+ validation: { schema: { type: 'boolean' }, defaultValue: true },
+ accordian: 'label',
+ conditionallyRender: {
+ key: 'alignment',
+ value: 'side',
+ },
+ isFxNotRequired: true,
+ },
+
+ backgroundColor: {
+ type: 'color',
+ displayName: 'Background',
+ validation: { schema: { type: 'string' }, defaultValue: '#fff' },
+ accordian: 'field',
+ },
+ borderColor: {
+ type: 'color',
+ displayName: 'Border',
+ validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
+ accordian: 'field',
+ },
+ accentColor: {
+ type: 'color',
+ displayName: 'Accent',
+ validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
+ accordian: 'field',
+ },
+ textColor: {
+ type: 'color',
+ displayName: 'Text',
+ validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
+ accordian: 'field',
+ },
+ errTextColor: {
+ type: 'color',
+ displayName: 'Error text',
+ validation: { schema: { type: 'string' }, defaultValue: '#D72D39' },
+ accordian: 'field',
+ },
+ icon: {
+ type: 'icon',
+ displayName: 'Icon',
+ validation: { schema: { type: 'string' }, defaultValue: 'IconMailFilled' },
+ accordian: 'field',
+ visibility: true,
+ },
+ iconColor: {
+ type: 'color',
+ displayName: 'Icon color',
+ validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
+ accordian: 'field',
+ visibility: false,
+ showLabel: false,
+ },
+ borderRadius: {
+ type: 'numberInput',
+ displayName: 'Border radius',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 6 },
+ accordian: 'field',
+ },
+ boxShadow: {
+ type: 'boxShadow',
+ displayName: 'Box Shadow',
+ validation: {
+ schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
+ defaultValue: '0px 0px 0px 0px #00000040',
+ },
+ accordian: 'field',
+ },
+ padding: {
+ type: 'switch',
+ displayName: 'Padding',
+ validation: {
+ schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
+ defaultValue: 'default',
+ },
+ isFxNotRequired: true,
+ options: [
+ { displayName: 'Default', value: 'default' },
+ { displayName: 'None', value: 'none' },
+ ],
+ accordian: 'container',
+ },
+ },
+ exposedVariables: {
+ value: '',
+ isMandatory: false,
+ isVisible: true,
+ isDisabled: false,
+ isLoading: false,
+ },
+ actions: [
+ {
+ handle: 'setText',
+ displayName: 'Set text',
+ params: [{ handle: 'text', displayName: 'text', defaultValue: 'New text' }],
+ },
+ {
+ handle: 'clear',
+ displayName: 'Clear',
+ },
+ {
+ handle: 'setFocus',
+ displayName: 'Set focus',
+ },
+ {
+ handle: 'setBlur',
+ displayName: 'Set blur',
+ },
+ {
+ handle: 'setVisibility',
+ displayName: 'Set visibility',
+ params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setDisable',
+ displayName: 'Set disable',
+ params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setLoading',
+ displayName: 'Set loading',
+ params: [{ handle: 'loading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ ],
+ definition: {
+ validation: {
+ mandatory: { value: '{{false}}' },
+ regex: { value: '' },
+ minLength: { value: '' },
+ maxLength: { value: '' },
+ customRule: { value: '' },
+ },
+
+ others: {
+ showOnDesktop: { value: '{{true}}' },
+ showOnMobile: { value: '{{false}}' },
+ },
+ properties: {
+ value: { value: '' },
+ label: { value: 'Label' },
+ placeholder: { value: 'Enter email' },
+ visibility: { value: '{{true}}' },
+ disabledState: { value: '{{false}}' },
+ loadingState: { value: '{{false}}' },
+ tooltip: { value: '' },
+ },
+ events: [],
+ styles: {
+ textColor: { value: '#1B1F24' },
+ borderColor: { value: '#CCD1D5' },
+ accentColor: { value: '#4368E3' },
+ errTextColor: { value: '#D72D39' },
+ borderRadius: { value: '{{6}}' },
+ backgroundColor: { value: '#fff' },
+ iconColor: { value: '#CCD1D5' },
+ direction: { value: 'left' },
+ width: { value: '{{33}}' },
+ alignment: { value: 'side' },
+ color: { value: '#1B1F24' },
+ auto: { value: '{{true}}' },
+ padding: { value: 'default' },
+ boxShadow: { value: '0px 0px 0px 0px #00000040' },
+ icon: { value: 'IconMailFilled' },
+ iconVisibility: { value: true },
+ },
+ },
+};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/form.js b/frontend/src/AppBuilder/WidgetManager/widgets/form.js
index 2d8eb7f0a8..d299ec2a6f 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/form.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/form.js
@@ -333,7 +333,7 @@ export const formConfig = {
},
},
backgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background color',
validation: {
schema: { type: 'string' },
@@ -351,7 +351,7 @@ export const formConfig = {
},
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border color',
validation: {
schema: { type: 'string' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/icon.js b/frontend/src/AppBuilder/WidgetManager/widgets/icon.js
index aea06c976c..761a2da425 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/icon.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/icon.js
@@ -61,7 +61,7 @@ export const iconConfig = {
},
styles: {
iconColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Color',
validation: {
schema: { type: 'string' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/image.js b/frontend/src/AppBuilder/WidgetManager/widgets/image.js
index c4bd7b6147..ca74e31d12 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/image.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/image.js
@@ -143,8 +143,17 @@ export const imageConfig = {
},
accordian: 'Image',
},
+ alignment: {
+ type: 'alignButtons',
+ displayName: 'Alignment',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: 'center',
+ },
+ accordian: 'Image',
+ },
backgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: {
schema: { type: 'string' },
@@ -153,7 +162,7 @@ export const imageConfig = {
accordian: 'Container',
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: {
schema: { type: 'string' },
@@ -179,11 +188,11 @@ export const imageConfig = {
padding: {
type: 'switch',
displayName: 'Padding',
- validation: { schema: { type: 'string' }, defaultValue: 'default' },
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'Custom', value: 'custom' },
],
+ validation: { schema: { type: 'string' }, defaultValue: 'default' },
accordian: 'Container',
isFxNotRequired: true,
},
@@ -244,7 +253,6 @@ export const imageConfig = {
loadingState: { value: '{{false}}' },
disabledState: { value: '{{false}}' },
visibility: { value: '{{true}}' },
- visible: { value: '{{true}}' },
},
events: [],
styles: {
@@ -256,6 +264,7 @@ export const imageConfig = {
boxShadow: { value: '0px 0px 0px 0px #00000090' },
padding: { value: 'default' },
customPadding: { value: '{{0}}' },
+ alignment: { value: 'center' },
},
},
};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/index.js b/frontend/src/AppBuilder/WidgetManager/widgets/index.js
index ce0e73fdf5..e79bee68c2 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/index.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/index.js
@@ -58,6 +58,9 @@ import { kanbanBoardConfig } from './kanbanBoard';
import { datetimePickerV2Config } from './datetimepickerV2';
import { datePickerV2Config } from './datepickerV2';
import { timePickerConfig } from './timepicker';
+import { emailinputConfig } from './emailinput';
+import { phoneinputConfig } from './phoneinput';
+import { currencyinputConfig } from './currencyinput';
export {
buttonConfig,
@@ -73,6 +76,9 @@ export {
datetimePickerV2Config,
datePickerV2Config,
timePickerConfig,
+ emailinputConfig,
+ phoneinputConfig,
+ currencyinputConfig,
checkboxConfig,
radiobuttonConfig, //!Depreciated
radiobuttonV2Config,
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/kanban.js b/frontend/src/AppBuilder/WidgetManager/widgets/kanban.js
index 0705581c51..cd39d0bd7b 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/kanban.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/kanban.js
@@ -77,7 +77,7 @@ export const kanbanConfig = {
styles: {
disabledState: { type: 'toggle', displayName: 'Disable' },
visibility: { type: 'toggle', displayName: 'Visibility' },
- accentColor: { type: 'color', displayName: 'Accent color' },
+ accentColor: { type: 'colorSwatches', displayName: 'Accent color' },
},
actions: [
{
@@ -157,7 +157,7 @@ export const kanbanConfig = {
styles: {
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
- accentColor: { value: '#4d72fa' },
+ accentColor: { value: 'var(--primary-brand)' },
},
},
};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/kanbanBoard.js b/frontend/src/AppBuilder/WidgetManager/widgets/kanbanBoard.js
index 4929bad9fe..4363c0d0b7 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/kanbanBoard.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/kanbanBoard.js
@@ -30,7 +30,7 @@ export const kanbanBoardConfig = {
visibility: { type: 'toggle', displayName: 'Visibility' },
width: { type: 'number', displayName: 'Width' },
minWidth: { type: 'number', displayName: 'Min Width' },
- accentColor: { type: 'color', displayName: 'Accent color' },
+ accentColor: { type: 'colorSwatches', displayName: 'Accent color' },
},
exposedVariables: {
columns: {},
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/link.js b/frontend/src/AppBuilder/WidgetManager/widgets/link.js
index 673abb1e75..16d5f13368 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/link.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/link.js
@@ -74,7 +74,7 @@ export const linkConfig = {
},
styles: {
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text color',
validation: {
schema: { type: 'string' },
@@ -159,6 +159,14 @@ export const linkConfig = {
],
accordian: 'container',
},
+ alignment: {
+ type: 'alignButtons',
+ displayName: 'Alignment',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: 'left',
+ },
+ },
},
exposedVariables: {},
actions: [
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js
index 62b55a7fea..6c599bc2fc 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js
@@ -125,7 +125,7 @@ export const listviewConfig = {
},
styles: {
backgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background color',
validation: {
schema: { type: 'string' },
@@ -133,7 +133,7 @@ export const listviewConfig = {
},
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border color',
validation: {
schema: { type: 'string' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/modal.js b/frontend/src/AppBuilder/WidgetManager/widgets/modal.js
index 8f0c34b566..3443c73c9b 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/modal.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/modal.js
@@ -81,7 +81,7 @@ export const modalConfig = {
},
styles: {
headerBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Header background color',
validation: {
schema: { type: 'string' },
@@ -89,7 +89,7 @@ export const modalConfig = {
},
},
headerTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Header title color',
validation: {
schema: { type: 'string' },
@@ -97,7 +97,7 @@ export const modalConfig = {
},
},
bodyBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Body background color',
validation: {
schema: { type: 'string' },
@@ -121,7 +121,7 @@ export const modalConfig = {
},
},
triggerButtonBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Trigger button background color',
validation: {
schema: { type: 'string' },
@@ -129,7 +129,7 @@ export const modalConfig = {
},
},
triggerButtonTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Trigger button text color',
validation: {
schema: { type: 'string' },
@@ -175,7 +175,7 @@ export const modalConfig = {
bodyBackgroundColor: { value: '#ffffffff' },
disabledState: { value: '{{false}}' },
visibility: { value: '{{true}}' },
- triggerButtonBackgroundColor: { value: '#4D72FA' },
+ triggerButtonBackgroundColor: { value: 'var(--primary-brand)' },
triggerButtonTextColor: { value: '#ffffffff' },
},
},
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js
index 6aefd71067..4ab4af57ce 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js
@@ -130,6 +130,18 @@ export const multiselectV2Config = {
},
accordian: 'Options',
},
+ sort: {
+ type: 'switch',
+ displayName: 'Sort options',
+ validation: { schema: { type: 'string' }, defaultValue: 'asc' },
+ options: [
+ { displayName: 'None', value: 'none' },
+ { displayName: 'a-z', value: 'asc' },
+ { displayName: 'z-a', value: 'desc' },
+ ],
+ accordian: 'Options',
+ isFxNotRequired: true,
+ },
loadingState: {
type: 'toggle',
displayName: 'Loading state',
@@ -165,7 +177,7 @@ export const multiselectV2Config = {
styles: {
labelColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Color',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
@@ -218,32 +230,32 @@ export const multiselectV2Config = {
},
fieldBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
accordian: 'field',
},
fieldBorderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
accordian: 'field',
},
accentColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Accent',
validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
accordian: 'field',
},
selectedTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'field',
},
errTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Error Text',
validation: { schema: { type: 'string' }, defaultValue: '#D72D39' },
accordian: 'field',
@@ -256,7 +268,7 @@ export const multiselectV2Config = {
visibility: false,
},
iconColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Icon color',
validation: {
schema: { type: 'string' },
@@ -313,6 +325,7 @@ export const multiselectV2Config = {
advanced: { value: `{{false}}` },
showAllOption: { value: '{{false}}' },
optionsLoadingState: { value: '{{false}}' },
+ sort: { value: 'asc' },
placeholder: { value: 'Select the options' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
@@ -365,7 +378,7 @@ export const multiselectV2Config = {
icon: { value: 'IconHome2' },
iconVisibility: { value: false },
iconColor: { value: '#6A727C' },
- accentColor: { value: '#4368E3' },
+ accentColor: { value: 'var(--primary-brand)' },
},
},
};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/numberinput.js b/frontend/src/AppBuilder/WidgetManager/widgets/numberinput.js
index fe3947e47b..cba9c04cd1 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/numberinput.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/numberinput.js
@@ -72,7 +72,7 @@ export const numberinputConfig = {
},
styles: {
color: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
@@ -124,31 +124,31 @@ export const numberinputConfig = {
},
backgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
accordian: 'field',
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
accordian: 'field',
},
accentColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Accent',
- validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
+ validation: { schema: { type: 'string' }, defaultValue: 'var(--primary-brand)' },
accordian: 'field',
},
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'field',
},
errTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Error text',
validation: { schema: { type: 'string' }, defaultValue: '#D72D39' },
accordian: 'field',
@@ -161,7 +161,7 @@ export const numberinputConfig = {
visibility: false,
},
iconColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Icon color',
validation: { schema: { type: 'string' }, defaultValue: '#CFD3D859' },
accordian: 'field',
@@ -279,7 +279,7 @@ export const numberinputConfig = {
borderRadius: { value: '{{6}}' },
backgroundColor: { value: '#fff' },
borderColor: { value: '#CCD1D5' },
- accentColor: { value: '#4368E3' },
+ accentColor: { value: 'var(--primary-brand)' },
errTextColor: { value: '#D72D39' },
textColor: { value: '#1B1F24' },
color: { value: '#1B1F24' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/pagination.js b/frontend/src/AppBuilder/WidgetManager/widgets/pagination.js
index 6fabfa889a..116d0e9849 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/pagination.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/pagination.js
@@ -50,6 +50,14 @@ export const paginationConfig = {
defaultValue: false,
},
},
+ alignment: {
+ type: 'alignButtons',
+ displayName: 'Alignment',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: 'left',
+ },
+ },
},
exposedVariables: {
totalPages: null,
@@ -73,6 +81,7 @@ export const paginationConfig = {
styles: {
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
+ alignment: { value: 'left' },
},
},
};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/passwordinput.js b/frontend/src/AppBuilder/WidgetManager/widgets/passwordinput.js
index 92a4a4711c..fad0d00735 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/passwordinput.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/passwordinput.js
@@ -84,7 +84,7 @@ export const passinputConfig = {
},
styles: {
color: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
@@ -136,31 +136,31 @@ export const passinputConfig = {
},
backgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
accordian: 'field',
},
accentColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Accent',
- validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
+ validation: { schema: { type: 'string' }, defaultValue: 'var(--primary-brand)' },
accordian: 'field',
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
accordian: 'field',
},
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'field',
},
errTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Error text',
validation: { schema: { type: 'string' }, defaultValue: '#D72D39' },
accordian: 'field',
@@ -173,7 +173,7 @@ export const passinputConfig = {
visibility: false,
},
iconColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Icon color',
validation: { schema: { type: 'string' }, defaultValue: '#CFD3D859' },
accordian: 'field',
@@ -278,7 +278,7 @@ export const passinputConfig = {
borderRadius: { value: '{{6}}' },
backgroundColor: { value: '#fff' },
borderColor: { value: '#CCD1D5' },
- accentColor: { value: '#4368E3' },
+ accentColor: { value: 'var(--primary-brand)' },
errTextColor: { value: '#D72D39' },
textColor: { value: '#1B1F24' },
iconColor: { value: '#CFD3D859' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/phoneinput.js b/frontend/src/AppBuilder/WidgetManager/widgets/phoneinput.js
new file mode 100644
index 0000000000..83e35ffc68
--- /dev/null
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/phoneinput.js
@@ -0,0 +1,288 @@
+export const phoneinputConfig = {
+ name: 'PhoneInput',
+ displayName: 'Phone Input',
+ description: 'Phone input field',
+ component: 'PhoneInput',
+ defaultSize: {
+ width: 10,
+ height: 40,
+ },
+ others: {
+ showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
+ showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
+ },
+ properties: {
+ label: {
+ type: 'code',
+ displayName: 'Label',
+ validation: { schema: { type: 'string' }, defaultValue: 'Label' },
+ },
+ placeholder: {
+ type: 'code',
+ displayName: 'Placeholder',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: 'Enter your input',
+ },
+ },
+ value: {
+ type: 'code',
+ displayName: 'Default value',
+ validation: {
+ schema: {
+ type: 'string',
+ },
+ defaultValue: 'Default value',
+ },
+ },
+ isCountryChangeEnabled: {
+ type: 'toggle',
+ displayName: 'Enable country change',
+ validation: { schema: { type: 'boolean' }, defaultValue: true },
+ },
+ loadingState: {
+ type: 'toggle',
+ displayName: 'Loading state',
+ validation: { schema: { type: 'boolean' }, defaultValue: false },
+ section: 'additionalActions',
+ },
+ visibility: {
+ type: 'toggle',
+ displayName: 'Visibility',
+ validation: { schema: { type: 'boolean' }, defaultValue: true },
+ section: 'additionalActions',
+ },
+ disabledState: {
+ type: 'toggle',
+ displayName: 'Disable',
+ validation: { schema: { type: 'boolean' }, defaultValue: false },
+ section: 'additionalActions',
+ },
+ tooltip: {
+ type: 'code',
+ displayName: 'Tooltip',
+ validation: { schema: { type: 'string' }, defaultValue: 'Tooltip text' },
+ section: 'additionalActions',
+ placeholder: 'Enter tooltip text',
+ },
+ },
+ validation: {
+ mandatory: { type: 'toggle', displayName: 'Make this field mandatory' },
+ regex: { type: 'code', displayName: 'Regex', placeholder: '^[a-zA-Z0-9_ -]{3,16}$' },
+ minLength: { type: 'code', displayName: 'Min length', placeholder: 'Enter min length' },
+ maxLength: { type: 'code', displayName: 'Max length', placeholder: 'Enter max length' },
+ customRule: {
+ type: 'code',
+ displayName: 'Custom validation',
+ placeholder: `{{components.text2.text=='yes'&&'valid'}}`,
+ },
+ },
+ events: {
+ onChange: { displayName: 'On change' },
+ onEnterPressed: { displayName: 'On enter pressed' },
+ onFocus: { displayName: 'On focus' },
+ onBlur: { displayName: 'On blur' },
+ },
+ styles: {
+ color: {
+ type: 'color',
+ displayName: 'Text',
+ validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
+ accordian: 'label',
+ },
+ alignment: {
+ type: 'switch',
+ displayName: 'Alignment',
+ validation: { schema: { type: 'string' }, defaultValue: 'side' },
+ options: [
+ { displayName: 'Side', value: 'side' },
+ { displayName: 'Top', value: 'top' },
+ ],
+ accordian: 'label',
+ },
+ direction: {
+ type: 'switch',
+ displayName: '',
+ validation: { schema: { type: 'string' }, defaultValue: 'left' },
+ showLabel: false,
+ isIcon: true,
+ options: [
+ { displayName: 'alignleftinspector', value: 'left', iconName: 'alignleftinspector' },
+ { displayName: 'alignrightinspector', value: 'right', iconName: 'alignrightinspector' },
+ ],
+ accordian: 'label',
+ isFxNotRequired: true,
+ },
+ width: {
+ type: 'slider',
+ displayName: 'Width',
+ accordian: 'label',
+ conditionallyRender: {
+ key: 'alignment',
+ value: 'side',
+ },
+ isFxNotRequired: true,
+ },
+ auto: {
+ type: 'checkbox',
+ displayName: 'auto',
+ showLabel: false,
+ validation: { schema: { type: 'boolean' }, defaultValue: true },
+ accordian: 'label',
+ conditionallyRender: {
+ key: 'alignment',
+ value: 'side',
+ },
+ isFxNotRequired: true,
+ },
+
+ backgroundColor: {
+ type: 'color',
+ displayName: 'Background',
+ validation: { schema: { type: 'string' }, defaultValue: '#fff' },
+ accordian: 'field',
+ },
+ borderColor: {
+ type: 'color',
+ displayName: 'Border',
+ validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
+ accordian: 'field',
+ },
+ accentColor: {
+ type: 'color',
+ displayName: 'Accent',
+ validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
+ accordian: 'field',
+ },
+ textColor: {
+ type: 'color',
+ displayName: 'Text',
+ validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
+ accordian: 'field',
+ },
+ errTextColor: {
+ type: 'color',
+ displayName: 'Error text',
+ validation: { schema: { type: 'string' }, defaultValue: '#D72D39' },
+ accordian: 'field',
+ },
+ borderRadius: {
+ type: 'numberInput',
+ displayName: 'Border radius',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 6 },
+ accordian: 'field',
+ },
+ boxShadow: {
+ type: 'boxShadow',
+ displayName: 'Box Shadow',
+ validation: {
+ schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
+ defaultValue: '0px 0px 0px 0px #00000040',
+ },
+ accordian: 'field',
+ },
+ padding: {
+ type: 'switch',
+ displayName: 'Padding',
+ validation: {
+ schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
+ defaultValue: 'default',
+ },
+ isFxNotRequired: true,
+ options: [
+ { displayName: 'Default', value: 'default' },
+ { displayName: 'None', value: 'none' },
+ ],
+ accordian: 'container',
+ },
+ },
+ exposedVariables: {
+ value: '',
+ isMandatory: false,
+ isVisible: true,
+ isDisabled: false,
+ isLoading: false,
+ },
+ actions: [
+ {
+ handle: 'setValue',
+ displayName: 'Set Value',
+ params: [
+ { handle: 'value', displayName: 'value', defaultValue: '' },
+ { handle: 'country', displayName: 'country', defaultValue: '' },
+ ],
+ },
+ {
+ handle: 'setCountryCode',
+ displayName: 'Set country code',
+ params: [{ handle: 'countryCode', displayName: 'Country code', defaultValue: '' }],
+ },
+ {
+ handle: 'clear',
+ displayName: 'Clear',
+ },
+ {
+ handle: 'setFocus',
+ displayName: 'Set focus',
+ },
+ {
+ handle: 'setBlur',
+ displayName: 'Set blur',
+ },
+ {
+ handle: 'setVisibility',
+ displayName: 'Set visibility',
+ params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setDisable',
+ displayName: 'Set disable',
+ params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setLoading',
+ displayName: 'Set loading',
+ params: [{ handle: 'loading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ ],
+ definition: {
+ validation: {
+ mandatory: { value: '{{false}}' },
+ regex: { value: '' },
+ minLength: { value: '' },
+ maxLength: { value: '' },
+ customRule: { value: '' },
+ },
+
+ others: {
+ showOnDesktop: { value: '{{true}}' },
+ showOnMobile: { value: '{{false}}' },
+ },
+ properties: {
+ value: { value: '' },
+ label: { value: 'Label' },
+ placeholder: { value: 'Enter your phone' },
+ visibility: { value: '{{true}}' },
+ disabledState: { value: '{{false}}' },
+ loadingState: { value: '{{false}}' },
+ tooltip: { value: '' },
+ isCountryChangeEnabled: { value: '{{true}}' },
+ },
+ events: [],
+ styles: {
+ textColor: { value: '#1B1F24' },
+ borderColor: { value: '#CCD1D5' },
+ accentColor: { value: '#4368E3' },
+ errTextColor: { value: '#D72D39' },
+ borderRadius: { value: '{{6}}' },
+ backgroundColor: { value: '#fff' },
+ direction: { value: 'left' },
+ width: { value: '{{33}}' },
+ alignment: { value: 'side' },
+ color: { value: '#1B1F24' },
+ auto: { value: '{{true}}' },
+ padding: { value: 'default' },
+ boxShadow: { value: '0px 0px 0px 0px #00000040' },
+ },
+ },
+};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/radioButtonV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/radioButtonV2.js
index 2ff6ca6c7c..62a084d75a 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/radioButtonV2.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/radioButtonV2.js
@@ -89,7 +89,7 @@ export const radiobuttonV2Config = {
},
styles: {
labelColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Color',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
@@ -139,7 +139,7 @@ export const radiobuttonV2Config = {
isFxNotRequired: true,
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: {
schema: { type: 'string' },
@@ -147,7 +147,7 @@ export const radiobuttonV2Config = {
accordian: 'switch',
},
switchOnBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Checked background',
validation: {
schema: { type: 'string' },
@@ -158,7 +158,7 @@ export const radiobuttonV2Config = {
tooltipPlacement: 'bottom',
},
switchOffBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Unchecked background',
validation: {
schema: { type: 'string' },
@@ -169,7 +169,7 @@ export const radiobuttonV2Config = {
tooltipPlacement: 'bottom',
},
handleColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Handle color',
validation: {
schema: { type: 'string' },
@@ -177,7 +177,7 @@ export const radiobuttonV2Config = {
accordian: 'switch',
},
optionsTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: {
schema: { type: 'string' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/radiobutton.js b/frontend/src/AppBuilder/WidgetManager/widgets/radiobutton.js
index d6ec057526..66d055ca5e 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/radiobutton.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/radiobutton.js
@@ -53,7 +53,7 @@ export const radiobuttonConfig = {
},
styles: {
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text color',
validation: {
schema: { type: 'string' },
@@ -61,11 +61,11 @@ export const radiobuttonConfig = {
},
},
activeColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Active color',
validation: {
schema: { type: 'string' },
- defaultValue: '#000000',
+ defaultValue: 'var(--primary-brand)',
},
},
visibility: {
@@ -113,7 +113,7 @@ export const radiobuttonConfig = {
events: [],
styles: {
textColor: { value: '' },
- activeColor: { value: '' },
+ activeColor: { value: 'var(--primary-brand)' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/rangeslider.js b/frontend/src/AppBuilder/WidgetManager/widgets/rangeslider.js
index 151dca3384..320e7a6741 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/rangeslider.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/rangeslider.js
@@ -53,7 +53,7 @@ export const rangeSliderConfig = {
},
styles: {
lineColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Line color',
validation: {
schema: { type: 'string' },
@@ -61,7 +61,7 @@ export const rangeSliderConfig = {
},
},
handleColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Handle color',
validation: {
schema: { type: 'string' },
@@ -69,7 +69,7 @@ export const rangeSliderConfig = {
},
},
trackColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Track color',
validation: {
schema: { type: 'string' },
@@ -109,7 +109,7 @@ export const rangeSliderConfig = {
styles: {
lineColor: { value: '' },
handleColor: { value: '' },
- trackColor: { value: '' },
+ trackColor: { value: 'var(--primary-brand)' },
visibility: { value: '{{true}}' },
},
},
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/spinner.js b/frontend/src/AppBuilder/WidgetManager/widgets/spinner.js
index a6bf80b9be..c38bfdc468 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/spinner.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/spinner.js
@@ -23,11 +23,11 @@ export const spinnerConfig = {
},
},
colour: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Colour',
validation: {
schema: { type: 'string' },
- defaultValue: '#0565ff',
+ defaultValue: 'var(--primary-brand)',
},
},
size: {
@@ -54,7 +54,7 @@ export const spinnerConfig = {
styles: {
visibility: { value: '{{true}}' },
size: { value: 'sm' },
- colour: { value: '#0565ff' },
+ colour: { value: 'var(--primary-brand)' },
},
},
};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/starrating.js b/frontend/src/AppBuilder/WidgetManager/widgets/starrating.js
index 01240d0369..8cb239133d 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/starrating.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/starrating.js
@@ -58,7 +58,7 @@ export const starratingConfig = {
},
styles: {
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Star color',
validation: {
schema: { type: 'string' },
@@ -66,7 +66,7 @@ export const starratingConfig = {
},
},
labelColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Label color',
validation: {
schema: { type: 'string' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/statistics.js b/frontend/src/AppBuilder/WidgetManager/widgets/statistics.js
index d364d78524..ecf1f70aed 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/statistics.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/statistics.js
@@ -55,22 +55,22 @@ export const statisticsConfig = {
events: {},
styles: {
primaryLabelColour: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Primary label colour',
validation: { schema: { type: 'string' }, defaultValue: '#8092AB' },
},
primaryTextColour: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Primary text colour',
validation: { schema: { type: 'string' }, defaultValue: '#000000' },
},
secondaryLabelColour: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Secondary label colour',
validation: { schema: { type: 'string' }, defaultValue: '#8092AB' },
},
secondaryTextColour: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Secondary text colour',
validation: { schema: { type: 'string' }, defaultValue: '#36AF8B' },
},
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/steps.js b/frontend/src/AppBuilder/WidgetManager/widgets/steps.js
index d076bb0b56..b9c575192a 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/steps.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/steps.js
@@ -147,16 +147,16 @@ export const stepsConfig = {
},
styles: {
incompletedAccent: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Incompleted accent',
validation: {
schema: { type: 'string' },
- defaultValue: '#E4E7EB',
+ defaultValue: '#CCD1D5',
},
accordian: 'steps',
},
incompletedLabel: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Incompleted label',
validation: {
schema: { type: 'string' },
@@ -165,25 +165,25 @@ export const stepsConfig = {
accordian: 'steps',
},
completedAccent: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Completed accent',
validation: {
schema: { type: 'string' },
- defaultValue: '#4368E3',
+ defaultValue: 'var(--primary-brand)',
},
accordian: 'steps',
},
completedLabel: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Completed label',
validation: {
schema: { type: 'string' },
- defaultValue: '#FBFCFD',
+ defaultValue: '#1B1F24',
},
accordian: 'steps',
},
currentStepLabel: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Current step label',
validation: {
schema: { type: 'string' },
@@ -236,8 +236,8 @@ export const stepsConfig = {
events: [],
styles: {
visibility: { value: '{{true}}' },
- color: { value: '' },
- textColor: { value: '' },
+ // color: { value: '' },
+ // textColor: { value: '' },
padding: { value: 'default' },
incompletedAccent: { value: '#E4E7EB' },
incompletedLabel: { value: '#1B1F24' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/svgImage.js b/frontend/src/AppBuilder/WidgetManager/widgets/svgImage.js
index 315a6e2c28..9780f0cdd6 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/svgImage.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/svgImage.js
@@ -32,6 +32,14 @@ export const svgImageConfig = {
defaultValue: true,
},
},
+ alignment: {
+ type: 'alignButtons',
+ displayName: 'Alignment',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: 'left',
+ },
+ },
},
exposedVariables: {
value: {},
@@ -50,6 +58,7 @@ export const svgImageConfig = {
events: [],
styles: {
visibility: { value: '{{true}}' },
+ alignment: { value: 'left' },
},
},
};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/table.js b/frontend/src/AppBuilder/WidgetManager/widgets/table.js
index 8a89701494..9f0c4fd723 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/table.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/table.js
@@ -157,7 +157,7 @@ export const tableConfig = {
defaultValue: 'clientSide',
},
actionButtonBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background color',
validation: {
schema: { type: 'string' },
@@ -165,7 +165,7 @@ export const tableConfig = {
},
},
actionButtonTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text color',
validation: {
schema: { type: 'string' },
@@ -293,7 +293,7 @@ export const tableConfig = {
},
styles: {
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text Color',
validation: {
schema: { type: 'string' },
@@ -404,7 +404,7 @@ export const tableConfig = {
accordian: 'Container',
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: {
schema: { type: 'string' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/tabs.js b/frontend/src/AppBuilder/WidgetManager/widgets/tabs.js
index 0ed1e2a320..a9c10bebaf 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/tabs.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/tabs.js
@@ -110,11 +110,11 @@ export const tabsConfig = {
events: { onTabSwitch: { displayName: 'On tab switch' } },
styles: {
highlightColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Highlight color',
validation: {
schema: { type: 'string' },
- defaultValue: '#375FCF',
+ defaultValue: 'var(--primary-brand)',
},
},
visibility: {
@@ -175,7 +175,7 @@ export const tabsConfig = {
},
events: [],
styles: {
- highlightColor: { value: '#375FCF' },
+ highlightColor: { value: 'var(--primary-brand)' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
tabWidth: { value: 'auto' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/tags.js b/frontend/src/AppBuilder/WidgetManager/widgets/tags.js
index 8af289b23a..73cd44b550 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/tags.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/tags.js
@@ -38,6 +38,14 @@ export const tagsConfig = {
defaultValue: true,
},
},
+ alignment: {
+ type: 'alignButtons',
+ displayName: 'Alignment',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: 'left',
+ },
+ },
},
exposedVariables: {},
definition: {
@@ -54,6 +62,7 @@ export const tagsConfig = {
events: [],
styles: {
visibility: { value: '{{true}}' },
+ alignment: { value: 'left' },
},
},
};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/text.js b/frontend/src/AppBuilder/WidgetManager/widgets/text.js
index ada530d127..8ed1edffd1 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/text.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/text.js
@@ -105,7 +105,7 @@ export const textConfig = {
accordian: 'Text',
},
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Color',
validation: {
schema: { type: 'string' },
@@ -185,7 +185,7 @@ export const textConfig = {
},
backgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: {
schema: { type: 'string' },
@@ -195,7 +195,7 @@ export const textConfig = {
colorPickerPosition: 'top',
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: {
schema: { type: 'string' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/textarea.js b/frontend/src/AppBuilder/WidgetManager/widgets/textarea.js
index c5e71be77d..d0e0364334 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/textarea.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/textarea.js
@@ -4,7 +4,7 @@ export const textareaConfig = {
description: 'Multi-line text input',
component: 'TextArea',
defaultSize: {
- width: 6,
+ width: 10,
height: 100,
},
others: {
@@ -12,82 +12,291 @@ export const textareaConfig = {
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
properties: {
- value: {
+ label: {
type: 'code',
- displayName: 'Default value',
- validation: {
- schema: { type: 'string' },
- defaultValue: 'default text',
- },
+ displayName: 'Label',
+ validation: { schema: { type: 'string' }, defaultValue: 'Label' },
},
placeholder: {
type: 'code',
displayName: 'Placeholder',
validation: {
schema: { type: 'string' },
- defaultValue: 'Placeholder text',
+ defaultValue: 'Enter your input',
},
},
- },
- events: {},
- styles: {
+ value: {
+ type: 'code',
+ displayName: 'Default value',
+ validation: {
+ schema: {
+ type: 'string',
+ },
+ defaultValue: 'Default value',
+ },
+ },
+ loadingState: {
+ type: 'toggle',
+ displayName: 'Loading state',
+ validation: { schema: { type: 'boolean' }, defaultValue: false },
+ section: 'additionalActions',
+ },
visibility: {
type: 'toggle',
displayName: 'Visibility',
- validation: {
- schema: { type: 'boolean' },
- defaultValue: true,
- },
+ validation: { schema: { type: 'boolean' }, defaultValue: true },
+ section: 'additionalActions',
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
- validation: {
- schema: { type: 'boolean' },
- defaultValue: false,
+ validation: { schema: { type: 'boolean' }, defaultValue: false },
+ section: 'additionalActions',
+ },
+ tooltip: {
+ type: 'code',
+ displayName: 'Tooltip',
+ validation: { schema: { type: 'string' }, defaultValue: 'Tooltip text' },
+ section: 'additionalActions',
+ placeholder: 'Enter tooltip text',
+ },
+ },
+ validation: {
+ mandatory: { type: 'toggle', displayName: 'Make this field mandatory' },
+ regex: { type: 'code', displayName: 'Regex', placeholder: '^[a-zA-Z0-9_ -]{3,16}$' },
+ minLength: { type: 'code', displayName: 'Min length', placeholder: 'Enter min length' },
+ maxLength: { type: 'code', displayName: 'Max length', placeholder: 'Enter max length' },
+ customRule: {
+ type: 'code',
+ displayName: 'Custom validation',
+ placeholder: `{{components.text2.text=='yes'&&'valid'}}`,
+ },
+ },
+ events: {
+ onChange: { displayName: 'On change' },
+ onEnterPressed: { displayName: 'On enter pressed' },
+ onFocus: { displayName: 'On focus' },
+ onBlur: { displayName: 'On blur' },
+ },
+ styles: {
+ color: {
+ type: 'color',
+ displayName: 'Text',
+ validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
+ accordian: 'label',
+ },
+ alignment: {
+ type: 'switch',
+ displayName: 'Alignment',
+ validation: { schema: { type: 'string' }, defaultValue: 'side' },
+ options: [
+ { displayName: 'Side', value: 'side' },
+ { displayName: 'Top', value: 'top' },
+ ],
+ accordian: 'label',
+ },
+ direction: {
+ type: 'switch',
+ displayName: '',
+ validation: { schema: { type: 'string' }, defaultValue: 'left' },
+ showLabel: false,
+ isIcon: true,
+ options: [
+ { displayName: 'alignleftinspector', value: 'left', iconName: 'alignleftinspector' },
+ { displayName: 'alignrightinspector', value: 'right', iconName: 'alignrightinspector' },
+ ],
+ accordian: 'label',
+ isFxNotRequired: true,
+ },
+ width: {
+ type: 'slider',
+ displayName: 'Width',
+ accordian: 'label',
+ conditionallyRender: {
+ key: 'alignment',
+ value: 'side',
},
+ isFxNotRequired: true,
+ },
+ auto: {
+ type: 'checkbox',
+ displayName: 'auto',
+ showLabel: false,
+ validation: { schema: { type: 'boolean' }, defaultValue: true },
+ accordian: 'label',
+ conditionallyRender: {
+ key: 'alignment',
+ value: 'side',
+ },
+ isFxNotRequired: true,
+ },
+
+ backgroundColor: {
+ type: 'color',
+ displayName: 'Background',
+ validation: { schema: { type: 'string' }, defaultValue: '#fff' },
+ accordian: 'field',
+ },
+ borderColor: {
+ type: 'color',
+ displayName: 'Border',
+ validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
+ accordian: 'field',
+ },
+ accentColor: {
+ type: 'color',
+ displayName: 'Accent',
+ validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
+ accordian: 'field',
+ },
+ textColor: {
+ type: 'color',
+ displayName: 'Text',
+ validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
+ accordian: 'field',
+ },
+ errTextColor: {
+ type: 'color',
+ displayName: 'Error text',
+ validation: { schema: { type: 'string' }, defaultValue: '#D72D39' },
+ accordian: 'field',
+ },
+ icon: {
+ type: 'icon',
+ displayName: 'Icon',
+ validation: { schema: { type: 'string' }, defaultValue: 'IconHome2' },
+ accordian: 'field',
+ visibility: false,
+ },
+ iconColor: {
+ type: 'color',
+ displayName: 'Icon color',
+ validation: { schema: { type: 'string' }, defaultValue: '#CFD3D859' },
+ accordian: 'field',
+ visibility: false,
+ showLabel: false,
},
borderRadius: {
- type: 'code',
+ type: 'numberInput',
displayName: 'Border radius',
+ validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 6 },
+ accordian: 'field',
+ },
+ boxShadow: {
+ type: 'boxShadow',
+ displayName: 'Box Shadow',
validation: {
- schema: { type: 'number' },
- defaultValue: 4,
+ schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
+ defaultValue: '0px 0px 0px 0px #00000040',
},
+ accordian: 'field',
+ },
+ padding: {
+ type: 'switch',
+ displayName: 'Padding',
+ validation: {
+ schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
+ defaultValue: 'default',
+ },
+ isFxNotRequired: true,
+ options: [
+ { displayName: 'Default', value: 'default' },
+ { displayName: 'None', value: 'none' },
+ ],
+ accordian: 'container',
},
},
exposedVariables: {
- value:
- 'ToolJet is an open-source low-code platform for building and deploying internal tools with minimal engineering efforts 🚀',
+ value: '',
+ isMandatory: false,
+ isVisible: true,
+ isDisabled: false,
+ isLoading: false,
},
actions: [
{
handle: 'setText',
- displayName: 'Set Text',
- params: [{ handle: 'text', displayName: 'text', defaultValue: 'New Text' }],
+ displayName: 'Set text',
+ params: [{ handle: 'text', displayName: 'text', defaultValue: 'New text' }],
},
{
handle: 'clear',
displayName: 'Clear',
},
+ {
+ handle: 'setFocus',
+ displayName: 'Set focus',
+ },
+ {
+ handle: 'setBlur',
+ displayName: 'Set blur',
+ },
+ {
+ handle: 'disable',
+ displayName: 'Disable(deprecated)',
+ params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'visibility',
+ displayName: 'Visibility(deprecated)',
+ params: [{ handle: 'visibility', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setVisibility',
+ displayName: 'Set visibility',
+ params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setDisable',
+ displayName: 'Set disable',
+ params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
+ {
+ handle: 'setLoading',
+ displayName: 'Set loading',
+ params: [{ handle: 'loading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
+ },
],
definition: {
+ validation: {
+ mandatory: { value: '{{false}}' },
+ regex: { value: '' },
+ minLength: { value: '' },
+ maxLength: { value: '' },
+ customRule: { value: '' },
+ },
+
others: {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
properties: {
- value: {
- value:
- 'ToolJet is an open-source low-code platform for building and deploying internal tools with minimal engineering efforts 🚀',
- },
- placeholder: { value: 'Placeholder text' },
+ value: { value: '' },
+ label: { value: 'Label' },
+ placeholder: { value: 'Enter your input' },
+ visibility: { value: '{{true}}' },
+ disabledState: { value: '{{false}}' },
+ loadingState: { value: '{{false}}' },
+ tooltip: { value: '' },
},
events: [],
styles: {
- visibility: { value: '{{true}}' },
- disabledState: { value: '{{false}}' },
- borderRadius: { value: '{{4}}' },
+ textColor: { value: '#1B1F24' },
+ borderColor: { value: '#CCD1D5' },
+ accentColor: { value: '#4368E3' },
+ errTextColor: { value: '#D72D39' },
+ borderRadius: { value: '{{6}}' },
+ backgroundColor: { value: '#fff' },
+ iconColor: { value: '#CFD3D859' },
+ direction: { value: 'left' },
+ width: { value: '{{33}}' },
+ alignment: { value: 'side' },
+ color: { value: '#1B1F24' },
+ auto: { value: '{{true}}' },
+ padding: { value: 'default' },
+ boxShadow: { value: '0px 0px 0px 0px #00000040' },
+ icon: { value: 'IconHome2' },
+ iconVisibility: { value: false },
},
},
};
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/textinput.js b/frontend/src/AppBuilder/WidgetManager/widgets/textinput.js
index 15a52ea306..e259ecddd0 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/textinput.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/textinput.js
@@ -80,7 +80,7 @@ export const textinputConfig = {
},
styles: {
color: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
@@ -132,31 +132,31 @@ export const textinputConfig = {
},
backgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
accordian: 'field',
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
accordian: 'field',
},
accentColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Accent',
- validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
+ validation: { schema: { type: 'string' }, defaultValue: 'var(--primary-brand)' },
accordian: 'field',
},
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'field',
},
errTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Error text',
validation: { schema: { type: 'string' }, defaultValue: '#D72D39' },
accordian: 'field',
@@ -169,7 +169,7 @@ export const textinputConfig = {
visibility: false,
},
iconColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Icon color',
validation: { schema: { type: 'string' }, defaultValue: '#CFD3D859' },
accordian: 'field',
@@ -283,7 +283,7 @@ export const textinputConfig = {
styles: {
textColor: { value: '#1B1F24' },
borderColor: { value: '#CCD1D5' },
- accentColor: { value: '#4368E3' },
+ accentColor: { value: 'var(--primary-brand)' },
errTextColor: { value: '#D72D39' },
borderRadius: { value: '{{6}}' },
backgroundColor: { value: '#fff' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/timeline.js b/frontend/src/AppBuilder/WidgetManager/widgets/timeline.js
index 6ece608f5c..63776f0b99 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/timeline.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/timeline.js
@@ -48,7 +48,7 @@ export const timelineConfig = {
properties: {
data: {
value:
- "{{ [ \n\t\t{ title: 'Product Launched', subTitle: 'First version of our product released to public', date: '20/10/2021', iconBackgroundColor: '#4d72fa'},\n\t\t { title: 'First Signup', subTitle: 'Congratulations! We got our first signup', date: '22/10/2021', iconBackgroundColor: '#4d72fa'}, \n\t\t { title: 'First Payment', subTitle: 'Hurray! We got our first payment', date: '01/11/2021', iconBackgroundColor: '#4d72fa'} \n] }}",
+ "{{ [ \n\t\t{ title: 'Product Launched', subTitle: 'First version of our product released to public', date: '20/10/2021', iconBackgroundColor: 'var(--primary-brand)'},\n\t\t { title: 'First Signup', subTitle: 'Congratulations! We got our first signup', date: '22/10/2021', iconBackgroundColor: 'var(--primary-brand)'}, \n\t\t { title: 'First Payment', subTitle: 'Hurray! We got our first payment', date: '01/11/2021', iconBackgroundColor: 'var(--primary-brand)'} \n] }}",
},
hideDate: { value: '{{false}}' },
},
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/timepicker.js b/frontend/src/AppBuilder/WidgetManager/widgets/timepicker.js
index 79a3d05d4e..a5a1fb6b06 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/timepicker.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/timepicker.js
@@ -164,7 +164,7 @@ export const timePickerConfig = {
],
styles: {
labelColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Color',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'label',
@@ -215,31 +215,31 @@ export const timePickerConfig = {
isFxNotRequired: true,
},
fieldBackgroundColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Background',
validation: { schema: { type: 'string' }, defaultValue: '#fff' },
accordian: 'field',
},
fieldBorderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border',
validation: { schema: { type: 'string' }, defaultValue: '#CCD1D5' },
accordian: 'field',
},
accentColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Accent',
validation: { schema: { type: 'string' }, defaultValue: '#4368E3' },
accordian: 'field',
},
selectedTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text',
validation: { schema: { type: 'string' }, defaultValue: '#1B1F24' },
accordian: 'field',
},
errTextColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Error text',
validation: { schema: { type: 'string' }, defaultValue: '#E54D2E' },
accordian: 'field',
@@ -252,7 +252,7 @@ export const timePickerConfig = {
visibility: false,
},
iconColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: '',
showLabel: false,
validation: {
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/toggleswitch.js b/frontend/src/AppBuilder/WidgetManager/widgets/toggleswitch.js
index ec3a97450d..bd646a47f3 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/toggleswitch.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/toggleswitch.js
@@ -32,14 +32,14 @@ export const toggleswitchConfig = {
},
styles: {
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text color',
validation: {
schema: { type: 'string' },
},
},
toggleSwitchColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Toggle switch color',
validation: {
schema: { type: 'string' },
@@ -75,7 +75,7 @@ export const toggleswitchConfig = {
events: [],
styles: {
textColor: { value: '' },
- toggleSwitchColor: { value: '' },
+ toggleSwitchColor: { value: 'var(--primary-brand)' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/toggleswitchv2.js b/frontend/src/AppBuilder/WidgetManager/widgets/toggleswitchv2.js
index 6753fbb50d..4bc21f3a86 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/toggleswitchv2.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/toggleswitchv2.js
@@ -71,7 +71,7 @@ export const toggleSwitchV2Config = {
},
styles: {
textColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Text Color',
validation: {
schema: { type: 'string' },
@@ -79,7 +79,7 @@ export const toggleSwitchV2Config = {
accordian: 'label',
},
borderColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Border color',
validation: {
schema: { type: 'string' },
@@ -87,7 +87,7 @@ export const toggleSwitchV2Config = {
accordian: 'switch',
},
toggleSwitchColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Checked color',
validation: {
schema: { type: 'string' },
@@ -95,7 +95,7 @@ export const toggleSwitchV2Config = {
accordian: 'switch',
},
uncheckedColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Unchecked color',
validation: {
schema: { type: 'string' },
@@ -103,7 +103,7 @@ export const toggleSwitchV2Config = {
accordian: 'switch',
},
handleColor: {
- type: 'color',
+ type: 'colorSwatches',
displayName: 'Handle color',
validation: {
schema: { type: 'string' },
@@ -181,7 +181,7 @@ export const toggleSwitchV2Config = {
events: [],
styles: {
textColor: { value: '#1B1F24' },
- toggleSwitchColor: { value: '#4368E3' }, //keeping same key for backward comopatibility
+ toggleSwitchColor: { value: 'var(--primary-brand)' }, //keeping same key for backward comopatibility
uncheckedColor: { value: '#E4E7EB' },
borderColor: { value: '#E4E7EB' },
handleColor: { value: '#FFFFFF' },
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/treeSelect.js b/frontend/src/AppBuilder/WidgetManager/widgets/treeSelect.js
index 1d7485b47a..898f0b3ed0 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/treeSelect.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/treeSelect.js
@@ -23,8 +23,8 @@ export const treeSelectConfig = {
onUnCheck: { displayName: 'On uncheck' },
},
styles: {
- textColor: { type: 'color', displayName: 'Text Color' },
- checkboxColor: { type: 'color', displayName: 'Checkbox color' },
+ textColor: { type: 'colorSwatches', displayName: 'Text Color' },
+ checkboxColor: { type: 'colorSwatches', displayName: 'Checkbox color' },
visibility: { type: 'toggle', displayName: 'Visibility' },
disabledState: { type: 'toggle', displayName: 'Disable' },
},
@@ -71,7 +71,7 @@ export const treeSelectConfig = {
events: [],
styles: {
textColor: { value: '' },
- checkboxColor: { value: '' },
+ checkboxColor: { value: 'var(--primary-brand)' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/verticalDivider.js b/frontend/src/AppBuilder/WidgetManager/widgets/verticalDivider.js
index 443526c3b8..df426c7b5f 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/verticalDivider.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/verticalDivider.js
@@ -1,27 +1,17 @@
export const verticalDividerConfig = {
name: 'VerticalDivider',
- displayName: 'Vertical Divider',
+ displayName: 'Vertical divider',
description: 'Vertical line separator',
component: 'VerticalDivider',
defaultSize: {
- width: 2,
+ width: 1,
height: 100,
},
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
- properties: {},
- events: {},
- styles: {
- dividerColor: {
- type: 'color',
- displayName: 'Divider color',
- validation: {
- schema: { type: 'string' },
- defaultValue: '#000000',
- },
- },
+ properties: {
visibility: {
type: 'toggle',
displayName: 'Visibility',
@@ -29,6 +19,61 @@ export const verticalDividerConfig = {
schema: { type: 'boolean' },
defaultValue: true,
},
+ section: 'additionalActions',
+ },
+ tooltip: {
+ type: 'code',
+ displayName: 'Tooltip',
+ validation: { schema: { type: 'string' }, defaultValue: 'Tooltip text' },
+ section: 'additionalActions',
+ placeholder: 'Enter tooltip text',
+ },
+ },
+ events: {},
+ styles: {
+ dividerColor: {
+ type: 'colorSwatches',
+ displayName: 'Divider color',
+ validation: {
+ schema: { type: 'string' },
+ defaultValue: '#000000',
+ },
+ accordian: 'Divider',
+ },
+ dividerStyle: {
+ type: 'switch',
+ displayName: 'Style',
+ validation: {
+ schema: { type: 'string' },
+ },
+ options: [
+ { displayName: 'Solid', value: 'solid' },
+ { displayName: 'Dashed', value: 'dashed' },
+ ],
+ accordian: 'Divider',
+ },
+ boxShadow: {
+ type: 'boxShadow',
+ displayName: 'Box Shadow',
+ validation: {
+ schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
+ defaultValue: '0px 0px 0px 0px #00000040',
+ },
+ accordian: 'Divider',
+ },
+ padding: {
+ type: 'switch',
+ displayName: 'Padding',
+ validation: {
+ schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
+ defaultValue: 'default',
+ },
+ isFxNotRequired: true,
+ options: [
+ { displayName: 'Default', value: 'default' },
+ { displayName: 'None', value: 'none' },
+ ],
+ accordian: 'container',
},
},
exposedVariables: {
@@ -39,11 +84,16 @@ export const verticalDividerConfig = {
showOnDesktop: { value: '{{true}}' },
showOnMobile: { value: '{{false}}' },
},
- properties: {},
+ properties: {
+ visibility: { value: '{{true}}' },
+ tooltip: { value: '' },
+ },
events: [],
styles: {
- visibility: { value: '{{true}}' },
- dividerColor: { value: '#000000' },
+ dividerColor: { value: '#CCD1D5' },
+ dividerStyle: { value: 'solid' },
+ padding: { value: 'default' },
+ boxShadow: { value: '0px 0px 0px 0px #00000040' },
},
},
};
diff --git a/frontend/src/AppBuilder/Widgets/BaseComponents/BaseInput.jsx b/frontend/src/AppBuilder/Widgets/BaseComponents/BaseInput.jsx
new file mode 100644
index 0000000000..74ce215737
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/BaseComponents/BaseInput.jsx
@@ -0,0 +1,240 @@
+import React, { forwardRef } from 'react';
+import Label from '@/_ui/Label';
+import Loader from '@/ToolJetUI/Loader/Loader';
+import * as Icons from '@tabler/icons-react';
+const tinycolor = require('tinycolor2');
+
+const RenderInput = forwardRef((props, ref) => {
+ return props.inputType !== 'textarea' ? : ;
+});
+
+export const BaseInput = ({
+ height,
+ styles,
+ properties,
+ darkMode,
+ componentName,
+ dataCy,
+ // From useInput hook
+ inputRef,
+ labelRef,
+ visibility,
+ loading,
+ labelWidth,
+ validationError,
+ showValidationError,
+ isFocused,
+ isMandatory,
+ disable,
+ value,
+ handleChange,
+ handleBlur,
+ handleFocus,
+ handleKeyUp,
+ isValid,
+ // Input specific props
+ inputType = 'text',
+ additionalInputProps = {},
+ rightIcon,
+ getCustomStyles,
+}) => {
+ const {
+ padding,
+ borderRadius,
+ borderColor,
+ backgroundColor,
+ textColor,
+ boxShadow,
+ width,
+ alignment,
+ direction,
+ color,
+ auto,
+ errTextColor,
+ iconColor,
+ accentColor,
+ iconVisibility: showLeftIcon,
+ icon,
+ } = styles;
+
+ const { label, placeholder } = properties;
+ const _width = (width / 100) * 70;
+ const defaultAlignment = alignment === 'side' || alignment === 'top' ? alignment : 'side';
+
+ const computedStyles = {
+ height: height == 36 ? (padding == 'default' ? '36px' : '40px') : padding == 'default' ? height : height + 4,
+ borderRadius: `${borderRadius}px`,
+ color: !['#1B1F24', '#000', '#000000ff'].includes(textColor)
+ ? textColor
+ : disable || loading
+ ? 'var(--text-disabled)'
+ : 'var(--text-primary)',
+ borderColor: isFocused
+ ? accentColor != '4368E3'
+ ? accentColor
+ : 'var(--primary-accent-strong)'
+ : borderColor != '#CCD1D5'
+ ? borderColor
+ : disable || loading
+ ? '1px solid var(--borders-disabled-on-white)'
+ : 'var(--borders-default)',
+ '--tblr-input-border-color-darker': tinycolor(borderColor).darken(24).toString(),
+ backgroundColor:
+ backgroundColor != '#fff'
+ ? backgroundColor
+ : disable || loading
+ ? darkMode
+ ? 'var(--surfaces-app-bg-default)'
+ : 'var(--surfaces-surface-03)'
+ : 'var(--surfaces-surface-01)',
+ boxShadow,
+ padding: showLeftIcon ? '8px 10px 8px 29px' : '8px 10px',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ };
+
+ let loaderStyle;
+ // for textarea loader position is fixed on top right of input box.
+ if (inputType !== 'textarea') {
+ loaderStyle = {
+ right:
+ direction === 'right' &&
+ defaultAlignment === 'side' &&
+ ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0))
+ ? `${labelWidth + 11}px`
+ : '11px',
+ top:
+ defaultAlignment === 'top'
+ ? ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)) &&
+ 'calc(50% + 10px)'
+ : '',
+ transform:
+ defaultAlignment === 'top' &&
+ ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)) &&
+ ' translateY(-50%)',
+ zIndex: 3,
+ };
+ } else {
+ loaderStyle = {
+ right:
+ direction === 'right' &&
+ defaultAlignment === 'side' &&
+ ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0))
+ ? `${labelWidth + 11}px`
+ : '11px',
+ top: defaultAlignment === 'top' ? '30px' : '10px',
+ transform: 'none',
+ zIndex: 3,
+ };
+ }
+
+ // eslint-disable-next-line import/namespace
+ const IconElement = Icons[icon] ?? Icons['IconHome2'];
+
+ const finalStyles = getCustomStyles ? getCustomStyles(computedStyles) : computedStyles;
+
+ return (
+ <>
+
+
+
+ {showLeftIcon && (
+ 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)
+ ? `${labelWidth + 11}px`
+ : '11px',
+ position: 'absolute',
+ top:
+ inputType === 'textarea'
+ ? defaultAlignment === 'top'
+ ? '38px'
+ : '18px'
+ : defaultAlignment === 'side'
+ ? '50%'
+ : (label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)
+ ? 'calc(50% + 10px)'
+ : '50%',
+ transform: 'translateY(-50%)',
+ color: iconColor !== '#CFD3D859' ? iconColor : 'var(--icons-weak-disabled)',
+ zIndex: 3,
+ }}
+ stroke={1.5}
+ />
+ )}
+
+
+ {rightIcon}
+ {loading && }
+
+
+ {showValidationError && visibility && (
+
+ {validationError}
+
+ )}
+ >
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/BaseComponents/hooks/useInput.js b/frontend/src/AppBuilder/Widgets/BaseComponents/hooks/useInput.js
new file mode 100644
index 0000000000..57260875a2
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/BaseComponents/hooks/useInput.js
@@ -0,0 +1,235 @@
+import { useState, useRef, useEffect } from 'react';
+import { useGridStore } from '@/_stores/gridStore';
+//eslint-disable-next-line import/no-unresolved
+import { getCountryCallingCode } from 'react-phone-number-input';
+
+export const useInput = ({
+ id,
+ properties,
+ styles,
+ validation,
+ validate,
+ setExposedVariable,
+ setExposedVariables,
+ fireEvent,
+ inputType,
+}) => {
+ const isInitialRender = useRef(true);
+ const inputRef = useRef();
+ const labelRef = useRef();
+
+ const { loadingState, disabledState, label, visibility: initialVisibility } = properties;
+ const isResizing = useGridStore((state) => state.resizingComponentId === id);
+
+ const [value, setValue] = useState(properties.value ?? '');
+ const [visibility, setVisibility] = useState(initialVisibility);
+ const [loading, setLoading] = useState(loadingState);
+ const [disable, setDisable] = useState(disabledState || loadingState);
+ const [validationStatus, setValidationStatus] = useState(validate(value));
+ const [showValidationError, setShowValidationError] = useState(false);
+ const [isFocused, setIsFocused] = useState(false);
+ const [labelWidth, setLabelWidth] = useState(0);
+ const [iconVisibility, setIconVisibility] = useState(false);
+ const [country, setCountry] = useState(properties.defaultCountry || 'US');
+
+ const { isValid, validationError } = validationStatus;
+ const isMandatory = validation?.mandatory ?? false;
+
+ const getCountryCallingCodeSafe = (country) => {
+ try {
+ return getCountryCallingCode(country);
+ } catch (error) {
+ return '';
+ }
+ };
+
+ useEffect(() => {
+ if (labelRef?.current) {
+ const absolutewidth = labelRef?.current?.getBoundingClientRect()?.width;
+ setLabelWidth(absolutewidth);
+ } else setLabelWidth(0);
+ }, [
+ isResizing,
+ styles.width,
+ styles.auto,
+ styles.alignment,
+ styles.iconVisibility,
+ label?.length,
+ isMandatory,
+ styles.padding,
+ styles.direction,
+ labelRef?.current?.getBoundingClientRect()?.width,
+ ]);
+
+ useEffect(() => {
+ if (isInitialRender.current) return;
+ setExposedVariable('label', label);
+ }, [label]);
+
+ useEffect(() => {
+ disable !== disabledState && setDisable(disabledState);
+ if (isInitialRender.current) return;
+ setExposedVariable('isDisabled', disabledState);
+ }, [disabledState]);
+
+ useEffect(() => {
+ visibility !== properties.visibility && setVisibility(properties.visibility);
+ if (isInitialRender.current) return;
+ setExposedVariable('isVisible', properties.visibility);
+ }, [properties.visibility]);
+
+ useEffect(() => {
+ loading !== loadingState && setLoading(loadingState);
+ if (isInitialRender.current) return;
+ setExposedVariable('isLoading', loadingState);
+ }, [loadingState]);
+
+ useEffect(() => {
+ if (isInitialRender.current) return;
+ setExposedVariable('isMandatory', isMandatory);
+ }, [isMandatory]);
+
+ useEffect(() => {
+ if (isInitialRender.current) return;
+ const validationStatus = validate(value);
+ setValidationStatus(validationStatus);
+ setExposedVariable('isValid', validationStatus?.isValid);
+ }, [validate]);
+
+ useEffect(() => {
+ if (inputType === 'phone') {
+ let code = getCountryCallingCodeSafe(country);
+ setInputValue(`+${code}${properties.value}`);
+ } else {
+ setInputValue(properties.value ?? '');
+ }
+ }, [properties.value]);
+
+ useEffect(() => {
+ if (inputType !== 'phone') return;
+ setExposedVariable('setValue', async function (value, countryCode = country) {
+ const code = getCountryCallingCodeSafe(country);
+ setInputValue(`+${code}${value}`);
+ setCountry(countryCode);
+ fireEvent('onChange');
+ });
+ }, [inputType, country]);
+
+ useEffect(() => {
+ if (inputType !== 'currency') return;
+ setExposedVariable('setValue', async function (value, countryCode = country) {
+ setInputValue(value);
+ setCountry(countryCode);
+ fireEvent('onChange');
+ });
+ }, [inputType, country]);
+
+ useEffect(() => {
+ const exposedVariables = {
+ ...(inputType !== 'phone' && {
+ setText: async function (text) {
+ setInputValue(text);
+ fireEvent('onChange');
+ },
+ }),
+ clear: async function () {
+ setInputValue('');
+ fireEvent('onChange');
+ },
+ setFocus: async function () {
+ inputRef.current.focus();
+ },
+ setBlur: async function () {
+ inputRef.current.blur();
+ },
+ setVisibility: async function (state) {
+ setVisibility(state);
+ setExposedVariable('isVisible', state);
+ },
+ setDisable: async function (disable) {
+ setDisable(disable);
+ setExposedVariable('isDisabled', disable);
+ },
+ setLoading: async function (loading) {
+ setLoading(loading);
+ setExposedVariable('isLoading', loading);
+ },
+ label,
+ isValid,
+ value: properties.value ?? '',
+ isMandatory,
+ isLoading: loading,
+ isVisible: visibility,
+ isDisabled: disable,
+ };
+
+ setExposedVariables(exposedVariables);
+ isInitialRender.current = false;
+ }, []);
+
+ const setInputValue = (value) => {
+ setValue(value);
+ setExposedVariable('value', value);
+ const validationStatus = validate(value);
+ setValidationStatus(validationStatus);
+ setExposedVariable('isValid', validationStatus?.isValid);
+ };
+
+ const handleChange = (e) => {
+ setInputValue(e.target.value);
+ fireEvent('onChange');
+ };
+
+ const handlePhoneCurrencyInputChange = (value) => {
+ setInputValue(value);
+ fireEvent('onChange');
+ };
+
+ const handleBlur = (e) => {
+ setShowValidationError(true);
+ setIsFocused(false);
+ e.stopPropagation();
+ fireEvent('onBlur');
+ };
+
+ const handleFocus = (e) => {
+ setIsFocused(true);
+ e.stopPropagation();
+ setTimeout(() => {
+ fireEvent('onFocus');
+ }, 0);
+ };
+
+ const handleKeyUp = (e) => {
+ if (e.key === 'Enter') {
+ setInputValue(e.target.value);
+ fireEvent('onEnterPressed');
+ }
+ };
+
+ return {
+ inputRef,
+ labelRef,
+ value,
+ visibility,
+ loading,
+ disable,
+ country,
+ setCountry,
+ validationStatus,
+ showValidationError,
+ isFocused,
+ labelWidth,
+ iconVisibility,
+ setIconVisibility,
+ isValid,
+ validationError,
+ isMandatory,
+ setInputValue,
+ handlePhoneCurrencyInputChange,
+ handleChange,
+ handleBlur,
+ handleFocus,
+ handleKeyUp,
+ };
+};
diff --git a/frontend/src/AppBuilder/Widgets/EmailInput.jsx b/frontend/src/AppBuilder/Widgets/EmailInput.jsx
new file mode 100644
index 0000000000..90061b0a66
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/EmailInput.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { BaseInput } from './BaseComponents/BaseInput';
+import { useInput } from './BaseComponents/hooks/useInput';
+
+export const EmailInput = (props) => {
+ const inputLogic = useInput(props);
+ const additionalInputProps = {
+ autocomplete: 'email',
+ name: 'email',
+ };
+ return ;
+};
diff --git a/frontend/src/AppBuilder/Widgets/Form/FormUtils.js b/frontend/src/AppBuilder/Widgets/Form/FormUtils.js
index f8366e0b29..2dea50bdf9 100644
--- a/frontend/src/AppBuilder/Widgets/Form/FormUtils.js
+++ b/frontend/src/AppBuilder/Widgets/Form/FormUtils.js
@@ -97,6 +97,9 @@ export function generateUIComponents(JSONSchema, advanced, componentName = '') {
if (uiComponentsDraft?.length > 0 && uiComponentsDraft[index * 2 + 1]) {
switch (typeResolver(value?.type)) {
case 'TextInput':
+ case 'EmailInput':
+ case 'PhoneInput':
+ case 'CurrencyInput':
if (value?.styles?.backgroundColor)
uiComponentsDraft[index * 2 + 1]['definition']['styles']['backgroundColor'] =
value?.styles?.backgroundColor;
@@ -127,6 +130,15 @@ export function generateUIComponents(JSONSchema, advanced, componentName = '') {
if (value?.value) uiComponentsDraft[index * 2 + 1]['definition']['properties']['value'] = value?.value;
if (value?.placeholder)
uiComponentsDraft[index * 2 + 1]['definition']['properties']['placeholder'] = value?.placeholder;
+
+ if (value?.defaultCountry && ['PhoneInput', 'CurrencyInput'].includes(typeResolver(value?.type))) {
+ uiComponentsDraft[index * 2 + 1]['definition']['properties']['defaultCountry'] = value?.defaultCountry;
+ }
+
+ if (value?.defaultCountry && typeResolver(value?.type) === 'CurrencyInput') {
+ uiComponentsDraft[index * 2 + 1]['definition']['properties']['decimalPlaces'] = value?.decimalPlaces;
+ }
+
// prevent label from showing up in text input, because it is already shown in the text component. (Defaults to "Label" if not updated explicitly with an empty string)
uiComponentsDraft[index * 2 + 1]['definition']['properties']['label'] = '';
break;
@@ -482,6 +494,12 @@ const typeResolver = (type) => {
return 'DropDown';
case 'button':
return 'Button';
+ case 'emailinput':
+ return 'EmailInput';
+ case 'phoneinput':
+ return 'PhoneInput';
+ case 'currencyinput':
+ return 'CurrencyInput';
case 'text':
return 'Text';
case 'number':
diff --git a/frontend/src/AppBuilder/Widgets/Form/RenderSchema.jsx b/frontend/src/AppBuilder/Widgets/Form/RenderSchema.jsx
index b5bfa9e4c3..89d34b65c7 100644
--- a/frontend/src/AppBuilder/Widgets/Form/RenderSchema.jsx
+++ b/frontend/src/AppBuilder/Widgets/Form/RenderSchema.jsx
@@ -26,6 +26,7 @@ const RenderSchema = ({ component, parent, id, onOptionChange, onOptionsChange,
return validateWidget({
...{ widgetValue: value },
...{ validationObject: component.definition.validation },
+ componentType: component?.component,
});
},
[component.definition.validation]
diff --git a/frontend/src/AppBuilder/Widgets/Modal.jsx b/frontend/src/AppBuilder/Widgets/Modal.jsx
index e0f099205f..c410cf4840 100644
--- a/frontend/src/AppBuilder/Widgets/Modal.jsx
+++ b/frontend/src/AppBuilder/Widgets/Modal.jsx
@@ -68,6 +68,54 @@ export const Modal = function Modal({
}, [showModal, id, mode]);
/**** End - Logic to reset the zIndex of modal control box ****/
+ // Side effects for modal, which include dom manipulation to hide overflow when opening
+ // And cleaning up dom when modal is closed
+
+ const onShowSideEffects = () => {
+ const canvasElement = document.querySelector('.page-container.canvas-container');
+ const realCanvasEl = document.getElementsByClassName('real-canvas')[0];
+ const allModalContainers = realCanvasEl.querySelectorAll('.modal');
+ const modalContainer = allModalContainers[allModalContainers.length - 1];
+
+ if (canvasElement && realCanvasEl && modalContainer) {
+ const currentScroll = canvasElement.scrollTop;
+ canvasElement.style.overflowY = 'hidden';
+
+ modalContainer.style.height = `${canvasElement.offsetHeight}px`;
+ modalContainer.style.top = `${currentScroll}px`;
+ fireEvent('onOpen');
+ }
+ };
+
+ const onHideSideEffects = () => {
+ const canvasElement = document.querySelector('.page-container.canvas-container');
+ const realCanvasEl = document.getElementsByClassName('real-canvas')[0];
+ const allModalContainers = realCanvasEl.querySelectorAll('.modal');
+ const modalContainer = allModalContainers[allModalContainers.length - 1];
+ const hasManyModalsOpen = allModalContainers.length > 1;
+
+ if (canvasElement && realCanvasEl && modalContainer) {
+ modalContainer.style.height = ``;
+ modalContainer.style.top = ``;
+ fireEvent('onClose');
+ }
+ if (canvasElement && !hasManyModalsOpen) {
+ canvasElement.style.overflow = 'auto';
+ }
+ };
+
+ // useEventListener('resize', onShowSideEffects, window);
+
+ const onShowModal = () => {
+ openModal();
+ onShowSideEffects();
+ };
+
+ const onHideModal = () => {
+ onHideSideEffects();
+ hideModal();
+ };
+
useEffect(() => {
const exposedVariables = {
open: async function () {
@@ -93,58 +141,48 @@ export const Modal = function Modal({
setShowModal(true);
}
+ // Add debounced version of handleModalOpen
+ const debouncedModalOpen = debounce(() => {
+ onShowSideEffects();
+ }, 10);
+
useEffect(() => {
- const handleModalOpen = () => {
- openModal();
- const canvasElement = document.getElementsByClassName('canvas-container')[0];
- const modalBackdropEl = document.getElementsByClassName('modal-backdrop')[0];
- const realCanvasEl = document.getElementsByClassName('real-canvas')[0];
- const modalCanvasEl = document.getElementById(`canvas-${id}`);
- if (canvasElement && modalBackdropEl && modalCanvasEl && realCanvasEl) {
- realCanvasEl.style.height = '100vh';
- realCanvasEl.style.position = 'absolute';
- realCanvasEl.style.overflow = 'hidden';
+ // Select the DOM element
+ const canvasElement = document.querySelector('.page-container.canvas-container');
- modalBackdropEl.style.height = '100vh';
- modalBackdropEl.style.minHeight = '100vh';
- modalBackdropEl.style.minHeight = '100vh';
- modalCanvasEl.style.height = modalHeight;
- }
+ if (!canvasElement) return; // Ensure the element exists
+
+ // Create a ResizeObserver
+ const resizeObserver = new ResizeObserver(() => {
+ debouncedModalOpen();
+ });
+
+ // Observe the canvas element
+ resizeObserver.observe(canvasElement);
+
+ return () => {
+ // Cleanup observer on component unmount
+ resizeObserver.disconnect();
};
+ }, []);
- // Add debounced version of handleModalOpen
- const debouncedModalOpen = debounce(() => {
- handleModalOpen();
- }, 10);
-
- const handleModalClose = () => {
- const canvasElement = document.getElementsByClassName('canvas-container')[0];
- const realCanvasEl = document.getElementsByClassName('real-canvas')[0];
- const canvasHeight = realCanvasEl?.getAttribute('canvas-height');
-
- if (canvasElement && realCanvasEl && canvasHeight) {
- realCanvasEl.style.height = canvasHeight;
- realCanvasEl.style.position = '';
-
- realCanvasEl.style.overflow = 'auto';
- }
- };
+ useEffect(() => {
if (showModal) {
debouncedModalOpen();
} else {
- // if (document.getElementsByClassName('modal-content')[0] == undefined) {
- handleModalClose();
- // }
+ if (document.getElementsByClassName('modal-content')[0] == undefined) {
+ onHideModal();
+ }
}
// Cleanup the effect
return () => {
if (document.getElementsByClassName('modal-content')[0] == undefined) {
- handleModalClose();
+ onHideModal();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [showModal, modalHeight]);
+ }, [modalHeight, size]);
useEffect(() => {
if (isInitialRender.current) {
@@ -180,6 +218,7 @@ export const Modal = function Modal({
display: visibility ? '' : 'none',
'--tblr-btn-color-darker': tinycolor(triggerButtonBackgroundColor).darken(8).toString(),
boxShadow,
+ borderColor: 'var(--primary-brand)',
},
};
@@ -244,7 +283,9 @@ export const Modal = function Modal({
keyboard={true}
enforceFocus={false}
animation={false}
- onEscapeKeyDown={() => hideOnEsc && hideModal()}
+ onShow={() => onShowModal()}
+ onHide={() => onHideModal()}
+ onEscapeKeyDown={() => hideOnEsc && onHideModal()}
id="modal-container"
component-id={id}
backdrop={'static'}
@@ -257,7 +298,7 @@ export const Modal = function Modal({
titleAlignment,
hideTitleBar,
hideCloseButton,
- hideModal,
+ hideModal: onHideModal,
component,
showConfigHandler: mode === 'edit',
}}
diff --git a/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx
index e25027ce33..8ff4c0cbb1 100644
--- a/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx
+++ b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Footer.jsx
@@ -19,6 +19,7 @@ export const ModalFooter = React.memo(({ id, isDisabled, customStyles, darkMode,
overflowX: 'hidden',
overflowY: isDisabled ? 'hidden' : 'auto',
}}
+ componentType="ModalV2"
/>
{isDisabled && (
{isDisabled && (
diff --git a/frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx
index 25796a9951..2615ca3d5c 100644
--- a/frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx
+++ b/frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx
@@ -105,9 +105,10 @@ export const ModalWidget = ({ ...restProps }) => {
>
) : (
diff --git a/frontend/src/AppBuilder/Widgets/ModalV2/helpers/stylesFactory.js b/frontend/src/AppBuilder/Widgets/ModalV2/helpers/stylesFactory.js
index ee536dcfe1..9c6d4eb0c8 100644
--- a/frontend/src/AppBuilder/Widgets/ModalV2/helpers/stylesFactory.js
+++ b/frontend/src/AppBuilder/Widgets/ModalV2/helpers/stylesFactory.js
@@ -1,4 +1,5 @@
-var tinycolor = require('tinycolor2');
+const tinycolor = require('tinycolor2');
+import { MODAL_CANVAS_PADDING } from '@/AppBuilder/AppCanvas/appCanvasConstants';
export function createModalStyles({
height,
@@ -17,25 +18,27 @@ export function createModalStyles({
boxShadow,
}) {
const backwardCompatibilityCheck = height == '34' || modalHeight != undefined ? true : false;
-
return {
modalBody: {
height: backwardCompatibilityCheck ? computedCanvasHeight : height,
backgroundColor:
['#fff', '#ffffffff'].includes(bodyBackgroundColor) && darkMode ? '#1F2837' : bodyBackgroundColor,
overflowY: isDisabledModal ? 'hidden' : 'auto',
+ padding: `${MODAL_CANVAS_PADDING}px`,
},
modalHeader: {
backgroundColor:
['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor,
height: headerHeightPx,
overflowY: isDisabledModal ? 'hidden' : 'auto',
+ padding: `${4.5}px ${MODAL_CANVAS_PADDING}px`,
},
modalFooter: {
backgroundColor:
['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor,
height: footerHeightPx,
overflowY: isDisabledModal ? 'hidden' : 'auto',
+ padding: `${4.5}px ${MODAL_CANVAS_PADDING}px`,
},
buttonStyles: {
backgroundColor: triggerButtonBackgroundColor,
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/Table.jsx b/frontend/src/AppBuilder/Widgets/NewTable/Table.jsx
new file mode 100644
index 0000000000..c5f1328aee
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/Table.jsx
@@ -0,0 +1,145 @@
+import React, { useEffect, useMemo, memo, useRef } from 'react';
+// eslint-disable-next-line import/no-unresolved
+import { diff } from 'deep-object-diff';
+import { shallow } from 'zustand/shallow';
+import { isEmpty, isEqual } from 'lodash';
+import { useEvents } from '@/AppBuilder/_stores/slices/eventsSlice';
+import useStore from '@/AppBuilder/_stores/store';
+import useTableStore from './_stores/tableStore';
+import TableContainer from './_components/TableContainer';
+import { transformTableData } from './_utils/transformTableData';
+import { usePrevious } from '@dnd-kit/utilities';
+
+export const Table = memo(
+ ({ id, componentName, width, height, properties, styles, darkMode, fireEvent, setExposedVariables }) => {
+ // get table store functions
+ const initializeComponent = useTableStore((state) => state.initializeComponent, shallow);
+ const removeComponent = useTableStore((state) => state.removeComponent, shallow);
+ const setTableProperties = useTableStore((state) => state.setTableProperties, shallow);
+ const setTableActions = useTableStore((state) => state.setTableActions, shallow);
+ const setTableEvents = useTableStore((state) => state.setTableEvents, shallow);
+ const setTableStyles = useTableStore((state) => state.setTableStyles, shallow);
+ const setColumnDetails = useTableStore((state) => state.setColumnDetails, shallow);
+ const transformations = useTableStore((state) => state.getColumnTransformations(id), shallow);
+
+ // get table properties
+ const visibility = useTableStore((state) => state.getTableProperties(id)?.visibility, shallow);
+ const disabledState = useTableStore((state) => state.getTableProperties(id)?.disabledState, shallow);
+ const borderRadius = useTableStore((state) => state.getTableStyles(id)?.borderRadius, shallow);
+
+ // get table styles
+ const boxShadow = useTableStore((state) => state.getTableStyles(id)?.boxShadow, shallow);
+ const borderColor = useTableStore((state) => state.getTableStyles(id)?.borderColor, shallow);
+
+ // get resolved value for transformations from app builder store
+ const getResolvedValue = useStore((state) => state.getResolvedValue);
+
+ const {
+ columns,
+ useDynamicColumn,
+ columnData,
+ columnDeletionHistory,
+ autogenerateColumns,
+ actions,
+ ...restOfProperties
+ } = properties;
+
+ const firstRowOfTable = !isEmpty(restOfProperties.data?.[0]) ? restOfProperties.data?.[0] : undefined;
+ const prevFirstRowOfTable = usePrevious(firstRowOfTable);
+
+ // Get all app events. Needed for certain events like onBulkUpdate
+ const allAppEvents = useEvents();
+
+ const shouldAutogenerateColumns = useRef(false);
+
+ // Initialize component on the table store
+ useEffect(() => {
+ initializeComponent(id);
+ return () => removeComponent(id);
+ }, [id, initializeComponent, removeComponent]);
+
+ // Set properties to the table store
+ useEffect(() => {
+ setTableProperties(id, restOfProperties);
+ }, [id, restOfProperties, setTableProperties]);
+
+ // Set actions to the table store
+ useEffect(() => {
+ setTableActions(id, actions);
+ }, [id, actions, setTableActions]);
+
+ useEffect(() => {
+ if (useDynamicColumn || (!isEqual(prevFirstRowOfTable, firstRowOfTable) && !isEmpty(firstRowOfTable)))
+ shouldAutogenerateColumns.current = true;
+ }, [firstRowOfTable, useDynamicColumn, columnData, prevFirstRowOfTable]);
+
+ // Set column details to the table store. This is responsible for auto-generating columns
+ useEffect(() => {
+ setColumnDetails(
+ id,
+ columns,
+ useDynamicColumn,
+ columnData,
+ firstRowOfTable,
+ autogenerateColumns,
+ columnDeletionHistory,
+ shouldAutogenerateColumns.current
+ );
+ shouldAutogenerateColumns.current = false;
+ }, [
+ id,
+ columns,
+ useDynamicColumn,
+ columnData,
+ setColumnDetails,
+ firstRowOfTable,
+ autogenerateColumns,
+ columnDeletionHistory,
+ ]);
+
+ // Set styles to the table store
+ useEffect(() => {
+ setTableStyles(id, styles, darkMode);
+ }, [id, styles, darkMode, setTableStyles]);
+
+ // Set events to the table store
+ useEffect(() => {
+ setTableEvents(id, allAppEvents);
+ }, [id, allAppEvents, setTableEvents]);
+
+ // Transform table data if transformations are present
+ const tableData = useMemo(() => {
+ return transformTableData(restOfProperties.data, transformations, getResolvedValue);
+ }, [getResolvedValue, restOfProperties.data, transformations]);
+
+ return (
+
+ );
+ },
+ (prevProps, nextProps) => {
+ // Avoid re-rendering if the props are same
+ return Object.keys(diff(prevProps, nextProps)).length === 0;
+ }
+);
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/ActionButtons/ActionButtons.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/ActionButtons/ActionButtons.jsx
new file mode 100644
index 0000000000..f94b095946
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/ActionButtons/ActionButtons.jsx
@@ -0,0 +1,44 @@
+import React, { useCallback } from 'react';
+import useTableStore from '../../_stores/tableStore';
+import { shallow } from 'zustand/shallow';
+import useStore from '@/AppBuilder/_stores/store';
+export const ActionButtons = ({ actions, row, cell, fireEvent, setExposedVariables, id }) => {
+ const { getTableActionEvents } = useTableStore();
+ const actionButtonRadius = useTableStore((state) => state.getTableStyles(id)?.actionButtonRadius, shallow);
+ const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
+
+ const handleActionClick = useCallback(
+ (action) => {
+ setExposedVariables({
+ selectedRowId: row.id,
+ selectedRow: row.original,
+ });
+
+ fireEvent('onTableActionButtonClicked', {
+ action,
+ tableActionEvents: getTableActionEvents(id),
+ });
+ },
+ [id, row, setExposedVariables, fireEvent, getTableActionEvents]
+ );
+
+ return (
+
+ {actions.map((action) => (
+
+ ))}
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Boolean.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Boolean.jsx
new file mode 100644
index 0000000000..96dd58e34e
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Boolean.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+
+export const BooleanColumn = ({ value = false, isEditable, onChange, toggleOnBg, toggleOffBg }) => {
+ const getCustomBgStyles = (value, toggleOnBg, toggleOffBg) => {
+ if (value && toggleOnBg) {
+ return { backgroundColor: toggleOnBg };
+ }
+ if (!value && toggleOffBg) {
+ return { backgroundColor: toggleOffBg };
+ }
+ return {};
+ };
+
+ const nonEditableContent = (isTruthyValue) =>
+ isTruthyValue ? (
+
+ ) : (
+
+ );
+
+ const editableContent = (isEditable, value, onChange) => (
+
+ );
+
+ return (
+
+ {isEditable ? editableContent(isEditable, value, onChange) : nonEditableContent(value)}
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/CustomDropdown.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/CustomDropdown.jsx
new file mode 100644
index 0000000000..e69e22a5fc
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/CustomDropdown.jsx
@@ -0,0 +1,135 @@
+import React, { useState, useEffect, useCallback, useMemo } from 'react';
+import SelectSearch from 'react-select-search';
+import '@/_styles/editor/react-select-search.scss';
+import { useTranslation } from 'react-i18next';
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+
+export const CustomDropdownColumn = ({
+ options = [],
+ value,
+ multiple,
+ onChange,
+ isEditable,
+ width,
+ contentWrap,
+ autoHeight,
+ darkMode,
+}) => {
+ const [showOverlay, setShowOverlay] = useState(false);
+ const { t } = useTranslation();
+
+ const sanitizedValue = useMemo(() => {
+ if (!multiple) return value;
+ if (!Array.isArray(value)) return [];
+ return value.every((val) => typeof val !== 'object') ? value : [];
+ }, [value, multiple]);
+
+ const handleMouseMove = useCallback(() => {
+ if (!showOverlay) {
+ setShowOverlay(true);
+ }
+ }, [showOverlay]);
+
+ const handleMouseLeave = useCallback(() => {
+ setShowOverlay(false);
+ }, []);
+
+ const renderValue = useCallback(
+ (valueProps) => {
+ if (!valueProps) return null;
+
+ const stringValue = String(valueProps.value);
+ const values = stringValue.includes(',') ? stringValue.split(', ') : stringValue.split(' ');
+
+ return (
+
+ {values.map((val, index) => (
+
+ {val}
+
+ ))}
+
+ );
+ },
+ [isEditable]
+ );
+
+ const getOverlay = useCallback(
+ (value, width, options) => {
+ if (!Array.isArray(value)) return
;
+
+ const labels = value
+ .map((val) => {
+ const option = options.find((opt) => opt.value === val);
+ return option?.name;
+ })
+ .filter(Boolean);
+
+ return (
+
+ {labels.map((label) => (
+
+ {label}
+
+ ))}
+
+ );
+ },
+ [darkMode]
+ );
+
+ const isOverflowing = useCallback((element) => {
+ if (!element) return false;
+ return element.clientHeight < element.scrollHeight || element.clientWidth < element.scrollWidth;
+ }, []);
+
+ return (
+
= 1 && ['hover']}
+ rootClose={true}
+ show={multiple && sanitizedValue?.length >= 1 && showOverlay}
+ >
+
+
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/CustomSelect.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/CustomSelect.jsx
new file mode 100644
index 0000000000..c3f4195a1a
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/CustomSelect.jsx
@@ -0,0 +1,327 @@
+import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
+import Select from '@/_ui/Select';
+import { components } from 'react-select';
+import defaultStyles from '@/_ui/Select/styles';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+import { Checkbox } from '@/_ui/CheckBox/CheckBox';
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+import { isArray } from 'lodash';
+import useStore from '@/AppBuilder/_stores/store';
+import { shallow } from 'zustand/shallow';
+import useTextColor from '../DataTypes/_hooks/useTextColor';
+
+const { MenuList } = components;
+
+const CustomMenuList = ({ optionsLoadingState, children, selectProps, inputRef, ...props }) => {
+ const { onInputChange, inputValue, onMenuInputFocus } = selectProps;
+
+ return (
+
e.stopPropagation()}>
+
+ {!inputValue && (
+
+
+
+ )}
+ onInputChange(e.currentTarget.value, { action: 'input-change' })}
+ onMouseDown={(e) => {
+ e.stopPropagation();
+ e.target.focus();
+ }}
+ onFocus={onMenuInputFocus}
+ placeholder="Search..."
+ className="table-select-column-type-search-box"
+ autoCorrect="off"
+ autoComplete="off"
+ spellCheck="false"
+ />
+
+
+ {optionsLoadingState ? (
+
+ ) : (
+ children
+ )}
+
+
+ );
+};
+
+const CustomOption = ({ innerRef, innerProps, children, isSelected, ...props }) => (
+
+ {props.selectProps.isMulti ? (
+
e.stopPropagation()} key="" value={children} />
+ ) : (
+
+ e.stopPropagation()} key="" value={children} />
+
+ )}
+ {children}
+
+);
+
+const MultiValueRemove = ({ innerProps }) =>
;
+
+const CustomMultiValueContainer = ({ children }) => (
+
+ {children}
+
+);
+
+const DropdownIndicator = ({ selectProps }) => (
+
+
+
+);
+
+const getOverlay = (value, containerWidth, darkMode) => {
+ if (!isArray(value)) return
;
+
+ return (
+
+ {value.map((option) => (
+
+ {option.label || option}
+
+ ))}
+
+ );
+};
+
+export const CustomSelectColumn = ({
+ options,
+ value,
+ onChange,
+ fuzzySearch = false,
+ placeholder,
+ disabled,
+ className,
+ darkMode,
+ defaultOptionsList = [],
+ textColor = '',
+ isMulti,
+ containerWidth,
+ optionsLoadingState = false,
+ horizontalAlignment = 'left',
+ isEditable,
+ column,
+ isNewRow,
+ id,
+}) => {
+ const validateWidget = useStore((state) => state.validateWidget, shallow);
+
+ const [isFocused, setIsFocused] = useState(false);
+ const containerRef = useRef(null);
+ const inputRef = useRef(null);
+ const cellTextColor = useTextColor(id, textColor);
+
+ const validationData = validateWidget({
+ validationObject: {
+ customRule: { value: column.customRule },
+ },
+ widgetValue: value,
+ customResolveObjects: { value },
+ });
+ const { isValid, validationError } = validationData;
+
+ useEffect(() => {
+ const handleDocumentClick = (event) => {
+ if ((isMulti || isNewRow) && !containerRef.current?.contains(event.target)) setIsFocused(false);
+ };
+ document.addEventListener('mousedown', handleDocumentClick);
+ return () => document.removeEventListener('mousedown', handleDocumentClick);
+ }, [isMulti, isNewRow]);
+
+ const customComponents = {
+ MenuList: (props) =>
,
+ Option: CustomOption,
+ DropdownIndicator: isEditable ? DropdownIndicator : null,
+ ...(isMulti && {
+ MultiValueRemove,
+ MultiValueContainer: CustomMultiValueContainer,
+ }),
+ };
+
+ const customStyles = useMemo(
+ () => ({
+ ...defaultStyles(darkMode, '100%'),
+ ...(isMulti && {
+ multiValue: (provided) => ({
+ ...provided,
+ display: 'inline-block',
+ marginRight: '4px',
+ }),
+ multiValueLabel: (provided) => ({
+ ...provided,
+ padding: '2px 6px',
+ background: 'var(--surfaces-surface-03)',
+ borderRadius: '6px',
+ color: cellTextColor || 'var(--text-primary)',
+ fontSize: '12px',
+ }),
+ }),
+ valueContainer: (provided) => ({
+ ...provided,
+ ...(isMulti && {
+ marginBottom: '0',
+ display: 'flex',
+ flexWrap: 'no-wrap',
+ overflow: 'hidden',
+ flexDirection: 'row',
+ }),
+ justifyContent: horizontalAlignment,
+ }),
+ menuList: (base) => ({
+ ...base,
+ backgroundColor: 'var(--surfaces-surface-01)',
+ color: 'var(--text-primary)',
+ cursor: 'pointer',
+ overflow: 'auto',
+ }),
+ singleValue: (provided) => ({
+ ...provided,
+ padding: '2px 6px',
+ background: 'var(--surfaces-surface-03)',
+ borderRadius: '6px',
+ color: cellTextColor || 'var(--text-primary)',
+ fontSize: '12px',
+ }),
+ }),
+ [darkMode, isMulti, horizontalAlignment, cellTextColor]
+ );
+
+ const defaultValue = useMemo(
+ () =>
+ defaultOptionsList.length >= 1
+ ? isMulti
+ ? defaultOptionsList
+ : defaultOptionsList.slice(-1)[0]
+ : isMulti
+ ? []
+ : {},
+ [isMulti, defaultOptionsList]
+ );
+
+ const selectedValue = useMemo(() => {
+ if (!value) return null;
+ if (isMulti && value?.length) {
+ return isArray(value)
+ ? options?.filter((option) =>
+ value?.find((val) => (val.hasOwnProperty('value') ? option.value === val.value : option.value === val))
+ )
+ : [];
+ }
+ return options?.find((option) => option.value === value) || [];
+ }, [options, value, isMulti]);
+
+ const handleChange = useCallback(
+ (newValue) => {
+ setIsFocused(false);
+ if (!isMulti && newValue === selectedValue?.value) {
+ onChange('');
+ } else {
+ onChange(newValue);
+ }
+ },
+ [isMulti, selectedValue, onChange]
+ );
+
+ const isOverflowing = useCallback(() => {
+ if (!containerRef.current) return false;
+ const valueContainer = containerRef.current.querySelector('.react-select__value-container');
+ return valueContainer?.clientHeight > containerRef.current?.clientHeight;
+ }, []);
+
+ return (
+
+ )
+ }
+ trigger={isMulti && !isFocused && isOverflowing() && ['hover', 'focus']}
+ rootClose={true}
+ >
+ <>
+
{
+ if (isNewRow && isEditable) {
+ setIsFocused((prev) => !prev);
+ }
+ }}
+ >
+
+
{
+ if (!isValid) {
+ setIsFocused(true); // Open the dropdown
+ }
+ }}
+ className={` ${isValid ? 'd-none' : 'invalid-feedback d-block'}`}
+ >
+ {validationError}
+
+ >
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Datepicker.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Datepicker.jsx
new file mode 100644
index 0000000000..23e9640902
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Datepicker.jsx
@@ -0,0 +1,279 @@
+import React, { useState, useCallback, useEffect, useRef, forwardRef } from 'react';
+import DatePickerComponent from 'react-datepicker';
+import moment from 'moment-timezone';
+import cx from 'classnames';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+import CustomDatePickerHeader from './_components/CustomDatePickerHeader';
+import 'react-datepicker/dist/react-datepicker.css';
+import useTextColor from '../DataTypes/_hooks/useTextColor';
+
+const DISABLED_DATE_FORMAT = 'MM/DD/YYYY';
+
+const DatepickerInput = forwardRef(({ value, onClick, styles, readOnly, onInputChange, onInputFocus }, ref) => (
+
+ {readOnly ? (
+
+ {value}
+
+ ) : (
+ <>
+
+ {!readOnly && (
+
+
+
+ )}
+ >
+ )}
+
+));
+
+const getDateTimeFormat = (dateDisplayFormat, isTimeChecked, isTwentyFourHrFormatEnabled, isDateSelectionEnabled) => {
+ const timeFormat = isTwentyFourHrFormatEnabled ? 'HH:mm' : 'LT';
+ if (isTimeChecked && !isDateSelectionEnabled) return timeFormat;
+ return isTimeChecked ? `${dateDisplayFormat} ${timeFormat}` : dateDisplayFormat;
+};
+
+const parseDate = ({
+ value,
+ parseDateFormat,
+ timeZoneValue,
+ timeZoneDisplay,
+ unixTimestamp,
+ parseInUnixTimestamp,
+ isTimeChecked,
+}) => {
+ if (!value) return null;
+
+ let momentObj;
+ if (parseInUnixTimestamp && unixTimestamp) {
+ momentObj = unixTimestamp === 'seconds' ? moment.unix(value) : moment(parseInt(value));
+ } else if (isTimeChecked && timeZoneValue && timeZoneDisplay) {
+ momentObj = moment.tz(value, parseDateFormat, timeZoneValue).tz(timeZoneDisplay);
+ } else {
+ momentObj = moment(value, parseDateFormat);
+ }
+
+ return momentObj?.isValid() ? momentObj.toDate() : null;
+};
+
+export const DatepickerColumn = ({
+ value,
+ onChange,
+ readOnly,
+ isTimeChecked,
+ dateDisplayFormat,
+ parseDateFormat,
+ timeZoneValue,
+ timeZoneDisplay,
+ isDateSelectionEnabled,
+ isTwentyFourHrFormatEnabled,
+ disabledDates,
+ unixTimestamp = 'seconds',
+ parseInUnixTimestamp,
+ darkMode,
+ textColor,
+ id,
+}) => {
+ const [date, setDate] = useState(null);
+ const [excludedDates, setExcludedDates] = useState([]);
+ const [isInputFocused, setIsInputFocused] = useState(false);
+ const [inputValue, setInputValue] = useState('');
+ const dateInputRef = useRef(null);
+ const cellTextColor = useTextColor(id, textColor);
+
+ const computeDateString = useCallback(
+ (date) => {
+ if (date === null && !value) return '';
+ if (!isDateSelectionEnabled && !isTimeChecked) return '';
+ if (date === null) return 'Invalid date';
+
+ const timeFormat = isTwentyFourHrFormatEnabled ? 'HH:mm' : 'LT';
+ const selectedDateFormat = isTimeChecked ? `${dateDisplayFormat} ${timeFormat}` : dateDisplayFormat;
+
+ if (isDateSelectionEnabled) {
+ if (isTimeChecked && parseInUnixTimestamp && unixTimestamp) {
+ return timeZoneDisplay
+ ? moment(date).tz(timeZoneDisplay).format(selectedDateFormat)
+ : moment(date).format(selectedDateFormat);
+ }
+ if (isTimeChecked && timeZoneValue && timeZoneDisplay) {
+ return moment.tz(date, parseDateFormat, timeZoneValue).tz(timeZoneDisplay).format(selectedDateFormat);
+ }
+ return moment(date).format(selectedDateFormat);
+ }
+
+ if (!isDateSelectionEnabled && isTimeChecked) {
+ return moment(date).format(timeFormat);
+ }
+ },
+ [
+ value,
+ isDateSelectionEnabled,
+ isTimeChecked,
+ isTwentyFourHrFormatEnabled,
+ dateDisplayFormat,
+ parseInUnixTimestamp,
+ unixTimestamp,
+ timeZoneDisplay,
+ timeZoneValue,
+ parseDateFormat,
+ ]
+ );
+
+ const handleDateChange = useCallback(
+ (newDate) => {
+ let processedValue = newDate;
+ if (parseInUnixTimestamp && unixTimestamp) {
+ processedValue = moment(newDate).unix();
+ }
+
+ const parsedDate = parseDate({
+ value: processedValue,
+ parseDateFormat: getDateTimeFormat(
+ parseDateFormat,
+ isTimeChecked,
+ isTwentyFourHrFormatEnabled,
+ isDateSelectionEnabled
+ ),
+ timeZoneValue,
+ timeZoneDisplay,
+ unixTimestamp,
+ parseInUnixTimestamp,
+ isTimeChecked,
+ });
+
+ setDate(parsedDate);
+ if (parseInUnixTimestamp && unixTimestamp) {
+ onChange(moment(parsedDate).unix());
+ } else {
+ onChange(computeDateString(parsedDate));
+ }
+ },
+ [
+ parseInUnixTimestamp,
+ unixTimestamp,
+ parseDateFormat,
+ isTimeChecked,
+ isTwentyFourHrFormatEnabled,
+ isDateSelectionEnabled,
+ timeZoneValue,
+ timeZoneDisplay,
+ onChange,
+ computeDateString,
+ ]
+ );
+
+ const handleInputDateChange = useCallback(
+ (value) => {
+ const inputDate = moment(value, parseDateFormat).toDate();
+ handleDateChange(inputDate);
+ },
+ [parseDateFormat, handleDateChange]
+ );
+
+ // Initialize date from value
+ useEffect(() => {
+ const parsedDate = parseDate({
+ value,
+ parseDateFormat: getDateTimeFormat(
+ parseDateFormat,
+ isTimeChecked,
+ isTwentyFourHrFormatEnabled,
+ isDateSelectionEnabled
+ ),
+ timeZoneValue,
+ timeZoneDisplay,
+ unixTimestamp,
+ parseInUnixTimestamp,
+ isTimeChecked,
+ });
+ setDate(parsedDate);
+ }, [
+ value,
+ parseDateFormat,
+ isTimeChecked,
+ isTwentyFourHrFormatEnabled,
+ isDateSelectionEnabled,
+ timeZoneValue,
+ timeZoneDisplay,
+ unixTimestamp,
+ parseInUnixTimestamp,
+ ]);
+
+ // Handle disabled dates
+ useEffect(() => {
+ if (Array.isArray(disabledDates) && disabledDates.length > 0) {
+ const excludedDates = disabledDates
+ .filter((date) => moment(date, DISABLED_DATE_FORMAT).isValid())
+ .map((date) => moment(date, DISABLED_DATE_FORMAT).toDate());
+ setExcludedDates(excludedDates);
+ }
+ }, [disabledDates]);
+
+ return (
+
+ {
+ setIsInputFocused(false);
+ handleDateChange(date);
+ }}
+ value={isInputFocused ? inputValue : computeDateString(date)}
+ dateFormat={!isDateSelectionEnabled && isTimeChecked ? 'HH:mm' : dateDisplayFormat}
+ customInput={
+ {
+ setIsInputFocused(true);
+ setInputValue(e.target.value);
+ }}
+ onInputFocus={() => setInputValue(computeDateString(date))}
+ />
+ }
+ showTimeSelect={isTimeChecked}
+ showTimeSelectOnly={!isDateSelectionEnabled && isTimeChecked}
+ showMonthDropdown
+ showYearDropdown
+ dropdownMode="select"
+ excludeDates={excludedDates}
+ showPopperArrow={false}
+ renderCustomHeader={(headerProps) => }
+ shouldCloseOnSelect
+ readOnly={readOnly}
+ popperProps={{ strategy: 'fixed' }}
+ timeIntervals={15}
+ timeFormat={isTwentyFourHrFormatEnabled ? 'HH:mm' : 'h:mm aa'}
+ onCalendarClose={() => {
+ if (isInputFocused) {
+ handleInputDateChange(inputValue);
+ }
+ setIsInputFocused(false);
+ }}
+ />
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Image.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Image.jsx
new file mode 100644
index 0000000000..d8787080ec
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Image.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+
+export const ImageColumn = ({ cellValue, width, height, borderRadius, objectFit }) => {
+ if (!cellValue) return null;
+
+ return (
+
+

+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/JSON.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/JSON.jsx
new file mode 100644
index 0000000000..415af08a38
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/JSON.jsx
@@ -0,0 +1,189 @@
+/* eslint-disable no-undef */
+import React, { useState, useEffect } from 'react';
+import { determineJustifyContentValue } from '@/_helpers/utils';
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+import useTextColor from '../DataTypes/_hooks/useTextColor';
+import { getMaxHeight } from '../../_utils/helper';
+import useTableStore from '../../_stores/tableStore';
+import { shallow } from 'zustand/shallow';
+
+export const JsonColumn = ({
+ isEditable,
+ jsonIndentation,
+ darkMode,
+ handleCellValueChange,
+ textColor,
+ cellValue,
+ column,
+ containerWidth,
+ horizontalAlignment,
+ id,
+}) => {
+ const cellTextColor = useTextColor(id, textColor, 'json');
+ const cellHeight = useTableStore((state) => state.getTableStyles(id)?.cellHeight, shallow);
+ const isMaxRowHeightAuto = useTableStore((state) => state.getTableStyles(id)?.isMaxRowHeightAuto, shallow);
+ const maxRowHeightValue = useTableStore((state) => state.getTableStyles(id)?.maxRowHeightValue, shallow);
+
+ const ref = React.useRef(null);
+
+ const [hovered, setHovered] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+
+ const cellStyles = {
+ color: cellTextColor ?? 'inherit',
+ };
+
+ useEffect(() => {
+ if (!isEditable && isEditing) {
+ setIsEditing(false);
+ }
+ }, [isEditable, isEditing]);
+
+ function format(obj) {
+ if (typeof obj !== 'object' || obj === null) {
+ return typeof obj === 'string' ? `"${obj}"` : obj;
+ }
+ if (Array.isArray(obj)) {
+ return `[ ${obj.map(format).join(', ')} ]`;
+ }
+ return `{ ${Object.entries(obj)
+ .map(([key, value]) => `"${key}": ${format(value)}`)
+ .join(', ')} }`;
+ }
+
+ const formatCellValue = (value, overlay = false) => {
+ try {
+ if (typeof value === 'object') {
+ if (jsonIndentation === true && !overlay) {
+ return JSON.stringify(value, null, 4).replace(/":/g, '": ');
+ }
+ const formattedJSON = format(value);
+ return formattedJSON;
+ } else {
+ if (jsonIndentation === true && !overlay) {
+ return JSON.stringify(JSON.parse(value), null, 4).replace(/":/g, '": ');
+ }
+ const formattedJSON = format(JSON.parse(value));
+ return formattedJSON;
+ }
+ } catch (error) {
+ return value;
+ }
+ };
+
+ const _renderString = () => (
+
{
+ setIsEditing(false);
+ if (cellValue !== e.target.textContent) {
+ const value = JSON.stringify(JSON.parse(e.target.textContent.replace(/\n/g, '')));
+ handleCellValueChange(row.index, column.key || column.name, value, row.original);
+ }
+ }}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ ref.current.blur();
+ if (cellValue !== e.target.textContent) {
+ const value = JSON.stringify(JSON.parse(e.target.textContent.replace(/\n/g, '')));
+ handleCellValueChange(row.index, column.key || column.name, value, row.original);
+ }
+ }
+ }}
+ onFocus={(e) => {
+ setIsEditing(true);
+ e.stopPropagation();
+ }}
+ >
+ {String(formatCellValue(cellValue))}
+
+ );
+
+ const getOverlay = () => {
+ return (
+
setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ style={{ color: 'var(--text-primary)' }}
+ >
+
+ {String(formatCellValue(cellValue, true))}
+
+
+ );
+ };
+
+ const _showOverlay =
+ ref?.current &&
+ (ref?.current?.clientWidth < ref?.current?.children[0]?.offsetWidth ||
+ ref?.current?.clientHeight < ref?.current?.children[0]?.offsetHeight);
+
+ return (
+ <>
+
}
+ trigger={_showOverlay && ['hover', 'focus']}
+ rootClose={true}
+ show={_showOverlay && hovered && !isEditing}
+ >
+ {!isEditable ? (
+
{
+ if (!hovered) setHovered(true);
+ }}
+ onMouseLeave={() => {
+ setHovered(false);
+ }}
+ ref={ref}
+ >
+
+ {String(formatCellValue(cellValue))}
+
+
+ ) : (
+
+
{
+ if (!hovered) setHovered(true);
+ }}
+ onMouseLeave={() => setHovered(false)}
+ className={`${isEditing ? 'h-100 content-editing' : ''} h-100`}
+ >
+ {_renderString()}
+
+
+ )}
+
+ >
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Link.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Link.jsx
new file mode 100644
index 0000000000..3e17d173db
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Link.jsx
@@ -0,0 +1,41 @@
+import React, { useMemo } from 'react';
+import useTextColor from '../DataTypes/_hooks/useTextColor';
+
+export const LinkColumn = ({
+ cellValue,
+ linkTarget,
+ underline,
+ underlineColor,
+ textColor,
+ displayText,
+ darkMode,
+ id,
+}) => {
+ const cellTextColor = useTextColor(id, textColor);
+ const linkTextColor = useMemo(
+ () =>
+ cellTextColor !== '#1B1F24' ? cellTextColor : darkMode && cellTextColor === '#1B1F24' ? '#FFFFFF' : cellTextColor,
+ [cellTextColor, darkMode]
+ );
+
+ return (
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Markdown.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Markdown.jsx
new file mode 100644
index 0000000000..7ea9a7b3b0
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Markdown.jsx
@@ -0,0 +1,169 @@
+import React, { useState, useEffect, useRef } from 'react';
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+import { determineJustifyContentValue } from '@/_helpers/utils';
+import { default as ReactMarkdown } from 'react-markdown';
+import useTextColor from '../DataTypes/_hooks/useTextColor';
+import useTableStore from '../../_stores/tableStore';
+import { getMaxHeight } from '../../_utils/helper';
+import { shallow } from 'zustand/shallow';
+import DOMPurify from 'dompurify';
+
+export const MarkdownColumn = ({
+ id,
+ isEditable,
+ darkMode,
+ handleCellValueChange,
+ textColor,
+ cellValue,
+ column,
+ containerWidth,
+ cell,
+ horizontalAlignment,
+ cellSize,
+}) => {
+ const ref = useRef(null);
+ const cellTextColor = useTextColor(id, textColor);
+ const isMaxRowHeightAuto = useTableStore((state) => state.getTableStyles(id)?.isMaxRowHeightAuto, shallow);
+ const maxRowHeightValue = useTableStore((state) => state.getTableStyles(id)?.maxRowHeightValue, shallow);
+ const [hovered, setHovered] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+
+ const cellStyles = {
+ color: cellTextColor ?? 'inherit',
+ };
+
+ const getCellValue = (value) => {
+ let transformedValue = value;
+ if (typeof value !== 'string') {
+ try {
+ transformedValue = String(value);
+ } catch (e) {
+ transformedValue = '';
+ }
+ }
+ return DOMPurify.sanitize(transformedValue.trim());
+ };
+
+ useEffect(() => {
+ if (!isEditable && isEditing) {
+ setIsEditing(false);
+ }
+ }, [isEditable]);
+
+ const getOverlay = () => {
+ return (
+
setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ style={{ color: 'var(--text-primary)' }}
+ >
+
+ {getCellValue(cellValue)}
+
+
+ );
+ };
+
+ const renderEditable = () => {
+ const onChange = (e) => {
+ if (cellValue !== e.target.textContent) {
+ const value = e.target.textContent;
+ handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
+ }
+ };
+
+ return (
+
{
+ setIsEditing(false);
+ onChange(e);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ ref.current.blur();
+ onChange(e);
+ }
+ }}
+ onFocus={(e) => {
+ setIsEditing(true);
+ e.stopPropagation();
+ }}
+ >
+
{isEditing ? cellValue : {getCellValue(cellValue)}}
+
+ );
+ };
+
+ const _showOverlay =
+ ref?.current &&
+ (ref?.current?.clientWidth < ref?.current?.children[0]?.offsetWidth ||
+ ref?.current?.clientHeight < ref?.current?.children[0]?.offsetHeight);
+
+ return (
+
}
+ trigger={_showOverlay && ['hover', 'focus']}
+ rootClose={true}
+ show={_showOverlay && hovered && !isEditing}
+ >
+ {!isEditable ? (
+
{
+ if (!hovered) setHovered(true);
+ }}
+ onMouseLeave={() => {
+ setHovered(false);
+ }}
+ ref={ref}
+ >
+
+ {getCellValue(cellValue)}
+
+
+ ) : (
+
+
{
+ if (!hovered) setHovered(true);
+ }}
+ onMouseLeave={() => setHovered(false)}
+ className={`${isEditing ? 'h-100 content-editing' : ''} h-100`}
+ >
+ {renderEditable()}
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/MultiSelect.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/MultiSelect.jsx
new file mode 100644
index 0000000000..2ece12200c
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/MultiSelect.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { CustomSelect } from './CustomSelect';
+
+export const MultiSelectColumn = ({
+ options,
+ value,
+ onChange,
+ isEditable,
+ darkMode,
+ defaultOptionsList,
+ textColor,
+ containerWidth,
+ optionsLoadingState,
+ horizontalAlignment,
+ isMaxRowHeightAuto,
+}) => {
+ return (
+
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Number.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Number.jsx
new file mode 100644
index 0000000000..4b168b015a
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Number.jsx
@@ -0,0 +1,164 @@
+import React, { useState, useEffect } from 'react';
+import useStore from '@/AppBuilder/_stores/store';
+import { shallow } from 'zustand/shallow';
+import { determineJustifyContentValue } from '@/_helpers/utils';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+import HighLightSearch from '@/AppBuilder/Widgets/NewTable/_components/HighLightSearch';
+import useTextColor from '../DataTypes/_hooks/useTextColor';
+
+export const NumberColumn = ({
+ id,
+ isEditable,
+ handleCellValueChange,
+ textColor,
+ horizontalAlignment,
+ cellValue,
+ column,
+ cell,
+ row,
+ searchText,
+}) => {
+ const [displayValue, setDisplayValue] = useState(cellValue);
+ const validateWidget = useStore((state) => state.validateWidget, shallow);
+ const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
+ const cellTextColor = useTextColor(id, textColor);
+
+ useEffect(() => {
+ setDisplayValue(cellValue);
+ }, [cellValue]);
+
+ const removingExcessDecimalPlaces = (value, allowedDecimalPlaces) => {
+ if (value?.toString()?.includes('.')) {
+ const [integerPart, decimalPart] = value.toString().split('.');
+ const truncatedDecimalPart = decimalPart.slice(0, allowedDecimalPlaces);
+ return Number(`${integerPart}.${truncatedDecimalPart}`);
+ }
+ return value;
+ };
+
+ const allowedDecimalPlaces = getResolvedValue(column?.decimalPlaces) ?? null;
+ cellValue = allowedDecimalPlaces ? removingExcessDecimalPlaces(cellValue, allowedDecimalPlaces) : cellValue;
+
+ const validationData = validateWidget({
+ validationObject: {
+ minValue: {
+ value: column?.minValue,
+ },
+ maxValue: {
+ value: column?.maxValue,
+ },
+ regex: {
+ value: column?.regex,
+ },
+ customRule: {
+ value: column?.customRule,
+ },
+ },
+ widgetValue: cellValue,
+ customResolveObjects: { cellValue },
+ });
+
+ const { isValid, validationError } = validationData;
+
+ const handleIncrement = (e) => {
+ e.preventDefault();
+ const newValue = (cellValue || 0) + 1;
+ if (!isNaN(newValue)) {
+ handleCellValueChange(row.index, column.key || column.name, Number(newValue), row.original);
+ }
+ };
+
+ const handleDecrement = (e) => {
+ e.preventDefault();
+ const newValue = (cellValue || 0) - 1;
+ if (!isNaN(newValue)) {
+ handleCellValueChange(row.index, column.key || column.name, Number(newValue), row.original);
+ }
+ };
+
+ const handleValueChange = (value) => {
+ if (value === '') return;
+
+ const numValue = Number(value);
+ if (isNaN(numValue)) return;
+
+ const processedValue =
+ allowedDecimalPlaces !== null ? removingExcessDecimalPlaces(numValue, allowedDecimalPlaces) : numValue;
+ setDisplayValue(processedValue);
+ handleCellValueChange(row.index, column.key || column.name, processedValue, row.original);
+ };
+
+ if (isEditable) {
+ return (
+
+
setDisplayValue(e.target.value)}
+ step={allowedDecimalPlaces !== null ? `0.${'0'.repeat(allowedDecimalPlaces - 1)}1` : 'any'}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ if (displayValue !== cellValue) {
+ handleValueChange(displayValue);
+ }
+ }
+ }}
+ onBlur={() => {
+ if (displayValue !== cellValue) {
+ handleValueChange(displayValue);
+ }
+ }}
+ onFocus={(e) => e.stopPropagation()}
+ />
+
+ {!isValid &&
{validationError}
}
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Radio.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Radio.jsx
new file mode 100644
index 0000000000..29cfed17ae
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Radio.jsx
@@ -0,0 +1,97 @@
+import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+
+export const RadioColumn = ({ options = [], value, onChange, readOnly, containerWidth }) => {
+ const [showOverlay, setShowOverlay] = useState(false);
+ const [hovered, setHovered] = useState(false);
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ setShowOverlay(hovered);
+ }, [hovered]);
+
+ // Memoize sanitized value and options
+ const sanitizedValue = useMemo(() => (value === undefined ? [] : value), [value]);
+ const sanitizedOptions = useMemo(() => (Array.isArray(options) ? options : []), [options]);
+
+ const handleOptionClick = useCallback(
+ (optionValue) => {
+ if (!readOnly) {
+ onChange(optionValue);
+ }
+ },
+ [readOnly, onChange]
+ );
+
+ const renderOption = useCallback(
+ (option, index) => (
+
+ ),
+ [sanitizedValue, readOnly, handleOptionClick]
+ );
+
+ const getOverlay = useCallback(
+ (options, containerWidth) => {
+ const darkMode = localStorage.getItem('darkMode') === 'true';
+
+ return Array.isArray(options) ? (
+
setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ role="radiogroup"
+ aria-label="Radio options overlay"
+ >
+ {options.map(renderOption)}
+
+ ) : (
+
+ );
+ },
+ [renderOption]
+ );
+
+ const isOverflowing = useCallback(() => {
+ if (!containerRef.current) return false;
+ return (
+ containerRef.current.clientHeight < containerRef.current.scrollHeight ||
+ containerRef.current.clientWidth < containerRef.current.scrollWidth
+ );
+ }, []);
+
+ return (
+
+ !hovered && setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ role="radiogroup"
+ aria-label="Radio options"
+ >
+
+ {sanitizedOptions.map(renderOption)}
+
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Select.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Select.jsx
new file mode 100644
index 0000000000..44e6d3c54a
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Select.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { CustomSelect } from './CustomSelect';
+
+export const SelectColumn = ({
+ options,
+ value,
+ onChange,
+ isEditable,
+ darkMode,
+ defaultOptionsList,
+ textColor,
+ containerWidth,
+ optionsLoadingState,
+ horizontalAlignment,
+ isMaxRowHeightAuto,
+}) => {
+ return (
+
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/String.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/String.jsx
new file mode 100644
index 0000000000..fbe15205f8
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/String.jsx
@@ -0,0 +1,163 @@
+import React, { useState, useEffect, useRef } from 'react';
+import useStore from '@/AppBuilder/_stores/store';
+import { shallow } from 'zustand/shallow';
+import { determineJustifyContentValue } from '@/_helpers/utils';
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+import useTableStore from '@/AppBuilder/Widgets/NewTable/_stores/tableStore';
+import HighLightSearch from '@/AppBuilder/Widgets/NewTable/_components/HighLightSearch';
+import { getMaxHeight } from '../../_utils/helper';
+import useTextColor from '../DataTypes/_hooks/useTextColor';
+import useValidationStyle from '../DataTypes/_hooks/useValidationStyle';
+
+export const StringColumn = ({
+ isEditable,
+ darkMode,
+ handleCellValueChange,
+ textColor,
+ horizontalAlignment,
+ cellValue,
+ column,
+ containerWidth,
+ cell,
+ row,
+ searchText,
+ id,
+}) => {
+ const validateWidget = useStore((state) => state.validateWidget, shallow);
+
+ const cellHeight = useTableStore((state) => state.getTableStyles(id)?.cellHeight, shallow);
+ const isMaxRowHeightAuto = useTableStore((state) => state.getTableStyles(id)?.isMaxRowHeightAuto, shallow);
+ const maxRowHeightValue = useTableStore((state) => state.getTableStyles(id)?.maxRowHeightValue, shallow);
+ const cellTextColor = useTextColor(id, textColor);
+
+ const ref = useRef(null);
+ const [showOverlay, setShowOverlay] = useState(false);
+ const [hovered, setHovered] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+
+ const validationData = validateWidget({
+ validationObject: {
+ regex: { value: column.regex },
+ minLength: { value: column.minLength },
+ maxLength: { value: column.maxLength },
+ customRule: { value: column.customRule },
+ },
+ widgetValue: cellValue,
+ customResolveObjects: { cellValue },
+ });
+ const { isValid, validationError } = validationData;
+ useValidationStyle(id, row, validationError);
+
+ useEffect(() => {
+ setShowOverlay(hovered);
+ }, [hovered]);
+
+ useEffect(() => {
+ if (!isEditable && isEditing) {
+ setIsEditing(false);
+ }
+ }, [isEditable, isEditing]);
+
+ const getOverlay = () => (
+
setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ style={{ color: 'var(--text-primary)' }}
+ >
+ {String(cellValue)}
+
+ );
+
+ const _showOverlay =
+ ref?.current &&
+ (ref?.current?.clientWidth < ref?.current?.children[0]?.offsetWidth ||
+ ref?.current?.clientHeight < ref?.current?.children[0]?.offsetHeight);
+
+ const focusInput = () => {
+ if (ref.current) {
+ ref.current.focus();
+ }
+ };
+
+ const renderEditableContent = () => (
+
+
!hovered && setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ className={`${!isValid ? 'is-invalid h-100' : ''} ${isEditing ? 'h-100 content-editing' : ''} h-100`}
+ >
+
{
+ setIsEditing(false);
+ if (cellValue !== e.target.textContent) {
+ handleCellValueChange(row.index, column.key || column.name, e.target.textContent, row.original);
+ }
+ }}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ e.target.blur();
+ }
+ }}
+ onFocus={(e) => {
+ setIsEditing(true);
+ e.stopPropagation();
+ }}
+ suppressContentEditableWarning={true}
+ >
+
+
+
+ {!isValid && (
+
+ {validationError}
+
+ )}
+
+ );
+
+ const renderReadOnlyContent = () => (
+
!hovered && setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ ref={ref}
+ >
+
+
+
+
+ );
+
+ return (
+
}
+ trigger={_showOverlay && ['hover', 'focus']}
+ rootClose={true}
+ show={_showOverlay && showOverlay && !isEditing}
+ >
+ {isEditable ? renderEditableContent() : renderReadOnlyContent()}
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Tags.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Tags.jsx
new file mode 100644
index 0000000000..aee8f8aa6d
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Tags.jsx
@@ -0,0 +1,181 @@
+import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+
+export const TagsColumn = ({ value: initialValue, onChange, readOnly, containerWidth = '' }) => {
+ const [showOverlay, setShowOverlay] = useState(false);
+ const [hovered, setHovered] = useState(false);
+ const [showForm, setShowForm] = useState(false);
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ setShowOverlay(hovered);
+ }, [hovered]);
+
+ const isValid = Array.isArray(initialValue);
+ if (!isValid) console.warn('[Tags]: value provided is not an array');
+ const value = useMemo(() => (isValid ? initialValue : []), [initialValue, isValid]);
+
+ const handleAddTag = useCallback(
+ (text) => {
+ if (!text.trim()) {
+ setShowForm(false);
+ return;
+ }
+ onChange([...value, text]);
+ setShowForm(false);
+ },
+ [value, onChange]
+ );
+
+ const handleRemoveTag = useCallback(
+ (text) => {
+ const newValue = value.filter((tag) => tag !== text);
+ onChange(newValue);
+ setShowForm(false);
+ },
+ [value, onChange]
+ );
+
+ const handleFormKeyDown = useCallback(
+ (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleAddTag(e.target.value);
+ } else if (e.key === 'Escape') {
+ setShowForm(false);
+ }
+ },
+ [handleAddTag]
+ );
+
+ const renderTag = useCallback(
+ (text) => (
+
+ {String(text)}
+ {!readOnly && (
+ handleRemoveTag(text)}
+ onKeyDown={(e) => e.key === 'Enter' && handleRemoveTag(text)}
+ role="button"
+ tabIndex={0}
+ aria-label={`Remove ${text}`}
+ >
+ x
+
+ )}
+
+ ),
+ [readOnly, handleRemoveTag]
+ );
+
+ const getOverlay = useCallback(
+ (value, containerWidth) => {
+ const darkMode = localStorage.getItem('darkMode') === 'true';
+ return Array.isArray(value) ? (
+
setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ role="list"
+ aria-label="Tags overlay"
+ >
+ {value?.map((tag, index) => (
+
+ {renderTag(tag)}
+
+ ))}
+
+ ) : (
+
+ );
+ },
+ [renderTag]
+ );
+
+ const isOverflowing = useCallback(() => {
+ if (!containerRef.current) return false;
+ return (
+ containerRef.current.clientHeight < containerRef.current.scrollHeight ||
+ containerRef.current.clientWidth < containerRef.current.scrollWidth
+ );
+ }, []);
+
+ return (
+
= 1 ? ['click'] : []}
+ rootClose={true}
+ show={isOverflowing() && value?.length >= 1 && showOverlay}
+ >
+ !hovered && setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ role="region"
+ aria-label="Tags container"
+ >
+ {!showForm && (
+
+ {value.map((item, index) => (
+
+ {renderTag(item)}
+
+ ))}
+
+ )}
+
+ {!showForm && !readOnly && (
+
+
+ setShowForm(true)}
+ onKeyDown={(e) => e.key === 'Enter' && setShowForm(true)}
+ role="button"
+ tabIndex={0}
+ aria-label="Add new tag"
+ >
+ +
+
+
+
+ )}
+
+ {showForm && (
+
+ handleAddTag(e.target.value)}
+ onKeyDown={handleFormKeyDown}
+ aria-label="New tag input"
+ />
+
+ )}
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Text.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Text.jsx
new file mode 100644
index 0000000000..48f37bd589
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Text.jsx
@@ -0,0 +1,188 @@
+import React, { useState, useRef, useCallback, useMemo } from 'react';
+import { determineJustifyContentValue } from '@/_helpers/utils';
+import DOMPurify from 'dompurify';
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+import useStore from '@/AppBuilder/_stores/store';
+import { shallow } from 'zustand/shallow';
+import HighLightSearch from '@/AppBuilder/Widgets/NewTable/_components/HighLightSearch';
+import useTableStore from '@/AppBuilder/Widgets/NewTable/_stores/tableStore';
+import { getMaxHeight } from '../../_utils/helper';
+import useTextColor from '../DataTypes/_hooks/useTextColor';
+
+export const TextColumn = ({
+ id,
+ isEditable,
+ darkMode,
+ handleCellValueChange,
+ textColor,
+ cellValue,
+ column,
+ containerWidth,
+ cell,
+ horizontalAlignment,
+ searchText,
+}) => {
+ const cellHeight = useTableStore((state) => state.getTableStyles(id)?.cellHeight, shallow);
+ const isMaxRowHeightAuto = useTableStore((state) => state.getTableStyles(id)?.isMaxRowHeightAuto, shallow);
+ const maxRowHeightValue = useTableStore((state) => state.getTableStyles(id)?.maxRowHeightValue, shallow);
+ const cellTextColor = useTextColor(id, textColor);
+
+ const [showOverlay, setShowOverlay] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+ const containerRef = useRef(null);
+ const cellRef = useRef(null);
+
+ const validateWidget = useStore((state) => state.validateWidget, shallow);
+ const { isValid, validationError } = validateWidget({
+ validationObject: {
+ minLength: { value: column.minLength },
+ maxLength: { value: column.maxLength },
+ customRule: { value: column.customRule },
+ },
+ widgetValue: cellValue,
+ customResolveObjects: { cellValue },
+ });
+
+ const handleContentChange = useCallback(
+ (content) => {
+ handleCellValueChange(cell.row.index, column.key || column.name, content, cell.row.original);
+ },
+ [cell.row, column, handleCellValueChange]
+ );
+
+ const handleKeyDown = useCallback(
+ (e) => {
+ if (e.key === 'Enter' && !e.shiftKey && isEditable) {
+ e.preventDefault();
+ e.target.blur();
+ }
+ },
+ [isEditable]
+ );
+
+ const isOverflowing = useCallback(() => {
+ if (!containerRef.current || !cellRef.current) return false;
+ const { clientWidth, clientHeight, scrollWidth, scrollHeight } = cellRef.current;
+ return clientWidth < scrollWidth || clientHeight < scrollHeight;
+ }, []);
+
+ const cellStyle = useMemo(
+ () => ({
+ color: cellTextColor || 'inherit',
+ maxHeight: getMaxHeight(isMaxRowHeightAuto, maxRowHeightValue, cellHeight),
+ }),
+ [cellTextColor, isMaxRowHeightAuto, maxRowHeightValue, cellHeight]
+ );
+
+ const focusInput = () => {
+ if (cellRef.current) {
+ cellRef.current.focus();
+ }
+ };
+
+ const renderContent = useCallback(() => {
+ if (!isEditable) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
{
+ setIsEditing(false);
+ if (cellValue !== e.target.textContent) {
+ handleContentChange(e.target.innerHTML);
+ }
+ }}
+ onKeyDown={handleKeyDown}
+ onFocus={() => setIsEditing(true)}
+ suppressContentEditableWarning={true}
+ >
+
+
+ );
+ }, [
+ isEditable,
+ isValid,
+ darkMode,
+ cellTextColor,
+ containerWidth,
+ handleKeyDown,
+ cellValue,
+ searchText,
+ horizontalAlignment,
+ cellStyle,
+ handleContentChange,
+ ]);
+
+ return (
+
+
+
+ ) : (
+
+ )
+ }
+ trigger={isOverflowing() && ['hover']}
+ rootClose={true}
+ show={isOverflowing() && showOverlay && !isEditing}
+ >
+
+
setShowOverlay(true)}
+ onMouseLeave={() => setShowOverlay(false)}
+ ref={containerRef}
+ className={`${!isValid ? 'is-invalid h-100' : ''} ${isEditing ? 'h-100 content-editing' : ''}`}
+ >
+ {renderContent()}
+
+ {isEditable && !isValid && (
+
+ {validationError}
+
+ )}
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Toggle.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Toggle.jsx
new file mode 100644
index 0000000000..1d209bd78a
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/Toggle.jsx
@@ -0,0 +1,39 @@
+// TODO: Remove getTableColumnEvents when the toggle column is removed as it is used only for the toggle column
+import React, { useState, useCallback, useEffect } from 'react';
+import useTableStore from '../../_stores/tableStore';
+
+export const ToggleColumn = ({ id, value, readOnly, onChange, activeColor }) => {
+ const [isOn, setIsOn] = useState(() => value);
+
+ const { getTableColumnEvents } = useTableStore();
+
+ // Sync internal state with external value
+ useEffect(() => {
+ setIsOn(value);
+ }, [value]);
+
+ const handleToggle = useCallback(() => {
+ if (!readOnly) {
+ const newValue = !isOn;
+ setIsOn(newValue);
+ onChange(newValue, getTableColumnEvents(id));
+ }
+ }, [isOn, readOnly, onChange, id, getTableColumnEvents]);
+
+ return (
+
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/_components/CustomDatePickerHeader.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/_components/CustomDatePickerHeader.jsx
new file mode 100644
index 0000000000..98cab64487
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/_components/CustomDatePickerHeader.jsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+import moment from 'moment';
+import { range } from 'lodash';
+
+const CustomDatePickerHeader = ({
+ date,
+ changeYear,
+ changeMonth,
+ decreaseMonth,
+ increaseMonth,
+ prevMonthButtonDisabled,
+ nextMonthButtonDisabled,
+}) => {
+ const months = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+ ];
+
+ const years = range(1900, 2101);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default CustomDatePickerHeader;
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/_hooks/useTextColor.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/_hooks/useTextColor.js
new file mode 100644
index 0000000000..e336b70a18
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/_hooks/useTextColor.js
@@ -0,0 +1,11 @@
+import useTableStore from '../../../_stores/tableStore';
+import { shallow } from 'zustand/shallow';
+
+export default function useTextColor(id, cellTextColor) {
+ const textColor = useTableStore((state) => state.getTableStyles(id)?.textColor, shallow);
+
+ if (!cellTextColor || cellTextColor === '#11181C') {
+ return textColor;
+ }
+ return cellTextColor;
+}
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/_hooks/useValidationStyle.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/_hooks/useValidationStyle.js
new file mode 100644
index 0000000000..2dda1603b5
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/_hooks/useValidationStyle.js
@@ -0,0 +1,10 @@
+export default function useValidationStyle(id, row, validationError) {
+ if (validationError) {
+ const elem = document.getElementById(id)?.querySelector(`[data-index="${row.index}"]`);
+ if (elem) {
+ elem.style.maxHeight = '';
+ elem.style.height = '';
+ }
+ }
+ return null;
+}
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/index.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/index.js
new file mode 100644
index 0000000000..ee2fb701a0
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/index.js
@@ -0,0 +1,16 @@
+export { StringColumn } from './String';
+export { NumberColumn } from './Number';
+export { BooleanColumn } from './Boolean';
+export { TagsColumn } from './Tags';
+export { RadioColumn } from './Radio';
+export { ToggleColumn } from './Toggle';
+export { DatepickerColumn } from './Datepicker';
+export { LinkColumn } from './Link';
+export { ImageColumn } from './Image';
+export { CustomSelectColumn } from './CustomSelect';
+export { CustomDropdownColumn } from './CustomDropdown';
+// export { MultiSelectColumn } from './MultiSelect';
+// export { SelectColumn } from './Select';
+export { TextColumn } from './Text';
+export { JsonColumn } from './JSON';
+export { MarkdownColumn } from './Markdown';
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/Footer.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/Footer.jsx
new file mode 100644
index 0000000000..9fb6fe7cba
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/Footer.jsx
@@ -0,0 +1,109 @@
+import React, { useState, memo } from 'react';
+// Store files
+import useTableStore from '../../_stores/tableStore';
+// Local components
+import { Pagination } from './_components/Pagination/Pagination';
+import { AddNewRow } from './_components/AddNewRow';
+import { shallow } from 'zustand/shallow';
+import { LoadingFooter } from './_components/LoadingFooter';
+import { ControlButtons } from './_components/ControlButtons';
+import { ChangeSetUI } from './_components/ChangeSetUI';
+import { RowCount } from './_components/RowCount';
+
+export const Footer = memo(
+ ({
+ id,
+ darkMode,
+ height,
+ width,
+ allColumns = [],
+ table,
+ pageIndex,
+ componentName,
+ handleChangesSaved,
+ handleChangesDiscarded,
+ fireEvent,
+ setExposedVariables,
+ pageCount,
+ dataLength,
+ columnVisibility, // Passed to trigger a re-render when columnVisibility changes
+ }) => {
+ const isFooterVisible = useTableStore((state) => state.getFooterVisibility(id), shallow);
+ const loadingState = useTableStore((state) => state.getLoadingState(id), shallow);
+ const editedRows = useTableStore((state) => state.getAllEditedRows(id), shallow);
+
+ const enablePagination = useTableStore((state) => state.getTableProperties(id)?.enablePagination, shallow);
+ const showBulkUpdateActions = useTableStore(
+ (state) => state.getTableProperties(id)?.showBulkUpdateActions,
+ shallow
+ );
+
+ const [showAddNewRowPopup, setShowAddNewRowPopup] = useState(false);
+
+ // Hide footer if the properties are not enabled
+ if (!isFooterVisible) return null;
+
+ // Loading state for footer
+ if (loadingState) {
+ return
;
+ }
+
+ const hideAddNewRowPopup = () => {
+ setShowAddNewRowPopup(false);
+ };
+
+ const renderRowCount = () => {
+ return
;
+ };
+
+ const renderChangeSetUI = () => {
+ return (
+
+ );
+ };
+
+ return (
+ <>
+
+
+
+ {editedRows.size > 0 && showBulkUpdateActions ? renderChangeSetUI() : renderRowCount()}
+
+ {enablePagination && (
+
+ )}
+
+
+
+ {showAddNewRowPopup && (
+
+ )}
+ >
+ );
+ }
+);
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/AddNewRow.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/AddNewRow.jsx
new file mode 100644
index 0000000000..8d50f9e513
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/AddNewRow.jsx
@@ -0,0 +1,214 @@
+import React, { useEffect, useMemo, useCallback, useRef } from 'react';
+import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table';
+import { ButtonSolid } from '@/_ui/AppButton/AppButton';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+import { Tooltip } from 'react-tooltip';
+import cx from 'classnames';
+import useTableStore from '../../../_stores/tableStore';
+import { shallow } from 'zustand/shallow';
+import generateColumnsData from '../../../_utils/generateColumnsData';
+
+export function AddNewRow({ id, hideAddNewRowPopup, darkMode, allColumns, fireEvent, setExposedVariables }) {
+ const columnProperties = useTableStore((state) => state.getColumnProperties(id), shallow);
+ const addNewRowDetails = useTableStore((state) => state.getAllAddNewRowDetails(id), shallow);
+ const updateAddNewRowDetails = useTableStore((state) => state.updateAddNewRowDetails, shallow);
+ const clearAddNewRowDetails = useTableStore((state) => state.clearAddNewRowDetails, shallow);
+ const updateShouldPersistAddNewRow = useTableStore((state) => state.updateShouldPersistAddNewRow, shallow);
+
+ const addNewRowDetailsLength = addNewRowDetails.size;
+
+ const newEmptyRow = useMemo(() => {
+ return allColumns.reduce((accumulator, column) => {
+ if (column.columnDef?.meta?.skipAddNewRow) return accumulator;
+ const key = column.columnDef.accessorKey;
+ accumulator[key] = '';
+ return accumulator;
+ }, {});
+ }, [allColumns]);
+
+ useEffect(() => {
+ clearAddNewRowDetails(id);
+ updateShouldPersistAddNewRow(id, false);
+ }, [updateShouldPersistAddNewRow, clearAddNewRowDetails, id]);
+
+ useEffect(() => {
+ function discardNewlyAddedRows() {
+ clearAddNewRowDetails(id);
+ hideAddNewRowPopup();
+ }
+ setExposedVariables({ discardNewlyAddedRows });
+ }, [clearAddNewRowDetails, setExposedVariables, id, hideAddNewRowPopup]);
+
+ useEffect(() => {
+ if (addNewRowDetailsLength === 0) {
+ updateAddNewRowDetails(id, addNewRowDetailsLength, newEmptyRow);
+ }
+ }, [addNewRowDetailsLength, id, newEmptyRow, updateAddNewRowDetails]);
+
+ const handleCellValueChange = useCallback(
+ (index, name, value, row) => {
+ const rowDetails = addNewRowDetails.get(index) ?? row;
+ updateAddNewRowDetails(id, index, { ...rowDetails, [name]: value });
+ },
+ [addNewRowDetails, id, updateAddNewRowDetails]
+ );
+
+ const addRowTableRef = useRef();
+
+ const columns = useMemo(
+ () =>
+ [
+ ...generateColumnsData({
+ columnProperties: columnProperties.map((column) => ({
+ ...column,
+ isEditable: true,
+ })),
+ columnSizes: {},
+ defaultColumn: { width: 150 },
+ tableData: addNewRowDetailsLength > 0 ? [...addNewRowDetails.values()] : [newEmptyRow],
+ id,
+ darkMode,
+ fireEvent: () => {},
+ tableRef: addRowTableRef,
+ handleCellValueChange,
+ searchText: '',
+ columnForAddNewRow: true,
+ }).filter(Boolean),
+ ].filter(Boolean),
+ [id, columnProperties, darkMode, handleCellValueChange, newEmptyRow, addNewRowDetails, addNewRowDetailsLength]
+ );
+
+ const table = useReactTable({
+ data: [...addNewRowDetails.values()],
+ columns: columns,
+ getCoreRowModel: getCoreRowModel(),
+ });
+
+ const addNewRow = () => {
+ updateAddNewRowDetails(id, addNewRowDetailsLength, newEmptyRow);
+ };
+
+ const closeAddNewRowPopup = () => {
+ hideAddNewRowPopup();
+ updateShouldPersistAddNewRow(id, true);
+ };
+
+ return (
+
+
+
+
+ Add new rows
+
+
+
+
+
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+
+
+
+
+
+ {flexRender(header.column.columnDef.header, header.getContext())}
+
+
+ |
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+ |
+
+ {flexRender(cell.column.columnDef.cell, {
+ ...cell.getContext(),
+ isEditable: true,
+ })}
+
+ |
+ ))}
+
+ ))}
+
+
+
+
+
+
+ {
+ await fireEvent('onNewRowsAdded');
+ hideAddNewRowPopup();
+ }}
+ size="sm"
+ customStyles={{ padding: '10px 20px' }}
+ >
+ Save
+
+ {
+ hideAddNewRowPopup();
+ clearAddNewRowDetails(id);
+ }}
+ size="sm"
+ customStyles={{ padding: '10px 20px' }}
+ >
+ Discard
+
+
+
+ );
+}
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/ChangeSetUI.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/ChangeSetUI.jsx
new file mode 100644
index 0000000000..b27e429713
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/ChangeSetUI.jsx
@@ -0,0 +1,46 @@
+import React, { memo } from 'react';
+import { ButtonSolid } from '@/_ui/AppButton/AppButton';
+import useTableStore from '../../../_stores/tableStore';
+import { shallow } from 'zustand/shallow';
+import useStore from '@/AppBuilder/_stores/store';
+
+export const ChangeSetUI = memo(({ width, handleChangesSaved, handleChangesDiscarded, id }) => {
+ const onEvent = useStore((state) => state.eventsSlice.onEvent);
+ const tableComponentEvents = useTableStore((state) => state.getTableComponentEvents(id), shallow);
+
+ return (
+ <>
+
{
+ onEvent('onBulkUpdate', tableComponentEvents).then(() => {
+ handleChangesSaved();
+ });
+ }}
+ data-cy={`table-button-save-changes`}
+ size="md"
+ // isLoading={tableDetails.isSavingChanges ? true : false}
+ customStyles={{ minWidth: '32px', padding: width > 650 ? '6px 16px' : 0 }}
+ leftIcon={width > 650 ? '' : 'save'}
+ fill="#FDFDFE"
+ iconWidth="16"
+ >
+ {width > 650 ? Save changes : ''}
+
+
650 ? '6px 16px' : 0 }}
+ leftIcon={width > 650 ? '' : 'cross'}
+ fill={'var(--slate11)'}
+ iconWidth="16"
+ >
+ {width > 650 ? Discard : ''}
+
+ >
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/ControlButtons.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/ControlButtons.jsx
new file mode 100644
index 0000000000..d91b84da40
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/ControlButtons.jsx
@@ -0,0 +1,184 @@
+import React, { memo } from 'react';
+import { ButtonSolid } from '@/_ui/AppButton/AppButton';
+import { Tooltip } from 'react-tooltip';
+import { OverlayTriggerComponent } from './OverlayTriggerComponent';
+import useTableStore from '../../../_stores/tableStore';
+import { shallow } from 'zustand/shallow';
+import IndeterminateCheckbox from '../../IndeterminateCheckbox';
+import Popover from 'react-bootstrap/Popover';
+import { exportToCSV, exportToExcel, exportToPDF } from '@/AppBuilder/Widgets/NewTable/_utils/exportData';
+
+export const ControlButtons = memo(
+ ({ id, table, darkMode, height, componentName, setShowAddNewRowPopup, fireEvent }) => {
+ const showAddNewRowButton = useTableStore((state) => state.getTableProperties(id)?.showAddNewRowButton, shallow);
+ const showDownloadButton = useTableStore((state) => state.getTableProperties(id)?.showDownloadButton, shallow);
+ const hideColumnSelectorButton = useTableStore(
+ (state) => state.getTableProperties(id)?.hideColumnSelectorButton,
+ shallow
+ );
+ const clientSidePagination = useTableStore((state) => state.getTableProperties(id)?.clientSidePagination, shallow);
+ const hasDownloadEvent = useTableStore((state) => state.getHasDownloadEvent(id), shallow);
+
+ const renderButton = (icon, onClick, tooltipId, tooltipContent) => {
+ return (
+ <>
+
+
+ >
+ );
+ };
+
+ const renderOverlay = (id, icon, callBack, tooltipId, tooltipContent) => {
+ const onClick = (e) => {
+ if (document.activeElement === e.currentTarget) {
+ e.currentTarget.blur();
+ }
+ };
+
+ return (
+ <>
+
+
+ {renderButton(icon, onClick, tooltipId, tooltipContent)}
+
+ >
+ );
+ };
+
+ // Haven't seperated this into a separate component because of UI issues
+ const hideColumnsPopover = () => (
+
+
+
+
+
+ Selects All
+
+
+ {table.getAllLeafColumns().map((column) => {
+ const header = column?.columnDef?.header;
+ return (
+ typeof header === 'string' && (
+
+
+
+
+
+ )
+ );
+ })}
+
+
+ );
+
+ // Haven't seperated this into a separate component because of UI issues
+ const downlaodPopover = () => (
+
+
+
+ exportToCSV(table, componentName)}
+ >
+ Download as CSV
+
+ exportToExcel(table, componentName)}
+ >
+ Download as Excel
+
+ exportToPDF(table, componentName)}
+ >
+ Download as PDF
+
+
+
+
+ );
+
+ const renderDownloadButton = () => {
+ // if server side pagination is enabled and download event is associated with the table, then directly fire download event without displaying popover
+ if (hasDownloadEvent && !clientSidePagination) {
+ const onClick = () => {
+ fireEvent('onTableDataDownload');
+ };
+ return renderButton('filedownload', onClick, 'tooltip-for-download-serverside-pagingation', 'Download');
+ }
+
+ return renderOverlay(id, 'filedownload', downlaodPopover, 'tooltip-for-download', 'Download');
+ };
+
+ return (
+
+ {showAddNewRowButton && (
+ <>
+
+ {
+ setShowAddNewRowPopup(true);
+ }}
+ size="md"
+ data-tooltip-id="tooltip-for-add-new-row"
+ data-tooltip-content="Add new row"
+ >
+ >
+ )}
+ {showDownloadButton && renderDownloadButton()}
+ {!hideColumnSelectorButton &&
+ renderOverlay(id, 'eye1', hideColumnsPopover, 'tooltip-for-manage-columns', 'Manage columns')}
+
+ );
+ }
+);
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/LoadingFooter.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/LoadingFooter.jsx
new file mode 100644
index 0000000000..2488ffa381
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/LoadingFooter.jsx
@@ -0,0 +1,20 @@
+import React, { memo } from 'react';
+import Loader from '../../Loader';
+
+export const LoadingFooter = memo(() => {
+ return (
+
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/OverlayTriggerComponent.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/OverlayTriggerComponent.jsx
new file mode 100644
index 0000000000..df0052c225
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/OverlayTriggerComponent.jsx
@@ -0,0 +1,31 @@
+import React, { useState } from 'react';
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+
+// This overlayTriggerComponent is used in the Table component, for download and manage columns popover
+export const OverlayTriggerComponent = ({
+ trigger = 'click',
+ overlay,
+ rootClose = true,
+ placement = 'top',
+ children,
+}) => {
+ const [isOpen, setOpen] = useState(false);
+ const modifiedChild = React.cloneElement(children, {
+ className: `${children.props.className} ${isOpen && 'always-active-btn'}`,
+ });
+
+ return (
+
{
+ setOpen(show);
+ }}
+ >
+ {modifiedChild}
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/Pagination/Pagination.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/Pagination/Pagination.jsx
new file mode 100644
index 0000000000..a16b00a242
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/Pagination/Pagination.jsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { PaginationButton } from './PaginationButton';
+import { PaginationInput } from './PaginationInput';
+import useTableStore from '../../../../_stores/tableStore';
+import { shallow } from 'zustand/shallow';
+
+// TODO: Need to replace all the default data
+
+export const Pagination = function Pagination({ id, pageIndex = 1, tableWidth, table, pageCount }) {
+ const serverSidePagination = useTableStore((state) => state.getTableProperties(id)?.serverSidePagination, shallow);
+ const enablePrevButton = useTableStore((state) => state.getTableProperties(id)?.enablePrevButton, shallow);
+ const enableNextButton = useTableStore((state) => state.getTableProperties(id)?.enableNextButton, shallow);
+
+ const canGoToNextPage = serverSidePagination ? enableNextButton : table.getCanNextPage();
+ const canGoToPreviousPage = serverSidePagination ? enablePrevButton : table.getCanPreviousPage();
+
+ const showGoToFirstAndLast = !serverSidePagination && tableWidth > 460;
+
+ function goToNextPage() {
+ table.nextPage();
+ }
+
+ function goToPreviousPage() {
+ table.previousPage();
+ }
+
+ return (
+
+
+
+ {showGoToFirstAndLast && (
+
{
+ table.firstPage();
+ }}
+ disabled={!canGoToPreviousPage}
+ icon="cheveronleftdouble"
+ dataCy="pagination-button-to-first"
+ />
+ )}
+
+ {
+ goToPreviousPage();
+ }}
+ disabled={!canGoToPreviousPage}
+ icon="cheveronleft"
+ dataCy="pagination-button-to-previous"
+ />
+
+
+
+
+
+
{
+ goToNextPage();
+ }}
+ disabled={!canGoToNextPage}
+ icon="cheveronright"
+ dataCy="pagination-button-to-next"
+ />
+ {showGoToFirstAndLast && (
+ {
+ table.lastPage();
+ }}
+ disabled={!canGoToNextPage}
+ icon="cheveronrightdouble"
+ dataCy="pagination-button-to-last"
+ />
+ )}
+
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/Pagination/PaginationButton.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/Pagination/PaginationButton.jsx
new file mode 100644
index 0000000000..4d8902cad7
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/Pagination/PaginationButton.jsx
@@ -0,0 +1,21 @@
+import React, { memo } from 'react';
+import { ButtonSolid } from '@/_ui/AppButton/AppButton';
+
+export const PaginationButton = memo(({ onClick, disabled, icon, dataCy }) => {
+ return (
+
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/Pagination/PaginationInput.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/Pagination/PaginationInput.jsx
new file mode 100644
index 0000000000..67f1b34718
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/Pagination/PaginationInput.jsx
@@ -0,0 +1,47 @@
+import React, { memo, useState, useEffect } from 'react';
+
+export const PaginationInput = memo(({ pageIndex, serverSidePagination, pageCount, table }) => {
+ // Add local state to handle input value
+ const [inputValue, setInputValue] = useState(pageIndex);
+
+ // Update input value when pageIndex prop changes
+ useEffect(() => {
+ setInputValue(pageIndex);
+ }, [pageIndex]);
+
+ function gotoPage(page) {
+ if (!serverSidePagination) {
+ table.setPageIndex(page - 1);
+ }
+ }
+
+ return (
+
+ {serverSidePagination && {pageIndex}}
+ {!serverSidePagination && (
+ <>
+ {
+ const value = event.target.value;
+
+ // Only update page if value is a valid number and within range
+ const pageNumber = parseInt(value, 10);
+ if (!isNaN(pageNumber) && pageNumber >= 1 && pageNumber <= pageCount) {
+ setInputValue(pageNumber);
+ gotoPage(pageNumber);
+ } else if (value === '') {
+ setInputValue('');
+ }
+ }}
+ />
+
+ of {pageCount || 1}
+
+ >
+ )}
+
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/RowCount.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/RowCount.jsx
new file mode 100644
index 0000000000..fad96e5933
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/_components/RowCount.jsx
@@ -0,0 +1,16 @@
+import React, { memo } from 'react';
+import useTableStore from '../../../_stores/tableStore';
+import { shallow } from 'zustand/shallow';
+
+export const RowCount = memo(({ dataLength, id }) => {
+ const clientSidePagination = useTableStore((state) => state.getTableProperties(id)?.clientSidePagination, shallow);
+ const serverSidePagination = useTableStore((state) => state.getTableProperties(id)?.serverSidePagination, shallow);
+ const totalRecords = useTableStore((state) => state.getTableProperties(id)?.totalRecords, shallow);
+
+ return (
+
+ {clientSidePagination && !serverSidePagination && `${dataLength} Records`}
+ {serverSidePagination && totalRecords ? `${totalRecords} Records` : ''}
+
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/index.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/index.js
new file mode 100644
index 0000000000..467bde3164
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Footer/index.js
@@ -0,0 +1 @@
+export { Footer as default } from './Footer';
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/Header.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/Header.jsx
new file mode 100644
index 0000000000..9e9a6f656f
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/Header.jsx
@@ -0,0 +1,125 @@
+import React, { useState, memo } from 'react';
+import { Tooltip } from 'react-tooltip';
+import { ButtonSolid } from '@/_ui/AppButton/AppButton';
+// Store files
+import useTableStore from '../../_stores/tableStore';
+import { shallow } from 'zustand/shallow';
+// Local Components
+import { SearchBar } from './_components/SearchBar';
+import Loader from '../Loader';
+import { Filter } from './_components/Filter/Filter';
+
+export const Header = memo(
+ ({
+ id,
+ darkMode,
+ fireEvent,
+ setExposedVariables,
+ setGlobalFilter,
+ globalFilter,
+ table,
+ setFilters,
+ appliedFiltersLength,
+ }) => {
+ const displaySearchBox = useTableStore((state) => state.getTableProperties(id)?.displaySearchBox, shallow);
+ const showFilterButton = useTableStore((state) => state.getTableProperties(id)?.showFilterButton, shallow);
+
+ const loadingState = useTableStore((state) => state.getLoadingState(id), shallow);
+ const headerVisibility = useTableStore((state) => state.getHeaderVisibility(id), shallow);
+
+ const appliedFilters = table.getState().columnFilters;
+
+ const [showFilter, setShowFilter] = useState(false);
+
+ // Hide header if the properties are not enabled
+ if (!headerVisibility) return null;
+
+ // Loading state for header
+ if (loadingState) {
+ return (
+
+ );
+ }
+
+ const renderFilter = () => {
+ return (
+
+
+
{
+ if (showFilter) {
+ setShowFilter(false);
+ if (document.activeElement === e.currentTarget) {
+ e.currentTarget.blur();
+ }
+ } else {
+ setShowFilter(true);
+ }
+ }}
+ size="md"
+ data-tooltip-id="tooltip-for-filter-data"
+ data-tooltip-content="Filter data"
+ >
+ {appliedFiltersLength > 0 && (
+
+
+
+ )}
+
+ );
+ };
+
+ const renderSearchBox = () => (
+
+ );
+
+ return (
+ <>
+
+
{showFilterButton && renderFilter()}
+
+ {displaySearchBox && renderSearchBox()}
+
+
+ {showFilter && (
+
+ )}
+ >
+ );
+ }
+);
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/Filter.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/Filter.jsx
new file mode 100644
index 0000000000..905142fc59
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/Filter.jsx
@@ -0,0 +1,167 @@
+import React, { useEffect, useState, useMemo, useCallback, memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { deepClone } from '@/_helpers/utilities/utils.helpers';
+import { FilterRow } from './FilterRow';
+import { FilterFooter } from './FilterFooter';
+import { FilterHeader } from './FilterHeader';
+import { debounce, isEqual } from 'lodash';
+
+export const Filter = memo(({ table, darkMode, setFilters, setShowFilter }) => {
+ const { t } = useTranslation();
+ const [localFilters, setLocalFilters] = useState(table.getState().columnFilters);
+
+ // Memoize columns
+ const columns = useMemo(
+ () =>
+ table
+ .getAllColumns()
+ .filter((column) => !column.columnDef?.meta?.skipFilter)
+ .map((column) => ({
+ name: column.columnDef.header,
+ value: column.id,
+ })),
+ [table]
+ );
+
+ // Memoize callbacks
+ const filterColumnChanged = useCallback(
+ (index, value) => {
+ const filter = columns.find((column) => column.value === value);
+ setLocalFilters((prevFilters) =>
+ prevFilters.map((f, i) =>
+ i === index
+ ? {
+ ...f,
+ id: filter.value,
+ value: { ...f.value, column: filter.name },
+ }
+ : f
+ )
+ );
+ },
+ [columns]
+ );
+
+ const isFilterComplete = (filter) => {
+ if (!filter.id) return false;
+ if (!filter.value?.condition) return false;
+ // For isEmpty/isNotEmpty operations, we don't need a value
+ if (['isEmpty', 'isNotEmpty'].includes(filter.value.condition)) return true;
+ // For other operations, we need a value
+ return filter.value?.value !== undefined && filter.value?.value !== '';
+ };
+
+ const filterOperationChanged = (index, value) => {
+ const newFilters = [...localFilters];
+ newFilters[index].value = {
+ ...newFilters[index].value,
+ condition: value,
+ };
+
+ if (value === 'isEmpty' || value === 'isNotEmpty') {
+ newFilters[index].value.value = '';
+ }
+ setLocalFilters(newFilters);
+ debouncedFilterChanged(newFilters);
+ };
+
+ const debouncedFilterChanged = useCallback(
+ (newFilters) => {
+ const validFilters = newFilters.filter(isFilterComplete);
+ applyFilters(validFilters);
+ },
+ [applyFilters]
+ );
+
+ const filterValueChanged = (index, value) => {
+ const newFilters = [...localFilters];
+ newFilters[index].value = {
+ ...newFilters[index].value,
+ value: value,
+ };
+ setLocalFilters(newFilters);
+ debouncedFilterChanged(newFilters);
+ };
+
+ const addFilter = useCallback(() => {
+ setLocalFilters([...localFilters, { id: '', value: { condition: 'contains', value: '' } }]);
+ }, [localFilters]);
+
+ const clearFilters = useCallback(() => {
+ setLocalFilters([]);
+ applyFilters([]);
+ }, [applyFilters]);
+
+ const removeFilter = (index) => {
+ const newFilters = [...localFilters];
+ newFilters.splice(index, 1);
+ setLocalFilters(newFilters);
+ applyFilters(newFilters.filter((filter) => filter.id !== ''));
+ };
+
+ const debouncedApplyFilters = useMemo(
+ () =>
+ debounce((filters) => {
+ const currentTableFilters = table.getState().columnFilters;
+ if (!isEqual(filters, currentTableFilters)) {
+ setFilters(
+ filters.map((filter) => ({
+ id: filter.id,
+ value: filter.value,
+ }))
+ );
+ }
+ }, 500),
+ [setFilters, table]
+ );
+
+ const applyFilters = useCallback(
+ (filters) => {
+ debouncedApplyFilters(filters);
+ },
+ [debouncedApplyFilters]
+ );
+
+ // Cleanup debounce on unmount
+ useEffect(() => {
+ return () => {
+ debouncedApplyFilters.cancel();
+ };
+ }, [debouncedApplyFilters]);
+
+ useEffect(() => {
+ if (localFilters.length > 0) {
+ const tableFilters = deepClone(localFilters);
+ debouncedFilterChanged(tableFilters);
+ }
+ }, [debouncedFilterChanged, localFilters]);
+
+ return (
+
+
+
+ {localFilters.map((filter, index) => (
+
+ ))}
+ {localFilters.length === 0 && (
+
+
+ no filters yet.
+
+
+ )}
+
+
+
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/FilterFooter.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/FilterFooter.jsx
new file mode 100644
index 0000000000..cbdbb62dc2
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/FilterFooter.jsx
@@ -0,0 +1,30 @@
+import React, { memo } from 'react';
+import { ButtonSolid } from '@/_ui/AppButton/AppButton';
+
+export const FilterFooter = memo(({ addFilter, clearFilters }) => {
+ return (
+
+
+ + add filter
+
+
+
+ clear filters
+
+
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/FilterHeader.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/FilterHeader.jsx
new file mode 100644
index 0000000000..18372739aa
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/FilterHeader.jsx
@@ -0,0 +1,24 @@
+import React, { memo } from 'react';
+
+export const FilterHeader = memo(({ setShowFilter }) => {
+ return (
+
+
+
+ Filters
+
+
+
+
+
+
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/FilterRow.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/FilterRow.jsx
new file mode 100644
index 0000000000..1a3ad2b30d
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/FilterRow.jsx
@@ -0,0 +1,76 @@
+import React, { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import Select from '@/_ui/Select';
+import defaultStyles from '@/_ui/Select/styles';
+import { FILTER_OPTIONS } from './filterConstants';
+
+export const FilterRow = memo(
+ ({ filter, index, columns, darkMode, onColumnChange, onOperationChange, onValueChange, onRemove }) => {
+ const { t } = useTranslation();
+
+ const selectStyles = (width) => {
+ return {
+ ...defaultStyles(darkMode, width),
+ menuPortal: (provided) => ({ ...provided, zIndex: 999 }),
+ menuList: (base) => ({
+ ...base,
+ }),
+ };
+ };
+
+ return (
+
+
+ {index > 0 ? 'and' : 'column'}
+
+
+
+
+
+
+ {!['isEmpty', 'isNotEmpty'].includes(filter.value.condition) && (
+ onValueChange(index, e.target.value)}
+ />
+ )}
+
+
+
+
+
+ );
+ }
+);
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/filterConstants.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/filterConstants.js
new file mode 100644
index 0000000000..a176ffee22
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/filterConstants.js
@@ -0,0 +1,14 @@
+export const FILTER_OPTIONS = [
+ { name: 'contains', value: 'contains' },
+ { name: 'does not contains', value: 'doesNotContains' },
+ { name: 'matches', value: 'matches' },
+ { name: 'does not match', value: 'nl' },
+ { name: 'equals', value: 'equals' },
+ { name: 'does not equal', value: 'ne' },
+ { name: 'is empty', value: 'isEmpty' },
+ { name: 'is not empty', value: 'isNotEmpty' },
+ { name: 'greater than', value: 'gt' },
+ { name: 'less than', value: 'lt' },
+ { name: 'greater than or equals', value: 'gte' },
+ { name: 'less than or equals', value: 'lte' },
+];
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/filterUtils.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/filterUtils.js
new file mode 100644
index 0000000000..cd85a63795
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/Filter/filterUtils.js
@@ -0,0 +1,110 @@
+// eslint-disable-next-line import/no-unresolved
+import { diff as deepDiff } from 'deep-object-diff';
+
+export const filterFunctions = {
+ contains: (row, columnId, filterValue) => {
+ const value = String(row.getValue(columnId) || '').toLowerCase();
+ return value.includes(String(filterValue.value || '').toLowerCase());
+ },
+ doesNotContains: (row, columnId, filterValue) => {
+ const value = String(row.getValue(columnId) || '').toLowerCase();
+ return !value.includes(String(filterValue.value || '').toLowerCase());
+ },
+ matches: (row, columnId, filterValue) => {
+ const value = String(row.getValue(columnId) || '');
+ try {
+ const regex = new RegExp(filterValue.value);
+ return regex.test(value);
+ } catch (e) {
+ return false;
+ }
+ },
+ nl: (row, columnId, filterValue) => {
+ const value = String(row.getValue(columnId) || '');
+ try {
+ const regex = new RegExp(filterValue.value);
+ return !regex.test(value);
+ } catch (e) {
+ return false;
+ }
+ },
+ equals: (row, columnId, filterValue) => {
+ const value = String(row.getValue(columnId) || '').toLowerCase();
+ return value === String(filterValue.value || '').toLowerCase();
+ },
+ ne: (row, columnId, filterValue) => {
+ const value = String(row.getValue(columnId) || '').toLowerCase();
+ return value !== String(filterValue.value || '').toLowerCase();
+ },
+ isEmpty: (row, columnId) => {
+ const value = row.getValue(columnId);
+ return !value || value.length === 0;
+ },
+ isNotEmpty: (row, columnId) => {
+ const value = row.getValue(columnId);
+ return value && value.length > 0;
+ },
+ gt: (row, columnId, filterValue) => {
+ const value = row.getValue(columnId);
+ return Number(value) > Number(filterValue.value);
+ },
+ lt: (row, columnId, filterValue) => {
+ const value = row.getValue(columnId);
+ return Number(value) < Number(filterValue.value);
+ },
+ gte: (row, columnId, filterValue) => {
+ const value = row.getValue(columnId);
+ return Number(value) >= Number(filterValue.value);
+ },
+ lte: (row, columnId, filterValue) => {
+ const value = row.getValue(columnId);
+ return Number(value) <= Number(filterValue.value);
+ },
+};
+
+export const findFilterDiff = (oldFilters, newFilters) => {
+ const filterDiff = deepDiff(oldFilters, newFilters);
+
+ const getType = (obj) => {
+ if (!obj?.column && !obj?.condition) return 'value';
+ if (obj?.column) return 'column';
+ if (obj?.condition) return 'condition';
+ };
+
+ const diff = Object.entries(filterDiff).reduce((acc, [key, value]) => {
+ const type = getType(value?.value);
+ return { ...acc, keyIndex: key, type: type, diff: value?.value?.[type] };
+ }, {});
+
+ return shouldFireEvent(diff, newFilters);
+};
+
+const shouldFireEvent = (diff, filter) => {
+ if (!diff || !filter) return false;
+
+ const forEmptyOperationAndNotEmptyOperation = (condition) => {
+ if (condition !== 'isEmpty' || condition !== 'isNotEmpty') {
+ return filter[diff.keyIndex]?.value?.column ? true : false;
+ }
+ return filter[diff.keyIndex]?.value?.value && filter[diff.keyIndex]?.value?.column ? true : false;
+ };
+
+ switch (diff.type) {
+ case 'value':
+ return filter[diff.keyIndex]?.value?.column && filter[diff.keyIndex]?.value?.condition ? true : false;
+ case 'column':
+ return filter[diff.keyIndex]?.value?.value && filter[diff.keyIndex]?.value?.condition ? true : false;
+ case 'condition':
+ return forEmptyOperationAndNotEmptyOperation(filter[diff.keyIndex]?.value?.condition);
+ default:
+ return false;
+ }
+};
+
+export const applyFilters = (row, columnId, columnFilters) =>
+ columnFilters.every((filter) => {
+ const { value, condition } = filter.value;
+ const filterFn = filterFunctions[condition];
+ const result = filterFn(row, columnId, { value });
+ return result;
+ });
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/SearchBar.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/SearchBar.jsx
new file mode 100644
index 0000000000..8a4945f808
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/_components/SearchBar.jsx
@@ -0,0 +1,51 @@
+import React, { useMemo, useState, memo } from 'react';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+import { debounce } from 'lodash';
+
+// Table Search
+export const SearchBar = memo(({ globalFilter = '', setGlobalFilter }) => {
+ const [value, setValue] = useState(globalFilter);
+
+ const onChange = (filterValue) => {
+ setGlobalFilter(filterValue || undefined);
+ };
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const debouncedChange = useMemo(() => debounce(onChange, 500), []);
+
+ return (
+
+
+
+
{
+ setValue(e.target.value);
+ debouncedChange(e.target.value);
+ }}
+ placeholder="Search"
+ data-cy="search-input-field"
+ style={{
+ border: '0',
+ }}
+ />
+
{
+ // setGlobalFilter(undefined);
+ setValue('');
+ debouncedChange('');
+ }}
+ >
+
+
+
+
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/index.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/index.js
new file mode 100644
index 0000000000..a70ad39d09
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Header/index.js
@@ -0,0 +1 @@
+export { Header as default } from './Header';
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/HighLightSearch/HighLightSearch.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/HighLightSearch/HighLightSearch.jsx
new file mode 100644
index 0000000000..af44ba429b
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/HighLightSearch/HighLightSearch.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+export const HighLightSearch = React.memo(({ text, searchTerm }) => {
+ if (text === '') return null;
+
+ if (searchTerm === '' || !text.toString()?.toLowerCase().includes(searchTerm?.toLowerCase()))
+ return
{text};
+
+ const parts = String(text).split(new RegExp(`(${searchTerm})`, 'gi'));
+
+ return (
+
+ {parts.map((part, index) =>
+ part?.toLowerCase() === searchTerm?.toLowerCase() ? (
+
+ {part}
+
+ ) : (
+ part
+ )
+ )}
+
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/HighLightSearch/index.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/HighLightSearch/index.js
new file mode 100644
index 0000000000..06aa5a2c33
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/HighLightSearch/index.js
@@ -0,0 +1 @@
+export { HighLightSearch as default } from './HighLightSearch';
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/IndeterminateCheckbox/IndeterminateCheckbox.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/IndeterminateCheckbox/IndeterminateCheckbox.jsx
new file mode 100644
index 0000000000..5c2df369da
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/IndeterminateCheckbox/IndeterminateCheckbox.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+
+export const IndeterminateCheckbox = React.forwardRef(({ indeterminate, fireEvent, isCell = false, ...rest }, ref) => {
+ const defaultRef = React.useRef();
+ const resolvedRef = ref || defaultRef;
+
+ React.useEffect(() => {
+ resolvedRef.current.indeterminate = indeterminate;
+ }, [resolvedRef, indeterminate]);
+
+ return (
+
+ {
+ if (fireEvent) {
+ //! This is a hack to make sure the event is fired after exposed values are updated
+ setTimeout(() => {
+ fireEvent('onRowClicked');
+ }, 0);
+ }
+ }}
+ {...rest}
+ style={{
+ width: 16,
+ height: 16,
+ }}
+ onMouseDown={(e) => e.preventDefault()}
+ />
+
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/IndeterminateCheckbox/index.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/IndeterminateCheckbox/index.js
new file mode 100644
index 0000000000..0ae23da5cc
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/IndeterminateCheckbox/index.js
@@ -0,0 +1 @@
+export { IndeterminateCheckbox as default } from './IndeterminateCheckbox';
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Loader/Loader.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/Loader/Loader.jsx
new file mode 100644
index 0000000000..e2ef0cbbae
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Loader/Loader.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+
+export const Loader = React.memo(({ width, height }) => (
+
+
+
+));
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/Loader/index.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/Loader/index.js
new file mode 100644
index 0000000000..771bddd231
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/Loader/index.js
@@ -0,0 +1 @@
+export { Loader as default } from './Loader';
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableContainer/TableContainer.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableContainer/TableContainer.jsx
new file mode 100644
index 0000000000..11a3fbe18f
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableContainer/TableContainer.jsx
@@ -0,0 +1,193 @@
+import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
+import useTableStore from '../../_stores/tableStore';
+import TableExposedVariables from '../TableExposedVariables';
+import Header from '../Header';
+import Footer from '../Footer';
+import { buildTableColumn } from '../../_utils/buildTableColumn';
+import { useTable } from '../../_hooks/useTable';
+import { shallow } from 'zustand/shallow';
+import TableData from '../TableData';
+
+export const TableContainer = ({
+ id,
+ data,
+ width,
+ height,
+ darkMode,
+ componentName,
+ fireEvent,
+ setExposedVariables,
+}) => {
+ const { getColumnProperties, getEditedRowFromIndex, getEditedFieldsOnIndex, updateEditedRowsAndFields } =
+ useTableStore();
+
+ const columnProperties = getColumnProperties(id);
+ // Table properties
+ const showBulkSelector = useTableStore((state) => state.getTableProperties(id)?.showBulkSelector, shallow);
+ const enableSorting = useTableStore((state) => state.getTableProperties(id)?.enabledSort, shallow);
+ const columnSizes = useTableStore((state) => state.getTableProperties(id)?.columnSizes, shallow);
+
+ // Server side properties
+ const serverSidePagination = useTableStore((state) => state.getTableProperties(id)?.serverSidePagination, shallow);
+ const serverSideSort = useTableStore((state) => state.getTableProperties(id)?.serverSideSort, shallow);
+ const serverSideFilter = useTableStore((state) => state.getTableProperties(id)?.serverSideFilter, shallow);
+ const serverSideSearch = useTableStore((state) => state.getTableProperties(id)?.serverSideSearch, shallow);
+ const rowsPerPage = useTableStore((state) => state.getTableProperties(id)?.rowsPerPage, shallow);
+ const clearEditedRows = useTableStore((state) => state.clearEditedRows, shallow);
+
+ const actions = useTableStore((state) => state.getActions(id), shallow);
+
+ const [globalFilter, setGlobalFilter] = useState('');
+ const lastClickedRowRef = useRef([]);
+ const tableBodyRef = useRef(null);
+
+ const handleCellValueChange = useCallback(
+ (index, name, value, row) => {
+ const rowDetails = getEditedRowFromIndex(id, index) ?? row;
+ const editedFields = getEditedFieldsOnIndex(id, index) ?? {};
+ updateEditedRowsAndFields(id, index, { ...rowDetails, [name]: value }, { ...editedFields, [name]: value });
+ },
+ [getEditedRowFromIndex, id, getEditedFieldsOnIndex, updateEditedRowsAndFields]
+ );
+
+ const columns = useMemo(() => {
+ return buildTableColumn(
+ showBulkSelector,
+ actions,
+ fireEvent,
+ setExposedVariables,
+ id,
+ columnProperties,
+ columnSizes,
+ data,
+ darkMode,
+ handleCellValueChange,
+ globalFilter,
+ serverSideSearch,
+ tableBodyRef
+ );
+ }, [
+ actions,
+ fireEvent,
+ setExposedVariables,
+ id,
+ columnProperties,
+ columnSizes,
+ data,
+ darkMode,
+ handleCellValueChange,
+ globalFilter,
+ showBulkSelector,
+ serverSideSearch,
+ ]);
+
+ const { table, pagination, setPagination, columnVisibility, setColumnFilters, columnOrder, setColumnOrder } =
+ useTable({
+ data,
+ columns,
+ enableSorting,
+ showBulkSelector,
+ serverSidePagination,
+ serverSideSort,
+ serverSideFilter,
+ rowsPerPage,
+ globalFilter,
+ setGlobalFilter,
+ });
+
+ // Memoizing allColumns to avoid re-rendering on every render
+ // New reference for columnOrder is created on every render, so stringifying it
+ const allColumns = useMemo(() => {
+ return table.getAllLeafColumns();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [JSON.stringify(table.getState().columnOrder), table]);
+
+ useEffect(() => {
+ setPagination((prev) => ({
+ ...prev,
+ pageSize: rowsPerPage,
+ }));
+ }, [rowsPerPage, setPagination]);
+
+ useEffect(() => {
+ if (serverSideSearch && globalFilter?.trim() !== '') {
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }));
+ }
+ }, [globalFilter, serverSideSearch, setPagination]);
+
+ useEffect(() => {
+ setColumnOrder(columns.map((column) => column.id));
+ }, [columns, setColumnOrder]);
+
+ const handleFilterChange = useCallback(
+ (filters) => {
+ setColumnFilters(filters);
+ },
+ [setColumnFilters]
+ );
+
+ const clearChangeSet = useCallback(() => {
+ setExposedVariables({ dataUpdates: [], changeSet: {} });
+ clearEditedRows(id);
+ }, [setExposedVariables, clearEditedRows, id]);
+
+ const handleChangesDiscarded = useCallback(() => {
+ clearChangeSet();
+ fireEvent('onCancelChanges');
+ }, [clearChangeSet, fireEvent]);
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableContainer/index.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableContainer/index.js
new file mode 100644
index 0000000000..ec50f834a1
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableContainer/index.js
@@ -0,0 +1 @@
+export { TableContainer as default } from './TableContainer';
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/TableData.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/TableData.jsx
new file mode 100644
index 0000000000..aa9cd74a87
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/TableData.jsx
@@ -0,0 +1,148 @@
+import React, { useEffect, useMemo } from 'react';
+import { LoadingState } from './_components/LoadingState';
+import { EmptyState } from './_components/EmptyState';
+import { TableHeader } from './_components/TableHeader';
+import { TableRow } from './_components/TableRow';
+// eslint-disable-next-line import/no-unresolved
+import { useVirtualizer } from '@tanstack/react-virtual';
+import useTableStore from '../../_stores/tableStore';
+import useStore from '@/AppBuilder/_stores/store';
+import { shallow } from 'zustand/shallow';
+
+export const TableData = ({
+ id,
+ data,
+ tableBodyRef,
+ darkMode,
+ table,
+ columnOrder,
+ setColumnOrder,
+ setExposedVariables,
+ fireEvent,
+ lastClickedRowRef,
+}) => {
+ const getResolvedValue = useStore((state) => state.getResolvedValue);
+ const loadingState = useTableStore((state) => state.getLoadingState(id), shallow);
+
+ const isMaxRowHeightAuto = useTableStore((state) => state.getTableStyles(id)?.isMaxRowHeightAuto, shallow);
+ const rowStyle = useTableStore((state) => state.getTableStyles(id)?.rowStyle, shallow);
+ const cellHeight = useTableStore((state) => state.getTableStyles(id)?.cellHeight, shallow);
+ const maxRowHeightValue = useTableStore((state) => state.getTableStyles(id)?.maxRowHeightValue, shallow);
+ const contentWrap = useTableStore((state) => state.getTableStyles(id)?.contentWrap, shallow);
+
+ const allowSelection = useTableStore((state) => state.getTableProperties(id)?.allowSelection, shallow);
+ const highlightSelectedRow = useTableStore((state) => state.getTableProperties(id)?.highlightSelectedRow, shallow);
+
+ useEffect(() => {
+ if (allowSelection) {
+ if (highlightSelectedRow) {
+ table.getColumn('selection')?.toggleVisibility(false);
+ } else {
+ table.getColumn('selection')?.toggleVisibility(true);
+ }
+ } else {
+ table.getColumn('selection')?.toggleVisibility(false);
+ }
+ }, [allowSelection, highlightSelectedRow, table]);
+
+ const rowStyles = useMemo(() => {
+ let styles = {
+ minHeight: cellHeight === 'condensed' ? '39px' : '45px',
+ display: 'flex',
+ };
+
+ let cellMaxHeight;
+ let calculatedCellHeight;
+ if (contentWrap) {
+ cellMaxHeight = isMaxRowHeightAuto ? 'fit-content' : maxRowHeightValue + 'px';
+ styles.maxHeight = cellMaxHeight;
+ } else {
+ calculatedCellHeight = cellHeight === 'condensed' ? 40 : 46;
+ styles.maxHeight = `${calculatedCellHeight}px`;
+ styles.height = `${calculatedCellHeight}px`;
+ }
+ return styles;
+ }, [cellHeight, contentWrap, isMaxRowHeightAuto, maxRowHeightValue]);
+
+ // Setup virtualizer
+ const rowVirtualizer = useVirtualizer({
+ count: table.getRowModel().rows.length,
+ getScrollElement: () => tableBodyRef.current,
+ estimateSize: () => (cellHeight === 'condensed' ? 40 : 46),
+ overscan: 5,
+ scrollMargin: 0,
+ });
+
+ // Handles row click for row selection
+ const handleRowClick = (row) => {
+ if (!allowSelection) return;
+ lastClickedRowRef.current = row.original;
+
+ // Update row selection
+ row.toggleSelected();
+ };
+
+ const renderTableHeader = () => {
+ return (
+
+ );
+ };
+
+ if (loadingState) {
+ return (
+
+ {renderTableHeader()}
+
+
+ );
+ } else if (data.length === 0) {
+ return (
+
+ {renderTableHeader()}
+
+
+ );
+ }
+
+ return (
+
+
+ {renderTableHeader()}
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => {
+ const row = table.getRowModel().rows[virtualRow.index];
+ return (
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/EmptyState.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/EmptyState.jsx
new file mode 100644
index 0000000000..81df598627
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/EmptyState.jsx
@@ -0,0 +1,23 @@
+import React, { memo } from 'react';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+
+export const EmptyState = memo(() => {
+ return (
+
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/LoadingState.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/LoadingState.jsx
new file mode 100644
index 0000000000..b3acead0cc
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/LoadingState.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+export const LoadingState = React.memo(() => {
+ return (
+
+ );
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableHeader.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableHeader.jsx
new file mode 100644
index 0000000000..67240c5887
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableHeader.jsx
@@ -0,0 +1,167 @@
+import React from 'react';
+import cx from 'classnames';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+import Loader from '../../Loader';
+import useTableStore from '../../../_stores/tableStore';
+import { flexRender } from '@tanstack/react-table';
+import { DndContext, useSensor, useSensors, PointerSensor, closestCenter } from '@dnd-kit/core';
+import { SortableContext, horizontalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
+import { CSS } from '@dnd-kit/utilities';
+import useStore from '@/AppBuilder/_stores/store';
+import { determineJustifyContentValue } from '@/_helpers/utils';
+import { shallow } from 'zustand/shallow';
+
+const DraggableHeader = ({ header, darkMode, id }) => {
+ const { attributes, listeners, setNodeRef, transform, isDragging, setActivatorNodeRef } = useSortable({
+ id: header.id,
+ });
+
+ const columnHeaderWrap = useTableStore((state) => state.getTableStyles(id)?.columnHeaderWrap, shallow);
+ const headerCasing = useTableStore((state) => state.getTableStyles(id)?.headerCasing, shallow);
+
+ const getResolvedValue = useStore.getState().getResolvedValue;
+
+ const column = header.column.columnDef.meta;
+ const isEditable = getResolvedValue(column.isEditable);
+
+ const style = {
+ opacity: isDragging ? 0.8 : 1,
+ position: 'relative',
+ transform: CSS.Translate.toString(transform),
+ transition: 'width transform 0.2s ease-in-out',
+ whiteSpace: 'nowrap',
+ width: header.column.getSize(),
+ zIndex: isDragging ? 1 : 0,
+ };
+
+ return (
+
+ header.column.toggleSorting() : undefined}
+ style={{ cursor: header.column.getCanSort() ? 'pointer' : 'default', width: '100%' }}
+ >
+
+
+ {column.columnType !== 'selector' && column.columnType !== 'image' && isEditable && (
+
+ )}
+
+
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
+
+
+ {header.column.getIsSorted() && (
+
+
+
+ )}
+
+ {header.column.getCanResize() && (
+
+ )}
+ |
+ );
+};
+
+export const TableHeader = ({ id, table, darkMode, columnOrder, setColumnOrder }) => {
+ const { getLoadingState } = useTableStore();
+ const loadingState = getLoadingState(id);
+
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 1,
+ delay: 250,
+ },
+ })
+ );
+
+ const onDragEnd = (event) => {
+ const { active, over } = event;
+ if (!over || active.id === over.id) return;
+
+ setColumnOrder((currentOrder) => {
+ const oldIndex = currentOrder.indexOf(active.id);
+ const newIndex = currentOrder.indexOf(over.id);
+ return arrayMove(columnOrder, oldIndex, newIndex);
+ });
+ };
+
+ if (loadingState) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+
+ {headerGroup.headers.map((header) => (
+
+ ))}
+
+
+ ))}
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableRow.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableRow.jsx
new file mode 100644
index 0000000000..56009fd71b
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableRow.jsx
@@ -0,0 +1,129 @@
+import React from 'react';
+import cx from 'classnames';
+import { flexRender } from '@tanstack/react-table';
+import useTableStore from '../../../_stores/tableStore';
+import { shallow } from 'zustand/shallow';
+import { determineJustifyContentValue } from '@/_helpers/utils';
+
+export const TableRow = ({
+ id,
+ row,
+ virtualRow,
+ cellHeight,
+ getResolvedValue,
+ handleRowClick,
+ allowSelection,
+ contentWrap,
+ highlightSelectedRow,
+ setExposedVariables,
+ fireEvent,
+ rowStyles,
+ measureElement,
+}) => {
+ const selectRowOnCellEdit = useTableStore((state) => state.getTableProperties(id)?.selectRowOnCellEdit, shallow);
+ const hasHoveredEvent = useTableStore((state) => state.getHasHoveredEvent(id), shallow);
+
+ if (!row) return null;
+
+ return (
+
{
+ handleRowClick(row);
+ }}
+ onMouseEnter={() => {
+ if (hasHoveredEvent) {
+ const hoveredRowDetails = { hoveredRowId: row.id, hoveredRow: row.original };
+ setExposedVariables(hoveredRowDetails);
+ fireEvent('onRowHovered');
+ }
+ }}
+ >
+ {row.getVisibleCells().map((cell) => {
+ const cellStyles = {
+ width: cell.column.getSize(),
+ backgroundColor: getResolvedValue(cell.column.columnDef?.meta?.cellBackgroundColor ?? 'inherit', {
+ rowData: row.original,
+ cellValue: cell.getValue(),
+ }),
+ justifyContent: determineJustifyContentValue(cell.column.columnDef?.meta?.horizontalAlignment),
+ display: 'flex',
+ alignItems: 'center',
+ textAlign: cell.column.columnDef?.meta?.horizontalAlignment,
+ };
+
+ const isEditable = getResolvedValue(cell.column.columnDef?.meta?.isEditable ?? false, {
+ rowData: row.original,
+ cellValue: cell.getValue(),
+ });
+
+ return (
+ | {
+ if (
+ (isEditable || ['rightActions', 'leftActions'].includes(cell.column.id)) &&
+ allowSelection &&
+ !selectRowOnCellEdit
+ ) {
+ // to avoid on click event getting propagating to row when td is editable or has action button and allowSelection is true and selectRowOnCellEdit is false
+ e.stopPropagation();
+ }
+ setExposedVariables({
+ selectedCell: {
+ columnName: cell.column.columnDef?.header,
+ columnKey: cell.column.columnDef?.accessorKey,
+ value: cell.getValue(),
+ },
+ });
+ }}
+ >
+
+ {flexRender(cell.column?.columnDef?.cell, cell.getContext())}
+
+ |
+ );
+ })}
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/index.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/index.js
new file mode 100644
index 0000000000..db7bd89d00
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/index.js
@@ -0,0 +1 @@
+export { TableData as default } from './TableData';
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableExposedVariables/TableExposedVariables.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableExposedVariables/TableExposedVariables.jsx
new file mode 100644
index 0000000000..6ec4f66994
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableExposedVariables/TableExposedVariables.jsx
@@ -0,0 +1,333 @@
+import { useEffect, useCallback, useRef } from 'react';
+import useTableStore from '../../_stores/tableStore';
+import { shallow } from 'zustand/shallow';
+import useStore from '@/AppBuilder/_stores/store';
+import { exportToCSV, exportToExcel, exportToPDF } from '@/AppBuilder/Widgets/NewTable/_utils/exportData';
+import { filterFunctions } from '../Header/_components/Filter/filterUtils';
+import { isArray, debounce } from 'lodash';
+import { useMounted } from '@/_hooks/use-mount';
+import { usePrevious } from '@dnd-kit/utilities';
+// Component to expose variables & fire events from the table
+// It might miss some variables which are tightly coupled with the component state
+export const TableExposedVariables = ({
+ id,
+ data,
+ setExposedVariables,
+ fireEvent,
+ table,
+ componentName,
+ pageIndex = 1,
+ lastClickedRow,
+}) => {
+ const editedRows = useTableStore((state) => state.getAllEditedRows(id), shallow);
+ const editedFields = useTableStore((state) => state.getAllEditedFields(id), shallow);
+ const addNewRowDetails = useTableStore((state) => state.getAllAddNewRowDetails(id), shallow);
+ const allowSelection = useTableStore((state) => state.getTableProperties(id)?.allowSelection, shallow);
+ const showBulkSelector = useTableStore((state) => state.getTableProperties(id)?.showBulkSelector, shallow);
+ const clientSidePagination = useTableStore((state) => state.getTableProperties(id)?.clientSidePagination, shallow);
+ const defaultSelectedRow = useTableStore((state) => state.getTableProperties(id)?.defaultSelectedRow, shallow);
+ const columnSizes = useTableStore((state) => state.getTableProperties(id)?.columnSizes, shallow);
+
+ const setComponentProperty = useStore((state) => state.setComponentProperty, shallow);
+
+ const mounted = useMounted();
+ const previousLastClickedRow = usePrevious(lastClickedRow);
+
+ const {
+ selectedRows,
+ sorting,
+ currentPageData,
+ filteredData,
+ searchText,
+ appliedFilters,
+ toggleAllRowsSelected,
+ setPageIndex,
+ setRowSelection,
+ setColumnFilters,
+ tableData,
+ columns,
+ columnSizing,
+ resetRowSelection,
+ } = {
+ selectedRows: table.getFilteredSelectedRowModel()?.rows,
+ sorting: table.getState()?.sorting,
+ currentPageData: table.getPaginationRowModel()?.rows,
+ filteredData: table.getFilteredRowModel()?.rows,
+ searchText: table.getState().globalFilter ?? '',
+ appliedFilters: table.getState().columnFilters,
+ toggleAllRowsSelected: table.toggleAllRowsSelected,
+ setPageIndex: table.setPageIndex,
+ setRowSelection: table.setRowSelection,
+ setColumnFilters: table.setColumnFilters,
+ tableData: table.getRowModel().rows,
+ columns: table.getAllColumns(),
+ columnSizing: table.getState().columnSizing,
+ resetRowSelection: table.resetRowSelection,
+ };
+
+ const prevSortingLength = useRef(null);
+
+ const getColumnName = useCallback(
+ (columnId) => {
+ const column = table.getColumn(columnId);
+ return column.columnDef.header;
+ },
+ [table]
+ );
+
+ useEffect(() => {
+ setExposedVariables({
+ currentData: data,
+ });
+ }, [data, setExposedVariables]);
+
+ useEffect(() => {
+ let updatedData = [...data];
+ editedRows.forEach((value, key) => {
+ updatedData[key] = value;
+ });
+
+ setExposedVariables({
+ changeSet: Object.fromEntries(editedFields),
+ dataUpdates: Object.fromEntries(editedRows),
+ updatedData: updatedData,
+ });
+ if (editedRows.size > 0) {
+ fireEvent('onCellValueChanged');
+ }
+ }, [editedRows, editedFields, data, setExposedVariables, fireEvent]);
+
+ useEffect(() => {
+ if (addNewRowDetails) {
+ setExposedVariables({
+ newRows: [...addNewRowDetails.values()],
+ });
+ }
+ }, [addNewRowDetails, setExposedVariables]);
+
+ useEffect(() => {
+ if (!allowSelection) {
+ return table.toggleAllRowsSelected(false);
+ }
+ if (allowSelection && !showBulkSelector) {
+ return table.toggleAllRowsSelected(false);
+ }
+ }, [allowSelection, showBulkSelector, table, setExposedVariables]);
+
+ // Expose selected rows
+ useEffect(() => {
+ if (allowSelection && showBulkSelector) {
+ setExposedVariables({
+ selectedRows: selectedRows.map((row) => row.original),
+ selectedRowsId: selectedRows.map((row) => row.id),
+ });
+ } else {
+ setExposedVariables({
+ selectedRows: [],
+ selectedRowsId: [],
+ });
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [selectedRows, allowSelection, setExposedVariables, fireEvent, lastClickedRow, showBulkSelector]); // Didn't add mounted as it's not a dependency
+
+ useEffect(() => {
+ if (allowSelection) {
+ if (previousLastClickedRow?.id !== lastClickedRow?.id) {
+ fireEvent('onRowClicked');
+ }
+ }
+ }, [previousLastClickedRow, lastClickedRow, fireEvent, allowSelection]);
+
+ // Expose page index
+ useEffect(() => {
+ setExposedVariables({ pageIndex });
+ mounted && fireEvent('onPageChanged');
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [pageIndex, setExposedVariables, fireEvent]); // Didn't add mounted as it's not a dependency
+
+ // Expose sort applied
+ useEffect(() => {
+ if (sorting.length > 0) {
+ const sortApplied = [{ column: getColumnName(sorting[0].id), direction: sorting[0].desc ? 'desc' : 'asc' }];
+ setExposedVariables({ sortApplied });
+ fireEvent('onSort');
+ prevSortingLength.current = sorting.length;
+ } else {
+ setExposedVariables({ sortApplied: undefined });
+ prevSortingLength.current && fireEvent('onSort');
+ prevSortingLength.current = null;
+ }
+ }, [sorting, getColumnName, setExposedVariables, fireEvent]);
+
+ // // Expose current page data
+ useEffect(() => {
+ setExposedVariables({ currentPageData: currentPageData.map((row) => row.original) });
+ }, [currentPageData, setExposedVariables]);
+
+ // Expose filtered data
+ useEffect(() => {
+ setExposedVariables({ filteredData: filteredData.map((row) => row.original) });
+ }, [filteredData, setExposedVariables, fireEvent]);
+
+ // Expose search text
+ useEffect(() => {
+ setExposedVariables({ searchText });
+ mounted && fireEvent('onSearch');
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [searchText, setExposedVariables, fireEvent]); // Didn't add mounted as it's not a dependency
+
+ // Expose applied filters
+ useEffect(() => {
+ setExposedVariables({ filters: appliedFilters.map((filter) => filter.value) });
+ mounted && fireEvent('onFilterChanged');
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [appliedFilters, setExposedVariables, fireEvent]); // Didn't add mounted as it's not a dependency
+
+ // CSA for select & deselect all rows in table
+ useEffect(() => {
+ function selectAllRows() {
+ if (showBulkSelector) {
+ toggleAllRowsSelected(true);
+ }
+ }
+ function deselectAllRows() {
+ if (showBulkSelector) {
+ toggleAllRowsSelected(false);
+ }
+ }
+ setExposedVariables({
+ selectAllRows,
+ deselectAllRows,
+ });
+ }, [setExposedVariables, showBulkSelector, toggleAllRowsSelected]);
+
+ // CSA to set page index
+ useEffect(() => {
+ function setPage(targetPageIndex) {
+ setExposedVariables({ pageIndex: targetPageIndex });
+ if (clientSidePagination) setPageIndex(targetPageIndex - 1);
+ }
+ setExposedVariables({ setPage });
+ }, [setPageIndex, setExposedVariables, clientSidePagination]);
+
+ useEffect(() => {
+ resetRowSelection();
+ function selectRow(key, value) {
+ const index = data.findIndex((item) => item[key] == value);
+ const item = index !== -1 ? data[index] : null;
+ if (item) {
+ setRowSelection({ [index]: true });
+ }
+ setExposedVariables({
+ selectedRow: item,
+ selectedRowId: isNaN(item?.id) ? String(item?.id) : item?.id,
+ });
+ }
+ if (defaultSelectedRow) {
+ const key = Object?.keys(defaultSelectedRow)[0] ?? '';
+ const value = defaultSelectedRow?.[key] ?? undefined;
+ if (key && value) {
+ selectRow(key, value);
+ }
+ } else {
+ setExposedVariables({
+ selectedRow: {},
+ selectedRowId: null,
+ });
+ }
+ }, [data, defaultSelectedRow, setExposedVariables, setRowSelection, resetRowSelection]);
+
+ useEffect(() => {
+ if (lastClickedRow) {
+ setExposedVariables({
+ selectedRow: lastClickedRow,
+ selectedRowId: isNaN(lastClickedRow?.id) ? String(lastClickedRow?.id) : lastClickedRow?.id,
+ });
+ }
+ }, [lastClickedRow, setExposedVariables]);
+
+ useEffect(() => {
+ function selectRow(key, value) {
+ const index = data.findIndex((item) => item[key] == value);
+ const item = index !== -1 ? data[index] : null;
+ if (item) {
+ setRowSelection({ [index]: true });
+ }
+ setExposedVariables({
+ selectedRow: item,
+ selectedRowId: isNaN(item?.id) ? String(item?.id) : item?.id,
+ });
+ }
+
+ function deselectRow(key, value) {
+ const index = data.findIndex((item) => item[key] == value);
+ const item = index !== -1 ? data[index] : null;
+ if (item) {
+ setRowSelection({ [index]: false });
+ }
+ setExposedVariables({
+ selectedRow: {},
+ selectedRowId: null,
+ });
+ }
+ setExposedVariables({ selectRow, deselectRow });
+ }, [data, setExposedVariables, setRowSelection]);
+
+ // CSA to set & clear filters
+ useEffect(() => {
+ function setFilters(_filters) {
+ if (!isArray(_filters)) return;
+ const filterArr = [];
+ _filters.forEach((_filter) => {
+ const { column = '', value = '', condition = '' } = _filter;
+ const columnId = columns.find((col) => col.columnDef?.header === column)?.id;
+ if (columnId && filterFunctions[condition]) {
+ filterArr.push({ id: columnId, value: { column, condition, value } });
+ }
+ });
+ setColumnFilters(filterArr);
+ }
+ function clearFilters() {
+ setColumnFilters([]);
+ }
+ setExposedVariables({ clearFilters, setFilters });
+ }, [setColumnFilters, setExposedVariables, columns]);
+
+ // CSA to download table data
+ useEffect(() => {
+ function downloadTableData(format) {
+ switch (format) {
+ case 'csv':
+ exportToCSV(table, componentName);
+ break;
+ case 'xlsx':
+ exportToExcel(table, componentName);
+ break;
+ case 'pdf':
+ exportToPDF(table, componentName);
+ break;
+ }
+ }
+ setExposedVariables({ downloadTableData });
+ }, [componentName, setExposedVariables, table]);
+
+ // Create debounced function using useRef to persist between renders
+ const debouncedSetProperty = useRef(
+ debounce((sizing) => {
+ setComponentProperty(id, 'columnSizes', sizing, 'properties');
+ }, 300)
+ ).current;
+
+ useEffect(() => {
+ if (Object.keys(columnSizing).length > 0) {
+ debouncedSetProperty({ ...columnSizes, ...columnSizing });
+ }
+
+ // Cleanup debounced function on unmount
+ return () => {
+ debouncedSetProperty.cancel();
+ };
+ }, [columnSizing, columnSizes, debouncedSetProperty, id]);
+
+ return null;
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableExposedVariables/index.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableExposedVariables/index.js
new file mode 100644
index 0000000000..c4f2a78571
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableExposedVariables/index.js
@@ -0,0 +1 @@
+export { TableExposedVariables as default } from './TableExposedVariables';
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTable.js b/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTable.js
new file mode 100644
index 0000000000..45d2b4bd11
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTable.js
@@ -0,0 +1,90 @@
+import { useState, useMemo } from 'react';
+import {
+ getCoreRowModel,
+ useReactTable,
+ getSortedRowModel,
+ getPaginationRowModel,
+ getFilteredRowModel,
+} from '@tanstack/react-table';
+import { applyFilters } from '../_components/Header/_components/Filter/filterUtils';
+
+export function useTable({
+ data,
+ columns,
+ enableSorting,
+ showBulkSelector,
+ serverSidePagination,
+ serverSideSort,
+ serverSideFilter,
+ rowsPerPage,
+ globalFilter,
+ setGlobalFilter,
+}) {
+ // Pagination state
+ const [pagination, setPagination] = useState({
+ pageIndex: 0,
+ pageSize: rowsPerPage,
+ });
+
+ const [columnVisibility, setColumnVisibility] = useState({});
+ const [columnFilters, setColumnFilters] = useState([]);
+ const [columnOrder, setColumnOrder] = useState(columns.map((column) => column.id));
+
+ // When the columns change, the data is not getting re-rendered. So, we need to create a new data array
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const newData = useMemo(() => [...data], [data, columns]);
+
+ const table = useReactTable({
+ data: newData,
+ columns,
+ enableSorting,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ enableColumnResizing: true,
+ columnResizeMode: 'onChange',
+ enableRowSelection: true,
+ enableMultiRowSelection: showBulkSelector,
+ state: {
+ pagination,
+ columnVisibility,
+ columnOrder,
+ globalFilter,
+ columnFilters,
+ },
+ onPaginationChange: setPagination,
+ onColumnVisibilityChange: setColumnVisibility,
+ onColumnOrderChange: setColumnOrder,
+ onGlobalFilterChange: setGlobalFilter,
+ onColumnFiltersChange: setColumnFilters,
+ filterFns: {
+ applyFilters: (row, columnId) => {
+ const filters = columnFilters.filter((f) => f.id === columnId);
+ if (filters.length === 0) return true;
+ return applyFilters(row, columnId, filters);
+ },
+ },
+ globalFilterFn: (row, columnId, filterValue) => {
+ const value = String(row.getValue(columnId) || '').toLowerCase();
+ return value.includes(String(filterValue).toLowerCase());
+ },
+ manualPagination: serverSidePagination,
+ manualSorting: serverSideSort,
+ manualFiltering: serverSideFilter,
+ });
+
+ return {
+ table,
+ pagination,
+ setPagination,
+ columnVisibility,
+ setColumnVisibility,
+ globalFilter,
+ setGlobalFilter,
+ columnFilters,
+ setColumnFilters,
+ columnOrder,
+ setColumnOrder,
+ };
+}
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTableProperties.js b/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTableProperties.js
new file mode 100644
index 0000000000..493461ca99
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTableProperties.js
@@ -0,0 +1,18 @@
+import { useMemo } from 'react';
+
+export function useTableProperties(properties) {
+ return useMemo(() => {
+ let visibility = properties?.visibility ?? true;
+ visibility = visibility ? '' : 'none';
+ const disabledState = properties?.disabledState ?? false;
+ const displaySearchBox = properties?.displaySearchBox ?? true;
+ const showFilterButton = properties?.showFilterButton ?? true;
+
+ return {
+ visibility,
+ disabledState,
+ displaySearchBox,
+ showFilterButton,
+ };
+ }, [properties]);
+}
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTableStyles.js b/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTableStyles.js
new file mode 100644
index 0000000000..95eba338af
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_hooks/useTableStyles.js
@@ -0,0 +1,13 @@
+import { useMemo } from 'react';
+
+export function useTableStyles(styles) {
+ return useMemo(() => {
+ const { borderRadius = 0, boxShadow, borderColor } = styles;
+
+ return {
+ borderRadius: Number.parseFloat(borderRadius),
+ boxShadow,
+ borderColor,
+ };
+ }, [styles]);
+}
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_stores/helper.js b/frontend/src/AppBuilder/Widgets/NewTable/_stores/helper.js
new file mode 100644
index 0000000000..6252cab3be
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_stores/helper.js
@@ -0,0 +1,20 @@
+export const removeNullValues = (arr = []) => arr.filter((element) => element !== null);
+
+// utilityForNestedNewRow function is used to construct nested object while adding or updating new row when '.' is present in column key for adding new row
+export const utilityForNestedNewRow = (row) => {
+ let obj = {};
+
+ Object.keys(row).forEach((key) => {
+ let nestedKeys = key.split('.');
+ let tempObj = obj;
+
+ nestedKeys.forEach((nestedKey, i) => {
+ if (!tempObj[nestedKey]) {
+ tempObj[nestedKey] = i === nestedKeys.length - 1 ? row[key] : {};
+ }
+ tempObj = tempObj[nestedKey];
+ });
+ });
+
+ return obj;
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_stores/slices/columnSlice.js b/frontend/src/AppBuilder/Widgets/NewTable/_stores/slices/columnSlice.js
new file mode 100644
index 0000000000..6663f4f2f9
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_stores/slices/columnSlice.js
@@ -0,0 +1,104 @@
+import useStore from '@/AppBuilder/_stores/store';
+import autogenerateColumns from '../../_utils/autoGenerateColumns';
+import { removeNullValues } from '../helper';
+import { isEqual } from 'lodash';
+export const createColumnSlice = (set, get) => ({
+ getColumnProperties: (id) => {
+ return get().components[id]?.columnDetails?.columnProperties ?? [];
+ },
+ setColumnDetails: (
+ id,
+ columns,
+ useDynamicColumn,
+ columnData,
+ firstRowOfTable,
+ autogenerateColumnsFlag,
+ columnDeletionHistory,
+ shouldAutogenerateColumns
+ ) => {
+ set(
+ (state) => {
+ const isDynamicColumnSelected = useDynamicColumn ?? false;
+ if (isDynamicColumnSelected) {
+ state.components[id].columnDetails.useDynamicColumn = isDynamicColumnSelected;
+ state.components[id].columnDetails.columnData = columnData ?? [];
+ } else {
+ state.components[id].columnDetails.useDynamicColumn = false;
+ }
+ if (shouldAutogenerateColumns) {
+ const columnProperties = get().generateColumns(
+ id,
+ columns,
+ firstRowOfTable,
+ isDynamicColumnSelected,
+ autogenerateColumnsFlag,
+ columnDeletionHistory,
+ columnData
+ );
+ state.components[id].columnDetails.columnProperties = columnProperties;
+ state.components[id].columnDetails.transformations = get().generateColumnTransformations(
+ id,
+ columnProperties
+ );
+ } else {
+ const columnProperties = removeNullValues(columns);
+ state.components[id].columnDetails.columnProperties = columnProperties;
+ state.components[id].columnDetails.transformations = get().generateColumnTransformations(
+ id,
+ columnProperties
+ );
+ }
+ },
+ false,
+ { type: 'setColumnDetails', payload: { id, columns, useDynamicColumn, columnData } }
+ );
+ },
+ generateColumns: (
+ id,
+ columns,
+ firstRowOfTable,
+ isDynamicColumnSelected,
+ autogenerateColumnsFlag,
+ columnDeletionHistory,
+ columnData
+ ) => {
+ if (autogenerateColumnsFlag) {
+ const setComponentProperty = useStore.getState().setComponentProperty;
+ const existingGeneratedColumn = get().getColumnProperties(id);
+ const generatedColumns = autogenerateColumns(
+ firstRowOfTable,
+ columns,
+ columnDeletionHistory ?? [],
+ isDynamicColumnSelected,
+ columnData ?? [],
+ autogenerateColumns ?? false
+ );
+
+ if (!isDynamicColumnSelected && !isEqual(existingGeneratedColumn, generatedColumns)) {
+ setComponentProperty(id, 'columns', generatedColumns, 'properties', 'value', false, 'canvas', {
+ skipUndoRedo: true,
+ saveAfterAction: true,
+ });
+ }
+ return generatedColumns;
+ }
+ },
+ generateColumnTransformations: (id, columnProperties) => {
+ const transformations = columnProperties
+ .filter((column) => column.transformation && column.transformation != '{{cellValue}}')
+ .map((column) => {
+ return {
+ key: column.key ? column.key : column.name,
+ transformation: column.transformation,
+ };
+ });
+ const existingTransformations = get().getColumnTransformations(id);
+ if (!isEqual(existingTransformations, transformations)) {
+ return transformations;
+ }
+ return existingTransformations;
+ },
+ getColumnTransformations: (id) => {
+ return get().components[id]?.columnDetails?.transformations ?? [];
+ },
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_stores/slices/initSlice.js b/frontend/src/AppBuilder/Widgets/NewTable/_stores/slices/initSlice.js
new file mode 100644
index 0000000000..f6b8b8cb09
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_stores/slices/initSlice.js
@@ -0,0 +1,282 @@
+import { has } from 'lodash';
+import { utilityForNestedNewRow } from '../helper';
+import { deepClone } from '@/_helpers/utilities/utils.helpers';
+
+export const createInitSlice = (set, get) => ({
+ initializeComponent: (id) => {
+ set(
+ (state) => {
+ if (!state.components[id]) {
+ state.components[id] = {
+ properties: {},
+ styles: {},
+ filters: {},
+ addNewRow: new Map(),
+ shouldPersistAddNewRow: false,
+ editedRowDetails: {
+ editedRows: new Map(),
+ editedFields: new Map(),
+ },
+ events: {
+ hasHoveredEvent: false,
+ hasDownloadEvent: false,
+ tableComponentEvents: [],
+ tableColumnEvents: [],
+ tableActionEvents: [],
+ },
+ columnDetails: {
+ columnProperties: [],
+ transformations: [],
+ },
+ };
+ }
+ },
+ false,
+ { type: 'initializeComponent', payload: { id } }
+ );
+ },
+
+ setTableProperties: (id, properties) =>
+ set(
+ (state) => {
+ const visibility = properties?.visibility ?? true;
+ state.components[id].properties.visibility = visibility ? '' : 'none';
+ state.components[id].properties.disabledState = properties?.disabledState ?? false;
+ state.components[id].loadingState = properties?.loadingState ?? false;
+ state.components[id].properties.displaySearchBox = properties?.displaySearchBox ?? true;
+ state.components[id].properties.showFilterButton = properties?.showFilterButton ?? true;
+ state.components[id].properties.showAddNewRowButton = properties?.showAddNewRowButton ?? true;
+ state.components[id].properties.showDownloadButton = properties?.showDownloadButton ?? true;
+ state.components[id].properties.showBulkUpdateActions = properties?.showBulkUpdateActions ?? true;
+ state.components[id].properties.totalRecords = properties?.totalRecords ?? 10;
+ state.components[id].properties.enablePrevButton = properties?.enablePrevButton ?? true;
+ state.components[id].properties.enableNextButton = properties?.enableNextButton ?? true;
+ state.components[id].properties.hideColumnSelectorButton = properties?.hideColumnSelectorButton ?? false;
+ state.components[id].properties.serverSideSearch = properties?.serverSideSearch ?? false;
+ state.components[id].properties.serverSideSort = properties?.serverSideSort ?? false;
+ state.components[id].properties.serverSideFilter = properties?.serverSideFilter ?? false;
+ state.components[id].properties.showBulkSelector = properties?.showBulkSelector ?? false;
+ state.components[id].properties.highlightSelectedRow = properties?.highlightSelectedRow ?? false;
+ state.components[id].properties.rowsPerPage = properties?.rowsPerPage ?? 10;
+ state.components[id].properties.enabledSort = properties?.enabledSort ?? true;
+ state.components[id].properties.columnSizes = properties?.columnSizes ?? {};
+ state.components[id].properties.allowSelection =
+ properties?.allowSelection ?? (properties?.showBulkSelector || properties?.highlightSelectedRow)
+ ? true
+ : false;
+ state.components[id].properties.defaultSelectedRow = properties?.defaultSelectedRow ?? { id: 1 };
+ state.components[id].properties.selectRowOnCellEdit = properties?.selectRowOnCellEdit ?? true;
+
+ let serverSidePagination = properties.serverSidePagination ?? false;
+ if (typeof serverSidePagination !== 'boolean') state.components[id].properties.serverSidePagination = false;
+ else state.components[id].properties.serverSidePagination = serverSidePagination;
+
+ let clientSidePagination = false;
+ if (
+ properties.clientSidePagination ||
+ typeof clientSidePagination !== 'boolean' ||
+ (properties.enablePagination && !serverSidePagination)
+ ) {
+ clientSidePagination = true;
+ }
+ state.components[id].properties.clientSidePagination = clientSidePagination;
+ if (
+ !has(properties, 'enablePagination') &&
+ (properties.clientSidePagination || properties.serverSidePagination)
+ ) {
+ state.components[id].properties.enablePagination = true;
+ } else {
+ state.components[id].properties.enablePagination = properties.enablePagination;
+ }
+ },
+ false,
+ { type: 'setProperties', payload: { id, properties } }
+ ),
+
+ setTableStyles: (id, styles, darkMode) =>
+ set(
+ (state) => {
+ const {
+ borderRadius = 0,
+ boxShadow,
+ borderColor = 'var(--borders-weak-disabled)',
+ contentWrap = true,
+ textColor,
+ tableType = 'table-bordered',
+ cellSize,
+ actionButtonRadius = 0,
+ maxRowHeight = 'auto',
+ maxRowHeightValue = 80,
+ columnHeaderWrap = 'fixed',
+ headerCasing = 'uppercase',
+ } = styles;
+
+ state.components[id].styles.borderRadius = Number.parseFloat(borderRadius);
+ state.components[id].styles.boxShadow = boxShadow;
+ state.components[id].styles.borderColor = borderColor;
+ state.components[id].styles.contentWrap = contentWrap;
+ state.components[id].styles.textColor = textColor !== '#000' ? textColor : darkMode && '#fff';
+ state.components[id].styles.rowStyle = tableType;
+ state.components[id].styles.cellHeight = cellSize;
+ state.components[id].styles.actionButtonRadius = parseFloat(actionButtonRadius);
+ state.components[id].styles.isMaxRowHeightAuto = maxRowHeight === 'auto';
+ state.components[id].styles.maxRowHeightValue = maxRowHeightValue;
+ state.components[id].styles.columnHeaderWrap = columnHeaderWrap;
+ state.components[id].styles.headerCasing = headerCasing;
+ },
+ false,
+ { type: 'setStyles', payload: { id, styles } }
+ ),
+
+ setTableActions: (id, actions = []) =>
+ set(
+ (state) => {
+ state.components[id].properties.actions = actions.map((action) => {
+ return {
+ ...action,
+ position: action?.position ?? 'right',
+ actionButtonRadius: 0,
+ };
+ });
+ },
+ false,
+ { type: 'setTableActions', payload: { id, actions } }
+ ),
+
+ setTableEvents: (id, events = []) =>
+ set(
+ (state) => {
+ const tableEvents = events.filter((event) => event.sourceId === id);
+ const tableComponentEvents = tableEvents.filter((event) => event.target === 'component');
+ state.components[id].events.tableComponentEvents = tableComponentEvents;
+ state.components[id].events.hasHoveredEvent = tableComponentEvents.some(
+ (event) => event.event.eventId === 'onRowHovered'
+ );
+ state.components[id].events.hasDownloadEvent = tableComponentEvents.some(
+ (event) => event.event.eventId === 'onTableDataDownload'
+ );
+ state.components[id].events.tableColumnEvents = tableEvents.filter((event) => event.target === 'table_column');
+ state.components[id].events.tableActionEvents = tableEvents.filter((event) => event.target === 'table_action');
+ },
+ false,
+ { type: 'setTableEvents', payload: { id, events } }
+ ),
+
+ updateEditedRowsAndFields: (id, index, rowDetail, editedFields) =>
+ set(
+ (state) => {
+ state.components[id].editedRowDetails.editedRows.set(index, rowDetail);
+ state.components[id].editedRowDetails.editedFields.set(index, editedFields);
+ },
+ false,
+ { type: 'updateEditedRowsAndFields', payload: { id, index, rowDetail, editedFields } }
+ ),
+
+ removeComponent: (id) =>
+ set(
+ (state) => {
+ // if the component is not present in the DOM, then remove it - this is to handle the case where the component is removed from the DOM and then added back
+ // Like when the component is moved from one container to another
+ if (state.components[id] && !document.querySelector(`[id="${id}"]`)) {
+ delete state.components[id];
+ }
+ },
+ false,
+ { type: 'removeComponent', payload: { id } }
+ ),
+
+ getTableStyles: (id) => {
+ return get().components[id] ? get().components[id].styles : {};
+ },
+ getTableProperties: (id) => {
+ return get().components[id] ? get().components[id].properties : {};
+ },
+ getLoadingState: (id) => {
+ return get().components[id] ? get().components[id].loadingState : false;
+ },
+ getHeaderVisibility: (id) => {
+ return get().components[id]
+ ? get().components[id].properties.showFilterButton || get().components[id].properties.displaySearchBox
+ : false;
+ },
+ getFooterVisibility: (id) => {
+ return get().components[id]
+ ? get().components[id].properties.enablePagination ||
+ get().components[id].properties.showAddNewRowButton ||
+ get().components[id].properties.showDownloadButton
+ : false;
+ },
+ getMaxRowHeightValue: (id) => {
+ return get().components[id] ? get().components[id].styles.maxRowHeightValue : 80;
+ },
+ getSelectRowOnCellEdit: (id) => {
+ return get().components[id] ? get().components[id].properties.selectRowOnCellEdit : false;
+ },
+ getActions: (id) => {
+ return get().components[id] ? get().components[id].properties.actions : [];
+ },
+ getEnablePagination: (id) => get().components[id]?.properties.enablePagination ?? true,
+ getRowsPerPage: (id) => get().components[id]?.properties.rowsPerPage ?? 10,
+ getAllEditedRows: (id) => get().components[id]?.editedRowDetails.editedRows ?? new Map(),
+ getAllEditedFields: (id) => get().components[id]?.editedRowDetails.editedFields ?? new Map(),
+ getEditedRowFromIndex: (id, index) => get().components[id]?.editedRowDetails.editedRows.get(index),
+ getEditedFieldsOnIndex: (id, index) => get().components[id]?.editedRowDetails.editedFields.get(index),
+ clearEditedRows: (id) =>
+ set(
+ (state) => {
+ state.components[id].editedRowDetails.editedRows.clear();
+ state.components[id].editedRowDetails.editedFields.clear();
+ },
+ false,
+ { type: 'clearEditedRows', payload: { id } }
+ ),
+ getAllAddNewRowDetails: (id) => get().components[id]?.addNewRow,
+ getAddNewRowDetailFromIndex: (id, index) => get().components[id]?.addNewRow.get(index),
+ updateAddNewRowDetails: (id, index, newRow) =>
+ set(
+ (state) => {
+ let transformedNewRow = deepClone(newRow);
+ if (Object.keys(newRow).find((key) => key.includes('.'))) {
+ transformedNewRow = utilityForNestedNewRow(transformedNewRow);
+ }
+ state.components[id].addNewRow.set(index, transformedNewRow);
+ },
+ false,
+ { type: 'updateAddNewRowDetails', payload: { id, index, newRow } }
+ ),
+ clearAddNewRowDetails: (id) =>
+ set(
+ (state) => {
+ if (!state.components[id].shouldPersistAddNewRow) {
+ state.components[id].addNewRow.clear();
+ }
+ },
+ false,
+ { type: 'clearNewRow', payload: { id } }
+ ),
+ updateShouldPersistAddNewRow: (id, value) =>
+ set(
+ (state) => {
+ state.components[id].shouldPersistAddNewRow = value;
+ },
+ false,
+ { type: 'updateShouldPersistAddNewRow', payload: { id, value } }
+ ),
+ getTableComponentEvents: (id) => {
+ return get().components[id]?.events?.tableComponentEvents || [];
+ },
+ getTableActionEvents: (id) => {
+ return get().components[id]?.events?.tableActionEvents || [];
+ },
+ // Remove this when the toggle column is removed as it is used only for the toggle column
+ // TODO: Remove the above comment if this function is added for other columns
+ getTableColumnEvents: (id) => {
+ return get().components[id]?.events?.tableColumnEvents || [];
+ },
+ getHasHoveredEvent: (id) => {
+ return get().components[id]?.events?.hasHoveredEvent || false;
+ },
+ getHasDownloadEvent: (id) => {
+ return get().components[id]?.events?.hasDownloadEvent || false;
+ },
+});
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_stores/tableStore.js b/frontend/src/AppBuilder/Widgets/NewTable/_stores/tableStore.js
new file mode 100644
index 0000000000..93929a14d8
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_stores/tableStore.js
@@ -0,0 +1,113 @@
+import { create, zustandDevTools } from '@/_stores/utils';
+import { immer } from 'zustand/middleware/immer';
+// eslint-disable-next-line import/no-unresolved
+import { enableMapSet } from 'immer';
+
+import { createInitSlice } from './slices/initSlice';
+import { createColumnSlice } from './slices/columnSlice';
+const initialState = {
+ components: {},
+};
+
+enableMapSet();
+const useTableStore = create(
+ zustandDevTools(
+ immer((set, get) => ({
+ ...initialState,
+ ...createInitSlice(set, get),
+ ...createColumnSlice(set, get),
+ })),
+ { name: 'Table Store' }
+ )
+);
+
+export default useTableStore;
+
+// // filterSlice.js
+// export const createFilterSlice = (set) => ({
+// components: {},
+// setFilter: (id, key, value) =>
+// set((state) => {
+// state.components[id] = state.components[id] || { filters: {} };
+// state.components[id].filters[key] = value;
+// }, false, 'filters/setFilter'),
+
+// clearFilters: (id) =>
+// set((state) => {
+// state.components[id] = state.components[id] || { filters: {} };
+// state.components[id].filters = {};
+// }, false, 'filters/clearFilters'),
+// });
+
+// // addRowSlice.js
+// export const createAddRowSlice = (set) => ({
+// components: {},
+// addNewRow: (id, key, value) =>
+// set((state) => {
+// state.components[id] = state.components[id] || { addRow: {} };
+// state.components[id].addRow[key] = value;
+// }, false, 'addRow/addNewRow'),
+
+// removeRow: (id, key) =>
+// set((state) => {
+// if (state.components[id]?.addRow) {
+// delete state.components[id].addRow[key];
+// }
+// }, false, 'addRow/removeRow'),
+// });
+
+// // store.js
+// import { create } from 'zustand';
+// import { devtools } from 'zustand/middleware';
+// import { immer } from 'zustand/middleware/immer';
+// import { createCommonSlice } from './commonSlice';
+// import { createFilterSlice } from './filterSlice';
+// import { createAddRowSlice } from './addRowSlice';
+
+// export const useStore = create(
+// devtools(
+// immer((set) => ({
+// ...createCommonSlice(set),
+// ...createFilterSlice(set),
+// ...createAddRowSlice(set),
+
+// initializeComponent: (id) =>
+// set((state) => {
+// state.components[id] = {
+// filters: {},
+// addRow: {},
+// };
+// }, false, 'components/initialize'),
+
+// removeComponent: (id) =>
+// set((state) => {
+// delete state.components[id];
+// }, false, 'components/remove'),
+// }))
+// )
+// );
+
+// // Selectors
+// export const useCommon = () => useStore((state) => ({
+// darkMode: state.darkMode,
+// toggleDarkMode: state.toggleDarkMode,
+// }));
+
+// export const useFilters = (id) => useStore((state) => ({
+// filters: state.components[id]?.filters || {},
+// setFilter: state.setFilter.bind(null, id),
+// clearFilters: state.clearFilters.bind(null, id),
+// }));
+
+// export const useAddRow = (id) => useStore((state) => ({
+// addRow: state.components[id]?.addRow || {},
+// addNewRow: state.addNewRow.bind(null, id),
+// removeRow: state.removeRow.bind(null, id),
+// }));
+
+// export const useInitializeComponent = () => useStore((state) => ({
+// initializeComponent: state.initializeComponent,
+// removeComponent: state.removeComponent,
+// }));
+
+// export default useStore;
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_utils/autoGenerateColumns.js b/frontend/src/AppBuilder/Widgets/NewTable/_utils/autoGenerateColumns.js
new file mode 100644
index 0000000000..b48f055344
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_utils/autoGenerateColumns.js
@@ -0,0 +1,118 @@
+import _ from 'lodash';
+import { v4 as uuidv4 } from 'uuid';
+
+export default function autogenerateColumns(
+ firstRow = {},
+ existingColumns = [],
+ columnDeletionHistory,
+ useDynamicColumn,
+ dynamicColumn = [],
+ generateNestedColumns
+) {
+ if (useDynamicColumn) {
+ if (dynamicColumn.length > 0 && dynamicColumn[0].name) {
+ const generatedColumns = dynamicColumn.map((item) => {
+ return {
+ id: uuidv4(),
+ ...item,
+ name: item?.name,
+ key: item?.key || item?.name,
+ autogenerated: true,
+ };
+ });
+ return generatedColumns;
+ }
+ return [];
+ }
+
+ const isValueIsPremitiveOrArray = (value) => {
+ if (typeof value !== 'object' || Array.isArray(value)) return true;
+ };
+
+ const isValueIsPlainObject = (value) => {
+ if (typeof value === 'object' && !Array.isArray(value) && value !== null) return true;
+ };
+
+ const limitToOneLevelNestingHelperFunc = (data, presentKey) => {
+ return Object.entries(data).reduce((accumulator, [key, value]) => {
+ if (isValueIsPremitiveOrArray(value)) {
+ accumulator.push(`${presentKey}.${key}`);
+ }
+ return accumulator;
+ }, []);
+ };
+ const generateNestedColumnsHelperFunc = (data, parentKey = '') => {
+ return Object.entries(data).reduce((accumulator, [key, value]) => {
+ const currentKey = parentKey ? `${parentKey}.${key}` : key;
+ if (isValueIsPlainObject(value)) {
+ // if value is object particularly, then we only want nested keys till one level of nesting
+ accumulator.push(...limitToOneLevelNestingHelperFunc(value, currentKey));
+ } else if (isValueIsPremitiveOrArray(value)) {
+ // check if value is premitive or array then simply push current key in the accumulator.
+ accumulator.push(currentKey);
+ }
+ return accumulator;
+ }, []);
+ };
+
+ const generateColumnKeys = (firstRow, generateNestedColumns) => {
+ if (generateNestedColumns) {
+ // This block is responsible to get all the keys, nested keys till one level of nesting from the firstRow
+ return generateNestedColumnsHelperFunc(firstRow);
+ } else {
+ /*
+ return keys whose value is premitive data type to support backward compatibility for older app,
+ where we do not auto-generate column for nested data
+ */
+ return Object.entries(firstRow).reduce((accumulator, [key, value]) => {
+ if (typeof value !== 'object') accumulator.push(key);
+ return accumulator;
+ }, []);
+ }
+ };
+
+ // mapping the keys of first row with one level of nested elements.
+ const keysOfTableData = generateColumnKeys(firstRow, generateNestedColumns);
+
+ const keysOfExistingColumns = existingColumns.map((column) => column?.key || column?.name);
+
+ const keysFromWhichNewColumnsShouldBeGenerated = _.difference(keysOfTableData, [
+ ...keysOfExistingColumns,
+ ...columnDeletionHistory,
+ ]);
+
+ const keysAndDataTypesToGenerateNewColumns = keysFromWhichNewColumnsShouldBeGenerated?.map((key) => [
+ key,
+ typeof firstRow[key],
+ ]);
+
+ const keysOfExistingColumnsThatNeedToPersist = existingColumns
+ .filter((column) => !column?.autogenerated || keysOfTableData.includes(column.key || column.name))
+ .map((column) => column?.key || column?.name);
+
+ const generatedColumns = keysAndDataTypesToGenerateNewColumns.map(([key, dataType]) => ({
+ id: uuidv4(),
+ name: key,
+ key: key,
+ columnType: convertDataTypeToColumnType(dataType),
+ autogenerated: true,
+ }));
+
+ const finalKeys = [...keysFromWhichNewColumnsShouldBeGenerated, ...keysOfExistingColumnsThatNeedToPersist];
+ const finalColumns = [...existingColumns, ...generatedColumns].filter((column) =>
+ finalKeys.includes(column?.key || column?.name)
+ );
+
+ return finalColumns;
+}
+
+const dataTypeToColumnTypeMapping = {
+ string: 'string',
+ number: 'number',
+ boolean: 'boolean',
+};
+
+const convertDataTypeToColumnType = (dataType) => {
+ if (Object.keys(dataTypeToColumnTypeMapping).includes(dataType)) return dataTypeToColumnTypeMapping[dataType];
+ else return 'string';
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_utils/buildTableColumn.js b/frontend/src/AppBuilder/Widgets/NewTable/_utils/buildTableColumn.js
new file mode 100644
index 0000000000..c46dac58bf
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_utils/buildTableColumn.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import { generateActionColumns } from './generateActionColumns';
+import generateColumnsData from './generateColumnsData';
+import IndeterminateCheckbox from '../_components/IndeterminateCheckbox';
+
+export const buildTableColumn = (
+ showBulkSelector,
+ actions,
+ fireEvent,
+ setExposedVariables,
+ id,
+ columnProperties,
+ columnSizes,
+ data,
+ darkMode,
+ handleCellValueChange,
+ globalFilter,
+ serverSideSearch,
+ tableBodyRef
+) => {
+ return [
+ {
+ id: 'selection',
+ accessorKey: 'selection',
+ enableSorting: false,
+ enableResizing: false,
+ meta: { columnType: 'selector', skipExport: true, skipFilter: true, skipAddNewRow: true },
+ size: 40,
+ header: ({ table }) =>
+ showBulkSelector ? (
+
+ ) : null,
+ cell: ({ row }) => (
+
+ ),
+ },
+ ...generateActionColumns({
+ actions: actions.filter((action) => action.position === 'left'),
+ fireEvent,
+ setExposedVariables,
+ id,
+ }),
+ ...generateColumnsData({
+ columnProperties,
+ columnSizes,
+ defaultColumn: { width: 150 },
+ tableData: data,
+ id,
+ darkMode,
+ fireEvent,
+ tableRef: tableBodyRef,
+ handleCellValueChange,
+ searchText: globalFilter,
+ }).filter(Boolean),
+
+ ...generateActionColumns({
+ actions: actions.filter((action) => action.position === 'right'),
+ fireEvent,
+ setExposedVariables,
+ id,
+ }),
+ ].filter(Boolean);
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_utils/exportData.js b/frontend/src/AppBuilder/Widgets/NewTable/_utils/exportData.js
new file mode 100644
index 0000000000..560a71c68e
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_utils/exportData.js
@@ -0,0 +1,72 @@
+import * as XLSX from 'xlsx';
+import Papa from 'papaparse';
+import generateFile from '@/_lib/generate-file';
+import JsPDF from 'jspdf';
+// eslint-disable-next-line import/no-unresolved
+import 'jspdf-autotable';
+import moment from 'moment';
+
+// Helper function to get table data
+const getData = (table) => {
+ // Get headers from all visible columns
+ const headers = table
+ .getAllColumns()
+ .filter((column) => !column.columnDef.meta?.skipExport)
+ .map((column) => column.columnDef.header);
+
+ // Get data rows
+ const data = table.getCoreRowModel().rows.map((row) => {
+ const rowData = [];
+ headers.forEach((header) => rowData.push(row.original[header]));
+ return rowData;
+ });
+
+ const headersWithUpperCase = headers.map((header) => header.toUpperCase());
+
+ return { headers: headersWithUpperCase, data };
+};
+
+// Export to CSV
+export const exportToCSV = (table, componentName) => {
+ const { headers, data } = getData(table);
+ const fileName = getExportFileName(componentName);
+ const csvString = Papa.unparse({ fields: headers, data });
+ generateFile(fileName, csvString, 'csv');
+};
+
+// Export to Excel
+export const exportToExcel = (table, componentName) => {
+ const { headers, data } = getData(table);
+ const ws = XLSX.utils.json_to_sheet(data);
+ // Add headers to the first row
+ XLSX.utils.sheet_add_aoa(ws, [headers], { origin: 'A1' });
+ const wb = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
+ const fileName = getExportFileName(componentName);
+ XLSX.writeFile(wb, `${fileName}.xlsx`);
+};
+
+// Export to PDF
+export const exportToPDF = async (table, componentName) => {
+ const { headers, data } = getData(table);
+ const pdfData = data.map((obj) => Object.values(obj));
+ const fileName = getExportFileName(componentName);
+ const doc = new JsPDF();
+ doc.autoTable({
+ head: [headers],
+ body: pdfData,
+ styles: {
+ minCellHeight: 9,
+ minCellWidth: 20,
+ fontSize: 11,
+ color: 'black',
+ },
+ theme: 'grid',
+ });
+ doc.save(`${fileName}.pdf`);
+ return;
+};
+
+const getExportFileName = (componentName) => {
+ return `${componentName}_${moment().format('DD-MM-YYYY_HH-mm')}`;
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_utils/generateActionColumns.js b/frontend/src/AppBuilder/Widgets/NewTable/_utils/generateActionColumns.js
new file mode 100644
index 0000000000..91240c90e8
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_utils/generateActionColumns.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { ActionButtons } from '../_components/ActionButtons/ActionButtons';
+
+export const generateActionColumns = ({ actions, fireEvent, setExposedVariables, id }) => {
+ const leftActions = actions?.filter((action) => action.position === 'left') || [];
+ const rightActions = actions?.filter((action) => [undefined, 'right'].includes(action.position)) || [];
+
+ const createActionColumn = (position) => ({
+ id: `${position}Actions`,
+ accessorKey: 'actions',
+ enableResizing: false,
+ meta: { columnType: 'action', position, skipFilter: true, skipAddNewRow: true },
+ size: 90,
+ header: 'Actions',
+ cell: ({ row, cell }) => (
+
+ ),
+ });
+
+ const columns = [];
+ if (leftActions.length > 0) columns.push(createActionColumn('left'));
+ if (rightActions.length > 0) columns.push(createActionColumn('right'));
+
+ return columns;
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_utils/generateColumnsData.js b/frontend/src/AppBuilder/Widgets/NewTable/_utils/generateColumnsData.js
new file mode 100644
index 0000000000..817493a4d7
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_utils/generateColumnsData.js
@@ -0,0 +1,390 @@
+import React from 'react';
+import _ from 'lodash';
+import moment from 'moment';
+import { v4 as uuidv4 } from 'uuid';
+import useStore from '@/AppBuilder/_stores/store';
+import {
+ StringColumn,
+ NumberColumn,
+ BooleanColumn,
+ TagsColumn,
+ RadioColumn,
+ ToggleColumn,
+ DatepickerColumn,
+ LinkColumn,
+ // SelectColumn,
+ // MultiSelectColumn,
+ ImageColumn,
+ CustomSelectColumn,
+ CustomDropdownColumn,
+ TextColumn,
+ JsonColumn,
+ MarkdownColumn,
+} from '../_components/DataTypes';
+import useTableStore from '../_stores/tableStore';
+
+import SelectSearch from 'react-select-search';
+
+export default function generateColumnsData({
+ columnProperties,
+ columnSizes,
+ defaultColumn = { width: 150 },
+ tableData,
+ id,
+ darkMode,
+ fireEvent,
+ tableRef,
+ handleCellValueChange,
+ validateDates,
+ searchText,
+ columnForAddNewRow = false,
+}) {
+ const getResolvedValue = useStore.getState().getResolvedValue;
+ const getEditedRowFromIndex = useTableStore.getState().getEditedRowFromIndex;
+ const getAddNewRowDetailFromIndex = useTableStore.getState().getAddNewRowDetailFromIndex;
+ if (!columnProperties) return [];
+
+ return columnProperties
+ .map((column) => {
+ if (!column) return null;
+
+ const columnSize = columnSizes[column?.id] || columnSizes[column?.name] || column.columnSize;
+ const columnType = column?.columnType;
+
+ // Process column options for select types
+ let columnOptions = {};
+ if (['dropdown', 'multiselect', 'badge', 'badges', 'radio', 'image'].includes(columnType)) {
+ const values = getResolvedValue(column.values) || [];
+ const labels = getResolvedValue(column.labels) || [];
+
+ columnOptions.selectOptions = labels.map((label, index) => ({
+ name: label,
+ value: values[index],
+ }));
+ }
+
+ // Handle select and multiselect options
+ let useDynamicOptions = false;
+ if (columnType === 'select' || columnType === 'newMultiSelect') {
+ useDynamicOptions = getResolvedValue(column?.useDynamicOptions);
+ if (useDynamicOptions) {
+ const dynamicOptions = getResolvedValue(column?.dynamicOptions || []);
+ columnOptions.selectOptions = Array.isArray(dynamicOptions) ? dynamicOptions : [];
+ } else {
+ const options = column?.options ?? [];
+ columnOptions.selectOptions =
+ options?.map((option) => ({
+ label: option.label,
+ value: option.value,
+ })) ?? [];
+ }
+ }
+
+ const isEditable = getResolvedValue(column.isEditable);
+ const isVisible = getResolvedValue(column.columnVisibility) ?? true;
+
+ if (!isVisible) return null;
+
+ const columnDef = {
+ id: column.id || uuidv4(),
+ accessorKey: column.key || column.name,
+ header: getResolvedValue(column.name) ?? '',
+ enableSorting: true,
+ enableResizing: true,
+ enableHiding: true,
+ enableColumnFilter: true,
+ filterFn: 'applyFilters',
+ size: columnSize || defaultColumn.width,
+ minSize: 60,
+ show: isVisible,
+ meta: {
+ columnType,
+ isEditable: isEditable,
+ textColor: column.textColor,
+ cellBackgroundColor: column.cellBackgroundColor,
+ horizontalAlignment: column?.horizontalAlignment ?? 'left',
+ transformation: column.transformation,
+ validation: column.validation,
+ columnVisibility: isVisible,
+ ...column,
+ },
+
+ cell: ({ cell, row }) => {
+ const changeSet = columnForAddNewRow
+ ? getAddNewRowDetailFromIndex(id, row.index)
+ : getEditedRowFromIndex(id, row.index);
+
+ let cellValue = changeSet ? changeSet[cell.column.columnDef?.meta?.name] ?? cell.getValue() : cell.getValue();
+ cellValue = cellValue === undefined || cellValue === null ? '' : cellValue;
+ const rowData = tableData?.[row.index];
+
+ switch (columnType) {
+ case 'string':
+ case undefined:
+ case 'default':
+ return (
+
+ );
+
+ case 'text':
+ return (
+
+ );
+
+ case 'number':
+ return (
+
+ );
+
+ case 'boolean':
+ return (
+
+ handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original)
+ }
+ toggleOnBg={column?.toggleOnBg}
+ toggleOffBg={column?.toggleOffBg}
+ />
+ );
+
+ case 'tags':
+ return ;
+
+ case 'dropdown':
+ case 'multiselect':
+ return (
+ handleCellValueChange(row.index, column.key || column.name, value, row.original)}
+ readOnly={!isEditable}
+ darkMode={darkMode}
+ containerWidth={columnSize}
+ isEditable={isEditable}
+ multiple={columnType === 'multiselect'}
+ />
+ );
+
+ case 'select':
+ case 'newMultiSelect':
+ return (
+ handleCellValueChange(row.index, column.key || column.name, value, row.original)}
+ disabled={!isEditable}
+ darkMode={darkMode}
+ containerWidth={columnSize}
+ defaultOptionsList={column?.defaultOptionsList || []}
+ optionsLoadingState={
+ getResolvedValue(column?.useDynamicOptions) && getResolvedValue(column?.optionsLoadingState)
+ ? true
+ : false
+ }
+ isEditable={isEditable}
+ isMulti={columnType === 'newMultiSelect'}
+ className="select-search table-select-search"
+ column={column}
+ isNewRow={columnForAddNewRow}
+ horizontalAlignment={column?.horizontalAlignment}
+ textColor={getResolvedValue(column.textColor, { cellValue, rowData })}
+ id={id}
+ />
+ );
+
+ case 'badge':
+ case 'badges':
+ return (
+ handleCellValueChange(row.index, column.key || column.name, value, row.original)}
+ readOnly={!isEditable}
+ darkMode={darkMode}
+ multiple={columnType === 'badges'}
+ width={columnSize}
+ isEditable={isEditable}
+ />
+ );
+
+ case 'radio':
+ return (
+ handleCellValueChange(row.index, column.key || column.name, value, row.original)}
+ containerWidth={columnSize}
+ />
+ );
+
+ case 'toggle':
+ return (
+ {
+ handleCellValueChange(row.index, column.key || column.name, value, row.original);
+ fireEvent('OnTableToggleCellChanged', {
+ column: column,
+ tableColumnEvents,
+ });
+ }}
+ />
+ );
+
+ case 'datepicker':
+ return (
+ handleCellValueChange(row.index, column.key || column.name, value, row.original)}
+ tableRef={tableRef}
+ isDateSelectionEnabled={
+ getResolvedValue(column?.isDateSelectionEnabled, { cellValue, rowData }) ?? true
+ }
+ isTwentyFourHrFormatEnabled={
+ getResolvedValue(column?.isTwentyFourHrFormatEnabled, { cellValue, rowData }) ?? false
+ }
+ darkMode={darkMode}
+ textColor={getResolvedValue(column.textColor, { cellValue, rowData })}
+ id={id}
+ />
+ );
+
+ case 'link':
+ return (
+
+ );
+
+ case 'image':
+ return (
+
+ );
+
+ case 'json':
+ return (
+
+ );
+
+ case 'markdown': {
+ return (
+
+ );
+ }
+
+ default:
+ return cellValue || '';
+ }
+ },
+ };
+
+ // Add sorting configuration for specific column types
+ if (columnType === 'number') {
+ columnDef.sortingFn = (rowA, rowB, columnId) => {
+ const a = rowA.getValue(columnId);
+ const b = rowB.getValue(columnId);
+ return a < b ? -1 : a > b ? 1 : 0;
+ };
+ } else if (columnType === 'date' || columnType === 'datetime') {
+ columnDef.sortingFn = (rowA, rowB, columnId) => {
+ const a = rowA.getValue(columnId);
+ const b = rowB.getValue(columnId);
+
+ if (!a) return 1;
+ if (!b) return -1;
+
+ const dateA = moment(a);
+ const dateB = moment(b);
+ return dateA.isBefore(dateB) ? -1 : dateA.isAfter(dateB) ? 1 : 0;
+ };
+ }
+
+ return columnDef;
+ })
+ .filter(Boolean);
+}
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_utils/helper.js b/frontend/src/AppBuilder/Widgets/NewTable/_utils/helper.js
new file mode 100644
index 0000000000..ea816bd794
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_utils/helper.js
@@ -0,0 +1,12 @@
+export const getMaxHeight = (isMaxRowHeightAuto, maxRowHeightValue, cellHeight) => {
+ if (isMaxRowHeightAuto) {
+ return 'fit-content';
+ }
+ if (maxRowHeightValue) {
+ return `${maxRowHeightValue}px`;
+ }
+ if (cellHeight === 'condensed') {
+ return '39px';
+ }
+ return '45px';
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_utils/transformTableData.js b/frontend/src/AppBuilder/Widgets/NewTable/_utils/transformTableData.js
new file mode 100644
index 0000000000..e5132a52b5
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_utils/transformTableData.js
@@ -0,0 +1,45 @@
+import { isArray } from 'lodash';
+
+export const transformTableData = (dataFromProps, transformations, getResolvedValue) => {
+ const resolvedData = dataFromProps;
+ if (!Array.isArray(resolvedData) && !isArray(resolvedData)) {
+ return [];
+ } else {
+ return resolvedData
+ .filter((data) => data !== null && data !== undefined)
+ .map((row) => {
+ const transformedObject = {};
+
+ transformations.forEach(({ key, transformation }) => {
+ const nestedKeys = key.includes('.') && key.split('.');
+ if (nestedKeys) {
+ // Single-level nested property
+ const [nestedKey, subKey] = nestedKeys;
+ const nestedObject = transformedObject?.[nestedKey] || { ...row[nestedKey] }; // Retain existing nested object
+ const newValue =
+ getResolvedValue(transformation, {
+ cellValue: row?.[nestedKey]?.[subKey],
+ rowData: row,
+ }) ?? row[key];
+
+ // Apply transformation to subKey
+ nestedObject[subKey] = newValue;
+
+ // Update transformedObject with the new nested object
+ transformedObject[nestedKey] = nestedObject;
+ } else {
+ // Non-nested property
+ transformedObject[key] =
+ getResolvedValue(transformation, {
+ cellValue: row[key],
+ rowData: row,
+ }) ?? row[key];
+ }
+ });
+ return {
+ ...row,
+ ...transformedObject,
+ };
+ });
+ }
+};
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/index.js b/frontend/src/AppBuilder/Widgets/NewTable/index.js
new file mode 100644
index 0000000000..1f43154bbd
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/index.js
@@ -0,0 +1 @@
+export { Table as default } from './Table';
diff --git a/frontend/src/AppBuilder/Widgets/NumberInput.jsx b/frontend/src/AppBuilder/Widgets/NumberInput.jsx
new file mode 100644
index 0000000000..f40288acd9
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NumberInput.jsx
@@ -0,0 +1,169 @@
+import React, { useEffect } from 'react';
+import { BaseInput } from './BaseComponents/BaseInput';
+import { useInput } from './BaseComponents/hooks/useInput';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+
+export const NumberInput = (props) => {
+ const inputLogic = useInput({
+ ...props,
+ properties: {
+ ...props.properties,
+ value: Number(parseFloat(props.properties.value).toFixed(props.properties.decimalPlaces)),
+ },
+ });
+
+ const handleChange = (e) => {
+ if (e.target.value === '') {
+ inputLogic.setInputValue(null);
+ props.fireEvent('onChange');
+ } else {
+ const newValue = Number(parseFloat(e.target.value));
+ inputLogic.setInputValue(newValue);
+ if (!isNaN(newValue)) {
+ props.fireEvent('onChange');
+ }
+ }
+ };
+
+ const handleBlur = (e) => {
+ const value = Number(parseFloat(e.target.value).toFixed(props.properties.decimalPlaces));
+ inputLogic.setInputValue(value);
+ e.stopPropagation();
+ props.fireEvent('onBlur');
+ inputLogic.setIsFocused(false);
+ };
+
+ const handleIncrement = (e) => {
+ e.preventDefault();
+ const newValue = (inputLogic.value || 0) + 1;
+ inputLogic.setInputValue(newValue);
+ if (!isNaN(newValue)) {
+ props.fireEvent('onChange');
+ }
+ };
+
+ const handleDecrement = (e) => {
+ e.preventDefault();
+ const newValue = (inputLogic.value || 0) - 1;
+ inputLogic.setInputValue(newValue);
+ if (!isNaN(newValue)) {
+ props.fireEvent('onChange');
+ }
+ };
+
+ // Override the base input styles to account for number controls
+ const getCustomStyles = (baseStyles) => {
+ return {
+ ...baseStyles,
+ paddingRight: '20px', // Make room for number controls
+ };
+ };
+
+ const numberControls = !inputLogic.isResizing && (
+
+
+ 0 && props.styles.width > 0
+ ? '21px'
+ : '1px',
+ right: '1px',
+ borderLeft:
+ inputLogic.disable || inputLogic.loading
+ ? '1px solid var(--borders-weak-disabled)'
+ : '1px solid var(--borders-default)',
+ borderBottom:
+ inputLogic.disable || inputLogic.loading
+ ? '1px solid var(--borders-weak-disabled)'
+ : '.5px solid var(--borders-default)',
+ borderTopRightRadius: props.styles.borderRadius - 1,
+ backgroundColor: 'transparent',
+ }}
+ className="numberinput-up-arrow arrow number-input-arrow"
+ name="TriangleDownCenter"
+ />
+
+
+
+
+
+ );
+
+ useEffect(() => {
+ if (isNaN(inputLogic.value) || inputLogic.value === '') {
+ props.setExposedVariable('value', null);
+ }
+ }, [inputLogic.value]);
+
+ return (
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/PasswordInput.jsx b/frontend/src/AppBuilder/Widgets/PasswordInput.jsx
new file mode 100644
index 0000000000..6ca13f2048
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/PasswordInput.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { BaseInput } from './BaseComponents/BaseInput';
+import { useInput } from './BaseComponents/hooks/useInput';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+
+export const PasswordInput = (props) => {
+ const inputLogic = useInput(props);
+
+ const toggleVisibility = () => {
+ inputLogic.setIconVisibility(!inputLogic.iconVisibility);
+ };
+
+ const passwordIcon = (
+
+
+
+ );
+
+ return (
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/PhoneCurrency/CountrySelect.jsx b/frontend/src/AppBuilder/Widgets/PhoneCurrency/CountrySelect.jsx
new file mode 100644
index 0000000000..5f4d1a6efc
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/PhoneCurrency/CountrySelect.jsx
@@ -0,0 +1,141 @@
+import React, { useEffect, useRef, useState } from 'react';
+import Select from 'react-select';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+import { CustomMenuList } from './CustomMenuList';
+import { CustomOption } from './CustomOption';
+import { CustomValueContainer } from './CustomValueContainer';
+
+export const CountrySelect = ({ value, onChange, options, ...rest }) => {
+ const {
+ isCountryChangeEnabled,
+ isCurrencyInput = false,
+ disabledState,
+ borderRadius,
+ isValid,
+ showValidationError,
+ computedStyles,
+ darkMode,
+ filterOption,
+ } = rest;
+ const [menuIsOpen, setMenuIsOpen] = useState(false);
+ const dropdownRef = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
+ setMenuIsOpen(false);
+ }
+ };
+
+ if (menuIsOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [menuIsOpen]);
+
+ const customStyles = {
+ container: (provided) => ({
+ ...provided,
+ minWidth: !isCountryChangeEnabled || disabledState ? '77px' : isCurrencyInput ? '87px' : '93px',
+ width: !isCountryChangeEnabled || disabledState ? '77px' : isCurrencyInput ? '87px' : '93px',
+ height: '100%',
+ }),
+ valueContainer: (provided) => ({
+ ...provided,
+ padding: '0px',
+ }),
+ control: (provided) => ({
+ ...provided,
+ minHeight: '0px',
+ height: '100%',
+ borderTopLeftRadius: `${borderRadius}px`,
+ borderBottomLeftRadius: `${borderRadius}px`,
+ borderTopRightRadius: '0px',
+ borderBottomRightRadius: '0px',
+ borderColor: `${
+ !isValid && showValidationError ? 'var(--status-error-strong)' : computedStyles?.borderColor
+ } !important`,
+ backgroundColor: `${computedStyles?.backgroundColor} !important`,
+ }),
+ menu: (provided) => ({
+ ...provided,
+ width: '208px',
+ height: '236px',
+ borderRadius: '8px',
+ marginTop: '2px',
+ boxShadow: 'var(--elevation-400-box-shadow)',
+ }),
+ menuList: (provided) => ({
+ ...provided,
+ maxHeight: '196px',
+ overflowY: 'auto',
+ scrollbarWidth: 'none',
+ gap: '1px',
+ padding: '8px',
+ borderRadius: '0px 0px 8px 8px',
+ display: 'flex',
+ flexDirection: 'column',
+ backgroundColor: 'var(--surfaces-surface-01)',
+ }),
+ option: (provided, state) => ({
+ ...provided,
+ backgroundColor: state.isSelected ? '#4368E31A' : 'var(--surfaces-surface-01)',
+ ...(state.isSelected && { borderRadius: '8px' }),
+ '&:hover': {
+ backgroundColor: 'var(--interactive-overlays-fill-hover)',
+ borderRadius: '8px',
+ },
+ display: 'flex',
+ cursor: 'pointer',
+ padding: '1px 14px',
+ }),
+ };
+ return (
+ {
+ if (disabledState || !isCountryChangeEnabled) return;
+ setMenuIsOpen((prev) => !prev);
+ }}
+ ref={dropdownRef}
+ >
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/PhoneCurrency/CurrencyInput.jsx b/frontend/src/AppBuilder/Widgets/PhoneCurrency/CurrencyInput.jsx
new file mode 100644
index 0000000000..786299fc3a
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/PhoneCurrency/CurrencyInput.jsx
@@ -0,0 +1,266 @@
+import React, { useEffect, useMemo, useRef } from 'react';
+import { default as ReactCurrencyInput } from 'react-currency-input-field';
+import { useInput } from '../BaseComponents/hooks/useInput';
+import Loader from '@/ToolJetUI/Loader/Loader';
+import Label from '@/_ui/Label';
+import { CountrySelect } from './CountrySelect';
+import { CurrencyMap } from './constants';
+const tinycolor = require('tinycolor2');
+
+export const CurrencyInput = (props) => {
+ const { properties, styles, componentName, darkMode, setExposedVariables, fireEvent } = props;
+ const transformedProps = {
+ ...props,
+ inputType: 'currency',
+ };
+ const inputLogic = useInput(transformedProps);
+
+ const {
+ inputRef,
+ labelRef,
+ visibility,
+ loading,
+ disable,
+ showValidationError,
+ handlePhoneCurrencyInputChange,
+ isFocused,
+ labelWidth,
+ isValid,
+ validationError,
+ isMandatory,
+ handleBlur,
+ handleFocus,
+ value,
+ country,
+ setCountry,
+ } = inputLogic;
+ const { label, placeholder, decimalPlaces, isCountryChangeEnabled, defaultCountry = 'US' } = properties;
+
+ const handleKeyUp = (e) => {
+ if (e.key === 'Enter') {
+ fireEvent('onEnterPressed');
+ }
+ };
+
+ const options = useMemo(() => {
+ return Object.keys(CurrencyMap).map((ele) => ({
+ label: `${CurrencyMap[ele].prefix} (${CurrencyMap[ele].currency})`,
+ value: ele,
+ country: CurrencyMap[ele].country,
+ }));
+ }, []);
+
+ const onInputValueChange = (value) => {
+ handlePhoneCurrencyInputChange(value);
+ setExposedVariables({
+ country: country,
+ formattedValue: `${CurrencyMap[country]?.prefix} ${inputRef.current?.value}`,
+ });
+ };
+
+ const {
+ textColor,
+ backgroundColor,
+ alignment,
+ width,
+ direction,
+ auto,
+ color,
+ borderColor,
+ accentColor,
+ errTextColor,
+ boxShadow,
+ borderRadius,
+ } = styles;
+ const _width = (width / 100) * 70;
+ const defaultAlignment = alignment === 'side' || alignment === 'top' ? alignment : 'side';
+ const disabledState = disable || loading;
+ const isInitialRender = useRef(true);
+ const computedStyles = {
+ height: '100%',
+ borderRadius: `${borderRadius}px`,
+ color: !['#1B1F24', '#000', '#000000ff'].includes(textColor)
+ ? textColor
+ : disabledState
+ ? 'var(--text-disabled)'
+ : 'var(--text-primary)',
+ borderColor: isFocused
+ ? accentColor != '4368E3'
+ ? accentColor
+ : 'var(--primary-accent-strong)'
+ : borderColor != '#CCD1D5'
+ ? borderColor
+ : disabledState
+ ? '1px solid var(--borders-disabled-on-white)'
+ : 'var(--borders-default)',
+ '--tblr-input-border-color-darker': tinycolor(borderColor).darken(24).toString(),
+ backgroundColor:
+ backgroundColor != '#fff'
+ ? backgroundColor
+ : disabledState
+ ? darkMode
+ ? 'var(--surfaces-app-bg-default)'
+ : 'var(--surfaces-surface-03)'
+ : 'var(--surfaces-surface-01)',
+ padding: '8px 10px',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ borderBottomLeftRadius: '0px',
+ borderTopLeftRadius: '0px',
+ borderLeft: 'none',
+ };
+
+ const loaderStyle = {
+ right:
+ direction === 'right' &&
+ defaultAlignment === 'side' &&
+ ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0))
+ ? `${labelWidth + 11}px`
+ : '11px',
+ top:
+ defaultAlignment === 'top'
+ ? ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)) &&
+ 'calc(50% + 10px)'
+ : '',
+ transform:
+ defaultAlignment === 'top' &&
+ ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)) &&
+ ' translateY(-50%)',
+ zIndex: 3,
+ };
+
+ useEffect(() => {
+ if (!isInitialRender.current) {
+ setCountry(defaultCountry);
+ }
+ }, [defaultCountry]);
+
+ useEffect(() => {
+ if (!isInitialRender.current) {
+ setExposedVariables({
+ country: country,
+ formattedValue: `${CurrencyMap[country]?.prefix} ${value}`,
+ });
+ }
+ }, [country]);
+
+ useEffect(() => {
+ if (isInitialRender.current) {
+ setExposedVariables({
+ country: country,
+ formattedValue: `${CurrencyMap[country]?.prefix} ${value}`,
+ value: value,
+ setCountryCode: (code) => {
+ setCountry(code);
+ },
+ });
+ isInitialRender.current = false;
+ }
+ }, []);
+
+ return (
+ <>
+
+
+
+ {
+ return (
+ option.label.toLowerCase().includes(inputValue.toLowerCase()) ||
+ option.data.country.toLowerCase().includes(inputValue.toLowerCase())
+ );
+ }}
+ computedStyles={computedStyles}
+ showValidationError={showValidationError}
+ darkMode={darkMode}
+ isCurrencyInput={true}
+ onChange={(selectedOption) => {
+ if (selectedOption) {
+ setCountry(selectedOption.value);
+ fireEvent('onChange');
+ setExposedVariables({
+ country: selectedOption.value,
+ formattedValue: `${CurrencyMap[selectedOption.value]?.prefix} ${selectedOption.value}`,
+ });
+ }
+ }}
+ />
+ {
+ if (newVal === value) return;
+ onInputValueChange(newVal);
+ }}
+ // prefix={`${CurrencyMap?.[country]?.prefix || ''} `}
+ prefix={''}
+ disabled={disabledState}
+ onBlur={handleBlur}
+ onFocus={handleFocus}
+ onKeyUp={handleKeyUp}
+ />
+
+ {loading &&
}
+
+ {showValidationError && visibility && (
+
+ {validationError}
+
+ )}
+ >
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/PhoneCurrency/CustomMenuList.jsx b/frontend/src/AppBuilder/Widgets/PhoneCurrency/CustomMenuList.jsx
new file mode 100644
index 0000000000..cff9f4fd9f
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/PhoneCurrency/CustomMenuList.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { components } from 'react-select';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+import cx from 'classnames';
+
+export const CustomMenuList = (props) => {
+ const { children, selectProps } = props;
+ const { onInputChange, inputValue } = selectProps;
+
+ return (
+ e.stopPropagation()}
+ >
+
+
+
+
+ {
+ onInputChange(e.currentTarget.value, {
+ action: 'input-change',
+ });
+ }}
+ onMouseDown={(e) => {
+ e.stopPropagation();
+ e.target.focus();
+ }}
+ onTouchEnd={(e) => {
+ e.stopPropagation();
+ e.target.focus();
+ }}
+ />
+
+
+
+ {children?.length > 0 ? children : No options
}
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/PhoneCurrency/CustomOption.jsx b/frontend/src/AppBuilder/Widgets/PhoneCurrency/CustomOption.jsx
new file mode 100644
index 0000000000..a8dff94bed
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/PhoneCurrency/CustomOption.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { components } from 'react-select';
+// eslint-disable-next-line import/no-unresolved
+import flags from 'react-phone-number-input/flags';
+import TickV3 from '@/_ui/Icon/solidIcons/TickV3';
+
+export const CustomOption = (props) => {
+ const { label, value: optionValue, isSelected } = props;
+ const { darkMode } = props?.selectProps || {};
+
+ const optionStyle = {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'start',
+ minHeight: '32px',
+ gap: '6px',
+ cursor: 'pointer',
+ fontFamily: 'IBM Plex Sans',
+ fontSize: '12px',
+ lineHeight: '18px',
+ fontWeight: '400',
+ color: darkMode ? '#fff' : '#1B1F24',
+ width: '100%',
+ };
+ const FlagIcon = flags[optionValue];
+
+ return (
+
+
+
{FlagIcon ? : null}
+ {label}
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/PhoneCurrency/CustomValueContainer.jsx b/frontend/src/AppBuilder/Widgets/PhoneCurrency/CustomValueContainer.jsx
new file mode 100644
index 0000000000..6cea56d47c
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/PhoneCurrency/CustomValueContainer.jsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { components } from 'react-select';
+// eslint-disable-next-line import/no-unresolved
+import flags from 'react-phone-number-input/flags';
+import Planet from '@/_ui/Icon/bulkIcons/Planet';
+import { getCountryCallingCodeSafe } from './utils';
+import { CurrencyMap } from './constants';
+
+export const CustomValueContainer = ({ getValue, ...props }) => {
+ const selectedValue = getValue()[0];
+ const country = selectedValue?.value;
+ const { isCurrencyInput, isCountryChangeEnabled } = props?.selectProps || {};
+ const FlagIcon = selectedValue ? flags[selectedValue.value] : null;
+ const countryCode = getCountryCallingCodeSafe(selectedValue.value);
+
+ return (
+
+ {FlagIcon ? (
+
+ <>
+ {' '}
+
+ {isCurrencyInput ? ` ${CurrencyMap?.[country]?.prefix}` : ` +${countryCode}`}
+
+ >
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/PhoneCurrency/PhoneInput.jsx b/frontend/src/AppBuilder/Widgets/PhoneCurrency/PhoneInput.jsx
new file mode 100644
index 0000000000..70fd463f94
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/PhoneCurrency/PhoneInput.jsx
@@ -0,0 +1,254 @@
+import React, { useEffect, useMemo, useRef } from 'react';
+// eslint-disable-next-line import/no-unresolved
+import Input, { getCountries, getCountryCallingCode } from 'react-phone-number-input/input';
+import { getCountryCallingCodeSafe } from './utils';
+// eslint-disable-next-line import/no-unresolved
+import en from 'react-phone-number-input/locale/en';
+import 'react-phone-number-input/style.css';
+import { useInput } from '../BaseComponents/hooks/useInput';
+import Loader from '@/ToolJetUI/Loader/Loader';
+import Label from '@/_ui/Label';
+import { CountrySelect } from './CountrySelect';
+
+const tinycolor = require('tinycolor2');
+
+export const PhoneInput = (props) => {
+ const { properties, styles, componentName, darkMode, setExposedVariables, fireEvent } = props;
+ const transformedProps = {
+ ...props,
+ inputType: 'phone',
+ };
+ const inputLogic = useInput(transformedProps);
+ const {
+ inputRef,
+ labelRef,
+ visibility,
+ loading,
+ disable,
+ showValidationError,
+ isFocused,
+ labelWidth,
+ isValid,
+ validationError,
+ isMandatory,
+ handleBlur,
+ handleFocus,
+ value,
+ handlePhoneCurrencyInputChange,
+ country,
+ setCountry,
+ } = inputLogic;
+ const { label, placeholder, isCountryChangeEnabled, defaultCountry = 'US' } = properties;
+
+ const {
+ textColor,
+ backgroundColor,
+ alignment,
+ width,
+ direction,
+ auto,
+ color,
+ borderColor,
+ accentColor,
+ errTextColor,
+ boxShadow,
+ borderRadius,
+ } = styles;
+ const _width = (width / 100) * 70;
+ const defaultAlignment = alignment === 'side' || alignment === 'top' ? alignment : 'side';
+ const isInitialRender = useRef(true);
+
+ const options = useMemo(
+ () =>
+ getCountries()
+ .map((country) => ({
+ label: `${en[country]} +${getCountryCallingCodeSafe(country)}`,
+ value: country,
+ }))
+ .sort((a, b) => a.label.localeCompare(b.label)),
+ []
+ );
+
+ const onInputValueChange = (value) => {
+ setExposedVariables({
+ country: country,
+ countryCode: `+${getCountryCallingCodeSafe(country)}`,
+ formattedValue: `+${getCountryCallingCodeSafe(country)} ${inputRef.current?.value}`,
+ });
+ handlePhoneCurrencyInputChange(value);
+ };
+
+ const handleKeyUp = (e) => {
+ if (e.key === 'Enter') {
+ fireEvent('onEnterPressed');
+ }
+ };
+
+ useEffect(() => {
+ if (isInitialRender.current) {
+ setExposedVariables({
+ country: country,
+ countryCode: `+${getCountryCallingCodeSafe(country)}`,
+ formattedValue: `+${getCountryCallingCodeSafe(country)} ${inputRef.current?.value}`,
+ value: value,
+ setCountryCode: (code) => {
+ let value = getCountryCallingCodeSafe(code);
+ if (value) {
+ setCountry(code);
+ } else {
+ value = getCountries().find((country) => `+${getCountryCallingCode(country)}` === code);
+ setCountry(value ? value : '');
+ }
+ },
+ });
+ isInitialRender.current = false;
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!isInitialRender.current) {
+ setCountry(defaultCountry);
+ }
+ }, [defaultCountry]);
+
+ const disabledState = disable || loading;
+
+ const loaderStyle = {
+ right:
+ direction === 'right' &&
+ defaultAlignment === 'side' &&
+ ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0))
+ ? `${labelWidth + 11}px`
+ : '11px',
+ top:
+ defaultAlignment === 'top'
+ ? ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)) &&
+ 'calc(50% + 10px)'
+ : '',
+ transform:
+ defaultAlignment === 'top' &&
+ ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)) &&
+ ' translateY(-50%)',
+ zIndex: 3,
+ };
+
+ const computedStyles = {
+ height: '100%',
+ borderRadius: `${borderRadius}px`,
+ color: !['#1B1F24', '#000', '#000000ff'].includes(textColor)
+ ? textColor
+ : disabledState
+ ? 'var(--text-disabled)'
+ : 'var(--text-primary)',
+ borderColor: isFocused
+ ? accentColor != '4368E3'
+ ? accentColor
+ : 'var(--primary-accent-strong)'
+ : borderColor != '#CCD1D5'
+ ? borderColor
+ : disabledState
+ ? '1px solid var(--borders-disabled-on-white)'
+ : 'var(--borders-default)',
+ '--tblr-input-border-color-darker': tinycolor(borderColor).darken(24).toString(),
+ backgroundColor:
+ backgroundColor != '#fff'
+ ? backgroundColor
+ : disabledState
+ ? darkMode
+ ? 'var(--surfaces-app-bg-default)'
+ : 'var(--surfaces-surface-03)'
+ : 'var(--surfaces-surface-01)',
+ padding: '8px 10px',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ borderBottomLeftRadius: '0px',
+ borderTopLeftRadius: '0px',
+ borderLeft: 'none',
+ };
+
+ return (
+ <>
+
+
+
+ {
+ if (selectedOption) {
+ setCountry(selectedOption.value);
+ }
+ }}
+ />
+
+
+ {loading &&
}
+
+ {showValidationError && visibility && (
+
+ {validationError}
+
+ )}
+ >
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/PhoneCurrency/constants.js b/frontend/src/AppBuilder/Widgets/PhoneCurrency/constants.js
new file mode 100644
index 0000000000..d27c7665a8
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/PhoneCurrency/constants.js
@@ -0,0 +1,597 @@
+export const CurrencyMap = {
+ AE: {
+ currency: 'AED',
+ prefix: 'د.إ.',
+ country: 'United Arab Emirates',
+ },
+ AF: {
+ currency: 'AFN',
+ prefix: '؋',
+ country: 'Afghanistan',
+ },
+ AL: {
+ currency: 'ALL',
+ prefix: 'Lek',
+ country: 'Albania',
+ },
+ AM: {
+ currency: 'AMD',
+ prefix: 'դր.',
+ country: 'Armenia',
+ },
+ AR: {
+ currency: 'ARS',
+ prefix: '$',
+ country: 'Argentina',
+ },
+ AU: {
+ currency: 'AUD',
+ prefix: '$',
+ country: 'Australia',
+ },
+ AZ: {
+ currency: 'AZN',
+ prefix: 'ман.',
+ country: 'Azerbaijan',
+ },
+ BA: {
+ currency: 'BAM',
+ prefix: 'KM',
+ country: 'Bosnia and Herzegovina',
+ },
+ BD: {
+ currency: 'BDT',
+ prefix: '৳',
+ country: 'Bangladesh',
+ },
+ BG: {
+ currency: 'BGN',
+ prefix: 'лв.',
+ country: 'Bulgaria',
+ },
+ BH: {
+ currency: 'BHD',
+ prefix: 'د.ب.',
+ country: 'Bahrain',
+ },
+ BI: {
+ currency: 'BIF',
+ prefix: 'FBu',
+ country: 'Burundi',
+ },
+ BN: {
+ currency: 'BND',
+ prefix: '$',
+ country: 'Brunei Darussalam',
+ },
+ BO: {
+ currency: 'BOB',
+ prefix: 'Bs',
+ country: 'Bolivia',
+ },
+ BR: {
+ currency: 'BRL',
+ prefix: 'R$',
+ country: 'Brazil',
+ },
+ BV: {
+ currency: 'NOK',
+ prefix: 'kr',
+ country: 'Bouvet Island',
+ },
+ BW: {
+ currency: 'BWP',
+ prefix: 'P',
+ country: 'Botswana',
+ },
+ BY: {
+ currency: 'BYR',
+ prefix: 'BYR',
+ country: 'Belarus',
+ },
+ BZ: {
+ currency: 'BZD',
+ prefix: '$',
+ country: 'Belize',
+ },
+ CA: {
+ currency: 'CAD',
+ prefix: '$',
+ country: 'Canada',
+ },
+ CD: {
+ currency: 'CDF',
+ prefix: 'FrCD',
+ country: 'Congo, Democratic Republic of the',
+ },
+ CF: {
+ currency: 'XAF',
+ prefix: 'FCFA',
+ country: 'Central African Republic',
+ },
+ CH: {
+ currency: 'CHF',
+ prefix: 'CHF',
+ country: 'Switzerland',
+ },
+ CL: {
+ currency: 'CLP',
+ prefix: '$',
+ country: 'Chile',
+ },
+ CN: {
+ currency: 'CNY',
+ prefix: 'CN¥',
+ country: 'China',
+ },
+ CO: {
+ currency: 'COP',
+ prefix: '$',
+ country: 'Colombia',
+ },
+ CR: {
+ currency: 'CRC',
+ prefix: '₡',
+ country: 'Costa Rica',
+ },
+ CV: {
+ currency: 'CVE',
+ prefix: 'CV$',
+ country: 'Cape Verde',
+ },
+ CZ: {
+ currency: 'CZK',
+ prefix: 'Kč',
+ country: 'Czech Republic',
+ },
+ DJ: {
+ currency: 'DJF',
+ prefix: 'Fdj',
+ country: 'Djibouti',
+ },
+ DK: {
+ currency: 'DKK',
+ prefix: 'kr',
+ country: 'Denmark',
+ },
+ DO: {
+ currency: 'DOP',
+ prefix: 'RD$',
+ country: 'Dominican Republic',
+ },
+ DZ: {
+ currency: 'DZD',
+ prefix: 'د.ج.',
+ country: 'Algeria',
+ },
+ EG: {
+ currency: 'EGP',
+ prefix: 'ج.م.',
+ country: 'Egypt',
+ },
+ ER: {
+ currency: 'ERN',
+ prefix: 'Nfk',
+ country: 'Eritrea',
+ },
+ EU: {
+ currency: 'EUR',
+ prefix: '€',
+ country: 'European Union',
+ },
+ ET: {
+ currency: 'ETB',
+ prefix: 'Br',
+ country: 'Ethiopia',
+ },
+ GB: {
+ currency: 'GBP',
+ prefix: '£',
+ country: 'United Kingdom',
+ },
+ GE: {
+ currency: 'GEL',
+ prefix: 'GEL',
+ country: 'Georgia',
+ },
+ GH: {
+ currency: 'GHS',
+ prefix: 'GH₵',
+ country: 'Ghana',
+ },
+ GN: {
+ currency: 'GNF',
+ prefix: 'FG',
+ country: 'Guinea',
+ },
+ GT: {
+ currency: 'GTQ',
+ prefix: 'Q',
+ country: 'Guatemala',
+ },
+ HK: {
+ currency: 'HKD',
+ prefix: '$',
+ country: 'Hong Kong',
+ },
+ HN: {
+ currency: 'HNL',
+ prefix: 'L',
+ country: 'Honduras',
+ },
+ HR: {
+ currency: 'HRK',
+ prefix: 'kn',
+ country: 'Croatia',
+ },
+ HU: {
+ currency: 'HUF',
+ prefix: 'Ft',
+ country: 'Hungary',
+ },
+ ID: {
+ currency: 'IDR',
+ prefix: 'Rp',
+ country: 'Indonesia',
+ },
+ IL: {
+ currency: 'ILS',
+ prefix: '₪',
+ country: 'Israel',
+ },
+ IN: {
+ currency: 'INR',
+ prefix: '₹',
+ country: 'India',
+ },
+ IQ: {
+ currency: 'IQD',
+ prefix: 'د.ع.',
+ country: 'Iraq',
+ },
+ IR: {
+ currency: 'IRR',
+ prefix: '﷼',
+ country: 'Iran, Islamic Republic of',
+ },
+ IS: {
+ currency: 'ISK',
+ prefix: 'kr',
+ country: 'Iceland',
+ },
+ JM: {
+ currency: 'JMD',
+ prefix: '$',
+ country: 'Jamaica',
+ },
+ JO: {
+ currency: 'JOD',
+ prefix: 'د.أ.',
+ country: 'Jordan',
+ },
+ JP: {
+ currency: 'JPY',
+ prefix: '¥',
+ country: 'Japan',
+ },
+ KE: {
+ currency: 'KES',
+ prefix: 'Ksh',
+ country: 'Kenya',
+ },
+ KH: {
+ currency: 'KHR',
+ prefix: '៛',
+ country: 'Cambodia',
+ },
+ KM: {
+ currency: 'KMF',
+ prefix: 'FC',
+ country: 'Comoros',
+ },
+ KR: {
+ currency: 'KRW',
+ prefix: '₩',
+ country: 'Korea, Republic of',
+ },
+ KW: {
+ currency: 'KWD',
+ prefix: 'د.ك.',
+ country: 'Kuwait',
+ },
+ KZ: {
+ currency: 'KZT',
+ prefix: 'тңг.',
+ country: 'Kazakhstan',
+ },
+ LB: {
+ currency: 'LBP',
+ prefix: 'ل.ل.',
+ country: 'Lebanon',
+ },
+ LK: {
+ currency: 'LKR',
+ prefix: 'SL Re',
+ country: 'Sri Lanka',
+ },
+ LT: {
+ currency: 'LTL',
+ prefix: 'Lt',
+ country: 'Lithuania',
+ },
+ LY: {
+ currency: 'LYD',
+ prefix: 'د.ل.',
+ country: 'Libya',
+ },
+ MA: {
+ currency: 'MAD',
+ prefix: 'د.م.',
+ country: 'Morocco',
+ },
+ MD: {
+ currency: 'MDL',
+ prefix: 'MDL',
+ country: 'Moldova, Republic of',
+ },
+ MG: {
+ currency: 'MGA',
+ prefix: 'MGA',
+ country: 'Madagascar',
+ },
+ MK: {
+ currency: 'MKD',
+ prefix: 'MKD',
+ country: 'Macedonia, the Former Yugoslav Republic of',
+ },
+ MM: {
+ currency: 'MMK',
+ prefix: 'K',
+ country: 'Myanmar',
+ },
+ MO: {
+ currency: 'MOP',
+ prefix: 'MOP$',
+ country: 'Macao',
+ },
+ MU: {
+ currency: 'MUR',
+ prefix: 'MURs',
+ country: 'Mauritius',
+ },
+ MX: {
+ currency: 'MXN',
+ prefix: '$',
+ country: 'Mexico',
+ },
+ MY: {
+ currency: 'MYR',
+ prefix: 'RM',
+ country: 'Malaysia',
+ },
+ MZ: {
+ currency: 'MZN',
+ prefix: 'MTn',
+ country: 'Mozambique',
+ },
+ NA: {
+ currency: 'NAD',
+ prefix: 'N$',
+ country: 'Namibia',
+ },
+ NG: {
+ currency: 'NGN',
+ prefix: '₦',
+ country: 'Nigeria',
+ },
+ NI: {
+ currency: 'NIO',
+ prefix: 'C$',
+ country: 'Nicaragua',
+ },
+ NO: {
+ currency: 'NOK',
+ prefix: 'kr',
+ country: 'Norway',
+ },
+ NP: {
+ currency: 'NPR',
+ prefix: 'नेरू',
+ country: 'Nepal',
+ },
+ NZ: {
+ currency: 'NZD',
+ prefix: '$',
+ country: 'New Zealand',
+ },
+ OM: {
+ currency: 'OMR',
+ prefix: 'ر.ع.',
+ country: 'Oman',
+ },
+ PA: {
+ currency: 'PAB',
+ prefix: 'B/.',
+ country: 'Panama',
+ },
+ PE: {
+ currency: 'PEN',
+ prefix: 'S/.',
+ country: 'Peru',
+ },
+ PH: {
+ currency: 'PHP',
+ prefix: '₱',
+ country: 'Philippines',
+ },
+ PK: {
+ currency: 'PKR',
+ prefix: '₨',
+ country: 'Pakistan',
+ },
+ PL: {
+ currency: 'PLN',
+ prefix: 'zł',
+ country: 'Poland',
+ },
+ PY: {
+ currency: 'PYG',
+ prefix: '₲',
+ country: 'Paraguay',
+ },
+ QA: {
+ currency: 'QAR',
+ prefix: 'ر.ق.',
+ country: 'Qatar',
+ },
+ RO: {
+ currency: 'RON',
+ prefix: 'RON',
+ country: 'Romania',
+ },
+ RS: {
+ currency: 'RSD',
+ prefix: 'дин.',
+ country: 'Serbia',
+ },
+ RU: {
+ currency: 'RUB',
+ prefix: '₽',
+ country: 'Russian Federation',
+ },
+ RW: {
+ currency: 'RWF',
+ prefix: 'FR',
+ country: 'Rwanda',
+ },
+ SA: {
+ currency: 'SAR',
+ prefix: 'ر.س.',
+ country: 'Saudi Arabia',
+ },
+ SD: {
+ currency: 'SDG',
+ prefix: 'SDG',
+ country: 'Sudan',
+ },
+ SE: {
+ currency: 'SEK',
+ prefix: 'kr',
+ country: 'Sweden',
+ },
+ SG: {
+ currency: 'SGD',
+ prefix: '$',
+ country: 'Singapore',
+ },
+ SJ: {
+ currency: 'NOK',
+ prefix: 'kr',
+ country: 'Svalbard and Jan Mayen',
+ },
+ SN: {
+ currency: 'XOF',
+ prefix: 'CFA',
+ country: 'Senegal',
+ },
+ SO: {
+ currency: 'SOS',
+ prefix: 'Ssh',
+ country: 'Somalia',
+ },
+ SY: {
+ currency: 'SYP',
+ prefix: 'ل.س.',
+ country: 'Syrian Arab Republic',
+ },
+ TH: {
+ currency: 'THB',
+ prefix: '฿',
+ country: 'Thailand',
+ },
+ TN: {
+ currency: 'TND',
+ prefix: 'د.ت.',
+ country: 'Tunisia',
+ },
+ TO: {
+ currency: 'TOP',
+ prefix: 'T$',
+ country: 'Tonga',
+ },
+ TR: {
+ currency: 'TRY',
+ prefix: 'TL',
+ country: 'Turkey',
+ },
+ TT: {
+ currency: 'TTD',
+ prefix: '$',
+ country: 'Trinidad and Tobago',
+ },
+ TW: {
+ currency: 'TWD',
+ prefix: 'NT$',
+ country: 'Taiwan, Province of China',
+ },
+ TZ: {
+ currency: 'TZS',
+ prefix: 'TSh',
+ country: 'United Republic of Tanzania',
+ },
+ UA: {
+ currency: 'UAH',
+ prefix: '₴',
+ country: 'Ukraine',
+ },
+ UG: {
+ currency: 'UGX',
+ prefix: 'USh',
+ country: 'Uganda',
+ },
+ US: {
+ currency: 'USD',
+ prefix: '$',
+ country: 'United States',
+ },
+ UY: {
+ currency: 'UYU',
+ prefix: '$',
+ country: 'Uruguay',
+ },
+ UZ: {
+ currency: 'UZS',
+ prefix: 'UZS',
+ country: 'Uzbekistan',
+ },
+ VE: {
+ currency: 'VEF',
+ prefix: 'Bs.F.',
+ country: 'Venezuela',
+ },
+ VN: {
+ currency: 'VND',
+ prefix: '₫',
+ country: 'Vietnam',
+ },
+ VU: {
+ currency: 'VUV',
+ prefix: 'VT',
+ country: 'Vanuatu',
+ },
+ YE: {
+ currency: 'YER',
+ prefix: 'ر.ي.',
+ country: 'Yemen',
+ },
+ ZA: {
+ currency: 'ZAR',
+ prefix: 'R',
+ country: 'South Africa',
+ },
+ ZM: {
+ currency: 'ZMK',
+ prefix: 'ZK',
+ country: 'Zambia',
+ },
+};
diff --git a/frontend/src/AppBuilder/Widgets/PhoneCurrency/utils.js b/frontend/src/AppBuilder/Widgets/PhoneCurrency/utils.js
new file mode 100644
index 0000000000..9a23b45ca5
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/PhoneCurrency/utils.js
@@ -0,0 +1,10 @@
+// eslint-disable-next-line import/no-unresolved
+import { getCountryCallingCode } from 'react-phone-number-input/input';
+
+export const getCountryCallingCodeSafe = (country) => {
+ try {
+ return getCountryCallingCode(country);
+ } catch (error) {
+ return '';
+ }
+};
diff --git a/frontend/src/AppBuilder/Widgets/PhoneInput/PhoneInput.jsx b/frontend/src/AppBuilder/Widgets/PhoneInput/PhoneInput.jsx
new file mode 100644
index 0000000000..6a7dafceea
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/PhoneInput/PhoneInput.jsx
@@ -0,0 +1,256 @@
+import React, { useEffect, useMemo, useRef } from 'react';
+// eslint-disable-next-line import/no-unresolved
+import Input, { getCountries, getCountryCallingCode } from 'react-phone-number-input/input';
+import { getCountryCallingCodeSafe } from './utils';
+// eslint-disable-next-line import/no-unresolved
+import en from 'react-phone-number-input/locale/en';
+import 'react-phone-number-input/style.css';
+import { useInput } from '../BaseComponents/hooks/useInput';
+import Loader from '@/ToolJetUI/Loader/Loader';
+import Label from '@/_ui/Label';
+import { CountrySelect } from './CountrySelect';
+
+const tinycolor = require('tinycolor2');
+
+export const PhoneInput = (props) => {
+ const { properties, styles, componentName, darkMode, setExposedVariables, fireEvent } = props;
+ const transformedProps = {
+ ...props,
+ inputType: 'phone',
+ };
+ const inputLogic = useInput(transformedProps);
+ const {
+ inputRef,
+ labelRef,
+ visibility,
+ loading,
+ disable,
+ showValidationError,
+ isFocused,
+ labelWidth,
+ isValid,
+ validationError,
+ isMandatory,
+ handleBlur,
+ handleFocus,
+ value,
+ handlePhoneInputChange,
+ country,
+ setCountry,
+ } = inputLogic;
+ const { label, placeholder, isCountryChangeEnabled, defaultCountry = 'US' } = properties;
+
+ const {
+ textColor,
+ backgroundColor,
+ alignment,
+ width,
+ direction,
+ auto,
+ color,
+ borderColor,
+ accentColor,
+ errTextColor,
+ boxShadow,
+ borderRadius,
+ } = styles;
+ const _width = (width / 100) * 70;
+ const defaultAlignment = alignment === 'side' || alignment === 'top' ? alignment : 'side';
+ const isInitialRender = useRef(true);
+
+ const options = useMemo(
+ () =>
+ getCountries()
+ .map((country) => ({
+ label: `${en[country]} +${getCountryCallingCodeSafe(country)}`,
+ value: country,
+ }))
+ .sort((a, b) => a.label.localeCompare(b.label)),
+ []
+ );
+
+ const onInputValueChange = (value) => {
+ setExposedVariables({
+ country: country,
+ countryCode: `+${getCountryCallingCodeSafe(country)}`,
+ formattedValue: `+${getCountryCallingCodeSafe(country)} ${inputRef.current?.value}`,
+ });
+ handlePhoneInputChange(value);
+ };
+
+ const handleKeyUp = (e) => {
+ if (e.key === 'Enter') {
+ fireEvent('onEnterPressed');
+ }
+ };
+
+ useEffect(() => {
+ if (isInitialRender.current) {
+ setExposedVariables({
+ country: country,
+ countryCode: `+${getCountryCallingCodeSafe(country)}`,
+ formattedValue: `+${getCountryCallingCodeSafe(country)} ${inputRef.current?.value}`,
+ value: value,
+ setCountryCode: (code) => {
+ let value = getCountryCallingCodeSafe(code);
+ if (value) {
+ setCountry(code);
+ } else {
+ value = getCountries().find((country) => `+${getCountryCallingCode(country)}` === code);
+ setCountry(value ? value : '');
+ }
+ },
+ });
+ isInitialRender.current = false;
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!isInitialRender.current) {
+ setCountry(defaultCountry);
+ }
+ }, [defaultCountry]);
+
+ const disabledState = disable || loading;
+
+ const loaderStyle = {
+ right:
+ direction === 'right' &&
+ defaultAlignment === 'side' &&
+ ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0))
+ ? `${labelWidth + 11}px`
+ : '11px',
+ top:
+ defaultAlignment === 'top'
+ ? ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)) &&
+ 'calc(50% + 10px)'
+ : '',
+ transform:
+ defaultAlignment === 'top' &&
+ ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)) &&
+ ' translateY(-50%)',
+ zIndex: 3,
+ };
+
+ const computedStyles = {
+ height: '100%',
+ borderRadius: `0px ${borderRadius}px ${borderRadius}px 0px`,
+ color: !['#1B1F24', '#000', '#000000ff'].includes(textColor)
+ ? textColor
+ : disabledState
+ ? 'var(--text-disabled)'
+ : 'var(--text-primary)',
+ borderColor: isFocused
+ ? accentColor != '4368E3'
+ ? accentColor
+ : 'var(--primary-accent-strong)'
+ : borderColor != '#CCD1D5'
+ ? borderColor
+ : disabledState
+ ? '1px solid var(--borders-disabled-on-white)'
+ : 'var(--borders-default)',
+ '--tblr-input-border-color-darker': tinycolor(borderColor).darken(24).toString(),
+ backgroundColor:
+ backgroundColor != '#fff'
+ ? backgroundColor
+ : disabledState
+ ? darkMode
+ ? 'var(--surfaces-app-bg-default)'
+ : 'var(--surfaces-surface-03)'
+ : 'var(--surfaces-surface-01)',
+ padding: '8px 10px',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ borderBottomLeftRadius: '0px',
+ borderTopLeftRadius: '0px',
+ borderLeft: 'none',
+ };
+ const countryCode = getCountryCallingCodeSafe(country);
+ console.log(countryCode);
+ return (
+ <>
+
+
+
+ {
+ if (selectedOption) {
+ setCountry(selectedOption.value);
+ }
+ }}
+ />
+
+
+ {loading &&
}
+
+ {showValidationError && visibility && (
+
+ {validationError}
+
+ )}
+ >
+ );
+};
diff --git a/frontend/src/AppBuilder/Widgets/Table/String.jsx b/frontend/src/AppBuilder/Widgets/Table/String.jsx
index af593c0e5f..733c70953b 100644
--- a/frontend/src/AppBuilder/Widgets/Table/String.jsx
+++ b/frontend/src/AppBuilder/Widgets/Table/String.jsx
@@ -66,7 +66,6 @@ const StringColumn = ({
className={`${!isValid ? 'is-invalid' : ''} h-100 text-container long-text-input d-flex align-items-center ${
darkMode ? ' textarea-dark-theme' : ''
} justify-content-${determineJustifyContentValue(horizontalAlignment)}`}
- tabIndex={-1}
style={{
color: cellTextColor ? cellTextColor : 'inherit',
outline: 'none',
diff --git a/frontend/src/AppBuilder/Widgets/Table/columns/index.jsx b/frontend/src/AppBuilder/Widgets/Table/columns/index.jsx
index 257de7c4ae..b7ebe93721 100644
--- a/frontend/src/AppBuilder/Widgets/Table/columns/index.jsx
+++ b/frontend/src/AppBuilder/Widgets/Table/columns/index.jsx
@@ -130,6 +130,8 @@ export default function generateColumnsData({
return 1;
}
};
+ } else if (columnType === 'number') {
+ sortType = 'basic';
}
const width = columnSize || defaultColumn.width;
return {
diff --git a/frontend/src/AppBuilder/Widgets/Tabs.jsx b/frontend/src/AppBuilder/Widgets/Tabs.jsx
index 7f4fb527e4..dc3a55dcd2 100644
--- a/frontend/src/AppBuilder/Widgets/Tabs.jsx
+++ b/frontend/src/AppBuilder/Widgets/Tabs.jsx
@@ -2,7 +2,7 @@ import React, { useRef, useState, useEffect } from 'react';
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
import { resolveWidgetFieldValue, isExpectedDataType } from '@/_helpers/utils';
import useStore from '@/AppBuilder/_stores/store';
-
+import { TAB_CANVAS_PADDING } from '@/AppBuilder/AppCanvas/appCanvasConstants';
export const Tabs = function Tabs({
id,
component,
@@ -117,6 +117,7 @@ export const Tabs = function Tabs({
position: 'absolute',
top: parsedHideTabs ? '0px' : '41px',
width: '100%',
+ padding: TAB_CANVAS_PADDING,
}}
>
{
+ const inputLogic = useInput(props);
+
+ return ;
+};
diff --git a/frontend/src/AppBuilder/Widgets/TextInput.jsx b/frontend/src/AppBuilder/Widgets/TextInput.jsx
new file mode 100644
index 0000000000..119add300e
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/TextInput.jsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import { BaseInput } from './BaseComponents/BaseInput';
+import { useInput } from './BaseComponents/hooks/useInput';
+
+export const TextInput = (props) => {
+ const inputLogic = useInput(props);
+
+ return ;
+};
diff --git a/frontend/src/AppBuilder/_helpers/editorHelpers.js b/frontend/src/AppBuilder/_helpers/editorHelpers.js
index a8fc3efca1..183c5c1cec 100644
--- a/frontend/src/AppBuilder/_helpers/editorHelpers.js
+++ b/frontend/src/AppBuilder/_helpers/editorHelpers.js
@@ -2,11 +2,12 @@ import { Button } from '@/Editor/Components/Button';
import { Image } from '@/Editor/Components/Image/Image';
import { Text } from '@/Editor/Components/Text';
// import { Table } from '@/Editor/Components/Table/Table';
-import { Table } from '@/AppBuilder/Widgets/Table/Table';
+// import { Table } from '@/AppBuilder/Widgets/Table/Table';
+import { Table } from '@/AppBuilder/Widgets/NewTable/Table';
-import { TextInput } from '@/Editor/Components/TextInput';
-import { NumberInput } from '@/Editor/Components/NumberInput';
-import { TextArea } from '@/Editor/Components/TextArea';
+import { TextInput } from '@/AppBuilder/Widgets/TextInput';
+import { TextArea } from '@/AppBuilder/Widgets/TextArea';
+import { NumberInput } from '@/AppBuilder/Widgets/NumberInput';
import { RichTextEditor } from '@/Editor/Components/RichTextEditor';
import { DropDown } from '@/Editor/Components/DropDown';
import { DropdownV2 } from '@/Editor/Components/DropdownV2/DropdownV2';
@@ -29,7 +30,10 @@ import { RadioButtonV2 } from '@/Editor/Components/RadioButtonV2/RadioButtonV2';
import { StarRating } from '@/Editor/Components/StarRating';
import { Divider } from '@/Editor/Components/Divider';
import { FilePicker } from '@/Editor/Components/FilePicker';
-import { PasswordInput } from '@/Editor/Components/PasswordInput';
+import { PasswordInput } from '@/AppBuilder/Widgets/PasswordInput';
+import { EmailInput } from '@/AppBuilder/Widgets/EmailInput';
+import { PhoneInput } from '@/AppBuilder/Widgets/PhoneCurrency/PhoneInput';
+import { CurrencyInput } from '@/AppBuilder/Widgets/PhoneCurrency/CurrencyInput';
// import { Calendar } from '@/Editor/Components/Calendar';
// import { Listview } from '@/Editor/Components/Listview';
import { IFrame } from '@/Editor/Components/IFrame';
@@ -46,7 +50,7 @@ import { SvgImage } from '@/Editor/Components/SvgImage';
import { Html } from '@/Editor/Components/Html';
import { ButtonGroup } from '@/Editor/Components/ButtonGroup';
import { CustomComponent } from '@/Editor/Components/CustomComponent/CustomComponent';
-import { VerticalDivider } from '@/Editor/Components/verticalDivider';
+import { VerticalDivider } from '@/Editor/Components/VerticalDivider';
import { ColorPicker } from '@/Editor/Components/ColorPicker';
import { KanbanBoard } from '@/Editor/Components/KanbanBoard/KanbanBoard';
// import { Kanban } from '@/Editor/Components/Kanban/Kanban';
@@ -118,6 +122,9 @@ export const AllComponents = {
Divider,
FilePicker,
PasswordInput,
+ EmailInput,
+ PhoneInput,
+ CurrencyInput,
Calendar,
IFrame,
CodeEditor,
diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js
index 1b664cfa21..176951921c 100644
--- a/frontend/src/AppBuilder/_hooks/useAppData.js
+++ b/frontend/src/AppBuilder/_hooks/useAppData.js
@@ -20,13 +20,14 @@ import useRouter from '@/_hooks/use-router';
import { extractEnvironmentConstantsFromConstantsList, navigate } from '../_utils/misc';
import { getWorkspaceId } from '@/_helpers/utils';
import { shallow } from 'zustand/shallow';
-import { fetchAndSetWindowTitle, pageTitles, defaultWhiteLabellingSettings } from '@white-label/whiteLabelling';
+import { fetchAndSetWindowTitle, pageTitles, retrieveWhiteLabelText } from '@white-label/whiteLabelling';
import { initEditorWalkThrough } from '@/AppBuilder/_helpers/createWalkThrough';
import queryString from 'query-string';
import { distinctUntilChanged } from 'rxjs';
-import { convertAllKeysToSnakeCase } from '../_stores/utils';
+import { baseTheme, convertAllKeysToSnakeCase } from '../_stores/utils';
import { getPreviewQueryParams } from '@/_helpers/routes';
import { useLocation, useMatch, useParams } from 'react-router-dom';
+import useThemeAccess from './useThemeAccess';
/**
* this is to normalize the query transformation options to match the expected schema. Takes care of corrupted data.
@@ -101,12 +102,14 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
const selectedEnvironment = useStore((state) => state.selectedEnvironment);
const setIsEditorFreezed = useStore((state) => state.setIsEditorFreezed);
const appMode = useStore((state) => state.globalSettings.appMode);
+ const selectedTheme = useStore((state) => state.globalSettings.theme);
const previousEnvironmentId = usePrevious(selectedEnvironment?.id);
const isComponentLayoutReady = useStore((state) => state.isComponentLayoutReady, shallow);
const pageSwitchInProgress = useStore((state) => state.pageSwitchInProgress);
const setPageSwitchInProgress = useStore((state) => state.setPageSwitchInProgress);
const selectedVersion = useStore((state) => state.selectedVersion);
const setIsPublicAccess = useStore((state) => state.setIsPublicAccess);
+ const themeAccess = useThemeAccess();
const setConversation = useStore((state) => state.ai?.setConversation);
const setDocsConversation = useStore((state) => state.ai?.setDocsConversation);
@@ -297,10 +300,14 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
creationMode: appData.creation_mode,
});
setIsEditorFreezed(appData.should_freeze_editor);
- setGlobalSettings(
- mapKeys(appData.editing_version?.global_settings || appData.global_settings, (value, key) => camelCase(key))
+ const global_settings = mapKeys(
+ appData.editing_version?.global_settings || appData.global_settings,
+ (value, key) => camelCase(key)
);
-
+ if (!global_settings?.theme) {
+ global_settings.theme = baseTheme;
+ }
+ setGlobalSettings(global_settings);
setPages(pages, moduleId);
setPageSettings(
computePageSettings(deepCamelCase(appData?.editing_version?.page_settings ?? appData?.page_settings))
@@ -326,6 +333,16 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
// navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${startingPage.handle}`);
}
+
+ // Add page id and handle to the state on initial load
+ const currentState = window.history.state || {};
+ const pageInfo = {
+ id: startingPage.id,
+ handle: startingPage.handle,
+ };
+ const newState = { ...currentState, ...pageInfo };
+ window.history.replaceState(newState, '', window.location.href);
+
setCurrentPageHandle(startingPage.handle);
updateFeatureAccess();
setCurrentPageId(startingPage.id, moduleId);
@@ -412,7 +429,7 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
if (showWalkthrough) initEditorWalkThrough();
checkAndSetTrueBuildSuggestionsFlag();
return () => {
- document.title = defaultWhiteLabellingSettings.WHITE_LABEL_TEXT;
+ document.title = retrieveWhiteLabelText();
};
});
}, [setApp, setEditorLoading, currentSession]);
@@ -433,6 +450,16 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v
fetchAndSetWindowTitle({ page: pageTitles.EDITOR, appName: app.appName });
}, [app.appName]);
+ useEffect(() => {
+ if (!themeAccess) return;
+ const root = document.documentElement;
+ const brandColors = selectedTheme?.definition?.brand?.colors || {};
+ Object.keys(brandColors).forEach((colorType) => {
+ const color = brandColors[colorType][darkMode ? 'dark' : 'light'];
+ root.style.setProperty(`--${colorType}-brand`, `${color}`);
+ });
+ }, [darkMode, selectedTheme, themeAccess]);
+
useEffect(() => {
const exposedTheme =
appMode && appMode !== 'auto' ? appMode : localStorage.getItem('darkMode') === 'true' ? 'dark' : 'light';
diff --git a/frontend/src/AppBuilder/_hooks/usePopoverObserver.js b/frontend/src/AppBuilder/_hooks/usePopoverObserver.js
new file mode 100644
index 0000000000..a5c0cda1c4
--- /dev/null
+++ b/frontend/src/AppBuilder/_hooks/usePopoverObserver.js
@@ -0,0 +1,46 @@
+import { useEffect, useRef } from 'react';
+
+function usePopoverObserver(containerRef, triggerRef, popoverRef, show, onShow, onHide, threshold = 0.5) {
+ const prevShow = useRef(false);
+
+ // Check if it is a ref or a DOM element
+ const container = containerRef?.current ? containerRef.current : containerRef;
+ const trigger = triggerRef?.current ? triggerRef.current : triggerRef;
+ const popover = popoverRef?.current ? popoverRef.current : popoverRef;
+
+ useEffect(() => {
+ if (!container || !trigger) return;
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ if (prevShow.current) {
+ onShow();
+ prevShow.current = false;
+ }
+ } else if (show) {
+ onHide();
+ prevShow.current = true;
+ }
+ },
+ { root: container, threshold: [threshold] }
+ );
+
+ observer.observe(trigger);
+
+ const handleOutsideClick = (event) => {
+ if (popover && !popover.contains(event.target) && prevShow.current) {
+ prevShow.current = false;
+ }
+ };
+
+ document.addEventListener('mousedown', handleOutsideClick);
+
+ return () => {
+ observer.unobserve(trigger);
+ document.removeEventListener('mousedown', handleOutsideClick);
+ };
+ }, [containerRef, triggerRef, popoverRef, show, onShow, onHide, threshold]);
+}
+
+export default usePopoverObserver;
diff --git a/frontend/src/AppBuilder/_hooks/useSortedComponents.js b/frontend/src/AppBuilder/_hooks/useSortedComponents.js
new file mode 100644
index 0000000000..9877206444
--- /dev/null
+++ b/frontend/src/AppBuilder/_hooks/useSortedComponents.js
@@ -0,0 +1,49 @@
+import { useMemo, useRef } from 'react';
+import useStore from '@/AppBuilder/_stores/store';
+import { shallow } from 'zustand/shallow';
+
+const useSortedComponents = (components, currentLayout, id) => {
+ const getCurrentPageComponents = useStore((state) => state.getCurrentPageComponents, shallow);
+ const reorderContainerChildren = useStore((state) => state.reorderContainerChildren, shallow);
+ const prevForceUpdateRef = useRef(0);
+ const prevComponentsOrder = useRef(components);
+
+ // Function to sort the components based on position in container for tab navigation
+ const sortedComponents = useMemo(() => {
+ const { triggerUpdate, containerId } = reorderContainerChildren;
+
+ // If a forced update occurred for a different container, return the previous order
+ const isForcedUpdate = prevForceUpdateRef.current !== triggerUpdate;
+ if (isForcedUpdate) {
+ prevForceUpdateRef.current = triggerUpdate;
+ if (containerId !== id) {
+ return prevComponentsOrder.current;
+ }
+ }
+
+ const currentPageComponents = getCurrentPageComponents();
+
+ const newComponentsOrder = [...components].sort((a, b) => {
+ const aTop = currentPageComponents?.[a]?.layouts?.[currentLayout]?.top;
+ const bTop = currentPageComponents?.[b]?.layouts?.[currentLayout]?.top;
+ if (aTop !== bTop) {
+ return aTop - bTop;
+ } else {
+ const aLeft = currentPageComponents?.[a]?.layouts?.[currentLayout]?.left;
+ const bLeft = currentPageComponents?.[b]?.layouts?.[currentLayout]?.left;
+ if (aLeft !== bLeft) {
+ return aLeft - bLeft;
+ }
+ return 0;
+ }
+ });
+
+ prevComponentsOrder.current = newComponentsOrder;
+ return newComponentsOrder;
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [components, currentLayout, reorderContainerChildren.triggerUpdate, id]);
+
+ return sortedComponents;
+};
+
+export default useSortedComponents;
diff --git a/frontend/src/AppBuilder/_hooks/useThemeAccess.js b/frontend/src/AppBuilder/_hooks/useThemeAccess.js
new file mode 100644
index 0000000000..c38318194b
--- /dev/null
+++ b/frontend/src/AppBuilder/_hooks/useThemeAccess.js
@@ -0,0 +1,10 @@
+import useStore from '@/AppBuilder/_stores/store';
+import { shallow } from 'zustand/shallow';
+
+const useThemeAccess = () => {
+ const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
+ const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
+ return licenseValid && featureAccess?.customThemes;
+};
+
+export default useThemeAccess;
diff --git a/frontend/src/AppBuilder/_stores/slices/appSlice.js b/frontend/src/AppBuilder/_stores/slices/appSlice.js
index 39cc7a4986..4b0ded7023 100644
--- a/frontend/src/AppBuilder/_stores/slices/appSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/appSlice.js
@@ -6,14 +6,16 @@ import DependencyGraph from './DependencyClass';
import { getWorkspaceId } from '@/_helpers/utils';
import { navigate } from '@/AppBuilder/_utils/misc';
import queryString from 'query-string';
-import { replaceEntityReferencesWithIds } from '../utils';
+import { replaceEntityReferencesWithIds, baseTheme } from '../utils';
import _ from 'lodash';
const initialState = {
app: {},
canvasHeight: null,
isSaving: false,
- globalSettings: {},
+ globalSettings: {
+ theme: baseTheme,
+ },
pageSwitchInProgress: false,
isTJDarkMode: localStorage.getItem('darkMode') === 'true',
isViewer: false,
@@ -94,7 +96,7 @@ export const createAppSlice = (set, get) => ({
console.error('Error updating page:', error);
}
},
- switchPage: (pageId, handle, queryParams = []) => {
+ switchPage: (pageId, handle, queryParams = [], isBackOrForward = false) => {
get().debugger.resetUnreadErrorCount();
// reset stores
if (get().pageSwitchInProgress) {
@@ -139,14 +141,19 @@ export const createAppSlice = (set, get) => ({
const queryParamsString = filteredQueryParams.map(([key, value]) => `${key}=${value}`).join('&');
const slug = get().app.slug;
- navigate(
- `/${isPreview ? 'applications' : getWorkspaceId() + '/apps'}/${slug ?? appId}/${handle}?${queryParamsString}`,
- {
- state: {
- isSwitchingPage: true,
- },
- }
- );
+ if (!isBackOrForward) {
+ navigate(
+ `/${isPreview ? 'applications' : getWorkspaceId() + '/apps'}/${slug ?? appId}/${handle}?${queryParamsString}`,
+ {
+ state: {
+ isSwitchingPage: true,
+ id: pageId,
+ handle: handle,
+ },
+ }
+ );
+ }
+
const newPage = pages.find((p) => p.id === pageId);
setResolvedPageConstants({
id: newPage?.id,
diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js
index 8f325dd614..b746f1237b 100644
--- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js
@@ -439,7 +439,7 @@ export const createComponentsSlice = (set, get) => ({
}
},
- validateWidget: ({ validationObject, widgetValue, customResolveObjects }) => {
+ validateWidget: ({ validationObject, widgetValue, customResolveObjects, componentType }) => {
const { getResolvedValue } = get();
let isValid = true;
let validationError = null;
@@ -455,6 +455,17 @@ export const createComponentsSlice = (set, get) => ({
validationRegex = typeof validationRegex === 'string' ? validationRegex : '';
const re = new RegExp(validationRegex, 'g');
+ if (componentType === 'EmailInput' && widgetValue) {
+ const validationRegex = '^(?!.*\\.\\.)([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,})$';
+ const emailRegex = new RegExp(validationRegex, 'g');
+ if (!emailRegex.test(widgetValue)) {
+ return {
+ isValid: false,
+ validationError: 'Input should be a valid email',
+ };
+ }
+ }
+
if (!re.test(widgetValue)) {
return {
isValid: false,
@@ -1833,6 +1844,9 @@ export const createComponentsSlice = (set, get) => ({
![
'TextInput',
'PasswordInput',
+ 'EmailInput',
+ 'PhoneInput',
+ 'CurrencyInput',
'NumberInput',
'DropdownV2',
'MultiselectV2',
@@ -1841,6 +1855,7 @@ export const createComponentsSlice = (set, get) => ({
'DaterangePicker',
'DatePickerV2',
'TimePicker',
+ 'TextArea',
].includes(componentType)
) {
return layoutData?.height;
diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js
index 995050d023..3d2d461518 100644
--- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js
@@ -444,7 +444,7 @@ export const createEventsSlice = (set, get) => ({
component: `[Page ${pageName}] [Component ${componentName}] [Event ${event?.eventId}] [Action ${event.actionId}]`,
page: `[Page ${pageName}] [Event ${event.eventId}] [Action ${event.actionId}]`,
query: `[Query ${getQueryName()}] [Event ${event.eventId}] [Action ${event.actionId}]`,
- customLog: `${event.description}`,
+ customLog: `${event.key}`,
};
return headerMap[source] || '';
@@ -457,7 +457,7 @@ export const createEventsSlice = (set, get) => ({
page: 'Event Errors with page',
component: 'Component Event',
query: 'Event Errors with query',
- customLog: 'Custom Log',
+ customLog: 'Queries',
};
return errorTargetMap[source];
@@ -470,8 +470,9 @@ export const createEventsSlice = (set, get) => ({
error: {
message: error.message,
description: JSON.stringify(error.message, null, 2),
- ...(event.component && componentId && { componentId: componentId }),
+ ...(event.component === 'component' && componentId && { componentId: componentId }),
},
+ description: event?.description,
errorTarget: constructErrorTarget(),
options: options,
strace: 'app_level',
@@ -584,7 +585,7 @@ export const createEventsSlice = (set, get) => ({
//! if resolvecode default value should be the value itself not empty string ... Ask KAVIN
const resolvedValue = getResolvedValue(event.url, customVariables);
// const url = resolveReferences(event.url, undefined, customVariables);
- window.open(resolvedValue, '_blank');
+ window.open(resolvedValue, event?.windowTarget === 'newTab' ? '_blank' : '_self');
return Promise.resolve();
}
case 'go-to-app': {
@@ -1128,30 +1129,51 @@ export const createEventsSlice = (set, get) => ({
return executeAction(event, mode, {});
};
- const logInfo = (log) => {
+ const logInfo = (log, isFromTransformation) => {
+ const query = dataQuery.queries.modules['canvas'].find((query) => query.id == queryId);
const error = new Error();
- const stackLine = error.stack.split('\n')[2];
+ const stackLine = error.stack.split('\n')[isFromTransformation ? 3 : 2];
const lineNumberMatch = stackLine.match(/:(\d+):\d+\)$/);
const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown';
- const event = { actionId: 'log-info', description: `${log}, Line ${lineNumber - 2}`, eventType: 'customLog' };
+ const event = {
+ actionId: 'log-info',
+ key: `${query.name}${isFromTransformation ? ', transformation' : ''}, line ${lineNumber - 2}`,
+ description: log,
+ eventType: 'customLog',
+ query,
+ };
return executeAction(event, mode, {});
};
- const logError = (log) => {
+ const logError = (log, isFromTransformation = false) => {
+ const query = dataQuery.queries.modules['canvas'].find((query) => query.id == queryId);
const error = new Error();
- const stackLine = error.stack.split('\n')[2];
+ const stackLine = error.stack.split('\n')[isFromTransformation ? 3 : 2];
const lineNumberMatch = stackLine.match(/:(\d+):\d+\)$/);
const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown';
- const event = { actionId: 'log-error', description: `${log}, Line ${lineNumber - 2}`, eventType: 'customLog' };
+ const event = {
+ actionId: 'log-error',
+ key: `${query.name}${isFromTransformation ? ', transformation' : ''}, line ${lineNumber - 2}`,
+ description: log,
+ eventType: 'customLog',
+ query,
+ };
return executeAction(event, mode, {});
};
- const log = (log) => {
+ const log = (log, isFromTransformation = false) => {
+ const query = dataQuery.queries.modules['canvas'].find((query) => query.id == queryId);
const error = new Error();
- const stackLine = error.stack.split('\n')[2];
+ const stackLine = error.stack.split('\n')[isFromTransformation ? 3 : 2];
const lineNumberMatch = stackLine.match(/:(\d+):\d+\)$/);
const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown';
- const event = { actionId: 'log', description: `${log}, Line ${lineNumber - 2}`, eventType: 'customLog' };
+ const event = {
+ actionId: 'log',
+ key: `${query.name}${isFromTransformation ? ', transformation' : ''}, line ${lineNumber - 2}`,
+ description: log,
+ eventType: 'customLog',
+ query,
+ };
return executeAction(event, mode, {});
};
diff --git a/frontend/src/AppBuilder/_stores/slices/gridSlice.js b/frontend/src/AppBuilder/_stores/slices/gridSlice.js
index 642266a32b..8b2f61bd9a 100644
--- a/frontend/src/AppBuilder/_stores/slices/gridSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/gridSlice.js
@@ -8,6 +8,10 @@ const initialState = {
lastCanvasIdClick: '',
lastCanvasClickPosition: null,
draggingComponentId: null,
+ reorderContainerChildren: {
+ containerId: null,
+ triggerUpdate: 0,
+ },
};
export const createGridSlice = (set, get) => ({
@@ -73,4 +77,26 @@ export const createGridSlice = (set, get) => ({
setLastCanvasClickPosition: (position) => {
set({ lastCanvasClickPosition: position });
},
+ checkIfAnyWidgetVisibilityChanged: () => {
+ // This is required to reload the grid if visibility is turned off using CSA
+ const { getExposedValueOfComponent, getCurrentPageComponents } = get();
+ const currentPageComponents = getCurrentPageComponents();
+
+ const visibilityState = {};
+
+ Object.keys(currentPageComponents).forEach((componentId) => {
+ const componentExposedVisibility = getExposedValueOfComponent(componentId)?.isVisible;
+
+ // Determine if component is visible
+ visibilityState[componentId] = !(componentExposedVisibility === false);
+ });
+
+ return visibilityState;
+ },
+ setReorderContainerChildren: (containerId) => {
+ // Function to trigger reordering of specific container for tab navigation
+ set((state) => ({
+ reorderContainerChildren: { containerId, triggerUpdate: state.reorderContainerChildren.triggerUpdate + 1 },
+ }));
+ },
});
diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js
index fbfb931130..811ebfc958 100644
--- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js
@@ -446,7 +446,7 @@ export const createQueryPanelSlice = (set, get) => ({
query,
'edit'
);
- if (finalData.status === 'failed') {
+ if (finalData?.status === 'failed') {
setResolvedQuery(queryId, {
isLoading: false,
});
@@ -650,7 +650,7 @@ export const createQueryPanelSlice = (set, get) => ({
query,
'edit'
);
- if (finalData.status === 'failed') {
+ if (finalData?.status === 'failed') {
onEvent('onDataQueryFailure', queryEvents);
setPreviewLoading(false);
setIsPreviewQueryLoading(false);
@@ -778,18 +778,30 @@ export const createQueryPanelSlice = (set, get) => ({
runTransformation: async (rawData, transformation, transformationLanguage = 'javascript', query, mode = 'edit') => {
const data = rawData;
const {
- queryPanel: { runPythonTransformation },
+ queryPanel: { runPythonTransformation, createProxy },
getResolvedState,
} = get();
- let result = [];
+ let result = {};
const currentState = getResolvedState();
if (transformationLanguage === 'python') {
result = await runPythonTransformation(currentState, data, transformation, query, mode);
} else if (transformationLanguage === 'javascript') {
try {
+ const { eventsSlice } = get();
+ const { generateAppActions } = eventsSlice;
+ const queriesInResolvedState = deepClone(currentState.queries);
+ const actions = generateAppActions(query?.id, mode);
+
+ const proxiedComponents = createProxy(currentState?.components, 'components');
+ const proxiedGlobals = createProxy(currentState?.globals, 'globals');
+ const proxiedConstants = createProxy(currentState?.constants, 'constants');
+ const proxiedVariables = createProxy(currentState?.variables, 'variables');
+ const proxiedPage = createProxy(deepClone(currentState?.page, 'page'));
+ const proxiedQueriesInResolvedState = createProxy(queriesInResolvedState, 'queries');
+
const evalFunction = Function(
- ['data', 'moment', '_', 'components', 'queries', 'globals', 'variables', 'page', 'constants'],
+ ['data', 'moment', '_', 'components', 'queries', 'globals', 'variables', 'page', 'constants', 'actions'],
transformation
);
@@ -797,32 +809,51 @@ export const createQueryPanelSlice = (set, get) => ({
data,
moment,
_,
- currentState.components,
- currentState.queries,
- currentState.globals,
- currentState.variables,
- currentState.page,
- currentState.constants
+ proxiedComponents,
+ proxiedQueriesInResolvedState,
+ proxiedGlobals,
+ proxiedVariables,
+ proxiedPage,
+ proxiedConstants,
+ {
+ logError: function (log) {
+ return actions.logError.call(actions, log, true);
+ },
+ logInfo: function (log) {
+ return actions.logInfo.call(actions, log, true);
+ },
+ log: function (log) {
+ return actions.log.call(actions, log, true);
+ },
+ }
);
} catch (err) {
- result = {
- message: err.stack.split('\n')[0],
- status: 'failed',
- data: data,
- };
+ const stackLines = err.stack.split('\n');
+ const errorLocation =
+ stackLines[2]?.match(/:(\d+):(\d+)/) ?? stackLines[1]?.match(/:(\d+):(\d+)/);
+
+ let lineNumber = null;
+
+ if (errorLocation) {
+ lineNumber = errorLocation[1] - 2;
+ }
+
+ console.log('JS execution failed: ', err);
+ let error = err.message || err.stack.split('\n')[0] || 'JS execution failed';
+ result = { status: 'failed', data: { message: error, description: error, lineNumber } };
+ get().debugger.log({
+ logLevel: result?.status === 'failed' ? 'error' : 'success',
+ type: 'transformation',
+ kind: query.kind,
+ key: `${query.name}, transformation, line ${result?.data?.lineNumber}`,
+ message: result?.message,
+ error: result?.data,
+ isTransformation: true,
+ isQuerySuccessLog: result?.status === 'failed' ? false : true,
+ errorTarget: 'Queries',
+ });
}
}
- get().debugger.log({
- logLevel: result?.status === 'failed' ? 'error' : 'success',
- type: 'transformation',
- kind: query.kind,
- key: query.name,
- message: result?.message,
- error: result,
- isTransformation: true,
- isQuerySuccessLog: result?.status === 'failed' ? false : true,
- errorTarget: 'Queries',
- });
return result;
},
@@ -890,12 +921,13 @@ export const createQueryPanelSlice = (set, get) => ({
createProxy: (obj, path = '') => {
const { queryPanel } = get();
const { createProxy } = queryPanel;
+
return new Proxy(obj, {
get(target, prop) {
const fullPath = path ? `${path}.${prop}` : prop;
if (!(prop in target)) {
- throw new Error(`Property "${fullPath}" is not defined`);
+ throw new Error(`ReferenceError: ${fullPath} is not defined`);
}
const value = target[prop];
@@ -984,13 +1016,16 @@ export const createQueryPanelSlice = (set, get) => ({
//Proxy Func required to get current execution line number from stack to log in debugger
- const proxiedComponents = createProxy(resolvedState?.components);
- const proxiedGlobals = createProxy(resolvedState?.globals);
- const proxiedConstants = createProxy(resolvedState?.constants);
- const proxiedVariables = createProxy(resolvedState?.variables);
- const proxiedPage = createProxy(deepClone(resolvedState?.page));
- const proxiedQueriesInResolvedState = createProxy(queriesInResolvedState);
- const proxiedFormattedParams = createProxy(!_.isEmpty(proxiedFormattedParams) ? [proxiedFormattedParams] : []);
+ const proxiedComponents = createProxy(deepClone(resolvedState?.components), 'components');
+ const proxiedGlobals = createProxy(deepClone(resolvedState?.globals), 'globals');
+ const proxiedConstants = createProxy(deepClone(resolvedState?.constants), 'constants');
+ const proxiedVariables = createProxy(deepClone(resolvedState?.variables), 'variables');
+ const proxiedPage = createProxy(deepClone(resolvedState?.page, 'page'));
+ const proxiedQueriesInResolvedState = createProxy(deepClone(queriesInResolvedState), 'queries');
+ const proxiedFormattedParams = createProxy(
+ !_.isEmpty(proxiedFormattedParams) ? [proxiedFormattedParams] : [],
+ 'params'
+ );
const fnParams = [
'moment',
diff --git a/frontend/src/AppBuilder/_stores/slices/whiteLabellingSlice.js b/frontend/src/AppBuilder/_stores/slices/whiteLabellingSlice.js
new file mode 100644
index 0000000000..d1031fea65
--- /dev/null
+++ b/frontend/src/AppBuilder/_stores/slices/whiteLabellingSlice.js
@@ -0,0 +1,19 @@
+//this slice is only for the app builder
+const initialState = {
+ activeOrganizationId: null,
+ whiteLabelText: 'ToolJet',
+ whiteLabelLogo: null,
+ whiteLabelFavicon: null,
+ loadingWhiteLabelDetails: true,
+ isWhiteLabelDetailsFetched: false,
+};
+
+export const createWhiteLabellingSlice = (set) => ({
+ ...initialState,
+ resetWhiteLabellingState: () => {
+ set(initialState);
+ },
+ updateWhiteLabelDetails: (details) => {
+ set(details);
+ },
+});
diff --git a/frontend/src/AppBuilder/_stores/store.js b/frontend/src/AppBuilder/_stores/store.js
index 84bfa60084..4d1392fc7c 100644
--- a/frontend/src/AppBuilder/_stores/store.js
+++ b/frontend/src/AppBuilder/_stores/store.js
@@ -27,6 +27,7 @@ import { createCodeHinterSlice } from './slices/codeHinterSlice';
import { createDebuggerSlice } from './slices/debuggerSlice';
import { createGitSyncSlice } from './slices/gitSyncSlice';
import { createAiSlice } from './slices/aiSlice';
+import { createWhiteLabellingSlice } from './slices/whiteLabellingSlice';
export default create(
zustandDevTools(
@@ -58,6 +59,7 @@ export default create(
...createDebuggerSlice(...state),
...createGitSyncSlice(...state),
...createAiSlice(...state),
+ ...createWhiteLabellingSlice(...state),
})),
{ name: 'App Builder Store', anonymousActionType: 'unknown' }
)
diff --git a/frontend/src/AppBuilder/_stores/utils.js b/frontend/src/AppBuilder/_stores/utils.js
index 33e50eb9cc..012d59aaf5 100644
--- a/frontend/src/AppBuilder/_stores/utils.js
+++ b/frontend/src/AppBuilder/_stores/utils.js
@@ -710,3 +710,15 @@ export const parsePropertyPath = (property) => {
return result;
};
+
+export const baseTheme = {
+ definition: {
+ brand: {
+ colors: {
+ primary: { light: '#4368E3', dark: '#4A6DD9' },
+ secondary: { light: '#6A727C', dark: '#CFD3D8' },
+ tertiary: { light: '#1E823B', dark: '#318344' },
+ },
+ },
+ },
+};
diff --git a/frontend/src/ConfirmationPage/OrganizationInvitationPage.jsx b/frontend/src/ConfirmationPage/OrganizationInvitationPage.jsx
index 6c161b7027..0b8df4872b 100644
--- a/frontend/src/ConfirmationPage/OrganizationInvitationPage.jsx
+++ b/frontend/src/ConfirmationPage/OrganizationInvitationPage.jsx
@@ -21,7 +21,7 @@ class OrganizationInvitationPageComponent extends React.Component {
this.state = {
isLoading: false,
- defaultState: false,
+ defaultState: checkWhiteLabelsDefaultState(),
};
this.formRef = React.createRef(null);
this.organizationId = new URLSearchParams(props?.location?.search).get('oid');
@@ -33,14 +33,8 @@ class OrganizationInvitationPageComponent extends React.Component {
componentDidMount() {
authenticationService.deleteLoginOrganizationId();
- setFaviconAndTitle(this.whiteLabelText, this.whiteLabelFavicon, this.props?.location);
- checkWhiteLabelsDefaultState(this.organizationId).then((res) => {
- this.setState({ defaultState: res });
- this.whiteLabelText = retrieveWhiteLabelText();
- this.whiteLabelFavicon = retrieveWhiteLabelFavicon();
- });
+ setFaviconAndTitle(this.props?.location);
document.addEventListener('keydown', this.handleEnterKey);
- this.setState({ defaultState: checkWhiteLabelsDefaultState() });
}
handleEnterKey = (e) => {
diff --git a/frontend/src/Editor/Components/ButtonGroup.jsx b/frontend/src/Editor/Components/ButtonGroup.jsx
index 67618a61ab..3647743e3a 100644
--- a/frontend/src/Editor/Components/ButtonGroup.jsx
+++ b/frontend/src/Editor/Components/ButtonGroup.jsx
@@ -25,6 +25,7 @@ export const ButtonGroup = function Button({
selectedBackgroundColor,
selectedTextColor,
boxShadow,
+ alignment,
} = styles;
const computedStyles = {
@@ -115,38 +116,53 @@ export const ButtonGroup = function Button({
fireEvent('onClick');
}
};
+
+ const mapAlignment = (alignment) => {
+ switch (alignment) {
+ case 'left':
+ return 'flex-start';
+ case 'right':
+ return 'flex-end';
+ case 'center':
+ return 'center';
+ default:
+ return 'flex-start'; // Default to left alignment if the value is unknown
+ }
+ };
return (
-
- {label && (
-
- {label}
-
- )}
+
- {data?.map((item, index) => (
-
- ))}
+ {label}
+
+ )}
+
+ {data?.map((item, index) => (
+
+ ))}
+
);
diff --git a/frontend/src/Editor/Components/Divider.jsx b/frontend/src/Editor/Components/Divider.jsx
index 5935181bf7..349c6f0ddc 100644
--- a/frontend/src/Editor/Components/Divider.jsx
+++ b/frontend/src/Editor/Components/Divider.jsx
@@ -1,20 +1,99 @@
import React from 'react';
-export const Divider = function Divider({ styles, dataCy, height, width, darkMode }) {
- const { visibility, dividerColor, boxShadow } = styles;
+const DASH_WIDTH = 4;
+const DASH_GAP = 4;
+export const Divider = function Divider({ dataCy, height, width, darkMode, styles, properties }) {
+ const { labelAlignment, labelColor, dividerColor, boxShadow, dividerStyle, padding } = styles;
+ const { label, visibility } = properties;
const color =
dividerColor === '' || ['#000', '#000000'].includes(dividerColor) ? (darkMode ? '#fff' : '#000') : dividerColor;
+
+ const dividerLineStyle = {
+ width: '100%',
+ padding: '0rem',
+ boxShadow,
+ ...(dividerStyle === 'dashed'
+ ? {
+ backgroundImage: `linear-gradient(to right, ${color} ${DASH_WIDTH}px, transparent ${DASH_GAP}px)`,
+ backgroundSize: `${DASH_WIDTH + DASH_GAP}px 1px`,
+ backgroundRepeat: 'repeat-x',
+ backgroundColor: 'transparent',
+ borderTop: 'none',
+ height: '1px',
+ }
+ : {
+ height: '1px',
+ backgroundColor: color,
+ borderTop: 'none',
+ }),
+ };
+
+ const labelStyles = {
+ color: labelColor,
+ boxShadow,
+ fontSize: '11px',
+ fontWeight: '500',
+ lineHeight: '16px',
+ };
+
+ // If no label, render the original divider
+ if (!label) {
+ return (
+
+ );
+ }
+
+ // With label - handle different positions
return (
-
+ {labelAlignment === 'left' && (
+ <>
+
{label}
+
+ >
+ )}
+
+ {labelAlignment === 'center' && (
+
+ )}
+
+ {labelAlignment === 'right' && (
+ <>
+
+
{label}
+ >
+ )}
);
};
diff --git a/frontend/src/Editor/Components/DropDown.jsx b/frontend/src/Editor/Components/DropDown.jsx
index 891391f0f4..b98be4e57d 100644
--- a/frontend/src/Editor/Components/DropDown.jsx
+++ b/frontend/src/Editor/Components/DropDown.jsx
@@ -236,7 +236,7 @@ export const DropDown = function DropDown({
minWidth: 'max-content',
}
: {
- backgroundColor: state.value === currentValue ? '#7A95FB' : 'white',
+ backgroundColor: state.value === currentValue ? 'var(--primary-brand)' : 'white',
color: state.isDisabled ? '#88909694' : state.value === currentValue ? 'white' : 'black',
':hover': {
backgroundColor: state.isDisabled ? 'transparent' : state.value === currentValue ? '#3650AF' : '#d8dce9',
diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx
index feb3087920..382ba010ed 100644
--- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx
+++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx
@@ -13,7 +13,7 @@ import CustomMenuList from './CustomMenuList';
import CustomOption from './CustomOption';
import Label from '@/_ui/Label';
import cx from 'classnames';
-import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor } from './utils';
+import { getInputBackgroundColor, getInputBorderColor, getInputFocusedColor, sortArray } from './utils';
import { isMobileDevice } from '@/_helpers/appUtils';
import useStore from '@/AppBuilder/_stores/store';
@@ -68,6 +68,7 @@ export const DropdownV2 = ({
loadingState: dropdownLoadingState,
disabledState,
optionsLoadingState,
+ sort,
} = properties;
const {
selectedTextColor,
@@ -116,6 +117,7 @@ export const DropdownV2 = ({
const foundItem = _schema?.find((item) => item?.default === true);
return !hasVisibleFalse(foundItem?.value) ? foundItem?.value : undefined;
}
+
const selectOptions = useMemo(() => {
let _options = advanced ? schema : options;
if (Array.isArray(_options)) {
@@ -128,11 +130,11 @@ export const DropdownV2 = ({
isDisabled: data?.disable ?? false,
}));
- return _selectOptions;
+ return sortArray(_selectOptions, sort);
} else {
return [];
}
- }, [advanced, schema, options]);
+ }, [advanced, schema, options, sort]);
function selectOption(value) {
const val = selectOptions.filter((option) => !option.isDisabled)?.find((option) => option.value === value);
diff --git a/frontend/src/Editor/Components/DropdownV2/utils.js b/frontend/src/Editor/Components/DropdownV2/utils.js
index 3c8edb9b9b..ed58cbe73d 100644
--- a/frontend/src/Editor/Components/DropdownV2/utils.js
+++ b/frontend/src/Editor/Components/DropdownV2/utils.js
@@ -67,3 +67,12 @@ export const highlightText = (text = '', highlight) => {
);
};
+
+export const sortArray = (arr, sort) => {
+ if (sort === 'asc') {
+ return arr.sort((a, b) => a.label?.localeCompare(b.label));
+ } else if (sort === 'desc') {
+ return arr.sort((a, b) => b.label?.localeCompare(a.label));
+ }
+ return arr;
+};
diff --git a/frontend/src/Editor/Components/FilePicker.jsx b/frontend/src/Editor/Components/FilePicker.jsx
index b2e2b1ff86..29b127ec7c 100644
--- a/frontend/src/Editor/Components/FilePicker.jsx
+++ b/frontend/src/Editor/Components/FilePicker.jsx
@@ -1,4 +1,5 @@
import React, { useEffect, useMemo, useRef } from 'react';
+// eslint-disable-next-line import/no-unresolved
import { useDropzone } from 'react-dropzone';
import { toast } from 'react-hot-toast';
// eslint-disable-next-line import/no-unresolved
@@ -311,7 +312,11 @@ export const FilePicker = ({
-
+
{showSelectedFiles && !accepted ? (
diff --git a/frontend/src/Editor/Components/Image/Image.jsx b/frontend/src/Editor/Components/Image/Image.jsx
index 0f3ea0e2b0..6df6bd047e 100644
--- a/frontend/src/Editor/Components/Image/Image.jsx
+++ b/frontend/src/Editor/Components/Image/Image.jsx
@@ -23,8 +23,17 @@ export const Image = function Image({
}) {
const { imageFormat, source, jsSchema, alternativeText, zoomButtons, rotateButton, loadingState, disabledState } =
properties;
- const { imageFit, imageShape, backgroundColor, padding, customPadding, boxShadow, borderRadius, borderColor } =
- styles;
+ const {
+ imageFit,
+ imageShape,
+ backgroundColor,
+ padding,
+ customPadding,
+ boxShadow,
+ borderRadius,
+ borderColor,
+ alignment,
+ } = styles;
const isInitialRender = useRef(true);
@@ -168,6 +177,7 @@ export const Image = function Image({
border: '1px solid',
borderRadius: imageShape === 'circle' ? '50%' : `${borderRadius}px`,
borderColor: borderColor ? borderColor : 'transparent',
+ objectPosition: alignment,
}}
height={height}
onClick={() => fireEvent('onClick')}
diff --git a/frontend/src/Editor/Components/MultiselectV2/CustomOption.jsx b/frontend/src/Editor/Components/MultiselectV2/CustomOption.jsx
index ab87fb22b7..56b38b2edb 100644
--- a/frontend/src/Editor/Components/MultiselectV2/CustomOption.jsx
+++ b/frontend/src/Editor/Components/MultiselectV2/CustomOption.jsx
@@ -8,7 +8,7 @@ import { highlightText } from '../DropdownV2/utils';
const CustomOption = (props) => {
return (