Merge branch 'main' into release/community

This commit is contained in:
Akshay Sasidharan 2024-04-24 17:59:33 +05:30
commit 76300cf23c
142 changed files with 9064 additions and 2952 deletions

View file

@ -1 +1 @@
2.35.4
2.36.0

View file

@ -1 +1 @@
2.35.4
2.36.0

View file

@ -706,7 +706,7 @@
"columnType": "Column type",
"columnName": "Column name",
"overflow": "Overflow",
"key": "key",
"key": "Key",
"textColor": "Text color",
"validation": "Validation",
"regex": "Regex",
@ -716,8 +716,8 @@
"values": "Values",
"labels": "Labels",
"cellBgColor": "Cell background color",
"dateDisplayformat": "Date display format",
"dateParseformat": "Date parse format",
"dateDisplayformat": "Date format",
"dateParseformat": "Date",
"showTime": "show time",
"makeEditable": "make editable",
"buttonText": "Button text",
@ -727,7 +727,10 @@
"addColumn": "Add column",
"addNewColumn": "Add new column",
"noActionMessage": "This table doesn't have any action buttons",
"horizontalAlignment": "horizontal alignment"
"horizontalAlignment":"Horizontal alignment",
"textAlignment":"Text alignment",
"deciamalPlaces":"Decimal Places",
"imageFit":"Image fit"
},
"Button": {
"displayName": "Button",
@ -947,6 +950,7 @@
"maxWidthOfCanvas": "Max width of canvas",
"maxHeightOfCanvas": "Max height of canvas",
"backgroundColorOfCanvas": "Canvas background",
"appMode": "App mode",
"exportApp": "Export app"
},
"Back": {

View file

@ -355,7 +355,7 @@
"blankPage": {
"welcomeToToolJet": "¡Bienvenido a ToolJet!",
"getStartedCreateNewApp": "Puedes empezar creando una nueva aplicación o creando una aplicación usando una plantilla en la biblioteca de ToolJet.",
"importApplication": "Importar una aplicación",
"importApplication": "Importar una aplicación"
},
"foldersSection": {
"allApplications": "Todas las aplicaciones",
@ -387,11 +387,11 @@
"templateCard": {
"use": "Usar",
"preview": "Prevista",
"leadGeneretion": "Generación de líderes",
"leadGeneretion": "Generación de líderes"
},
"templateLibraryModal": {
"select": "Seleccionar plantilla",
"createAppfromTemplate": "Crear aplicación desde plantilla",
"createAppfromTemplate": "Crear aplicación desde plantilla"
}
},
"confirmationPage": {
@ -662,7 +662,7 @@
"setColor": "Establecer color",
"structure": "Estructura",
"checkedValues": "Valores marcados",
"expandedValues": "Valores expandidos",
"expandedValues": "Valores expandidos"
},
"Table": {
"displayName": "Tabla",
@ -689,7 +689,7 @@
"remove": "Eliminar",
"addButton": "+ Agregar botón",
"addColumn": "+ Agregar columna",
"noActionMessage": "Esta tabla no tiene botones de acción",
"noActionMessage": "Esta tabla no tiene botones de acción"
},
"Button": {
"displayName": "Botón",
@ -898,7 +898,7 @@
"text": "Comentarios",
"tip": "Habilitar comentarios",
"commentBody": "No hay comentarios para desplegar",
"typeComment": "Escriba su comentario aquí",
"typeComment": "Escriba su comentario aquí"
},
"Settings": {
"text": "Ajustes",
@ -907,7 +907,7 @@
"maintenanceMode": "Modo de mantenimiento",
"maxWidthOfCanvas": "Ancho máximo del lienzo",
"maxHeightOfCanvas": "Altura máxima del lienzo",
"backgroundColorOfCanvas": "Color de fondo del lienzo",
"backgroundColorOfCanvas": "Color de fondo del lienzo"
},
"Back": {
"text": "Atrás",

View file

@ -67,7 +67,7 @@
"react-circular-progressbar": "^2.1.0",
"react-color": "^2.19.3",
"react-copy-to-clipboard": "^5.1.0",
"react-datepicker": "^4.10.0",
"react-datepicker": "^4.25.0",
"react-dates": "^21.8.0",
"react-datetime": "^3.2.0",
"react-dnd": "^16.0.1",
@ -36427,9 +36427,9 @@
}
},
"node_modules/plotly.js-dist-min": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-2.29.1.tgz",
"integrity": "sha512-YvqX5TISWsJVTDIaUh2Qgt9uhLl0bWcQhO2rLPF0/hIY9BlinFa1JwSO2jFKcEmG0AJXSo4DnVdgKpsk9/8Apg=="
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/plotly.js-dist-min/-/plotly.js-dist-min-2.30.1.tgz",
"integrity": "sha512-xWLni6EpBDwG/EdjCBTu+GK1Asuqswgh+YC77t0UAxa5OFFgpjYkVuvozZmkWqT9TKpvzjxkK4LLGv5Rn+yo7Q=="
},
"node_modules/point-in-polygon": {
"version": "1.1.0",
@ -37237,8 +37237,9 @@
}
},
"node_modules/react-datepicker": {
"version": "4.24.0",
"license": "MIT",
"version": "4.25.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.25.0.tgz",
"integrity": "sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==",
"dependencies": {
"@popperjs/core": "^2.11.8",
"classnames": "^2.2.6",

View file

@ -62,7 +62,7 @@
"react-circular-progressbar": "^2.1.0",
"react-color": "^2.19.3",
"react-copy-to-clipboard": "^5.1.0",
"react-datepicker": "^4.10.0",
"react-datepicker": "^4.25.0",
"react-dates": "^21.8.0",
"react-datetime": "^3.2.0",
"react-dnd": "^16.0.1",

View file

@ -31,23 +31,28 @@ import 'react-tooltip/dist/react-tooltip.css';
import { getWorkspaceIdOrSlugFromURL } from '@/_helpers/routes';
import ErrorPage from '@/_components/ErrorComponents/ErrorPage';
import WorkspaceConstants from '@/WorkspaceConstants';
import { useAppDataStore } from '@/_stores/appDataStore';
import { useSuperStore } from '../_stores/superStore';
import { ModuleContext } from '../_contexts/ModuleContext';
import cx from 'classnames';
import useAppDarkMode from '@/_hooks/useAppDarkMode';
import { ManageOrgUsers } from '@/ManageOrgUsers';
import { ManageGroupPermissions } from '@/ManageGroupPermissions';
import OrganizationLogin from '@/_components/OrganizationLogin/OrganizationLogin';
import { ManageOrgVars } from '@/ManageOrgVars';
const AppWrapper = (props) => {
const { isAppDarkMode } = useAppDarkMode();
return (
<Suspense fallback={null}>
<BrowserRouter basename={window.public_config?.SUB_PATH || '/'}>
<AppWithRouter props={props} />
<AppWithRouter props={props} isAppDarkMode={isAppDarkMode} />
</BrowserRouter>
</Suspense>
);
};
class AppComponent extends React.Component {
static contextType = ModuleContext;
constructor(props) {
super(props);
@ -55,6 +60,7 @@ class AppComponent extends React.Component {
currentUser: null,
fetchedMetadata: false,
darkMode: localStorage.getItem('darkMode') === 'true',
isEditorOrViewer: '',
};
}
updateSidebarNAV = (val) => {
@ -62,7 +68,7 @@ class AppComponent extends React.Component {
};
fetchMetadata = () => {
tooljetService.fetchMetaData().then((data) => {
useAppDataStore.getState().actions.setMetadata(data);
useSuperStore.getState().modules[this.context].useAppDataStore.getState().actions.setMetadata(data);
localStorage.setItem('currentVersion', data.installed_version);
if (data.latest_version && lt(data.installed_version, data.latest_version) && data.version_ignored === false) {
this.setState({ updateAvailable: true });
@ -98,19 +104,19 @@ class AppComponent extends React.Component {
switchDarkMode = (newMode) => {
this.setState({ darkMode: newMode });
useSuperStore.getState().modules[this.context].useAppDataStore.getState().actions.updateIsTJDarkMode(newMode);
localStorage.setItem('darkMode', newMode);
};
render() {
const { updateAvailable, darkMode } = this.state;
const { updateAvailable, darkMode, isEditorOrViewer } = this.state;
let toastOptions = {
style: {
wordBreak: 'break-all',
},
};
if (darkMode) {
if (isEditorOrViewer === 'viewer' ? this.props.isAppDarkMode : darkMode) {
toastOptions = {
className: 'toast-dark-mode',
style: {
@ -125,7 +131,12 @@ class AppComponent extends React.Component {
const { updateSidebarNAV } = this;
return (
<>
<div className={`main-wrapper ${darkMode ? 'theme-dark dark-theme' : ''}`} data-cy="main-wrapper">
<div
className={cx('main-wrapper', {
'theme-dark dark-theme': !isEditorOrViewer && darkMode,
})}
data-cy="main-wrapper"
>
{updateAvailable && (
<div className="alert alert-info alert-dismissible" role="alert">
<h3 className="mb-1">Update available</h3>
@ -181,7 +192,11 @@ class AppComponent extends React.Component {
path="/:workspaceId/apps/:slug/:pageHandle?/*"
element={
<PrivateRoute>
<AppLoader switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
<AppLoader
switchDarkMode={this.switchDarkMode}
darkMode={darkMode}
setEditorOrViewer={(value) => this.setState({ isEditorOrViewer: value })}
/>
</PrivateRoute>
}
/>
@ -199,7 +214,11 @@ class AppComponent extends React.Component {
path="/applications/:slug/:pageHandle?"
element={
<PrivateRoute>
<Viewer switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
<Viewer
switchDarkMode={this.switchDarkMode}
darkMode={this.props.isAppDarkMode}
setEditorOrViewer={(value) => this.setState({ isEditorOrViewer: value })}
/>
</PrivateRoute>
}
/>
@ -208,7 +227,11 @@ class AppComponent extends React.Component {
path="/applications/:slug/versions/:versionId/:pageHandle?"
element={
<PrivateRoute>
<Viewer switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
<Viewer
switchDarkMode={this.switchDarkMode}
darkMode={this.props.isAppDarkMode}
setEditorOrViewer={(value) => this.setState({ isEditorOrViewer: value })}
/>
</PrivateRoute>
}
/>

View file

@ -19,6 +19,7 @@ export const AppVersionsManager = function ({
onVersionDelete,
isEditable = true,
isViewer,
darkMode,
}) {
const [appVersionStatus, setGetAppVersionStatus] = useState(appVersionLoadingStatus.loading);
const [deleteVersion, setDeleteVersion] = useState({
@ -53,8 +54,6 @@ export const AppVersionsManager = function ({
};
}, [appVersions]);
const darkMode = localStorage.getItem('darkMode') === 'true';
const selectVersion = (id) => {
appVersionService
.getAppVersionData(appId, id)
@ -173,8 +172,8 @@ export const AppVersionsManager = function ({
value={editingVersion?.id}
onChange={(id) => selectVersion(id)}
{...customSelectProps}
className={` ${darkMode && 'dark-theme'}`}
isEditable={isEditable}
darkMode={darkMode}
/>
</div>
</div>

View file

@ -67,6 +67,7 @@ import { EditorContext } from '@/Editor/Context/EditorContextWrapper';
import { useTranslation } from 'react-i18next';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppInfo } from '@/_stores/appDataStore';
import { useModuleName } from '../_contexts/ModuleContext';
import { isPDFSupported } from '@/_stores/utils';
export const AllComponents = {
@ -158,10 +159,12 @@ export const Box = memo(
isResizing,
adjustHeightBasedOnAlignment,
currentLayout,
darkMode,
}) => {
const { t } = useTranslation();
const backgroundColor = yellow ? 'yellow' : '';
const currentState = useCurrentState();
const moduleName = useModuleName();
const { events } = useAppInfo();
const shouldAddBoxShadowAndVisibility = ['TextInput', 'PasswordInput', 'NumberInput', 'Text'];
@ -204,7 +207,6 @@ export const Box = memo(
? validateProperties(resolvedGeneralStyles, componentMeta.generalStyles)
: [resolvedGeneralStyles, []];
const darkMode = localStorage.getItem('darkMode') === 'true';
const { variablesExposedForPreview, exposeToCodeHinter } = useContext(EditorContext) || {};
let styles = {

View file

@ -43,6 +43,8 @@ import { Input } from './Elements/Input';
import { Icon } from './Elements/Icon';
import { Visibility } from './Elements/Visibility';
import { NumberInput } from './Elements/NumberInput';
import TableRowHeightInput from './Elements/TableRowHeightInput';
import { validateProperty } from '../component-properties-validation';
const HIDDEN_CODE_HINTER_LABELS = ['Table data', 'Column data', 'Text Format', 'TextComponentTextInput'];
@ -63,6 +65,7 @@ const AllElements = {
Icon,
Visibility,
NumberInput,
TableRowHeightInput,
};
export function CodeHinter({
@ -407,7 +410,15 @@ export function CodeHinter({
const fxBtn = () => (
<div className="col-auto pt-0 fx-common">
{!['Type', 'selectRowOnCellEdit', 'Select row on cell edit', ' ', 'Padding', 'Width'].includes(paramLabel) && ( //add some key if these extends
{![
'Type',
'selectRowOnCellEdit',
'Select row on cell edit',
' ',
'Padding',
'Width',
'Make all columns editable',
].includes(paramLabel) && ( //add some key if these extends
<FxButton
active={codeShow}
onPress={() => {

View file

@ -29,7 +29,7 @@ export const BoxShadow = ({ value, onChange, cyLabel }) => {
const colorPickerStyle = {
position: 'absolute',
bottom: '260px',
top: '-220px',
};
useEffect(() => {

View file

@ -45,8 +45,9 @@ export const Color = ({ value, onChange, pickerStyle = {}, cyLabel, asBoxShadowP
// This is fix when color picker don't have much space to open in bottom side
{ 'inspector-color-input-popover': colorPickerPosition === 'top' }
)}
style={{ zIndex: 10000 }}
>
<Popover.Body className={!asBoxShadowPopover && 'boxshadow-picker'}>
<Popover.Body className={!asBoxShadowPopover && 'boxshadow-picker'} style={{ padding: '0px' }}>
<>{ColorPicker()}</>
</Popover.Body>
</Popover>

View file

@ -2,60 +2,82 @@ import React from 'react';
import SelectComponent from '@/_ui/Select';
import { components } from 'react-select';
import Check from '@/_ui/Icon/solidIcons/Check';
import Icon from '@/_ui/Icon/solidIcons/index';
import {
DeprecatedColumnTooltip,
checkIfTableColumnDeprecated,
} from '../../Inspector/Components/Table/ColumnManager/DeprecatedColumnTypeMsg';
const Option = (props) => {
export const Option = (props) => {
const isDeprecated = checkIfTableColumnDeprecated(props.value);
return (
<components.Option {...props}>
<div className="d-flex justify-content-between">
<span>{props.label}</span>
{props.isSelected && (
<span>
<Check width={'20'} fill={'#3E63DD'} />
</span>
)}
</div>
<DeprecatedColumnTooltip columnType={props.value}>
<div className="d-flex justify-content-between">
<span>{props.label}</span>
{props.isSelected && (
<span>
<Check width={'20'} fill={'#3E63DD'} />
</span>
)}
{isDeprecated && (
<span>
<Icon name={'warning'} height={16} width={16} fill="#DB4324" />
</span>
)}
</div>
</DeprecatedColumnTooltip>
</components.Option>
);
};
const selectCustomStyles = {
control: (base, state) => {
return {
...base,
border: state.isFocused ? '1px solid #3E63DD' : '1px solid #cccccc',
boxShadow: state.isFocused ? '0px 0px 6px #3E63DD' : 'none',
backgroundColor: state.isFocused ? 'var(--indigo2)' : 'var(--base)',
'&:hover': {
border: '1px solid #3E63DD !important',
boxShadow: '0px 0px 6px #3E63DD',
},
borderRadius: '6px',
width: '144px',
minHeight: '32px',
};
},
const selectCustomStyles = (width) => {
return {
control: (base, state) => {
return {
...base,
border: state.isFocused ? '1px solid #3E63DD' : '1px solid #cccccc',
boxShadow: state.isFocused ? '0px 0px 6px #3E63DD' : 'none',
backgroundColor: state.isFocused ? 'var(--indigo2)' : 'var(--base)',
'&:hover': {
border: '1px solid #3E63DD !important',
boxShadow: '0px 0px 6px #3E63DD',
},
borderRadius: '6px',
width: width,
minHeight: '32px',
color: 'var(--slate12)',
};
},
dropdownIndicator: (base) => ({
...base,
padding: '4px',
}),
menuList: (base) => ({
...base,
padding: '4px',
}),
option: (base, state) => ({
...base,
backgroundColor: state.isFocused ? '#F0F4FF !important' : 'white',
color: '#11181C',
borderRadius: '6px',
}),
dropdownIndicator: (base) => ({
...base,
padding: '4px',
}),
menuList: (base) => ({
...base,
padding: '4px',
}),
option: (base, state) => ({
...base,
backgroundColor: state.isFocused ? '#F0F4FF !important' : 'white',
color: '#11181C',
borderRadius: '6px',
}),
singleValue: (provided) => ({
...provided,
color: 'var(--slate12)',
}),
};
};
export const Select = ({ value, onChange, meta }) => {
export const Select = ({ value, onChange, meta, width = '144px' }) => {
return (
<div
className="row fx-container"
data-cy={`dropdown-${meta.displayName ? String(meta.displayName).toLowerCase().replace(/\s+/g, '-') : 'common'}`}
data-cy={`dropdown-${
meta?.displayName ? String(meta?.displayName).toLowerCase().replace(/\s+/g, '-') : 'common'
}`}
>
<div className="field" onClick={(e) => e.stopPropagation()}>
<SelectComponent
@ -65,7 +87,7 @@ export const Select = ({ value, onChange, meta }) => {
onChange={onChange}
width={224}
height={32}
styles={selectCustomStyles}
styles={selectCustomStyles(width)}
useCustomStyles={true}
classNamePrefix="inspector-select"
components={{

View file

@ -30,7 +30,7 @@ function Slider1({ value, onChange, component }) {
};
return (
<div className="d-flex flex-column " style={{ width: '142px', position: 'relative' }}>
<div className="d-flex flex-column " style={{ width: '142px', marginBottom: '16px', position: 'relative' }}>
<CustomInput
disabled={component.component.definition.styles.auto.value}
value={sliderValue}

View file

@ -0,0 +1,51 @@
import React, { useEffect, useState } from 'react';
const MIN_TABLE_ROW_HEIGHT_CONDENSED = 39;
const MIN_TABLE_ROW_HEIGHT_DEFAULT = 45;
const TableRowHeightInput = ({ value, onChange, cyLabel, staticText, component: { component } }) => {
const [inputValue, setInputValue] = useState(value);
useEffect(() => {
setInputValue(value < minValue ? minValue : value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, component?.definition?.styles?.cellSize?.value]);
const minValue =
component?.definition?.styles?.cellSize?.value === 'condensed'
? MIN_TABLE_ROW_HEIGHT_CONDENSED
: MIN_TABLE_ROW_HEIGHT_DEFAULT;
const handleBlur = () => {
const newValue = Math.max(inputValue, minValue);
setInputValue(newValue);
onChange(newValue);
};
const handleChange = (e) => {
setInputValue(e.target.value);
};
return (
<div className="form-text tj-number-input-element">
<input
style={{ width: '142px', height: '32px' }}
data-cy={`${cyLabel}-input`}
type="number"
className="tj-input-element tj-text-xsm"
value={inputValue}
placeholder=""
id="labelId"
min={minValue}
onChange={handleChange}
onBlur={handleBlur}
autoComplete="off"
/>
<label htmlFor="labelId" className="static-value tj-text-xsm">
{staticText ? staticText : 'px'}
</label>
</div>
);
};
export default TableRowHeightInput;

View file

@ -1,6 +1,6 @@
import React from 'react';
export const Toggle = ({ value, onChange, cyLabel }) => {
export const Toggle = ({ value, onChange, cyLabel, meta }) => {
return (
<div className="row fx-container">
<div className="col d-flex align-items-center">
@ -9,6 +9,14 @@ export const Toggle = ({ value, onChange, cyLabel }) => {
className="form-check form-switch mb-0 d-flex justify-content-end"
style={{ marginBottom: '0px', paddingLeft: '28px' }}
>
{meta.toggleLabel && (
<span
className="font-weight-400 font-size-12 d-flex align-items-center color-slate12"
style={{ marginRight: '78px' }}
>
{meta.toggleLabel}
</span>
)}
<input
className="form-check-input"
type="checkbox"

View file

@ -17,4 +17,5 @@ export const TypeMapping = {
icon: 'Icon',
visibility: 'Visibility',
numberInput: 'NumberInput',
tableRowHeightInput: 'TableRowHeightInput',
};

View file

@ -121,6 +121,7 @@ export const Chart = function Chart({
b: padding,
t: padding,
},
...(chartLayout.annotations && { annotations: chartLayout.annotations }),
barmode: barmode,
hoverlabel: { namelength: -1 },
};

View file

@ -127,7 +127,6 @@ export const Listview = function Listview({
key={index}
data-cy={`${String(component.name).toLowerCase()}-row-${index}`}
onClick={(event) => {
event.preventDefault();
onRecordClicked(index);
onRowClicked(index);
}}

View file

@ -42,6 +42,7 @@ export const Modal = function Modal({
boxShadow,
} = styles;
const parentRef = useRef(null);
const isInitialRender = useRef(true);
const title = properties.title ?? '';
const size = properties.size ?? 'lg';
@ -62,14 +63,24 @@ export const Modal = function Modal({
}, [setShowModal]);
useEffect(() => {
if (isInitialRender.current) {
isInitialRender.current = false;
return;
}
const canShowModal = exposedVariables.show ?? false;
setShowModal(exposedVariables.show ?? false);
fireEvent(canShowModal ? 'onOpen' : 'onClose');
setShowModal(exposedVariables.show ?? false);
const inputRef = document?.getElementsByClassName('tj-text-input-widget')?.[0];
inputRef?.blur();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [exposedVariables.show]);
function hideModal() {
setShowModal(false);
setExposedVariable('show', false);
fireEvent('onClose');
}
useEffect(() => {
const handleModalOpen = () => {
const canvasElement = document.getElementsByClassName('canvas-area')[0];
@ -127,11 +138,6 @@ export const Modal = function Modal({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showModal, modalHeight]);
function hideModal() {
setShowModal(false);
setExposedVariable('show', false);
fireEvent('onClose');
}
const backwardCompatibilityCheck = height == '34' || modalHeight != undefined ? true : false;
const customStyles = {

View file

@ -39,7 +39,7 @@ export const NumberInput = function NumberInput({
accentColor,
} = styles;
const textColor = darkMode && ['#232e3c', '#000000ff'].includes(styles.textColor) ? '#fff' : styles.textColor;
const textColor = darkMode && ['#232e3c', '#000000ff'].includes(styles.textColor) ? '#CFD3D8' : styles.textColor;
const isMandatory = resolveReferences(component?.definition?.validation?.mandatory?.value, currentState) ?? false;
const minValue = resolveReferences(component?.definition?.validation?.minValue?.value, currentState) ?? null;
const maxValue = resolveReferences(component?.definition?.validation?.maxValue?.value, currentState) ?? null;
@ -165,22 +165,34 @@ export const NumberInput = function NumberInput({
const computedStyles = {
height: height == 36 ? (padding == 'default' ? '36px' : '40px') : padding == 'default' ? height : height + 4,
borderRadius: `${borderRadius}px`,
color: darkMode && textColor === '#11181C' ? '#ECEDEE' : textColor,
borderColor: isFocused
? accentColor
: ['#D7DBDF'].includes(borderColor)
? darkMode
? '#6D757D7A'
: '#6A727C47'
: borderColor,
'--tblr-input-border-color-darker': tinycolor(borderColor).darken(24).toString(),
backgroundColor:
darkMode && ['#ffffff', '#ffffffff', '#fff'].includes(backgroundColor) ? '#313538' : backgroundColor,
boxShadow: boxShadow,
padding: styles.iconVisibility ? '8px 10px 8px 29px' : '8px 10px 8px 10px',
padding: styles.iconVisibility
? height < 20
? '0px 10px 0px 29px'
: '8px 10px 8px 29px'
: height < 20
? '0px 10px'
: '8px 10px',
overflow: 'hidden',
textOverflow: 'ellipsis',
color: textColor !== '#1B1F24' ? textColor : disable || loading ? 'var(--text-disabled)' : 'var(--text-primary)',
borderColor: isFocused
? accentColor != '4368E3'
? accentColor
: 'var(--primary-accent-strong)'
: borderColor != '#CCD1D5'
? borderColor
: disable || loading
? 'var(--borders-disabled-on-white-dimmed)'
: 'var(--borders-default)',
'--tblr-input-border-color-darker': tinycolor(borderColor).darken(24).toString(),
backgroundColor: !['#ffffff', '#ffffffff', '#fff'].includes(backgroundColor)
? backgroundColor
: disable || loading
? darkMode
? 'var(--surfaces-app-bg-default)'
: 'var(--surfaces-surface-03)'
: 'var(--surfaces-surface-01)',
};
const defaultAlignment = alignment === 'side' || alignment === 'top' ? alignment : 'side';
@ -278,7 +290,6 @@ export const NumberInput = function NumberInput({
<>
<div
data-cy={`label-${String(component.name).toLowerCase()}`}
data-disabled={disable || loading}
className={`text-input tj-number-input-widget d-flex ${
defaultAlignment === 'top' &&
((width != 0 && label && label?.length != 0) || (auto && width == 0 && label && label?.length != 0))
@ -293,6 +304,7 @@ export const NumberInput = function NumberInput({
display: !visibility ? 'none' : 'flex',
whiteSpace: 'nowrap',
}}
data-disabled={disable || loading}
>
<Label
label={label}
@ -320,7 +332,7 @@ export const NumberInput = function NumberInput({
? '11px'
: (label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)
? `${labelWidth + 11}px`
: '11px', //23 :: is 10 px inside the input + 1 px border + 12px margin right
: '11px', //11 :: is 10 px inside the input + 1 px border + 12px margin right
position: 'absolute',
top: `${
defaultAlignment === 'side'
@ -330,7 +342,7 @@ export const NumberInput = function NumberInput({
: '50%'
}`,
transform: ' translateY(-50%)',
color: iconColor,
color: iconColor !== '#CFD3D859' ? iconColor : 'var(--icons-weak-disabled)',
zIndex: 3,
}}
stroke={1.5}
@ -369,7 +381,8 @@ export const NumberInput = function NumberInput({
<div onClick={(e) => handleIncrement(e)}>
<SolidIcon
width={padding == 'default' ? `${height / 2 - 1}px` : `${height / 2 + 1}px`}
height={`${height / 2}px`}
height={padding == 'default' ? `${height / 2 - 1}px` : `${height / 2 + 1}px`}
fill={'var(--icons-default)'}
style={{
top: defaultAlignment === 'top' && label?.length > 0 && width > 0 ? '21px' : '1px',
right:
@ -378,19 +391,26 @@ export const NumberInput = function NumberInput({
: alignment == 'side' && direction === 'right'
? `${labelWidth + 1}px`
: '1px',
borderLeft: darkMode ? '1px solid #313538' : '1px solid #D7D7D7',
borderBottom: darkMode ? '.5px solid #313538' : '0.5px solid #D7D7D7',
borderLeft:
disable || loading
? '1px solid var(--borders-weak-disabled)'
: '1px solid var(--borders-default)',
borderBottom:
disable || loading
? '1px solid var(--borders-weak-disabled)'
: '.5px solid var(--borders-default)',
borderTopRightRadius: borderRadius - 1,
backgroundColor: !darkMode ? 'white' : 'black',
backgroundColor: 'transparent',
zIndex: 3,
}}
className="numberinput-up-arrow arrow"
name="cheveronup"
className="numberinput-up-arrow arrow number-input-arrow"
name="TriangleDownCenter"
></SolidIcon>
</div>
<div onClick={(e) => handleDecrement(e)}>
<SolidIcon
fill={'var(--icons-default)'}
style={{
right:
labelWidth == 0
@ -399,30 +419,37 @@ export const NumberInput = function NumberInput({
? `${labelWidth + 1}px`
: '1px',
bottom: '1px',
borderLeft: darkMode ? '1px solid #313538' : '1px solid #D7D7D7',
borderTop: darkMode ? '0.5px solid #313538' : '0.5px solid #D7D7D7',
borderLeft:
disable || loading
? '1px solid var(--borders-weak-disabled)'
: '1px solid var(--borders-default)',
borderTop:
disable || loading
? '1px solid var(--borders-weak-disabled)'
: '.5px solid var(--borders-default)',
borderBottomRightRadius: borderRadius - 1,
backgroundColor: !darkMode ? 'white' : 'black',
backgroundColor: 'transparent',
zIndex: 3,
}}
width={padding == 'default' ? `${height / 2 - 1}px` : `${height / 2 + 1}px`}
height={`${height / 2}px`}
className="numberinput-down-arrow arrow"
name="cheverondown"
height={padding == 'default' ? `${height / 2 - 1}px` : `${height / 2 + 1}px`}
className="numberinput-down-arrow arrow number-input-arrow"
name="TriangleUpCenter"
></SolidIcon>
</div>
</>
)}
{loading && <Loader style={{ ...loaderStyle }} width="16" />}
</div>
{showValidationError && visibility && (
<div
className="tj-text-sm"
data-cy={`${String(component.name).toLowerCase()}-invalid-feedback`}
style={{
color: errTextColor,
color: errTextColor !== '#D72D39' ? errTextColor : 'var(--status-error-strong)',
textAlign: direction == 'left' && 'end',
fontSize: '11px',
fontWeight: '400',
lineHeight: '16px',
}}
>
{showValidationError && validationError}

View file

@ -9,6 +9,7 @@ export const Pagination = ({
fireEvent,
darkMode,
dataCy,
width,
}) => {
const { visibility, disabledState, boxShadow } = styles;
const [currentPage, setCurrentPage] = useState(() => properties?.defaultPageIndex ?? 1);
@ -64,12 +65,7 @@ export const Pagination = ({
};
return (
<div
data-disabled={disabledState}
className="d-flex align-items-center px-1"
data-cy={dataCy}
style={{ boxShadow }}
>
<div data-disabled={disabledState} className="d-flex align-items-center" data-cy={dataCy} style={{ boxShadow }}>
<ul className="pagination m-0" style={computedStyles}>
<Pagination.Operator
operator="<<"
@ -90,6 +86,7 @@ export const Pagination = ({
totalPages={properties.numberOfPages}
callback={gotoPage}
darkMode={darkMode}
containerWidth={width}
/>
<Pagination.Operator
operator=">"
@ -139,7 +136,6 @@ function getOperator(operator) {
<polyline points="17 7 12 12 17 17" />
</svg>
);
case '>>':
return (
<svg
@ -159,7 +155,6 @@ function getOperator(operator) {
<polyline points="13 7 18 12 13 17" />
</svg>
);
case '<':
return (
<svg
@ -178,7 +173,6 @@ function getOperator(operator) {
<polyline points="15 6 9 12 15 18"></polyline>
</svg>
);
case '>':
return (
<svg
@ -197,7 +191,6 @@ function getOperator(operator) {
<polyline points="9 6 15 12 9 18"></polyline>
</svg>
);
default:
break;
}
@ -222,19 +215,100 @@ const Operator = ({ operator, currentPage, totalPages, handleOnClick, darkMode }
);
};
const PageLinks = ({ currentPage, totalPages, callback, darkMode }) => {
return Array.from(Array(totalPages).keys()).map((index) => {
const pageNumber = index + 1;
return (
<li
key={pageNumber}
onClick={() => callback(pageNumber)}
className={`page-item ${currentPage === pageNumber ? 'active' : ''}`}
>
<a className={`page-link ${darkMode && 'text-light'}`}>{pageNumber}</a>
</li>
);
});
const PageLinks = ({ currentPage, totalPages, callback, darkMode, containerWidth }) => {
const itemWidth = 28; // Width of each item
const [maxItems, setMaxItems] = useState(0); // for max items that can fit in container
useEffect(() => {
if (!containerWidth || isNaN(totalPages) || totalPages <= 0) {
return;
}
// Calculate the maximum number of items that can fit within the container width
const availableWidth = containerWidth;
const calculatedMaxItems = Math.floor(availableWidth / itemWidth);
setMaxItems(calculatedMaxItems);
}, [containerWidth, totalPages]);
const renderPageNumbers = () => {
const pageNumbers = [];
// Calculate the number of page numbers that can fit, excluding arrows
const maxPageNumbers = maxItems - 4;
// Calculate the starting and ending page numbers based on the current page and the total pages
let startPage = 1;
let endPage = totalPages;
// Adjust startPage and endPage if total pages exceed the maximum displayable page numbers
if (totalPages > maxPageNumbers) {
const leftPageNumbers = Math.ceil(maxPageNumbers / 2) - 1;
const rightPageNumbers = maxPageNumbers - leftPageNumbers - 1;
startPage = currentPage - leftPageNumbers;
endPage = currentPage + rightPageNumbers;
// Ensure startPage and endPage are within valid ranges
if (startPage <= 1) {
startPage = 1;
endPage = maxPageNumbers;
} else if (endPage >= totalPages) {
endPage = totalPages;
startPage = totalPages - maxPageNumbers + 1;
}
}
// Render page numbers
for (let i = startPage; i <= endPage; i++) {
pageNumbers.push(
<li key={i} onClick={() => callback(i)} className={`page-item ${currentPage === i ? 'active' : ''}`}>
<a className={`page-link ${darkMode && 'text-light'}`}>{i}</a>
</li>
);
}
// If total pages exceed the maximum displayable page numbers, add ellipsis
if (totalPages > maxPageNumbers) {
// If there is enough space, remove one number and add ellipsis
if (endPage < totalPages) {
pageNumbers.pop(); // Remove one number for ellipsis
pageNumbers.pop(); // Remove one number for 1
pageNumbers.push(
<li key="right-ellipsis" className="page-item">
<span className="page-link">...</span>
</li>
);
pageNumbers.push(
<li key={totalPages} onClick={() => callback(totalPages)} className={`page-item`}>
<a className={`page-link ${darkMode && 'text-light'}`}>{totalPages}</a>
</li>
);
}
// If the beginning of the pages is not visible, add ellipsis and remove one number
if (startPage > 1) {
pageNumbers.shift();
pageNumbers.shift();
pageNumbers.unshift(
<li key="left-ellipsis" className="page-item">
<span className="page-link">...</span>
</li>
);
pageNumbers.unshift(
<li key={1} onClick={() => callback(1)} className={`page-item`}>
<a className={`page-link ${darkMode && 'text-light'}`}>1</a>
</li>
);
}
}
return pageNumbers;
};
return <>{renderPageNumbers()}</>;
};
Pagination.Operator = Operator;

View file

@ -60,21 +60,28 @@ export const PasswordInput = function PasswordInput({
const computedStyles = {
height: height == 36 ? (padding == 'default' ? '36px' : '40px') : padding == 'default' ? height : height + 4,
borderRadius: `${borderRadius}px`,
color: darkMode && textColor === '#11181C' ? '#ECEDEE' : textColor,
borderColor: isFocused
? accentColor
: ['#D7DBDF'].includes(borderColor)
backgroundColor: !['#ffffff', '#fff'].includes(backgroundColor)
? backgroundColor
: disable || loading
? darkMode
? '#6D757D7A'
: '#6A727C47'
: borderColor,
'--tblr-input-border-color-darker': tinycolor(borderColor).darken(24).toString(),
backgroundColor: darkMode && ['#fff', '#ffffff'].includes(backgroundColor) ? '#313538' : backgroundColor,
? 'var(--surfaces-app-bg-default)'
: 'var(--surfaces-surface-03)'
: 'var(--surfaces-surface-01)',
boxShadow: boxShadow,
padding: styles.iconVisibility ? '8px 10px 8px 29px' : '8px 10px 8px 10px',
overflow: 'hidden',
textOverflow: 'ellipsis',
color: textColor !== '#1B1F24' ? 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(),
};
const loaderStyle = {
@ -232,7 +239,6 @@ export const PasswordInput = function PasswordInput({
<>
<div
data-cy={`label-${String(component.name).toLowerCase()}`}
data-disabled={disable || loading}
className={`text-input d-flex ${
defaultAlignment === 'top' &&
((width != 0 && label && label?.length != 0) || (auto && width == 0 && label && label?.length != 0))
@ -283,7 +289,7 @@ export const PasswordInput = function PasswordInput({
: '50%'
}`,
transform: ' translateY(-50%)',
color: iconColor,
color: iconColor !== '#CFD3D859' ? iconColor : 'var(--icons-weak-disabled)',
zIndex: 3,
}}
stroke={1.5}
@ -319,14 +325,19 @@ export const PasswordInput = function PasswordInput({
}}
stroke={1.5}
>
<SolidIcon width={16} className="password-component-eye" name={!iconVisibility ? 'eye1' : 'eyedisable'} />
<SolidIcon
width={16}
fill={'var(--icons-weak-disabled)'}
className="password-component-eye"
name={!iconVisibility ? 'eye1' : 'eyedisable'}
/>
</div>
)}
<input
data-cy={dataCy}
className={`tj-text-input-widget ${
!isValid && showValidationError ? 'is-invalid' : ''
} validation-without-icon ${darkMode && 'dark-theme-placeholder'}`}
} validation-without-icon `}
ref={textInputRef}
onKeyUp={(e) => {
if (e.key === 'Enter') {
@ -363,11 +374,13 @@ export const PasswordInput = function PasswordInput({
</div>
{showValidationError && visibility && (
<div
className="tj-text-sm"
data-cy={`${String(component.name).toLowerCase()}-invalid-feedback`}
style={{
color: errTextColor,
textAlign: direction === 'left' && 'end',
color: errTextColor !== '#D72D39' ? errTextColor : 'var(--status-error-strong)',
textAlign: direction == 'left' && 'end',
fontSize: '11px',
fontWeight: '400',
lineHeight: '16px',
}}
>
{showValidationError && validationError}

View file

@ -4,6 +4,7 @@ import _ from 'lodash';
import { Tooltip } from 'react-tooltip';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import cx from 'classnames';
export function AddNewRowComponent({
hideAddNewRowPopup,
@ -77,7 +78,7 @@ export function AddNewRowComponent({
<div className="table-responsive jet-data-table">
<table
{...getTableProps()}
className={`table table-vcenter table-nowrap ${tableType} ${darkMode && 'dark-theme'}`}
className={`table table-vcenter table-nowrap ${tableType} ${darkMode && 'dark-theme table-dark'}`}
>
<thead>
{headerGroups.map((headerGroup, index) => {
@ -114,7 +115,25 @@ export function AddNewRowComponent({
let cellProps = cell.getCellProps();
const isEditable = true;
return (
<td key={index} {...cellProps} style={{ ...cellProps.style }}>
<td
key={index}
{...cellProps}
style={{ ...cellProps.style, backgroundColor: 'inherit' }}
className={cx(`table-text-align-${cell.column.horizontalAlignment} td`, {
'has-actions': cell.column.id === 'rightActions' || cell.column.id === 'leftActions',
'has-left-actions': cell.column.id === 'leftActions',
'has-right-actions': cell.column.id === 'rightActions',
'has-text': cell.column.columnType === 'text' || isEditable,
'has-dropdown': cell.column.columnType === 'dropdown',
'has-multiselect': cell.column.columnType === 'multiselect',
'has-datepicker': cell.column.columnType === 'datepicker',
'align-items-center flex-column': cell.column.columnType === 'selector',
// [cellSize]: true,
'selector-column': cell.column.columnType === 'selector' && cell.column.id === 'selection',
'has-select': ['select', 'newMultiSelect'].includes(cell.column.columnType),
isEditable: isEditable,
})}
>
<div
className={`td-container ${cell.column.columnType === 'image' && 'jet-table-image-column'} ${
cell.column.columnType !== 'image' && 'w-100 h-100'

View file

@ -0,0 +1,44 @@
import React from 'react';
import SolidIcon from '@/_ui/Icon/SolidIcons';
export const Boolean = ({ value = false, isEditable, onChange, toggleOnBg, toggleOffBg }) => {
const nonEditableContent = (isTruthyValue) => {
return isTruthyValue ? (
<SolidIcon name="tick" width="24" fill={`var(--grass9)`} />
) : (
<SolidIcon name="remove" width="24" fill={`var(--tomato9)`} />
);
};
const getCustomBgStyles = (value, toggleOnBg, toggleOffBg) => {
if (value && toggleOnBg) {
return { backgroundColor: toggleOnBg };
}
if (!value && toggleOffBg) {
return { backgroundColor: toggleOffBg };
}
return {};
};
const editableContent = (isEditable, value, onChange) => {
return (
<label class="boolean-switch">
<input
type="checkbox"
disabled={!isEditable}
checked={value}
onClick={() => {
onChange(!value);
}}
/>
<span class="boolean-slider round" style={getCustomBgStyles(value, toggleOnBg, toggleOffBg)}></span>
</label>
);
};
return (
<div className="w-100" style={{ lineHeight: 1 }}>
{isEditable ? editableContent(isEditable, value, onChange) : nonEditableContent(value)}
</div>
);
};

View file

@ -0,0 +1,93 @@
import React from 'react';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { getMonth, getYear } from 'date-fns';
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(1990, getYear(new Date()) + 1, 1);
return (
<>
<div
style={{
marginBottom: 10,
marginTop: 10,
display: 'flex',
justifyContent: 'center',
}}
>
<button
className="tj-datepicker-widget-arrows tj-datepicker-widget-left"
onClick={(e) => {
e.stopPropagation();
decreaseMonth();
}}
disabled={prevMonthButtonDisabled}
>
<SolidIcon name="cheveronleft" width="12" />
</button>
<div style={{ marginRight: '8px' }}>
<select
value={months[getMonth(date)]}
onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
className="tj-datepicker-widget-month-selector"
>
{months.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<select
value={getYear(date)}
onChange={({ target: { value } }) => changeYear(value)}
className="tj-datepicker-widget-year-selector"
style={{ padding: '4px 6px' }}
>
{years.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
<button
className="tj-datepicker-widget-arrows tj-datepicker-widget-right "
onClick={(e) => {
e.stopPropagation();
increaseMonth();
}}
disabled={nextMonthButtonDisabled}
>
<SolidIcon name="cheveronright" width="12" />
</button>
</div>
</>
);
};
export default CustomDatePickerHeader;

View file

@ -0,0 +1,165 @@
import React, { useState, useEffect } from 'react';
import SelectSearch from 'react-select-search';
import { useTranslation } from 'react-i18next';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
export const CustomDropdown = ({
options,
value,
multiple,
onChange,
isEditable,
width,
contentWrap,
autoHeight,
darkMode,
}) => {
const [showOverlay, setShowOverlay] = useState(false);
const [hovered, setHovered] = useState(false);
const elem = document.querySelector(
'.table-custom-select-badge-badges .select-search-container.select-search-is-multiple .select-search-value > div'
);
useEffect(() => {
if (hovered) {
setShowOverlay(true);
} else {
setShowOverlay(false);
}
}, [hovered]);
const checkForValidValue = (value) => {
// value for badge should be ['premitive values'] and not [{}]
if (!Array.isArray(value)) {
return [];
}
const nonPremitiveValueExits = value.find((singleValue) => typeof singleValue === 'object');
return nonPremitiveValueExits ? [] : value;
};
value = multiple ? checkForValidValue(value) : value;
const { t } = useTranslation();
function renderValue(valueProps) {
if (!isEditable && valueProps) {
const stringifyValue = String(valueProps.value);
const arrayOfValueProps = stringifyValue.includes(',') ? stringifyValue.split(', ') : stringifyValue.split(' ');
return (
<div>
{arrayOfValueProps.map((value, index) => (
<span key={index} className="badge bg-blue-lt p-2 mx-1">
{value}
</span>
))}
</div>
);
} else if (valueProps) {
const stringifyValue = String(valueProps.value);
const arrayOfValueProps = stringifyValue.includes(',') ? stringifyValue.split(', ') : stringifyValue.split(' ');
return (
<div>
{arrayOfValueProps.map((value, index) => (
<span key={index} {...valueProps} className="badge bg-blue-lt p-2 mx-1">
{value}
</span>
))}
</div>
);
}
}
const getOverlay = (value, containerWidth, options) => {
const labels = Array.isArray(value)
? value.map((value) => {
const option = options.find((option) => option.value === value);
if (option) {
return option.name;
}
})
: [];
return Array.isArray(labels) ? (
<div
style={{
maxWidth: containerWidth,
width: containerWidth,
}}
className={`overlay-cell-table overlay-badges-table ${darkMode && 'dark-theme'}`}
>
{labels?.map((label) => {
return (
<span
style={{
padding: '2px 6px',
background: 'var(--surfaces-surface-03)',
borderRadius: '6px',
color: 'var(--text-primary)',
fontSize: '12px',
wordWrap: 'break-word', // Add word-wrap property for content wrapping
display: 'flex',
flexWrap: 'wrap',
overflow: 'auto',
}}
key={label}
>
{label}
</span>
);
})}
</div>
) : (
<div></div>
);
};
return (
<OverlayTrigger
placement="bottom"
overlay={
multiple &&
elem &&
(elem?.clientHeight < elem?.scrollHeight || elem?.clientWidth < elem?.scrollWidth) &&
getOverlay(value, width, options)
}
trigger={
multiple &&
elem &&
(elem?.clientHeight < elem?.scrollHeight || elem?.clientWidth < elem?.scrollWidth) &&
value?.length >= 1 && ['hover']
}
rootClose={true}
show={
multiple &&
elem &&
(elem?.clientHeight < elem?.scrollHeight || elem?.clientWidth < elem?.scrollWidth) &&
value?.length >= 1 &&
showOverlay
}
>
<div
className={`custom-select d-flex align-items-center table-custom-select-badge-badges w-100 h-100 position-relative ${
contentWrap
? autoHeight
? 'content--wrap'
: 'content--wrap-content--overflow-hidden'
: 'content--overflow-hidden'
}`}
onMouseMove={() => {
if (!hovered) setHovered(true);
}}
onMouseLeave={() => setHovered(false)}
>
<SelectSearch
options={options}
printOptions="on-focus"
value={value}
renderValue={renderValue}
search={false}
onChange={onChange}
multiple={multiple}
placeholder={t('globals.select', 'Select') + '...'}
className={'select-search'}
/>
</div>
</OverlayTrigger>
);
};

View file

@ -1,44 +1,326 @@
import React from 'react';
import SelectSearch from 'react-select-search';
import { useTranslation } from 'react-i18next';
import React, { useRef, useState, useEffect, useMemo } 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, isString } from 'lodash';
const { MenuList } = components;
export const CustomSelect = ({ options, value, multiple, onChange, isEditable, width }) => {
const { t } = useTranslation();
export const CustomSelect = ({
options,
value,
onChange,
fuzzySearch = false,
placeholder,
disabled,
className,
darkMode,
defaultOptionsList,
textColor = '',
isMulti,
containerWidth,
optionsLoadingState = false,
horizontalAlignment = 'left',
isEditable,
}) => {
const containerRef = useRef(null);
const inputRef = useRef(null); // Ref for the input search box
const [isFocused, setIsFocused] = useState(false);
useEffect(() => {
const handleDocumentClick = (event) => {
if (isMulti && !containerRef.current?.contains(event.target)) {
setIsFocused(false);
}
};
function renderValue(valueProps) {
if (!isEditable) {
const stringifyValue = String(valueProps.value);
const arrayOfValueProps = stringifyValue.includes(',') ? stringifyValue.split(', ') : stringifyValue.split(' ');
return arrayOfValueProps.map((value, index) => (
<span key={index} className="badge bg-blue-lt p-2 mx-1">
{value}
</span>
));
document.addEventListener('mousedown', handleDocumentClick);
return () => {
document.removeEventListener('mousedown', handleDocumentClick);
};
}, []);
useEffect(() => {
// Focus the input search box when the menu list is open and the component is focused
if (isFocused && inputRef.current) {
inputRef.current.focus();
}
if (valueProps) {
const stringifyValue = String(valueProps.value);
const arrayOfValueProps = stringifyValue.includes(',') ? stringifyValue.split(', ') : stringifyValue.split(' ');
return arrayOfValueProps.map((value, index) => (
<span key={index} {...valueProps} className="badge bg-blue-lt p-2 mx-1">
{value}
</span>
));
}, [isFocused]);
const customStyles = {
...defaultStyles(darkMode, '100%'),
...(isMulti && {
multiValue: (provided) => ({
...provided,
display: 'inline-block', // Display selected options inline
marginRight: '4px', // Add some space between options
}),
}),
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',
}),
multiValueLabel: (provided) => ({
...provided,
padding: '2px 6px',
background: 'var(--surfaces-surface-03)',
borderRadius: '6px',
color: textColor || 'var(--text-primary)',
fontSize: '12px',
}),
singleValue: (provided) => ({
...provided,
padding: '2px 6px',
background: 'var(--surfaces-surface-03)',
borderRadius: '6px',
color: textColor || 'var(--text-primary)',
fontSize: '12px',
}),
};
const customComponents = {
MenuList: (props) => <CustomMenuList {...props} optionsLoadingState={optionsLoadingState} inputRef={inputRef} />,
Option: CustomMultiSelectOption,
DropdownIndicator: isEditable ? DropdownIndicator : null,
...(isMulti && {
MultiValueRemove,
MultiValueContainer: CustomMultiValueContainer,
}),
};
const defaultValue = useMemo(() => {
if (defaultOptionsList.length >= 1) {
return !isMulti ? defaultOptionsList[defaultOptionsList.length - 1] : defaultOptionsList;
} else {
return isMulti ? [] : {};
}
}
}, [isMulti, defaultOptionsList]);
const _value = useMemo(() => {
// Return null to show default value
if (!value) {
return null;
}
if (isMulti && value?.length) {
if (isArray(value)) {
return options?.filter((option) =>
value?.find((val) => {
if (val.hasOwnProperty('value')) {
return option.value === val.value;
} else return option.value === val;
})
);
} else {
return []; // Return empty array to not show default value in case of wrong value to be set
}
} else {
// Condition for single select
return options?.find((option) => option.value === value) || [];
}
}, [options, value, isMulti]);
const selectContainerRef = useRef(null); // Ref for the input search box
const containerHeight = selectContainerRef.current?.clientHeight;
const valueContainerHeight = selectContainerRef.current?.querySelector(
'.react-select__value-container'
)?.clientHeight;
return (
<div className="custom-select" style={{ width: width }}>
<SelectSearch
options={options}
printOptions="on-focus"
value={value}
renderValue={renderValue}
search={false}
onChange={onChange}
multiple={multiple}
placeholder={t('globals.select', 'Select') + '...'}
className={'select-search'}
/>
<OverlayTrigger
placement="bottom"
overlay={
isMulti && (_value?.length || defaultValue?.length) && !isFocused ? (
getOverlay(_value ? _value : defaultValue, containerWidth, darkMode)
) : (
<div></div>
)
}
trigger={isMulti && !isFocused && valueContainerHeight > containerHeight && ['hover', 'focus']} //container width -24 -16 gives that select container size
rootClose={true}
>
<div className="w-100 h-100 d-flex align-items-center" ref={selectContainerRef}>
<Select
options={options}
hasSearch={false}
fuzzySearch={fuzzySearch}
isDisabled={disabled}
className={className}
components={customComponents}
value={_value}
onMenuInputFocus={() => setIsFocused(true)}
onChange={(value) => {
setIsFocused(false);
if (!isMulti && value === _value?.value) {
// for single select column type if on change value is similar to current value , then discard the current value
onChange('');
} else {
onChange(value);
}
}}
useCustomStyles={true}
styles={customStyles}
defaultValue={defaultValue}
placeholder={placeholder}
isMulti={isMulti}
hideSelectedOptions={false}
isClearable={false}
clearIndicator={false}
darkMode={darkMode}
{...{
menuIsOpen: isFocused || undefined,
isFocused: isFocused || undefined,
}}
/>
</div>
</OverlayTrigger>
);
};
const CustomMenuList = ({ optionsLoadingState, children, selectProps, inputRef, ...props }) => {
const { onInputChange, inputValue, onMenuInputFocus } = selectProps;
return (
<div className="table-select-custom-menu-list" onClick={(e) => e.stopPropagation()}>
<div className="table-select-column-type-search-box-wrapper ">
{!inputValue && (
<span className="">
<SolidIcon name="search" width="14" />
</span>
)}
<input
autoCorrect="off"
autoComplete="off"
spellCheck="false"
type="text"
value={inputValue}
onChange={(e) =>
onInputChange(e.currentTarget.value, {
action: 'input-change',
})
}
onMouseDown={(e) => {
e.stopPropagation();
e.target.focus();
}}
onTouchEnd={(e) => {
e.stopPropagation();
e.target.focus();
}}
onFocus={onMenuInputFocus}
placeholder="Search..."
className="table-select-column-type-search-box"
ref={inputRef} // Assign the ref to the input search box
/>
</div>
<MenuList {...props} selectProps={selectProps}>
{optionsLoadingState ? (
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="sr-only"></span>
</div>
</div>
) : (
children
)}
</MenuList>
</div>
);
};
const CustomMultiSelectOption = ({ innerRef, innerProps, children, isSelected, ...props }) => {
return (
<div ref={innerRef} {...innerProps} className="option-wrapper d-flex">
{props.isMulti ? (
<Checkbox label="" isChecked={isSelected} onChange={(e) => e.stopPropagation()} key="" value={children} />
) : (
<div style={{ visibility: isSelected ? 'visible' : 'hidden' }}>
<Checkbox label="" isChecked={isSelected} onChange={(e) => e.stopPropagation()} key="" value={children} />
</div>
)}
{children}
</div>
);
};
const MultiValueRemove = (props) => {
const { innerProps } = props;
return <div {...innerProps} />;
};
const CustomMultiValueContainer = (props) => {
return (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
}}
>
{props.children}
</div>
);
};
const getOverlay = (value, containerWidth, darkMode) => {
const getLabel = (option) => {
if (option?.hasOwnProperty('label')) {
return option.label;
} else if (isString(option)) {
return option;
} else return '';
};
return Array.isArray(value) ? (
<div
style={{
maxWidth: '266px',
}}
className={`overlay-cell-table overlay-multiselect-table ${darkMode && 'dark-theme'}`}
>
{value?.map((option) => {
return (
<span
style={{
padding: '2px 6px',
background: 'var(--surfaces-surface-03)',
borderRadius: '6px',
color: 'var(--text-primary)',
fontSize: '12px',
}}
key={getLabel(option)}
>
{getLabel(option)}
</span>
);
})}
</div>
) : (
<div></div>
);
};
const DropdownIndicator = (props) => {
return (
<div {...props} className="cell-icon-display">
{/* Your custom SVG */}
{props.selectProps.menuIsOpen ? (
<SolidIcon name="arrowUpTriangle" width="16" height="16" fill={'#6A727C'} />
) : (
<SolidIcon name="arrowDownTriangle" width="16" height="16" fill={'#6A727C'} />
)}
</div>
);
};

View file

@ -1,25 +1,76 @@
import React from 'react';
import Datetime from 'react-datetime';
import React, { forwardRef, useEffect, useRef } from 'react';
import moment from 'moment-timezone';
import 'react-datetime/css/react-datetime.css';
import '@/_styles/custom.scss';
import DatePickerComponent from 'react-datepicker';
import CustomDatePickerHeader from './CustomDatePickerHeader';
import 'react-datepicker/dist/react-datepicker.css';
import './datepicker.scss';
import cx from 'classnames';
import SolidIcon from '@/_ui/Icon/SolidIcons';
const getDate = (value, parseDateFormat, displayFormat, timeZoneValue, timeZoneDisplay) => {
const DISABLED_DATE_FORMAT = 'MM/DD/YYYY';
const TjDatepicker = forwardRef(({ value, onClick, styles, dateInputRef, readOnly }, ref) => {
return (
<div className="table-column-datepicker-input-container">
<input
onBlur={(e) => {
e.stopPropagation();
}}
className={cx('table-column-datepicker-input text-truncate', {
'pointer-events-none': readOnly,
})}
value={value}
onClick={onClick}
ref={dateInputRef}
style={styles}
/>
{!readOnly && (
<span className="cell-icon-display">
<SolidIcon
width="16"
fill={'var(--borders-strong)'}
name="calender"
className="table-column-datepicker-input-icon"
/>
</span>
)}
</div>
);
});
export const getDateTimeFormat = (
dateDisplayFormat,
isTimeChecked,
isTwentyFourHrFormatEnabled,
isDateSelectionEnabled
) => {
const timeFormat = isTwentyFourHrFormatEnabled ? 'HH:mm' : 'LT';
if (isTimeChecked && !isDateSelectionEnabled) {
return timeFormat;
}
return isTimeChecked ? `${dateDisplayFormat} ${timeFormat}` : dateDisplayFormat;
};
const getDate = ({
value,
parseDateFormat,
timeZoneValue,
timeZoneDisplay,
unixTimestamp,
parseInUnixTimestamp,
isTimeChecked,
}) => {
let momentObj = null;
if (value) {
const dateString = value;
if (timeZoneValue && timeZoneDisplay) {
let momentString = moment
.tz(dateString, parseDateFormat, timeZoneValue)
.tz(timeZoneDisplay)
.format(displayFormat);
return momentString;
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 {
const momentObj = moment(dateString, parseDateFormat);
const momentString = momentObj.format(displayFormat);
return momentString;
momentObj = moment(value, parseDateFormat);
}
}
return '';
return momentObj?.isValid() ? momentObj.toDate() : null;
};
export const Datepicker = function Datepicker({
@ -27,68 +78,151 @@ export const Datepicker = function Datepicker({
onChange,
readOnly,
isTimeChecked,
tableRef,
dateDisplayFormat, //?Display date format
parseDateFormat, //?Parse date format
timeZoneValue,
timeZoneDisplay,
isDateSelectionEnabled,
isTwentyFourHrFormatEnabled,
disabledDates,
unixTimestamp = 'seconds',
parseInUnixTimestamp,
cellStyles,
darkMode,
}) {
const [date, setDate] = React.useState(() =>
getDate(value, parseDateFormat, dateDisplayFormat, timeZoneValue, timeZoneDisplay)
);
const [date, setDate] = React.useState(null);
const [excludedDates, setExcludedDates] = React.useState([]);
const pickerRef = React.useRef();
const dateChange = (event) => {
const value = event._isAMomentObject ? event.format() : event;
let selectedDateFormat = isTimeChecked ? `${dateDisplayFormat} LT` : dateDisplayFormat;
const dateString = moment(value).format(selectedDateFormat);
setDate(() => dateString);
};
React.useEffect(() => {
let selectedDateFormat = isTimeChecked ? `${dateDisplayFormat} LT` : dateDisplayFormat;
const dateString = getDate(value, parseDateFormat, selectedDateFormat, timeZoneValue, timeZoneDisplay);
setDate(() => dateString);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isTimeChecked, readOnly, dateDisplayFormat]);
const onDatepickerClose = () => {
onChange(date);
};
let inputProps = {
disabled: !readOnly,
};
const calculatePosition = () => {
const dropdown = pickerRef.current && pickerRef.current.querySelectorAll('.rdtPicker')[0];
if (dropdown && tableRef.current) {
const tablePos = tableRef.current.getBoundingClientRect();
const dropDownPos = pickerRef.current.getBoundingClientRect();
const left = dropDownPos.left - tablePos.left;
const top = dropDownPos.bottom - tablePos.top;
dropdown.style.left = `${left}px`;
dropdown.style.top = `${top}px`;
const handleDateChange = (date) => {
let value = date;
if (parseInUnixTimestamp && unixTimestamp) {
value = moment(date).unix();
}
const _date = getDate({
value,
parseDateFormat: getDateTimeFormat(
parseDateFormat,
isTimeChecked,
isTwentyFourHrFormatEnabled,
isDateSelectionEnabled
),
dateDisplayFormat,
timeZoneValue,
timeZoneDisplay,
unixTimestamp,
parseInUnixTimestamp,
isTimeChecked,
});
setDate(_date);
if (parseInUnixTimestamp && unixTimestamp) {
onChange(moment(_date).unix());
} else {
onChange(computeDateString(_date));
}
};
useEffect(() => {
const date = getDate({
value,
parseDateFormat: getDateTimeFormat(
parseDateFormat,
isTimeChecked,
isTwentyFourHrFormatEnabled,
isDateSelectionEnabled
),
dateDisplayFormat,
timeZoneValue,
timeZoneDisplay,
unixTimestamp,
parseInUnixTimestamp,
isTimeChecked,
});
setDate(date);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
JSON.stringify(
value,
parseDateFormat,
dateDisplayFormat,
timeZoneValue,
timeZoneDisplay,
unixTimestamp,
isTimeChecked,
isTwentyFourHrFormatEnabled,
parseInUnixTimestamp
),
]);
const dateInputRef = useRef(null); // Create a ref
const computeDateString = (_date) => {
if (_date === null && !value) return ''; // If there is no value in table data, return empty string to display
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);
}
};
useEffect(() => {
if (Array.isArray(disabledDates) && disabledDates.length > 0) {
const _exluded = [];
disabledDates?.map((item) => {
if (moment(item, DISABLED_DATE_FORMAT).isValid()) {
_exluded.push(moment(item, DISABLED_DATE_FORMAT).toDate());
}
});
setExcludedDates(_exluded);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [disabledDates]);
return (
<div ref={pickerRef}>
<Datetime
inputProps={inputProps}
timeFormat={isTimeChecked}
className="cell-type-datepicker"
<DatePickerComponent
className={`input-field form-control validation-without-icon px-2`}
popperClassName={cx({
'tj-timepicker-widget': !isDateSelectionEnabled && isTimeChecked,
'tj-datepicker-widget': isDateSelectionEnabled,
'theme-dark dark-theme': darkMode,
})}
selected={date}
onChange={(date) => handleDateChange(date)}
value={computeDateString(date)}
dateFormat={dateDisplayFormat}
value={date}
onChange={dateChange}
closeOnSelect={true}
onClose={onDatepickerClose}
disabled={readOnly}
renderView={(viewMode, renderDefault) => {
calculatePosition();
return renderDefault();
}}
closeOnTab={false}
customInput={
<TjDatepicker dateInputRef={dateInputRef} readOnly={readOnly} styles={{ color: cellStyles.color }} />
}
showTimeSelect={isTimeChecked}
showTimeSelectOnly={!isDateSelectionEnabled && isTimeChecked}
showMonthDropdown
showYearDropdown
dropdownMode="select"
excludeDates={excludedDates}
showPopperArrow={false}
renderCustomHeader={(headerProps) => <CustomDatePickerHeader {...headerProps} />}
shouldCloseOnSelect
readOnly={readOnly}
popperProps={{ strategy: 'fixed' }}
timeIntervals={15}
timeFormat={isTwentyFourHrFormatEnabled ? 'HH:mm' : 'h:mm aa'}
/>
</div>
);

View file

@ -172,7 +172,7 @@ export function Filter(props) {
}
return (
<div className={`table-filters card ${darkMode && 'dark-theme'}`}>
<div className={`table-filters card ${darkMode ? 'dark-theme theme-dark' : 'light-theme'}`}>
<div className="card-header row">
<div className="col">
<h4 data-cy={`header-filters`} className="font-weight-normal">
@ -207,6 +207,7 @@ export function Filter(props) {
className={`${darkMode ? 'select-search-dark' : 'select-search'} mb-0`}
styles={selectStyles('100%')}
useCustomStyles={true}
darkMode={darkMode}
/>
</div>
<div data-cy={`select-operation-dropdown-${index ?? ''}`} className="col" style={{ maxWidth: '180px' }}>
@ -222,6 +223,7 @@ export function Filter(props) {
styles={selectStyles('100%')}
dataCy={`select-coloumn-dropdown-${index ?? ''}`}
useCustomStyles={true}
darkMode={darkMode}
/>
</div>
<div className="col">

View file

@ -1,7 +1,10 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import _ from 'lodash';
import { validateWidget } from '@/_helpers/utils';
import { useMounted } from '@/_hooks/use-mount';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import DOMPurify from 'dompurify';
import NullRenderer from './NullRenderer/NullRenderer';
export default function GenerateEachCellValue({
cellValue,
@ -15,13 +18,66 @@ export default function GenerateEachCellValue({
cellTextColor,
cell,
currentState,
darkMode,
cellWidth,
isCellValueChanged,
setIsCellValueChanged,
}) {
const mounted = useMounted();
const updateCellValue = useRef();
const isTabKeyPressed = useRef(false);
const cellRef = useRef(null);
const [showHighlightedCells, setHighlighterCells] = React.useState(globalFilter ? true : false);
const [isNullCellClicked, setIsNullCellClicked] = React.useState(false);
const columnTypeAllowToRenderMarkElement = ['text', 'string', 'default', 'number', undefined];
const ref = useRef();
const [showOverlay, setShowOverlay] = useState(false);
const [hovered, setHovered] = useState(false);
useEffect(() => {
if (hovered) {
setShowOverlay(true);
} else {
setShowOverlay(false);
}
}, [hovered]);
const _showOverlay =
ref?.current &&
(ref?.current?.clientWidth < ref?.current?.children[0]?.offsetWidth ||
ref?.current?.clientHeight < ref?.current?.children[0]?.offsetHeight);
const handleCellClick = () => {
setIsNullCellClicked(true);
if (isEditable && columnTypeAllowToRenderMarkElement.includes(columnType)) {
setHighlighterCells(false);
}
};
const handleCellBlur = (e) => {
e.stopPropagation();
if (isTabKeyPressed.current) {
isTabKeyPressed.current = false;
return;
} else {
updateCellValue.current = e.target.value;
if (!showHighlightedCells && updateCellValue.current === cellValue) {
updateCellValue.current = null;
setHighlighterCells(true);
}
}
};
const handleKeyUp = (e) => {
if (e.key === 'Tab') {
isTabKeyPressed.current = true;
setHighlighterCells(false);
}
};
let validationData = {};
if (cell.column.isEditable && showHighlightedCells) {
if (cell.column.columnType === 'number') {
validationData = {
@ -68,10 +124,27 @@ export default function GenerateEachCellValue({
if (mounted && _.isEmpty(rowChangeSet)) {
setHighlighterCells(true);
}
//In the dependency array to ingnore linting warning, added mounted but it's not working out, any way to avoid ingnoring dependency array
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rowData, rowChangeSet]);
const getOverlay = () => {
return (
<div
className={`overlay-cell-table ${darkMode && 'dark-theme'}`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{ whiteSpace: 'pre-wrap', color: 'var(--text-primary)' }}
>
<span
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(cellValue) }}
style={{
maxWidth: cellWidth,
}}
></span>
</div>
);
};
let htmlElement = cellValue;
if (cellValue?.toString()?.toLowerCase().includes(globalFilter?.toLowerCase())) {
if (globalFilter) {
@ -86,59 +159,106 @@ export default function GenerateEachCellValue({
.replace(new RegExp(`(${normReq.join('|')})`, 'gi'), (match) => `<mark>${match}</mark>`);
}
}
return (
<div
onClick={() => {
if (isEditable && columnTypeAllowToRenderMarkElement.includes(columnType)) {
setHighlighterCells(false);
}
}}
onBlur={(e) => {
e.stopPropagation();
if (isTabKeyPressed.current) {
isTabKeyPressed.current = false;
return;
} else {
updateCellValue.current = e.target.value;
//removing _.isEmpty(rowChangeSet) flag from if statement at the end
if (!showHighlightedCells && updateCellValue.current === cellValue) {
updateCellValue.current = null;
setHighlighterCells(true);
}
}
}}
onKeyUp={(e) => {
if (e.key === 'Tab') {
isTabKeyPressed.current = true;
setHighlighterCells(false);
}
}}
className={`w-100 h-100 ${columnType === 'selector' && 'd-flex align-items-center justify-content-center'}`}
>
{!isColumnTypeAction && columnTypeAllowToRenderMarkElement.includes(columnType) && showHighlightedCells ? (
<div className="d-flex justify-content-center flex-column w-100 h-100 generate-cell-value-component-div-wrapper">
const _renderCellWhenHighlighted = () => {
return (
<div
className="d-flex justify-content-center flex-column w-100 h-100 generate-cell-value-component-div-wrapper"
style={{ oveflow: 'hidden' }}
onMouseMove={() => {
if (!hovered) setHovered(true);
}}
onMouseOut={() => setHovered(false)}
>
{cellValue === null ? (
<NullRenderer darkMode={darkMode} />
) : (
<div
style={{
color: cellTextColor,
}}
dangerouslySetInnerHTML={{
__html: htmlElement,
}}
ref={ref}
tabIndex={0}
className={`form-control-plaintext form-control-plaintext-sm ${columnType === 'text' && 'h-100 my-1'}`}
></div>
<div
style={{
display: cell.column.isEditable && validationData.validationError ? 'block' : 'none',
width: '100%',
marginTop: ' 0.25rem',
fontSize: ' 85.7142857%',
color: '#d63939',
}}
className={`table-column-type-div-element ${columnType === 'text' && 'h-100 my-1'}`}
>
{validationData.validationError}
<span
dangerouslySetInnerHTML={{
__html: htmlElement,
}}
></span>
</div>
)}
<div
style={{
display: cell.column.isEditable && validationData.validationError ? 'block' : 'none',
width: '100%',
marginTop: ' 0.25rem',
fontSize: ' 85.7142857%',
color: '#d63939',
}}
>
{validationData.validationError}
</div>
</div>
);
};
const _renderNullCell = () => {
if (isEditable) {
if (!isNullCellClicked && !updateCellValue.current) {
return <NullRenderer darkMode={darkMode} />;
} else return cellRender;
} else {
return <NullRenderer darkMode={darkMode} />;
}
};
useEffect(() => {
const handleClickOutside = (event) => {
if (cellRef.current && !cellRef.current.contains(event.target) && !isCellValueChanged) {
// Adding setTimeout to avoid this event to be executed before input's blur event
setTimeout(() => {
setIsNullCellClicked(false);
setIsCellValueChanged(false);
}, 100);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isCellValueChanged, setIsCellValueChanged]);
useEffect(() => {
if (isNullCellClicked) {
const inputElement = document.getElementById(`table-input-${cell.column.id}-${cell.row.id}`);
if (inputElement) {
inputElement.focus();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isNullCellClicked]);
return (
<div
onClick={handleCellClick}
onBlur={handleCellBlur}
onKeyUp={handleKeyUp}
className={`w-100 h-100 ${columnType === 'selector' && 'd-flex align-items-center justify-content-center'}`}
ref={cellRef}
>
{!isColumnTypeAction && columnTypeAllowToRenderMarkElement.includes(columnType) && showHighlightedCells ? (
<OverlayTrigger
placement="bottom"
overlay={_showOverlay ? getOverlay() : <div></div>}
trigger={_showOverlay && ['hover']}
rootClose={true}
show={_showOverlay && showOverlay}
>
{_renderCellWhenHighlighted()}
</OverlayTrigger>
) : cellValue === null ? (
_renderNullCell()
) : (
cellRender
)}

View file

@ -24,14 +24,14 @@ export const GlobalFilter = ({
return (
<div
className="d-flex border align-items-center table-global-search"
className="d-flex align-items-center table-global-search"
style={{ padding: '0.4rem 0.6rem', borderRadius: '6px' }}
>
<div className="d-flex">
<SolidIcon name="search" width="16" height="16" />
<SolidIcon name="search" width="16" height="16" fill={'var(--icons-default)'} />
<input
type="text"
className={`align-self-center bg-transparent tj-text tj-text-xsm mx-lg-1`}
className={`align-self-center bg-transparent tj-text tj-text-sm mx-lg-1`}
value={value || ''}
onChange={(e) => {
setValue(e.target.value);
@ -45,14 +45,14 @@ export const GlobalFilter = ({
/>
<div
className={`d-flex table-clear-icon align-items-center ${globalFilter ? 'visible' : 'invisible'}`}
style={{ width: '20px', height: '20px', padding: '4px', cursor: 'pointer' }}
style={{ cursor: 'pointer' }}
onClick={() => {
setGlobalFilter(undefined);
setValue('');
onComponentOptionChanged(component, 'searchText', '');
}}
>
<SolidIcon name="remove" width="16" height="16px" fill={darkMode ? '#3E63DD' : '#3E63DD'} />
<SolidIcon name="removerectangle" width="16" height="16" fill={'var(--icons-default)'} />
</div>
</div>
</div>

View file

@ -1,16 +1,25 @@
import React from 'react';
export const Link = ({ cellValue, linkTarget }) => {
export const Link = ({ cellValue, linkTarget, underline, underlineColor, linkColor, displayText, darkMode }) => {
const linkTextColor =
linkColor !== '#1B1F24' ? linkColor : darkMode && linkColor === '#1B1F24' ? '#FFFFFF' : linkColor;
return (
<div className="w-100">
<div className="w-100 table-link-column">
<a
className={underline === 'hover' ? 'table-link-hover' : 'table-link'}
href={cellValue}
target={linkTarget}
target={linkTarget == '_self' || linkTarget == false ? '_self' : '_blank'}
onClick={(e) => {
e.stopPropagation();
}}
style={{
color: linkTextColor,
textDecoration: underline === 'always' && 'underline', // Apply underline always or only on hover
textDecorationColor: underlineColor,
}}
rel="noreferrer"
>
{cellValue}
{displayText ? displayText : cellValue}
</a>
</div>
);

View file

@ -0,0 +1,13 @@
import React from 'react';
import './nullRenderer.scss';
import classNames from 'classnames';
const NullRenderer = ({ darkMode }) => {
return (
<div className="d-flex align-items-center h-100">
<span className={classNames('null-renderer-text', { 'dark-theme': darkMode })}>NULL</span>
</div>
);
};
export default NullRenderer;

View file

@ -0,0 +1,17 @@
.null-renderer-text {
font-size: 12px;
line-height: 18px;
color: var(--text-primary);
background-color: var(--surfaces-surface-03);
padding: 2px 6px 2px 6px;
border-radius: 6px;
font-weight: 500;
}
.table-editor-component-row {
&:hover {
.null-renderer-text {
background-color: var(--surfaces-surface-01) !important
}
}
}

View file

@ -78,7 +78,7 @@ export const Pagination = function Pagination({
cursor: pageIndex === 1 ? 'not-allowed' : 'pointer',
}}
leftIcon="cheveronleftdouble"
fill={`var(--slate12)`}
fill={`var(--icons-default)`}
iconWidth="14"
size="md"
disabled={pageIndex === 1}
@ -103,7 +103,7 @@ export const Pagination = function Pagination({
cursor: pageIndex === 1 || !enablePrevButton ? 'not-allowed' : 'pointer',
}}
leftIcon="cheveronleft"
fill={`var(--slate12)`}
fill={`var(--icons-default)`}
iconWidth="14"
size="md"
disabled={pageIndex === 1 || !enablePrevButton}
@ -155,7 +155,7 @@ export const Pagination = function Pagination({
cursor: (!autoCanNextPage && !serverSide) || !enableNextButton ? 'not-allowed' : 'pointer',
}}
leftIcon="cheveronright"
fill={`var(--slate12)`}
fill={`var(--icons-default)`}
iconWidth="14"
size="md"
disabled={(!autoCanNextPage && !serverSide) || !enableNextButton}
@ -180,7 +180,7 @@ export const Pagination = function Pagination({
cursor: !autoCanNextPage && !serverSide ? 'not-allowed' : 'pointer',
}}
leftIcon="cheveronrightdouble"
fill={`var(--slate12)`}
fill={`var(--icons-default)`}
iconWidth="14"
size="md"
onClick={(event) => {

View file

@ -1,30 +1,82 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
export const Radio = ({ options, value, onChange, readOnly }) => {
export const Radio = ({ options, value, onChange, readOnly, containerWidth, darkMode }) => {
value = value === undefined ? [] : value;
options = Array.isArray(options) ? options : [];
return (
<div className="radio row">
<div>
{options.map((option, index) => (
<label
key={index}
className="form-check form-check-inline"
onClick={() => {
if (!readOnly) onChange(option.value);
}}
>
<input
className="form-check-input"
type="radio"
checked={option.value === value}
disabled={readOnly && option.value !== value}
/>
<span className="form-check-label">{option.name}</span>
</label>
))}
const elem = document.querySelector('.table-radio-column-list');
const [showOverlay, setShowOverlay] = useState(false);
const [hovered, setHovered] = useState(false);
useEffect(() => {
if (hovered) {
setShowOverlay(true);
} else {
setShowOverlay(false);
}
}, [hovered]);
const renderOptions = (options) => {
return options.map((option, index) => (
<label
key={index}
className="form-check form-check-inline"
onClick={() => {
if (!readOnly) onChange(option.value);
}}
>
<input
className="form-check-input"
type="radio"
checked={option.value === value}
disabled={readOnly && option.value !== value}
/>
<span className="form-check-label">{option.name}</span>
</label>
));
};
const getOverlay = (options, containerWidth) => {
return Array.isArray(options) ? (
<div
style={{
maxWidth: containerWidth,
width: containerWidth,
}}
className={`overlay-cell-table overlay-radio-table ${darkMode && 'dark-theme'}`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{renderOptions(options)}
</div>
</div>
) : (
<div></div>
);
};
return (
<OverlayTrigger
placement="bottom"
overlay={
elem &&
(elem?.clientHeight < elem?.scrollHeight || elem?.clientWidth < elem?.scrollWidth) &&
getOverlay(options, containerWidth)
}
trigger={elem && (elem?.clientHeight < elem?.scrollHeight || elem?.clientWidth < elem?.scrollWidth) && ['focus']}
rootClose={true}
show={elem && (elem?.clientHeight < elem?.scrollHeight || elem?.clientWidth < elem?.scrollWidth) && showOverlay}
>
<div
className="table-radio-column-cell radio row h-100"
onMouseMove={() => {
if (!hovered) setHovered(true);
}}
onMouseOut={() => setHovered(false)}
>
<div className="table-radio-column-list">{renderOptions(options)}</div>
</div>
</OverlayTrigger>
);
};

View file

@ -0,0 +1,180 @@
import React, { useState, useEffect } from 'react';
import { validateWidget, determineJustifyContentValue } from '@/_helpers/utils';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
const String = ({
isEditable,
darkMode,
handleCellValueChange,
cellTextColor,
cellValue,
column,
currentState,
containerWidth,
cell,
horizontalAlignment,
isMaxRowHeightAuto,
cellSize,
maxRowHeightValue,
}) => {
const validationData = validateWidget({
validationObject: {
regex: {
value: column.regex,
},
minLength: {
value: column.minLength,
},
maxLength: {
value: column.maxLength,
},
customRule: {
value: column.customRule,
},
},
widgetValue: cellValue,
currentState,
customResolveObjects: { cellValue },
});
const { isValid, validationError } = validationData;
const cellStyles = {
color: cellTextColor ?? 'inherit',
};
const ref = React.useRef(null);
const [showOverlay, setShowOverlay] = useState(false);
const [hovered, setHovered] = useState(false);
const [isEditing, setIsEditing] = useState(false);
useEffect(() => {
if (hovered) {
setShowOverlay(true);
} else {
setShowOverlay(false);
}
}, [hovered]);
useEffect(() => {
if (!isEditable && isEditing) {
setIsEditing(false);
}
}, [isEditable]);
const _renderString = () => (
<div
ref={ref}
contentEditable={'true'}
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',
border: 'none',
background: 'inherit',
position: 'relative',
height: '100%',
}}
readOnly={!isEditable}
onBlur={(e) => {
setIsEditing(false);
if (cellValue !== e.target.textContent) {
handleCellValueChange(cell.row.index, column.key || column.name, e.target.textContent, cell.row.original);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (cellValue !== e.target.textContent) {
handleCellValueChange(cell.row.index, column.key || column.name, e.target.textContent, cell.row.original);
}
}
}}
onFocus={(e) => {
setIsEditing(true);
e.stopPropagation();
}}
>
<span>{cellValue}</span>
</div>
);
const getOverlay = () => {
return (
<div
className={`overlay-cell-table ${darkMode && 'dark-theme'}`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{ color: 'var(--text-primary)' }}
>
<span
style={{
width: `${containerWidth}px`,
}}
>
{cellValue}
</span>
</div>
);
};
const _showOverlay =
ref?.current &&
(ref?.current?.clientWidth < ref?.current?.children[0]?.offsetWidth ||
ref?.current?.clientHeight < ref?.current?.children[0]?.offsetHeight);
return (
<>
<OverlayTrigger
placement="bottom"
overlay={_showOverlay ? getOverlay() : <div></div>}
trigger={_showOverlay && ['hover', 'focus']}
rootClose={true}
show={_showOverlay && showOverlay && !isEditing}
>
{!isEditable ? (
<div
className={`d-flex align-items-center h-100 w-100 justify-content-${determineJustifyContentValue(
horizontalAlignment
)}`}
style={cellStyles}
onMouseMove={() => {
if (!hovered) setHovered(true);
}}
onMouseLeave={() => {
setHovered(false);
}}
ref={ref}
>
<span
style={{
maxHeight: isMaxRowHeightAuto
? 'auto'
: maxRowHeightValue
? maxRowHeightValue
: cellSize === 'condensed'
? '39px'
: '45px',
}}
>
{cellValue}
</span>
</div>
) : (
<div className="h-100 d-flex flex-column justify-content-center position-relative">
<div
onMouseMove={() => {
if (!hovered) setHovered(true);
}}
onMouseLeave={() => setHovered(false)}
className={`${!isValid ? 'is-invalid h-100' : ''} ${isEditing ? 'h-100 content-editing' : ''} h-100`}
>
{_renderString()}
</div>
<div className={`${isValid ? '' : 'invalid-feedback text-truncate'} `}>{validationError}</div>
</div>
)}
</OverlayTrigger>
</>
);
};
export default String;

View file

@ -54,6 +54,8 @@ import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { OverlayTriggerComponent } from './OverlayTriggerComponent';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import { isRowInValid } from '../tableUtils';
import moment from 'moment';
// utilityForNestedNewRow function is used to construct nested object while adding or updating new row when '.' is present in column key for adding new row
const utilityForNestedNewRow = (row) => {
@ -132,9 +134,15 @@ export function Table({
showAddNewRowButton,
allowSelection,
enablePagination,
maxRowHeight,
autoHeight,
selectRowOnCellEdit,
contentWrapProperty,
boxShadow,
maxRowHeightValue,
borderColor,
isMaxRowHeightAuto,
} = loadPropertiesAndStyles(properties, styles, darkMode, component);
const updatedDataReference = useRef([]);
const preSelectRow = useRef(false);
const { events: allAppEvents } = useAppInfo();
@ -171,11 +179,12 @@ export function Table({
const [tableDetails, dispatch] = useReducer(reducer, initialState());
const [hoverAdded, setHoverAdded] = useState(false);
const [generatedColumn, setGeneratedColumn] = useState([]);
const [isCellValueChanged, setIsCellValueChanged] = useState(false);
const mergeToTableDetails = (payload) => dispatch(reducerActions.mergeToTableDetails(payload));
const mergeToFilterDetails = (payload) => dispatch(reducerActions.mergeToFilterDetails(payload));
const mergeToAddNewRowsDetails = (payload) => dispatch(reducerActions.mergeToAddNewRowsDetails(payload));
const mounted = useMounted();
const [resizingColumnId, setResizingColumnId] = useState(null);
const prevDataFromProps = useRef();
@ -231,6 +240,8 @@ export function Table({
function handleExistingRowCellValueChange(index, key, value, rowData) {
const changeSet = tableDetails.changeSet;
setIsCellValueChanged(true);
const dataUpdates = tableDetails.dataUpdates || [];
const clonedTableData = _.cloneDeep(tableData);
@ -268,6 +279,7 @@ export function Table({
}, [JSON.stringify(tableDetails)]);
function handleNewRowCellValueChange(index, key, value, rowData) {
setIsCellValueChanged(true);
const changeSet = copyOfTableDetails.current.addNewRowsDetails.newRowsChangeSet || {};
const dataUpdates = copyOfTableDetails.current.addNewRowsDetails.newRowsDataUpdates || {};
let obj = changeSet ? changeSet[index] || {} : {};
@ -344,6 +356,10 @@ export function Table({
}
}
function getExportFileName() {
return `${component?.name}_${moment().format('DD-MM-YYYY_HH-mm')}`;
}
function onPageIndexChanged(page) {
onComponentOptionChanged(component, 'pageIndex', page).then(() => {
onEvent('onPageChanged', tableEvents, { component });
@ -424,6 +440,9 @@ export function Table({
t,
darkMode,
tableColumnEvents: tableColumnEvents,
cellSize: cellSize,
maxRowHeightValue: maxRowHeightValue,
isMaxRowHeightAuto: isMaxRowHeightAuto,
});
columnData = useMemo(
@ -517,6 +536,7 @@ export function Table({
);
const textWrapActions = (id) => {
//should we remove this
let wrapOption = tableDetails.columnProperties?.find((item) => {
return item?.id == id;
});
@ -633,6 +653,7 @@ export function Table({
pageCount: -1,
manualPagination: false,
getExportFileBlob,
getExportFileName,
disableSortBy: !enabledSort,
manualSortBy: serverSideSort,
stateReducer: (newState, action, prevState) => {
@ -677,7 +698,7 @@ export function Table({
},
Cell: ({ row }) => {
return (
<div className="d-flex flex-column align-items-center">
<div className="d-flex flex-column align-items-center justify-content-center h-100">
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} fireEvent={fireEvent} />
</div>
);
@ -705,6 +726,7 @@ export function Table({
},
];
}, [JSON.stringify(state)]);
const getDetailsOfPreSelectedRow = () => {
const key = Object?.keys(defaultSelectedRow)[0] ?? '';
const value = defaultSelectedRow?.[key] ?? undefined;
@ -836,6 +858,7 @@ export function Table({
setPageSize(rows?.length || 10);
}
}, [clientSidePagination, serverSidePagination, rows, rowsPerPage]);
useEffect(() => {
const pageData = page.map((row) => row.original);
if (preSelectRow.current) {
@ -876,6 +899,7 @@ export function Table({
const [paginationInternalPageIndex, setPaginationInternalPageIndex] = useState(pageIndex ?? 1);
const [rowDetails, setRowDetails] = useState();
useEffect(() => {
if (pageCount <= pageIndex) gotoPage(pageCount - 1);
}, [pageCount]);
@ -1055,15 +1079,16 @@ export function Table({
<div
data-cy={`draggable-widget-${String(component.name).toLowerCase()}`}
data-disabled={parsedDisabledState}
className={`card jet-table table-component ${darkMode && 'dark-theme'}`}
className={`card jet-table table-component ${darkMode ? 'dark-theme' : 'light-theme'}`}
style={{
width: `100%`,
height: `${height}px`,
display: parsedWidgetVisibility ? '' : 'none',
overflow: 'hidden',
borderRadius: Number.parseFloat(borderRadius),
boxShadow: styles.boxShadow,
boxShadow,
padding: '8px',
borderColor: borderColor,
}}
onClick={(event) => {
onComponentClick(id, component, event);
@ -1091,7 +1116,7 @@ export function Table({
className={`tj-text-xsm ${tableDetails.filterDetails.filtersVisible && 'always-active-btn'}`}
customStyles={{ minWidth: '32px' }}
leftIcon="filter"
fill={`var(--slate11)`}
fill={`var(--icons-default)`}
iconWidth="16"
onClick={(e) => {
if (tableDetails?.filterDetails?.filtersVisible) {
@ -1160,7 +1185,9 @@ export function Table({
{...getTableProps()}
className={`table table-vcenter table-nowrap ${tableType} ${darkMode && 'table-dark'} ${
tableDetails.addNewRowsDetails.addingNewRows && 'disabled'
} ${!loadingState && page.length !== 0 && 'h-100'}`}
} ${!loadingState && page.length !== 0 && 'h-100'} ${
state?.columnResizing?.isResizingColumn ? 'table-resizing' : ''
}`}
style={computedStyles}
>
<thead>
@ -1285,15 +1312,17 @@ export function Table({
`}
>
<div>
{column.columnType !== 'selector' && isEditable && (
<SolidIcon
name="editable"
width="16px"
height="16px"
fill={darkMode ? '#4C5155' : '#C1C8CD'}
vievBox="0 0 16 16"
/>
)}
{column.columnType !== 'selector' &&
column.columnType !== 'image' &&
isEditable && (
<SolidIcon
name="editable"
width="16px"
height="16px"
fill={darkMode ? '#4C5155' : '#C1C8CD'}
vievBox="0 0 16 16"
/>
)}
</div>
<div
data-cy={`column-header-${String(column.exportValue)
@ -1354,7 +1383,14 @@ export function Table({
? ''
: 'resizer'
} ${column.isResizing ? 'isResizing' : ''}`}
></div>
>
<div
className="table-column-resize-handle"
style={{
...(column.isResizing && { display: 'block' }),
}}
></div>
</div>
</th>
);
}}
@ -1372,6 +1408,28 @@ export function Table({
<tbody {...getTableBodyProps()} style={{ color: computeFontColor() }}>
{page.map((row, index) => {
prepareRow(row);
let rowProps = { ...row.getRowProps() };
const contentWrap = resolveReferences(contentWrapProperty, currentState);
const isMaxRowHeightAuto = maxRowHeight === 'auto';
rowProps.style.minHeight = cellSize === 'condensed' ? '39px' : '45px'; // 1px is removed to accomodate 1px border-bottom
let cellMaxHeight;
let cellHeight;
if (contentWrap) {
cellMaxHeight = isMaxRowHeightAuto
? 'fit-content'
: resolveReferences(maxRowHeightValue, currentState) + 'px';
rowProps.style.maxHeight = cellMaxHeight;
} else {
cellMaxHeight = cellSize === 'condensed' ? 40 : 46;
cellHeight = cellSize === 'condensed' ? 40 : 46;
rowProps.style.maxHeight = cellMaxHeight + 'px';
rowProps.style.height = cellHeight + 'px';
}
const showInvalidError = row.cells.some((cell) => isRowInValid(cell, currentState, changeSet));
if (showInvalidError) {
rowProps.style.maxHeight = 'fit-content';
rowProps.style.height = '';
}
return (
<tr
key={index}
@ -1385,7 +1443,7 @@ export function Table({
? 'selected'
: ''
}`}
{...row.getRowProps()}
{...rowProps}
onClick={async (e) => {
e.stopPropagation();
// toggleRowSelected will triggered useRededcuer function in useTable and in result will get the selectedFlatRows consisting row which are selected
@ -1438,19 +1496,26 @@ export function Table({
) {
cellProps.style.flex = '1 1 auto';
}
//should we remove this
const wrapAction = textWrapActions(cell.column.id);
const rowChangeSet = changeSet ? changeSet[cell.row.index] : null;
const cellValue = rowChangeSet ? rowChangeSet[cell.column.name] || cell.value : cell.value;
const rowData = tableData[cell.row.index];
const cellBackgroundColor = resolveReferences(
cell.column?.cellBackgroundColor,
currentState,
'',
{
cellValue,
rowData,
}
);
const cellBackgroundColor = ![
'dropdown',
'badge',
'badges',
'tags',
'radio',
'link',
'multiselect',
'toggle',
].includes(cell?.column?.columnType)
? resolveReferences(cell.column?.cellBackgroundColor, currentState, '', {
cellValue,
rowData,
})
: '';
const cellTextColor = resolveReferences(cell.column?.textColor, currentState, '', {
cellValue,
rowData,
@ -1477,22 +1542,34 @@ export function Table({
cell.column.id === 'rightActions' || cell.column.id === 'leftActions' ? cell.column.id : ''
)}${String(cellValue ?? '').toLocaleLowerCase()}-cell-${index}`}
className={cx(
`table-text-align-${cell.column.horizontalAlignment} ${
wrapAction ? wrapAction : cell?.column?.Header === 'Actions' ? '' : 'wrap'
}-wrapper td`,
`table-text-align-${cell.column.horizontalAlignment}
${cell?.column?.Header !== 'Actions' && (contentWrap ? 'wrap-wrapper' : '')}
td`,
{
'has-actions': cell.column.id === 'rightActions' || cell.column.id === 'leftActions',
'has-left-actions': cell.column.id === 'leftActions',
'has-right-actions': cell.column.id === 'rightActions',
'has-text': cell.column.columnType === 'text' || isEditable,
'has-number': cell.column.columnType === 'number',
'has-dropdown': cell.column.columnType === 'dropdown',
'has-multiselect': cell.column.columnType === 'multiselect',
'has-datepicker': cell.column.columnType === 'datepicker',
'align-items-center flex-column': cell.column.columnType === 'selector',
'has-badge': ['badge', 'badges'].includes(cell.column.columnType),
[cellSize]: true,
'overflow-hidden':
['text', 'string', undefined, 'number'].includes(cell.column.columnType) &&
!contentWrap,
'selector-column':
cell.column.columnType === 'selector' && cell.column.id === 'selection',
'resizing-column': cell.column.isResizing || cell.column.id === resizingColumnId,
'has-select': ['select', 'newMultiSelect'].includes(cell.column.columnType),
'has-tags': cell.column.columnType === 'tags',
'has-link': cell.column.columnType === 'link',
'has-radio': cell.column.columnType === 'radio',
'has-toggle': cell.column.columnType === 'toggle',
'has-textarea': ['string', 'text'].includes(cell.column.columnType),
isEditable: isEditable,
}
)}
{...cellProps}
@ -1515,7 +1592,7 @@ export function Table({
>
<div
className={`td-container ${
cell.column.columnType === 'image' && 'jet-table-image-column'
cell.column.columnType === 'image' && 'jet-table-image-column h-100'
} ${cell.column.columnType !== 'image' && `w-100 h-100`}`}
>
<GenerateEachCellValue
@ -1526,6 +1603,10 @@ export function Table({
actionButtonsArray,
isEditable,
horizontalAlignment,
cellTextColor,
contentWrap,
autoHeight,
isMaxRowHeightAuto,
})}
rowChangeSet={rowChangeSet}
isEditable={isEditable}
@ -1534,6 +1615,10 @@ export function Table({
cellTextColor={cellTextColor}
cell={cell}
currentState={currentState}
cellWidth={cell.column.width}
isCellValueChanged={isCellValueChanged}
setIsCellValueChanged={setIsCellValueChanged}
darkMode={darkMode}
/>
</div>
</td>
@ -1611,6 +1696,7 @@ export function Table({
variant="primary"
className={`tj-text-xsm`}
onClick={() => {
setIsCellValueChanged(false);
onEvent('onBulkUpdate', tableEvents, { component }).then(() => {
handleChangesSaved();
});
@ -1629,6 +1715,7 @@ export function Table({
variant="tertiary"
className={`tj-text-xsm`}
onClick={() => {
setIsCellValueChanged(false);
handleChangesDiscarded();
}}
data-cy={`table-button-discard-changes`}
@ -1643,7 +1730,11 @@ export function Table({
</>
) : (
!loadingState && (
<span data-cy={`footer-number-of-records`} className="font-weight-500 color-slate11">
<span
data-cy={`footer-number-of-records`}
className="font-weight-500"
style={{ color: 'var(--text-placeholder)' }}
>
{clientSidePagination && !serverSidePagination && `${globalFilteredRows.length} Records`}
{serverSidePagination && totalRecords ? `${totalRecords} Records` : ''}
</span>
@ -1681,7 +1772,7 @@ export function Table({
<Tooltip id="tooltip-for-add-new-row" className="tooltip" />
<ButtonSolid
variant="ghostBlack"
fill={`var(--slate11)`}
fill={`var(--icons-default)`}
className={`tj-text-xsm ${
tableDetails.addNewRowsDetails.addingNewRows && 'cursor-not-allowed always-active-btn'
}`}
@ -1715,7 +1806,7 @@ export function Table({
minWidth: '32px',
}}
leftIcon="filedownload"
fill={`var(--slate11)`}
fill={`var(--icons-default)`}
iconWidth="16"
size="md"
data-tooltip-id="tooltip-for-download"
@ -1743,7 +1834,7 @@ export function Table({
className={`tj-text-xsm `}
customStyles={{ minWidth: '32px' }}
leftIcon="eye1"
fill={`var(--slate11)`}
fill={`var(--icons-default)`}
iconWidth="16"
size="md"
data-cy={`select-column-icon`}

View file

@ -1,6 +1,20 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
export const Tags = ({ value, onChange, readOnly, containerWidth = '' }) => {
const [showOverlay, setShowOverlay] = useState(false);
const [hovered, setHovered] = useState(false);
const elem = document.querySelector('.table-tags-col-container');
useEffect(() => {
if (hovered) {
setShowOverlay(true);
} else {
setShowOverlay(false);
}
}, [hovered]);
export const Tags = ({ value, onChange, readOnly }) => {
const isValid = Array.isArray(value);
if (!isValid) console.warn('[Tags]: value provided is not an array');
value = isValid ? value : [];
@ -30,10 +44,10 @@ export const Tags = ({ value, onChange, readOnly }) => {
function renderTag(text) {
return (
<span className="col-auto badge bg-blue-lt p-2 mx-1 tag mb-2">
<span className="col-auto badge bg-blue-lt p-2 mx-1 tag">
{text}
{!readOnly && (
<span className="badge badge-pill bg-red-lt remove-tag-button" onClick={() => removeTag(text)}>
<span className="badge badge-pill bg-red-lt remove-tag-button cursor-pointer" onClick={() => removeTag(text)}>
x
</span>
)}
@ -41,38 +55,101 @@ export const Tags = ({ value, onChange, readOnly }) => {
);
}
const getOverlay = (value, containerWidth) => {
const darkMode = localStorage.getItem('darkMode') === 'true';
return Array.isArray(value) ? (
<div
style={{
maxWidth: containerWidth,
width: containerWidth,
}}
className={`overlay-cell-table overlay-tags-table ${darkMode && 'dark-theme'}`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{value?.map((tag, index) => {
return (
<span
key={index}
style={{
wordWrap: 'break-word', // Add word-wrap property for content wrapping
display: 'flex',
flexWrap: 'wrap',
overflow: 'auto',
}}
>
{renderTag(tag)}
</span>
);
})}
</div>
) : (
<div></div>
);
};
return (
<div
className="tags row"
tabIndex={!readOnly ? 0 : null}
onKeyUp={(e) => {
e.stopPropagation();
if (e.key === 'Tab' && !readOnly) {
setShowForm(true);
}
}}
<OverlayTrigger
placement="bottom"
overlay={getOverlay(value, containerWidth)}
trigger={
elem &&
(elem?.clientHeight < elem?.scrollHeight || elem?.clientWidth < elem?.scrollWidth) &&
!showForm &&
value?.length >= 1 && ['click']
}
rootClose={true}
show={
elem &&
(elem?.clientHeight < elem?.scrollHeight || elem?.clientWidth < elem?.scrollWidth) &&
value?.length >= 1 &&
showOverlay
}
>
{value.map((item) => {
return renderTag(item);
})}
{!showForm && !readOnly && (
<span className="col-auto badge bg-green-lt mx-1 add-tag-button" onClick={() => setShowForm(true)}>
{'+'}
</span>
)}
{showForm && (
<span className="col-auto badge bg-green-lt mx-1">
<input
type="text"
autoFocus
className="form-control-plaintext"
onBlur={(e) => addTag(e.target.value)}
onKeyDown={handleFormKeyDown}
/>
</span>
)}
</div>
<div
className="tags row h-100"
style={{ display: 'flex', alignItems: 'center' }}
onMouseMove={() => {
if (!hovered) setHovered(true);
}}
onMouseOut={() => setHovered(false)}
>
{/* Container for + button */}
{!showForm && !readOnly && (
<div className="add-tag-container" style={{ width: '20%' }}>
<span className="col-auto">
<span className="badge bg-green-lt mx-1 add-tag-button" onClick={() => setShowForm(true)}>
{'+'}
</span>
</span>
</div>
)}
{/* Container for renderTags */}
{!showForm && (
<div
className="render-tags-container table-tags-col-container h-100 d-flex flex-wrap custom-gap-3"
style={{ width: '80%', overflow: 'hidden' }}
>
{value.map((item, index) => (
<span key={index} className="col-auto tag-wrapper">
{renderTag(item)}
</span>
))}
</div>
)}
{/* Input element */}
{showForm && (
<div className="col-auto badge bg-green-lt mx-1">
<input
type="text"
autoFocus
className="form-control-plaintext"
onBlur={(e) => addTag(e.target.value)}
onKeyDown={handleFormKeyDown}
/>
</div>
)}
</div>
</OverlayTrigger>
);
};

View file

@ -0,0 +1,180 @@
import React, { useState, useEffect, useRef } from 'react';
import { validateWidget, determineJustifyContentValue } from '@/_helpers/utils';
import DOMPurify from 'dompurify';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
const Text = ({
isEditable,
darkMode,
handleCellValueChange,
cellTextColor,
cellValue,
column,
currentState,
containerWidth,
cell,
horizontalAlignment,
isMaxRowHeightAuto,
cellSize,
maxRowHeightValue,
}) => {
const validationData = validateWidget({
validationObject: {
minLength: {
value: column.minLength,
},
maxLength: {
value: column.maxLength,
},
customRule: {
value: column.customRule,
},
},
widgetValue: cellValue,
currentState,
customResolveObjects: { cellValue },
});
const { isValid, validationError } = validationData;
const ref = useRef();
const nonEditableCellValueRef = useRef();
const [showOverlay, setShowOverlay] = useState(false);
const [hovered, setHovered] = useState(false);
const cellStyles = {
color: cellTextColor ?? 'inherit',
};
const [isEditing, setIsEditing] = useState(false);
useEffect(() => {
if (hovered) {
setShowOverlay(true);
} else {
setShowOverlay(false);
}
}, [hovered]);
const _renderTextArea = () => (
<div
contentEditable={'true'}
className={`${!isValid ? 'is-invalid' : ''} h-100 long-text-input text-container ${
darkMode ? ' textarea-dark-theme' : ''
}`}
style={{
color: cellTextColor ? cellTextColor : 'inherit',
maxWidth: containerWidth,
outline: 'none',
border: 'none',
background: 'inherit',
overflowY: 'auto',
whiteSpace: 'pre-wrap',
position: 'static',
display: 'flex',
alignItems: 'center',
}}
readOnly={!isEditable}
onBlur={(e) => {
setIsEditing(false);
if (isEditable && cellValue !== e.target.textContent) {
const div = e.target;
let content = div.innerHTML;
handleCellValueChange(cell.row.index, column.key || column.name, content, cell.row.original);
}
}}
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(cellValue) }}
onKeyDown={(e) => {
e.persist();
if (e.key === 'Enter' && !e.shiftKey && isEditable) {
const div = e.target;
let content = div.innerHTML;
handleCellValueChange(cell.row.index, column.key || column.name, content, cell.row.original);
}
}}
onFocus={(e) => {
setIsEditing(true);
// setShowOverlay(false);
e.stopPropagation();
}}
/>
);
const _renderNonEditableData = () => (
<div
className={`d-flex align-items-center h-100 w-100 justify-content-${determineJustifyContentValue(
horizontalAlignment
)}`}
style={cellStyles}
>
<span
style={{
maxHeight: isMaxRowHeightAuto
? 'auto'
: maxRowHeightValue
? maxRowHeightValue - 16 // decreasing 16px for padding fix
: cellSize === 'condensed'
? '23px'
: '29px',
}}
ref={nonEditableCellValueRef}
>
{cellValue}
</span>
</div>
);
const getOverlay = () => {
return (
<div
className={`overlay-cell-table ${darkMode && 'dark-theme'}`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{ whiteSpace: 'pre-wrap', color: 'var(--text-primary)' }}
>
<span
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(cellValue) }}
style={{
maxWidth: containerWidth,
width: containerWidth,
}}
></span>
</div>
);
};
const _showOverlay = isEditable
? ref.current && ref.current?.parentElement?.clientHeight < ref.current?.scrollHeight
: ref.current &&
(ref.current?.parentElement?.clientWidth < nonEditableCellValueRef.current?.clientWidth ||
ref.current?.parentElement?.clientHeight < ref.current?.scrollHeight);
return (
<>
<OverlayTrigger
placement="bottom"
overlay={_showOverlay ? getOverlay() : <div></div>}
trigger={_showOverlay && ['hover']}
rootClose={true}
show={_showOverlay && showOverlay && !isEditing}
>
<div
className={`h-100 d-flex ${
_showOverlay && isEditable ? '' : 'justify-content-center'
} flex-column position-relative`}
style={{ ...(isEditing && { zIndex: 2 }) }}
>
<div
onMouseMove={() => {
if (!hovered) setHovered(true);
}}
onMouseLeave={() => setHovered(false)}
ref={ref}
className={`${!isValid ? 'is-invalid h-100' : ''} ${isEditing ? 'h-100 content-editing' : ''}`}
>
{!isEditable ? _renderNonEditableData() : _renderTextArea()}
</div>
{isEditable && <div className={isValid ? '' : 'invalid-feedback text-truncate'}>{validationError}</div>}
</div>
</OverlayTrigger>
</>
);
};
export default Text;

View file

@ -0,0 +1,24 @@
import React from 'react';
import moment from 'moment';
import TimePickerComponent from '@/ToolJetUI/Timepicker/Timepicker';
export const TimePicker = ({ value, onChange, component }) => {
const currentDay = moment().format('YYYY-MM-DD');
const dateTimeString = `${currentDay} ${value}`;
const dateObject = moment(dateTimeString, 'YYYY-MM-DD hh:mm').toDate();
return (
<div className="xxx">
<TimePickerComponent
selected={value && moment(dateObject.toISOString()).toDate()}
timeFormat={
component.component.definition.properties.enableTwentyFourHour.value == '{{true}}' ? `HH:mm a` : `hh:mm a`
}
onChange={(value) => {
onChange(moment(value).format('hh:mm'));
}}
enableTime={true}
/>
</div>
);
};

View file

@ -77,7 +77,7 @@ export default function autogenerateColumns(
// 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 keysOfExistingColumns = existingColumns.map((column) => column?.key || column?.name);
const keysFromWhichNewColumnsShouldBeGenerated = _.difference(keysOfTableData, [
...keysOfExistingColumns,
@ -90,8 +90,8 @@ export default function autogenerateColumns(
]);
const keysOfExistingColumnsThatNeedToPersist = existingColumns
.filter((column) => !column.autogenerated || keysOfTableData.includes(column.key || column.name))
.map((column) => column.key || column.name);
.filter((column) => !column?.autogenerated || keysOfTableData.includes(column.key || column.name))
.map((column) => column?.key || column?.name);
const generatedColumns = keysAndDataTypesToGenerateNewColumns.map(([key, dataType]) => ({
id: uuidv4(),
@ -103,7 +103,7 @@ export default function autogenerateColumns(
const finalKeys = [...keysFromWhichNewColumnsShouldBeGenerated, ...keysOfExistingColumnsThatNeedToPersist];
const finalColumns = [...existingColumns, ...generatedColumns].filter((column) =>
finalKeys.includes(column.key || column.name)
finalKeys.includes(column?.key || column?.name)
);
setTimeout(() => setProperty('columns', finalColumns), 10);
@ -112,9 +112,10 @@ export default function autogenerateColumns(
const dataTypeToColumnTypeMapping = {
string: 'string',
number: 'number',
boolean: 'boolean',
};
const convertDataTypeToColumnType = (dataType) => {
if (Object.keys(dataTypeToColumnTypeMapping).includes(dataType)) return dataTypeToColumnTypeMapping[dataType];
else return 'default';
else return 'string';
};

View file

@ -1,14 +1,19 @@
import React from 'react';
import _ from 'lodash';
import SelectSearch from 'react-select-search';
import { resolveReferences, validateWidget, determineJustifyContentValue } from '@/_helpers/utils';
import { CustomSelect } from '../CustomSelect';
import { resolveReferences, validateWidget, determineJustifyContentValue, validateDates } from '@/_helpers/utils';
import { CustomDropdown } from '../CustomDropdown';
import { Tags } from '../Tags';
import { Radio } from '../Radio';
import { Toggle } from '../Toggle';
import { Datepicker } from '../Datepicker';
import { Link } from '../Link';
import moment from 'moment';
import { Boolean } from '../Boolean';
import { CustomSelect } from '../CustomSelect';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import Text from '../Text';
import String from '../String';
export default function generateColumnsData({
columnProperties,
@ -27,14 +32,15 @@ export default function generateColumnsData({
t,
darkMode,
tableColumnEvents,
cellSize,
maxRowHeightValue,
}) {
return columnProperties.map((column) => {
if (!column) return;
const columnSize = columnSizes[column?.id] || columnSizes[column?.name] || column.columnSize;
const columnSize = columnSizes[column?.id] || columnSizes[column?.name];
const columnType = column?.columnType;
let sortType = 'alphanumeric';
const columnOptions = {};
if (
columnType === 'dropdown' ||
@ -54,16 +60,35 @@ export default function generateColumnsData({
});
}
}
if (columnType === 'select' || columnType === 'newMultiSelect') {
columnOptions.selectOptions = [];
const useDynamicOptions = resolveReferences(column?.useDynamicOptions, currentState);
if (useDynamicOptions) {
const dynamicOptions = resolveReferences(column?.dynamicOptions || [], currentState);
columnOptions.selectOptions = Array.isArray(dynamicOptions) ? dynamicOptions : [];
} else {
const options = column?.options ?? [];
columnOptions.selectOptions =
options?.map((option) => ({
label: option.label,
value: option.value,
})) ?? [];
}
}
if (columnType === 'datepicker') {
column.isTimeChecked = column.isTimeChecked ? column.isTimeChecked : false;
column.dateFormat = column.dateFormat ? column.dateFormat : 'DD/MM/YYYY';
column.parseDateFormat = column.parseDateFormat ?? column.dateFormat; //backwards compatibility
column.isDateSelectionEnabled = column.isDateSelectionEnabled ?? true;
sortType = (firstDate, secondDate) => {
const columnKey = column.key || column.name;
// Return -1 if second date is higher, 1 if first date is higher
if (secondDate?.original[columnKey] === '') {
return 1;
} else if (firstDate?.original[columnKey] === '') return -1;
} else if (firstDate?.original[columnKey] === '') {
return -1;
}
const parsedFirstDate = moment(firstDate?.original[columnKey], column.parseDateFormat);
const parsedSecondDate = moment(secondDate?.original[columnKey], column.parseDateFormat);
@ -75,7 +100,6 @@ export default function generateColumnsData({
}
};
}
const width = columnSize || defaultColumn.width;
return {
id: column.id,
@ -98,11 +122,19 @@ export default function generateColumnsData({
sortType,
columnVisibility: column?.columnVisibility ?? true,
horizontalAlignment: column?.horizontalAlignment ?? 'left',
Cell: function ({ cell, isEditable, newRowsChangeSet = null, horizontalAlignment }) {
Cell: function ({
cell,
isEditable,
newRowsChangeSet = null,
horizontalAlignment,
cellTextColor = '',
contentWrap = true,
autoHeight = true,
isMaxRowHeightAuto,
}) {
const updatedChangeSet = newRowsChangeSet === null ? changeSet : newRowsChangeSet;
const rowChangeSet = updatedChangeSet ? updatedChangeSet[cell.row.index] : null;
let cellValue = rowChangeSet ? rowChangeSet[column.key || column.name] ?? cell.value : cell.value;
const rowData = tableData?.[cell?.row?.index];
if (
cell.row.index === 0 &&
@ -113,104 +145,133 @@ export default function generateColumnsData({
customResolvables[id] = { ...variablesExposedForPreview[id], rowData };
exposeToCodeHinter((prevState) => ({ ...prevState, ...customResolvables }));
}
cellValue = cellValue === undefined || cellValue === null ? '' : cellValue;
cellValue = cellValue === undefined ? '' : cellValue;
switch (columnType) {
case 'string':
case undefined:
case 'default': {
const textColor = resolveReferences(column.textColor, currentState, '', { cellValue, rowData });
const cellStyles = {
color: textColor ?? '',
};
if (isEditable) {
const validationData = validateWidget({
validationObject: {
regex: {
value: column.regex,
},
minLength: {
value: column.minLength,
},
maxLength: {
value: column.maxLength,
},
customRule: {
value: column.customRule,
},
},
widgetValue: cellValue,
currentState,
customResolveObjects: { cellValue },
});
const { isValid, validationError } = validationData;
const cellStyles = {
color: textColor ?? '',
};
return (
<div className="h-100 d-flex flex-column justify-content-center">
<input
type="text"
style={{ ...cellStyles }}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (e.target.defaultValue !== e.target.value) {
handleCellValueChange(
cell.row.index,
column.key || column.name,
e.target.value,
cell.row.original
);
}
}
}}
onBlur={(e) => {
if (e.target.defaultValue !== e.target.value) {
handleCellValueChange(
cell.row.index,
column.key || column.name,
e.target.value,
cell.row.original
);
}
}}
className={`form-control-plaintext form-control-plaintext-sm ${!isValid ? 'is-invalid' : ''}`}
defaultValue={cellValue}
onFocus={(e) => e.stopPropagation()}
/>
<div className="invalid-feedback">{validationError}</div>
</div>
);
}
const cellTextColor = resolveReferences(column.textColor, currentState, '', { cellValue, rowData });
return (
<div
className={`d-flex align-items-center h-100 w-100 justify-content-${determineJustifyContentValue(
horizontalAlignment
)}`}
style={cellStyles}
>
{String(cellValue)}
</div>
<String
isEditable={isEditable}
darkMode={darkMode}
handleCellValueChange={handleCellValueChange}
cellTextColor={cellTextColor}
horizontalAlignment={horizontalAlignment}
cellValue={cellValue}
column={column}
currentState={currentState}
containerWidth={width}
cell={cell}
isMaxRowHeightAuto={isMaxRowHeightAuto}
cellSize={cellSize}
maxRowHeightValue={maxRowHeightValue}
/>
);
// if (isEditable) {
// const validationData = validateWidget({
// validationObject: {
// regex: {
// value: column.regex,
// },
// minLength: {
// value: column.minLength,
// },
// maxLength: {
// value: column.maxLength,
// },
// customRule: {
// value: column.customRule,
// },
// },
// widgetValue: cellValue,
// currentState,
// customResolveObjects: { cellValue },
// });
// const { isValid, validationError } = validationData;
// const cellStyles = {
// color: textColor ?? 'inherit',
// };
// return (
// <div className="h-100 d-flex flex-column justify-content-center position-relative">
// <div
// rows="1"
// contentEditable={true}
// className={`${!isValid ? 'is-invalid' : ''} h-100 text-container long-text-input ${darkMode ? ' textarea-dark-theme' : ''
// }`}
// style={{
// ...cellStyles,
// maxWidth: width,
// outline: 'none',
// border: 'none',
// background: 'inherit',
// }}
// readOnly={!isEditable}
// onBlur={(e) => {
// if (cellValue !== e.target.textContent) {
// handleCellValueChange(
// cell.row.index,
// column.key || column.name,
// e.target.textContent,
// cell.row.original
// );
// }
// }}
// onKeyDown={(e) => {
// if (e.key === 'Enter') {
// if (cellValue !== e.target.textContent) {
// handleCellValueChange(
// cell.row.index,
// column.key || column.name,
// e.target.textContent,
// cell.row.original
// );
// }
// }
// }}
// onFocus={(e) => e.stopPropagation()}
// >
// {cellValue}
// </div>
// <div className={isValid ? '' : 'invalid-feedback'}>{validationError}</div>
// </div>
// );
// }
// return (
// <div
// className={`d-flex align-items-center h-100 w-100 justify-content-${determineJustifyContentValue(
// horizontalAlignment
// )}`}
// style={cellStyles}
// >
// {String(cellValue)}
// </div>
// );
}
case 'number': {
const textColor = resolveReferences(column.textColor, currentState, '', { cellValue, rowData });
const cellStyles = {
color: textColor ?? '',
overflow: 'hidden',
};
if (isEditable) {
const validationData = validateWidget({
validationObject: {
minValue: {
value: column.minValue,
value: column?.minValue,
},
maxValue: {
value: column.maxValue,
value: column?.maxValue,
},
regex: {
value: column?.regex,
},
customRule: {
value: column?.customRule,
},
},
widgetValue: cellValue,
@ -223,18 +284,62 @@ export default function generateColumnsData({
color: textColor ?? '',
};
const handleIncrement = (e) => {
e.preventDefault(); // Prevent the default button behavior (form submission, page reload)
const newValue = (cellValue || 0) + 1;
if (!isNaN(newValue)) {
handleCellValueChange(cell.row.index, column.key || column.name, Number(newValue), cell.row.original);
}
};
const handleDecrement = (e) => {
e.preventDefault();
const newValue = (cellValue || 0) - 1;
if (!isNaN(newValue)) {
handleCellValueChange(cell.row.index, column.key || column.name, Number(newValue), cell.row.original);
}
};
const allowedDecimalPlaces = column?.decimalPlaces ?? null;
const removingExcessDecimalPlaces = (cellValue, allowedDecimalPlaces) => {
allowedDecimalPlaces = resolveReferences(allowedDecimalPlaces, currentState);
if (cellValue?.toString()?.includes('.')) {
const splittedCellValue = cellValue?.toString()?.split('.');
const decimalPlacesUnderLimit = splittedCellValue[1]
.split('')
.splice(0, allowedDecimalPlaces)
.join('');
cellValue = Number(`${splittedCellValue[0]}.${decimalPlacesUnderLimit}`);
}
return cellValue;
};
cellValue = allowedDecimalPlaces
? removingExcessDecimalPlaces(cellValue, allowedDecimalPlaces)
: cellValue;
return (
<div className="h-100 d-flex flex-column justify-content-center">
<div className="h-100 d-flex flex-column justify-content-center position-relative">
<input
type="number"
style={{ ...cellStyles }}
style={{
...cellStyles,
outline: 'none',
border: 'none',
background: 'inherit',
paddingRight: '20px',
}}
id={`table-input-${column.id}-${cell.row.id}`}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (e.target.defaultValue !== e.target.value) {
const value = allowedDecimalPlaces
? removingExcessDecimalPlaces(e.target.value, allowedDecimalPlaces)
: e.target.value;
handleCellValueChange(
cell.row.index,
column.key || column.name,
Number(e.target.value),
Number(value),
cell.row.original
);
}
@ -242,19 +347,50 @@ export default function generateColumnsData({
}}
onBlur={(e) => {
if (e.target.defaultValue !== e.target.value) {
const value = allowedDecimalPlaces
? removingExcessDecimalPlaces(e.target.value, allowedDecimalPlaces)
: e.target.value;
handleCellValueChange(
cell.row.index,
column.key || column.name,
Number(e.target.value),
Number(value),
cell.row.original
);
}
}}
onFocus={(e) => e.stopPropagation()}
className={`form-control-plaintext form-control-plaintext-sm ${!isValid ? 'is-invalid' : ''}`}
className={`table-column-type-input-element input-number h-100 ${!isValid ? 'is-invalid' : ''}`}
defaultValue={cellValue}
/>
<div className="invalid-feedback">{validationError}</div>
<div className="arror-container">
<div onClick={(e) => handleIncrement(e)}>
<SolidIcon
width={'16px'}
style={{
top: '1px',
right: '1px',
zIndex: 3,
}}
className="numberinput-up-arrow-table "
name="uparrow"
fill={'var(--icons-default)'}
></SolidIcon>
</div>
<div onClick={(e) => handleDecrement(e)}>
<SolidIcon
style={{
right: '1px',
bottom: '1px',
zIndex: 3,
}}
width={'16px'}
className="numberinput-down-arrow-table"
name="downarrow"
fill={'var(--icons-default)'}
></SolidIcon>
</div>
</div>
<div className={isValid ? '' : 'invalid-feedback text-truncate'}>{validationError}</div>
</div>
);
}
@ -269,31 +405,29 @@ export default function generateColumnsData({
</div>
);
}
case 'text': {
case 'text':
return (
<textarea
rows="1"
className={`form-control-plaintext text-container ${
darkMode ? 'text-light textarea-dark-theme' : 'text-muted'
}`}
readOnly={!isEditable}
onBlur={(e) => {
if (isEditable && e.target.defaultValue !== e.target.value) {
handleCellValueChange(cell.row.index, column.key || column.name, e.target.value, cell.row.original);
}
}}
onKeyDown={(e) => {
e.persist();
if (e.key === 'Enter' && !e.shiftKey && isEditable) {
handleCellValueChange(cell.row.index, column.key || column.name, e.target.value, cell.row.original);
}
}}
defaultValue={cellValue}
onFocus={(e) => e.stopPropagation()}
></textarea>
// <div className="h-100 d-flex flex-column justify-content-center position-relative">
<Text
isEditable={isEditable}
darkMode={darkMode}
handleCellValueChange={handleCellValueChange}
cellTextColor={cellTextColor}
horizontalAlignment={horizontalAlignment}
cellValue={cellValue}
column={column}
currentState={currentState}
containerWidth={width}
cell={cell}
isMaxRowHeightAuto={isMaxRowHeightAuto}
cellSize={cellSize}
maxRowHeightValue={maxRowHeightValue}
/>
// </div>
);
}
case 'dropdown': {
case 'dropdown':
case 'select':
case 'newMultiSelect': {
const validationData = validateWidget({
validationObject: {
regex: {
@ -315,22 +449,54 @@ export default function generateColumnsData({
});
const { isValid, validationError } = validationData;
return (
<div className="h-100 d-flex align-items-center">
<SelectSearch
options={columnOptions.selectOptions}
value={cellValue}
search={true}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
fuzzySearch
placeholder={t('globals.select', 'Select') + '...'}
disabled={!isEditable}
className="select-search"
/>
<div className={`invalid-feedback ${isValid ? '' : 'd-flex'}`}>{validationError}</div>
<div
className="h-100 d-flex align-items-center flex-column justify-content-center"
styles={{ flex: '1 1 0' }}
>
{columnType === 'dropdown' && (
<SelectSearch
options={columnOptions.selectOptions}
value={cellValue}
search={true}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
fuzzySearch
placeholder={t('globals.select', 'Select') + '...'}
disabled={!isEditable}
className="select-search"
/>
)}
{['newMultiSelect', 'select'].includes(columnType) && (
<CustomSelect
options={columnOptions.selectOptions}
value={cellValue}
search={true}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
fuzzySearch
placeholder={t('globals.select', 'Select') + '...'}
disabled={!isEditable}
className="select-search table-select-search"
darkMode={darkMode}
defaultOptionsList={column?.defaultOptionsList || []}
textColor={column?.textColor || 'var(--slate12)'}
isMulti={columnType === 'newMultiSelect' ? true : false}
containerWidth={width}
optionsLoadingState={
resolveReferences(column?.useDynamicOptions, currentState) &&
resolveReferences(column?.optionsLoadingState, currentState)
? true
: false
}
horizontalAlignment={determineJustifyContentValue(horizontalAlignment)}
isEditable={isEditable}
isMaxRowHeightAuto={contentWrap && isMaxRowHeightAuto}
/>
)}
<div className={` ${isValid ? 'd-none' : 'invalid-feedback d-block'}`}>{validationError}</div>
</div>
);
}
@ -357,11 +523,11 @@ export default function generateColumnsData({
case 'badges': {
return (
<div
className={`h-100 d-flex align-items-center justify-content-${determineJustifyContentValue(
className={`h-100 w-100 d-flex align-items-center justify-content-${determineJustifyContentValue(
horizontalAlignment
)}`}
>
<CustomSelect
<CustomDropdown
options={columnOptions.selectOptions}
value={cellValue}
multiple={columnType === 'badges'}
@ -371,37 +537,38 @@ export default function generateColumnsData({
darkMode={darkMode}
isEditable={isEditable}
width={width}
contentWrap={contentWrap}
autoHeight={autoHeight}
/>
</div>
);
}
case 'tags': {
return (
<div>
<Tags
value={cellValue}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
readOnly={!isEditable}
/>
</div>
<Tags
value={cellValue}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
readOnly={!isEditable}
containerWidth={width}
/>
);
}
case 'image': {
const computeImageHeight = column?.height ? `${column?.height}px` : '100%';
return (
<div>
<div style={{ height: '100%', display: 'flex', alignItems: 'center' }}>
{cellValue && (
<img
src={cellValue}
style={{
pointerEvents: 'auto',
width: `${column?.width}px`,
height: `${column?.height}px`,
height: computeImageHeight,
borderRadius: `${column?.borderRadius}%`,
objectFit: `${column?.objectFit}`,
}}
alt={cellValue}
/>
)}
</div>
@ -417,6 +584,7 @@ export default function generateColumnsData({
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
containerWidth={width}
/>
</div>
);
@ -445,29 +613,112 @@ export default function generateColumnsData({
);
}
case 'datepicker': {
const textColor = resolveReferences(column.textColor, currentState, '', { cellValue, rowData });
const isTimeChecked = resolveReferences(column?.isTimeChecked, currentState);
const isTwentyFourHrFormatEnabled = resolveReferences(column?.isTwentyFourHrFormatEnabled, currentState);
const disabledDates = resolveReferences(column?.disabledDates, currentState);
const parseInUnixTimestamp = resolveReferences(column?.parseInUnixTimestamp, currentState);
const isDateSelectionEnabled = resolveReferences(column?.isDateSelectionEnabled, currentState);
const cellStyles = {
color: textColor ?? '',
};
const validationData = validateDates({
validationObject: {
minDate: {
value: isDateSelectionEnabled ? column.minDate : undefined,
},
maxDate: {
value: isDateSelectionEnabled ? column.maxDate : undefined,
},
minTime: {
value: isTimeChecked ? column.minTime : undefined,
},
maxTime: {
value: isTimeChecked ? column.maxTime : undefined,
},
parseDateFormat: {
value: column.parseDateFormat,
},
isTwentyFourHrFormatEnabled: {
value: isTwentyFourHrFormatEnabled,
},
isDateSelectionEnabled: {
value: isDateSelectionEnabled,
},
customRule: {
value: column.customRule,
},
},
widgetValue: cellValue,
currentState,
customResolveObjects: { cellValue },
});
const { isValid, validationError } = validationData;
return (
<div className="h-100 d-flex align-items-center">
<div className="h-100 d-flex flex-column justify-content-center">
<Datepicker
timeZoneValue={column.timeZoneValue}
timeZoneDisplay={column.timeZoneDisplay}
dateDisplayFormat={column.dateFormat}
isTimeChecked={column.isTimeChecked}
isTimeChecked={isTimeChecked}
value={cellValue}
readOnly={isEditable}
readOnly={!isEditable}
parseDateFormat={column.parseDateFormat}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
tableRef={tableRef}
isDateSelectionEnabled={isDateSelectionEnabled}
isTwentyFourHrFormatEnabled={isTwentyFourHrFormatEnabled}
timeFormat={column.timeFormat}
parseTimeFormat={column.parseTimeFormat}
parseInUnixTimestamp={parseInUnixTimestamp}
unixTimeStamp={column.unixTimestamp}
disabledDates={disabledDates}
unixTimestamp={column.unixTimestamp}
cellStyles={cellStyles}
darkMode={darkMode}
/>
{isEditable && (
<div className={isValid ? '' : 'invalid-feedback d-block text-truncate'}>{validationError}</div>
)}
</div>
);
}
case 'link': {
const linkTarget = resolveReferences(column?.linkTarget ?? '_blank', currentState);
const linkTarget = resolveReferences(column?.linkTarget ?? '{{true}}', currentState);
column = {
...column,
linkColor: column?.linkColor ?? '#1B1F24',
underlineColor: column?.underlineColor ?? '#4368E3',
};
return (
<div className="h-100 d-flex align-items-center">
<Link cellValue={cellValue} linkTarget={linkTarget} />
<Link
cellValue={cellValue}
linkTarget={linkTarget}
linkColor={column.linkColor}
underlineColor={column.underlineColor}
underline={column.underline}
displayText={column.displayText}
darkMode={darkMode}
/>
</div>
);
}
case 'boolean': {
return (
<div className="h-100 d-flex align-items-center">
<Boolean
value={!!cellValue}
isEditable={isEditable}
onChange={(value) =>
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original)
}
toggleOnBg={column?.toggleOnBg}
toggleOffBg={column?.toggleOffBg}
/>
</div>
);
}

View file

@ -0,0 +1,418 @@
.tj-datepicker-widget {
&.react-datepicker-popper {
z-index: 3;
}
border-radius: 6px !important;
border: 1px solid var(--slate5) !important;
box-shadow: 0px 32px 64px -12px rgba(16, 24, 40, 0.14);
width: 250px;
padding: 0px;
}
.tj-timepicker-widget {
&.react-datepicker-popper {
background-color: unset;
z-index: 3;
}
.react-datepicker__time-container {
right: 0px
}
}
.react-datepicker__header {
background-color: var(--surfaces-surface-01);
padding: 6px 0px;
border: none;
}
.tj-datepicker-widget-left {
position: absolute;
left: 10px;
}
.tj-datepicker-widget-right {
position: absolute;
right: 10px;
}
.tj-datepicker-widget-arrows {
box-shadow: 0px 1px 0px 0px #0000000B;
border: 1px solid var(--borders-default);
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
background-color: var(--surfaces-surface-01) !important;
}
.react-datepicker__navigation-icon--previous {
right: 0px;
top: 3px;
}
.react-datepicker__navigation-icon--next {
left: 0px;
top: 3px;
}
.react-datepicker {
width: 250px;
border: none;
background-color: var(--surfaces-surface-01);
}
.dark-theme {
.react-datepicker {
background-color: #232e3c;
}
}
.react-datepicker__month {
margin: 6px
}
.react-datepicker__navigation--next {
right: 14px;
}
.react-datepicker__navigation--previous {
left: 14px;
}
.react-datepicker__week,
.react-datepicker__day-names {
display: flex;
justify-content: space-between;
}
.react-datepicker__day-names {
.react-datepicker__day-name {
color: var(--text-placeholder)
}
}
.react-datepicker__day--selected {
border-radius: 8px;
background-color: var(--primary-brand) !important;
color: var(--surfaces-surface-01) !important
}
.react-datepicker__navigation-icon::before {
border-color: #000;
border-width: 1px 1px 0 0;
}
.react-datepicker__day {
font-size: 12px;
font-style: normal;
font-weight: 500;
color: var(--text-primary);
&:hover {
background-color: var(--interactive-overlays-fill-hover);
border-radius: 8px;
}
}
.react-datepicker__day--today {
border: 1px solid var(--primary-brand);
border-radius: 8px;
background-color: var(--surfaces-surface-01);
}
.react-datepicker__day--keyboard-selected {
background-color: var(--surfaces-surface-01);
}
.react-datepicker-time__input {
input {
width: 205px !important;
height: 32px !important;
border-radius: 6px;
background: #FFF;
box-shadow: 0px 32px 64px -12px rgba(16, 24, 40, 0.14);
border: 1px solid var(--tj-text-input-widget-border-default);
padding-left: 10px;
}
}
.datepicker-widget {
.input-field {
min-height: 32px;
padding: 0;
padding-left: 2px;
}
}
input[type="time"] {
position: relative;
}
input[type="time"]::-webkit-calendar-picker-indicator {
display: block;
top: 0;
right: 0;
height: 100%;
width: 100%;
position: absolute;
background: transparent;
}
.react-datepicker-popper,
.tj-datepicker-widget {
background-color: var(--surfaces-surface-01);
}
.react-datepicker__month-container {
float: inherit;
}
.react-datepicker__time-container .react-datepicker__time {
color: var(--text-primary);
background: var(--surfaces-surface-01);
}
.dark-theme {
.react-datepicker-popper,
.tj-datepicker-widget {
background-color: #232e3c;
}
.react-datepicker__time-container,
.react-datepicker__time-container .react-datepicker__time {
background-color: #232e3c;
color: #fff
}
.react-datepicker__header {
background-color: #232e3c;
color: #fff
}
.react-datepicker__month-container {
background-color: #232e3c;
color: #fff;
.react-datepicker__month {
background-color: #232e3c;
.react-datepicker__day {
color: #fff
}
}
}
li.react-datepicker__time-list-item:hover {
background-color: #CCD1D533 !important;
}
select {
color: #fff
}
}
.react-datepicker-popper[data-placement^=bottom] {
padding-top: 0px !important;
margin-top: 12px;
}
.react-datepicker__day {
margin: 0px;
}
.react-datepicker__week {
padding: 4px;
}
.react-datepicker__day-name,
.react-datepicker__day,
.react-datepicker__time-name {
margin: 0px;
// width: 32px;
}
.react-datepicker__day-names {
margin: 0rem 0.4rem;
height: 24px;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.tj-datepicker-widget-month-selector, .tj-datepicker-widget-year-selector {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
padding-right: 1em;
/* Add some padding on the right to create space for custom arrow */
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23424242" width="18px" height="18px"><path d="M7 10l5 5 5-5z" /></svg>');
/* Add a custom arrow (you can use your own SVG) */
background-repeat: no-repeat;
background-position: right center;
border: none;
/* Remove the default border */
padding: 8px;
/* Adjust padding as needed */
cursor: pointer;
/* Add pointer cursor for better usability */
background: none;
padding: 0px;
height: 24px;
text-align: center;
// margin-right: 6px !important;
color: var(--text-primary);
font-weight: 500;
}
/* Optional: Style when select is focused */
select:focus {
outline: none;
/* Remove default focus outline */
// box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
/* Add a subtle box-shadow when focused */
}
.table-column-datepicker-input {
border: 1px solid transparent;
background: inherit;
text-align: left;
width: 100%;
}
.table-dark {
.table-column-datepicker-input {
color: #fff
}
}
.react-datepicker-wrapper {
width: 100%;
}
.react-time-picker__wrapper {
border: none !important;
}
.timepicker-wrapper {
.react-datepicker-wrapper {
border: none !important;
}
}
.react-datepicker__triangle {
display: none;
}
.react-datepicker__time-container {
position: absolute;
right: -86px;
top: 0;
height: 271.12px;
background-color: #fff;
border: 1px solid var(--slate5);
box-shadow: 0px 32px 64px -12px rgba(16, 24, 40, 0.14);
border-radius: 0.3rem;
.react-datepicker__header {
padding-top: 13px;
padding-bottom: 7px;
border-bottom: 1px solid var(--slate7)
}
.react-datepicker__time-box {
width: 75px;
margin: 0px;
}
.react-datepicker__time-list {
// padding: 4px 6px;
}
.react-datepicker__time-list-item {
height: 28px !important;
color: var(--text-primary) !important;
font-weight: 400 !important;
padding: 0px !important;
display: flex;
align-items: center;
justify-content: center;
}
.react-datepicker__time-list-item--selected {
border-radius: 8px;
background-color: #4368E3 !important;
color: var(--surfaces-surface-01) !important
}
}
.react-datepicker-time__header {
color: #6A727C;
font-weight: 400;
}
.react-datepicker--time-only {
width: 66px;
}
.tj-datepicker-widget-hidden {
height: 0px;
.react-datepicker--time-only {
top: -16px;
}
}
.table-column-datepicker-input-container {
position: relative;
}
.table-column-datepicker-input-icon {
position: absolute;
top: 50%;
right: 1rem;
transform: translateY(-50%)
}
.tj-datepicker-widget-month-selector,
.tj-datepicker-widget-year-selector {
&:hover {
background-color: var(--interactive-overlays-fill-hover);
border-radius: 6px;
}
}
.react-datepicker__day--outside-month {
color: var(--text-disabled);
}
.react-datepicker__day--excluded {
color: var(--text-disabled);
pointer-events: none;
}
td {
&.isEditable {
&:hover, &:focus-within {
.table-column-datepicker-input {
padding-right: 40px;
}
}
}
}

View file

@ -57,10 +57,10 @@ export default function loadPropertiesAndStyles(properties, styles, darkMode, co
const borderRadius = styles.borderRadius ?? 0;
const widgetVisibility = styles?.visibility ?? true;
const widgetVisibility = properties?.visibility ?? true;
const parsedWidgetVisibility = widgetVisibility;
const disabledState = styles?.disabledState ?? false;
const disabledState = properties?.disabledState ?? false;
const parsedDisabledState = disabledState;
const actionButtonRadius = styles.actionButtonRadius ? parseFloat(styles.actionButtonRadius) : 0;
@ -76,7 +76,12 @@ export default function loadPropertiesAndStyles(properties, styles, darkMode, co
const showAddNewRowButton = properties?.showAddNewRowButton ?? true;
const allowSelection = properties?.allowSelection ?? (showBulkSelector || highlightSelectedRow) ? true : false;
const defaultSelectedRow = properties?.defaultSelectedRow ?? { id: 1 };
const maxRowHeight = styles?.maxRowHeight ?? 'auto';
const maxRowHeightValue = styles?.maxRowHeightValue ?? 80;
const boxShadow = styles?.boxShadow;
const selectRowOnCellEdit = properties?.selectRowOnCellEdit ?? true;
const contentWrapProperty = styles?.contentWrap ?? true;
const borderColor = styles?.borderColor ?? 'var(--borders-weak-disabled)';
return {
color,
@ -109,6 +114,11 @@ export default function loadPropertiesAndStyles(properties, styles, darkMode, co
defaultSelectedRow,
showAddNewRowButton,
allowSelection,
maxRowHeight,
maxRowHeightValue,
selectRowOnCellEdit,
contentWrapProperty,
boxShadow,
borderColor,
};
}

View file

@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import DOMPurify from 'dompurify';
// eslint-disable-next-line import/no-unresolved
import Markdown from 'react-markdown';
import './text.scss';
import Loader from '@/ToolJetUI/Loader/Loader';

View file

@ -58,18 +58,33 @@ export const TextInput = function TextInput({
const computedStyles = {
height: height == 36 ? (padding == 'default' ? '36px' : '40px') : padding == 'default' ? height : height + 4,
borderRadius: `${borderRadius}px`,
color: darkMode && textColor === '#11181C' ? '#ECEDEE' : textColor,
color: textColor !== '#1B1F24' ? textColor : disable || loading ? 'var(--text-disabled)' : 'var(--text-primary)',
borderColor: isFocused
? accentColor
: ['#D7DBDF'].includes(borderColor)
? darkMode
? '#6D757D7A'
: '#6A727C47'
: borderColor,
? 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: darkMode && ['#fff'].includes(backgroundColor) ? '#313538' : backgroundColor,
backgroundColor:
backgroundColor != '#fff'
? backgroundColor
: disable || loading
? darkMode
? 'var(--surfaces-app-bg-default)'
: 'var(--surfaces-surface-03)'
: 'var(--surfaces-surface-01)',
boxShadow: boxShadow,
padding: styles.iconVisibility ? '8px 10px 8px 29px' : '8px 10px 8px 10px',
padding: styles.iconVisibility
? height < 20
? '0px 10px 0px 29px'
: '8px 10px 8px 29px'
: height < 20
? '0px 10px'
: '8px 10px',
overflow: 'hidden',
textOverflow: 'ellipsis',
};
@ -238,8 +253,7 @@ export const TextInput = function TextInput({
const renderInput = () => (
<>
<div
data-cy={`label-${String(component.name).toLowerCase()}`}
data-disabled={disable || loading}
data-cy={`label-${String(component.name).toLowerCase()} `}
className={`text-input d-flex ${
defaultAlignment === 'top' &&
((width != 0 && label?.length != 0) || (auto && width == 0 && label && label?.length != 0))
@ -279,7 +293,7 @@ export const TextInput = function TextInput({
? '11px'
: (label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)
? `${labelWidth + 11}px`
: '11px', //23 :: is 10 px inside the input + 1 px border + 12px margin right
: '11px', //11 :: is 10 px inside the input + 1 px border + 12px margin right
position: 'absolute',
top: `${
defaultAlignment === 'side'
@ -289,7 +303,7 @@ export const TextInput = function TextInput({
: '50%'
}`,
transform: ' translateY(-50%)',
color: iconColor,
color: iconColor !== '#CFD3D859' ? iconColor : 'var(--icons-weak-disabled)',
zIndex: 3,
}}
stroke={1.5}
@ -300,7 +314,7 @@ export const TextInput = function TextInput({
ref={textInputRef}
className={`tj-text-input-widget ${
!isValid && showValidationError ? 'is-invalid' : ''
} validation-without-icon ${darkMode && 'dark-theme-placeholder'}`}
} validation-without-icon`}
onKeyUp={(e) => {
if (e.key === 'Enter') {
setValue(e.target.value);
@ -338,11 +352,13 @@ export const TextInput = function TextInput({
</div>
{showValidationError && visibility && (
<div
className="tj-text-sm"
data-cy={`${String(component.name).toLowerCase()}-invalid-feedback`}
style={{
color: errTextColor,
textAlign: direction == 'left' && 'end',
fontSize: '11px',
fontWeight: '400',
lineHeight: '16px',
}}
>
{showValidationError && validationError}

View file

@ -0,0 +1,79 @@
import { validateWidget, validateDates } from '@/_helpers/utils';
export const isRowInValid = (cell, currentState, changeSet) => {
const rowChangeSet = changeSet ? changeSet[cell.row.index] : null;
const key = cell.column.key || cell.column.Header;
const cellValue = rowChangeSet ? rowChangeSet[key] || cell.value : cell.value;
let validationData = {};
if (cell.column.isEditable) {
if (cell.column.columnType === 'number') {
validationData = {
...validateWidget({
validationObject: {
minValue: {
value: cell.column.minValue,
},
maxValue: {
value: cell.column.maxValue,
},
},
widgetValue: cellValue,
currentState,
customResolveObjects: { cellValue },
}),
};
}
if (['string', undefined, 'default', 'text'].includes(cell.column.columnType)) {
validationData = {
...validateWidget({
validationObject: {
regex: {
value: cell.column.regex,
},
minLength: {
value: cell.column.minLength,
},
maxLength: {
value: cell.column.maxLength,
},
customRule: {
value: cell.column.customRule,
},
},
widgetValue: cellValue,
currentState,
customResolveObjects: { cellValue: cellValue },
}),
};
}
}
if (cell.column.columnType === 'datepicker') {
validationData = {
...validateDates({
validationObject: {
minDate: {
value: cell.column.minDate,
},
maxDate: {
value: cell.column.maxDate,
},
minTime: {
value: cell.column.minTime,
},
maxTime: {
value: cell.column.maxTime,
},
parseDateFormat: {
value: cell.column.parseDateFormat,
},
},
widgetValue: cellValue,
currentState,
customResolveObjects: { cellValue },
}),
};
}
return validationData.isValid === false;
};

View file

@ -1,5 +1,5 @@
/* eslint-disable import/no-named-as-default */
import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
import React, { useCallback, useState, useEffect, useRef, useMemo, useContext } from 'react';
import cx from 'classnames';
import { useDrop, useDragLayer } from 'react-dnd';
import { ItemTypes } from './ItemTypes';
@ -17,8 +17,10 @@ import { addComponents, addNewWidgetToTheEditor } from '@/_helpers/appUtils';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { useEditorStore } from '@/_stores/editorStore';
import { useSuperStore } from '@/_stores/superStore';
import { useAppInfo } from '@/_stores/appDataStore';
import { shallow } from 'zustand/shallow';
import { ModuleContext } from '../_contexts/ModuleContext';
import _ from 'lodash';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
@ -42,16 +44,19 @@ export const Container = ({
zoomLevel,
removeComponent,
deviceWindowWidth,
darkMode,
socket,
handleUndo,
handleRedo,
sideBarDebugger,
currentPageId,
darkMode,
}) => {
// Dont update first time to skip
// redundant save on app definition load
const firstUpdate = useRef(true);
const moduleName = useContext(ModuleContext);
const { showComments, currentLayout } = useEditorStore(
(state) => ({
showComments: state?.showComments,
@ -341,7 +346,8 @@ export const Container = ({
let newBoxes = { ...boxes };
for (const selectedComponent of useEditorStore.getState().selectedComponents) {
for (const selectedComponent of useSuperStore.getState().modules[moduleName].useEditorStore.getState()
.selectedComponents) {
newBoxes = produce(newBoxes, (draft) => {
if (draft[selectedComponent.id]) {
const topOffset = draft[selectedComponent.id].layouts[currentLayout].top;

View file

@ -21,14 +21,17 @@ import { withTranslation, useTranslation } from 'react-i18next';
import { camelizeKeys, decamelizeKeys } from 'humps';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { ConfirmDialog } from '@/_components';
import { deepEqual } from '../../_helpers/utils';
import { shallow } from 'zustand/shallow';
import { useDataSourcesStore } from '../../_stores/dataSourcesStore';
import { withRouter } from '@/_hoc/withRouter';
import { useSuperStore } from '@/_stores/superStore';
import { ModuleContext } from '@/_contexts/ModuleContext';
class DataSourceManagerComponent extends React.Component {
static contextType = ModuleContext;
constructor(props) {
super(props);
@ -38,7 +41,6 @@ class DataSourceManagerComponent extends React.Component {
let options = {};
let dataSourceMeta = {};
let datasourceName = '';
if (props.selectedDataSource) {
selectedDataSource = props.selectedDataSource;
options = selectedDataSource.options;
@ -197,7 +199,8 @@ class DataSourceManagerComponent extends React.Component {
const name = selectedDataSource.name;
const kind = selectedDataSource.kind;
const pluginId = selectedDataSourcePluginId;
const appVersionId = useAppVersionStore?.getState()?.editingVersion?.id;
const appVersionId = useSuperStore.getState().modules[this.context].useAppVersionStore?.getState()
?.editingVersion?.id;
const currentEnvironment = this.props.currentEnvironment?.id;
const scope = this.state?.scope || selectedDataSource?.scope;

View file

@ -303,7 +303,7 @@ export const DraggableBox = React.memo(
setDragging(false);
onDragStop(e, id, direction, currentLayout, layoutData);
}}
cancel={`div.table-responsive.jet-data-table, div.calendar-widget, div.text-input, .textarea, .map-widget, .range-slider, .kanban-container, div.real-canvas`}
cancel={`div.table-responsive.jet-data-table, div.calendar-widget, div.text-input, .textarea, .map-widget, .range-slider, .kanban-container, div.real-canvas, .overlay-cell-table`}
onResizeStop={(e, direction, ref, d, position) => {
setResizing(false);
onResizeStop(id, e, direction, ref, d, position);

View file

@ -1,4 +1,4 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import React, { useEffect, useLayoutEffect, useRef, useState, useContext } from 'react';
import {
appService,
authenticationService,
@ -32,6 +32,7 @@ import {
buildComponentMetaDefinition,
} from '@/_helpers/appUtils';
import { Confirm } from './Viewer/Confirm';
// eslint-disable-next-line import/no-unresolved
import { Tooltip as ReactTooltip } from 'react-tooltip';
import CommentNotifications from './CommentNotifications';
import { WidgetManager } from './WidgetManager';
@ -54,7 +55,6 @@ import { ReleasedVersionError } from './AppVersionsManager/ReleasedVersionError'
import { useDataSourcesStore } from '@/_stores/dataSourcesStore';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useAppVersionStore, useAppVersionActions, useAppVersionState } from '@/_stores/appVersionStore';
import { useQueryPanelStore } from '@/_stores/queryPanelStore';
import { useCurrentStateStore, useCurrentState, getCurrentState } from '@/_stores/currentStateStore';
import { computeAppDiff, computeComponentPropertyDiff, isParamFromTableColumn, resetAllStores } from '@/_stores/utils';
import { setCookie } from '@/_helpers/cookie';
@ -64,11 +64,14 @@ import { useMounted } from '@/_hooks/use-mount';
import EditorSelecto from './EditorSelecto';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import useAppDarkMode from '@/_hooks/useAppDarkMode';
import useDebouncedArrowKeyPress from '@/_hooks/useDebouncedArrowKeyPress';
import { getQueryParams } from '@/_helpers/routes';
import RightSidebarTabManager from './RightSidebarTabManager';
import { shallow } from 'zustand/shallow';
import { ModuleContext } from '../_contexts/ModuleContext';
import { useSuperStore } from '../_stores/superStore';
import cx from 'classnames';
setAutoFreeze(false);
enablePatches();
@ -76,6 +79,7 @@ enablePatches();
const decimalToHex = (alpha) => (alpha === 0 ? '00' : Math.round(255 * alpha).toString(16));
const EditorComponent = (props) => {
const moduleName = useContext(ModuleContext);
const { socket } = createWebsocketConnection(props?.params?.id);
const mounted = useMounted();
@ -170,6 +174,7 @@ const EditorComponent = (props) => {
const [showPageDeletionConfirmation, setShowPageDeletionConfirmation] = useState(null);
const [isDeletingPage, setIsDeletingPage] = useState(false);
const { isAppDarkMode, appMode, onAppModeChange } = useAppDarkMode();
const [undoStack, setUndoStack] = useState([]);
const [redoStack, setRedoStack] = useState([]);
@ -214,18 +219,21 @@ const EditorComponent = (props) => {
updateState({
currentUser: appUserDetails,
});
useCurrentStateStore.getState().actions.setCurrentState({
globals: {
...currentState.globals,
theme: { name: props?.darkMode ? 'dark' : 'light' },
urlparams: JSON.parse(JSON.stringify(queryString.parse(props.location.search))),
currentUser: userVars,
/* Constant value.it will only change for viewer */
mode: {
value: 'edit',
useSuperStore
.getState()
.modules[moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
globals: {
...currentState.globals,
theme: { name: props?.darkMode ? 'dark' : 'light' },
urlparams: JSON.parse(JSON.stringify(queryString.parse(props.location.search))),
currentUser: userVars,
/* Constant value.it will only change for viewer */
mode: {
value: 'edit',
},
},
},
});
});
}
});
@ -237,8 +245,9 @@ const EditorComponent = (props) => {
socket && socket?.close();
subscription.unsubscribe();
if (config.ENABLE_MULTIPLAYER_EDITING) props?.provider?.disconnect();
useEditorStore.getState().actions.setIsEditorActive(false);
useSuperStore.getState().modules[moduleName].useEditorStore.getState().actions.setIsEditorActive(false);
prevAppDefinition.current = null;
props.setEditorOrViewer('');
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -255,11 +264,11 @@ const EditorComponent = (props) => {
if (mounted && didAppDefinitionChanged && currentPageId) {
const components = appDefinition?.pages[currentPageId]?.components || {};
computeComponentState(components);
computeComponentState(components, moduleName);
if (appDiffOptions?.skipAutoSave === true) return;
if (useEditorStore.getState().isUpdatingEditorStateInProcess) {
if (useSuperStore.getState().modules[moduleName].useEditorStore.getState().isUpdatingEditorStateInProcess) {
autoSave();
}
}
@ -269,7 +278,7 @@ const EditorComponent = (props) => {
useEffect(
() => {
const components = appDefinition?.pages?.[currentPageId]?.components || {};
computeComponentState(components);
computeComponentState(components, moduleName);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentPageId]
@ -296,7 +305,7 @@ const EditorComponent = (props) => {
useEffect(() => {
if (mounted) {
useCurrentStateStore.getState().actions.setCurrentState({
useSuperStore.getState().modules[moduleName].useCurrentStateStore.getState().actions.setCurrentState({
layout: currentLayout,
});
}
@ -321,12 +330,14 @@ const EditorComponent = (props) => {
const getEditorRef = () => {
const editorRef = {
appDefinition: useEditorStore.getState().appDefinition,
queryConfirmationList: useEditorStore.getState().queryConfirmationList,
appDefinition: useSuperStore.getState().modules[moduleName].useEditorStore.getState().appDefinition,
queryConfirmationList: useSuperStore.getState().modules[moduleName].useEditorStore.getState()
.queryConfirmationList,
updateQueryConfirmationList: updateQueryConfirmationList,
navigate: props.navigate,
switchPage: switchPage,
currentPageId: useEditorStore.getState().currentPageId,
currentPageId: useSuperStore.getState().modules[moduleName].useEditorStore.getState().currentPageId,
moduleName,
};
return editorRef;
};
@ -356,7 +367,7 @@ const EditorComponent = (props) => {
}
});
useCurrentStateStore.getState().actions.setCurrentState({
useSuperStore.getState().modules[moduleName].useCurrentStateStore.getState().actions.setCurrentState({
server: server_variables,
client: client_variables,
});
@ -372,7 +383,7 @@ const EditorComponent = (props) => {
orgConstants[constant.name] = constantValue;
});
useCurrentStateStore.getState().actions.setCurrentState({
useSuperStore.getState().modules[moduleName].useCurrentStateStore.getState().actions.setCurrentState({
constants: orgConstants,
});
});
@ -449,7 +460,7 @@ const EditorComponent = (props) => {
const $componentDidMount = async () => {
window.addEventListener('message', handleMessage);
props.setEditorOrViewer('editor');
await fetchApp(props.params.pageHandle, true);
await fetchApps(0);
await fetchOrgEnvironmentVariables();
@ -471,18 +482,22 @@ const EditorComponent = (props) => {
};
const fetchDataQueries = async (id, selectFirstQuery = false, runQueriesOnAppLoad = false) => {
await useDataQueriesStore
await useSuperStore
.getState()
.actions.fetchDataQueries(id, selectFirstQuery, runQueriesOnAppLoad, getEditorRef());
.modules[moduleName].useDataQueriesStore.getState()
.actions.fetchDataQueries(id, selectFirstQuery, runQueriesOnAppLoad, getEditorRef(), moduleName);
};
const fetchDataSources = (id) => {
useDataSourcesStore.getState().actions.fetchDataSources(id);
useSuperStore.getState().modules[moduleName].useDataSourcesStore.getState().actions.fetchDataSources(id);
};
const fetchGlobalDataSources = () => {
const { current_organization_id: organizationId } = currentUser;
useDataSourcesStore.getState().actions.fetchGlobalDataSources(organizationId);
useSuperStore
.getState()
.modules[moduleName].useDataSourcesStore.getState()
.actions.fetchGlobalDataSources(organizationId);
};
const onVersionDelete = () => {
@ -555,18 +570,21 @@ const EditorComponent = (props) => {
const computeCanvasContainerHeight = () => {
// 45 = (height of header)
// 85 = (the height of the query panel header when minimised) + (height of header)
return `calc(${100}% - ${Math.max(useQueryPanelStore.getState().queryPanelHeight + 45, 85)}px)`;
return `calc(${100}% - ${Math.max(
useSuperStore.getState().modules[moduleName].useQueryPanelStore.getState().queryPanelHeight + 45,
85
)}px)`;
};
const handleQueryPaneDragging = (bool) => setIsQueryPaneDragging(bool);
const handleQueryPaneExpanding = (bool) => setIsQueryPaneExpanded(bool);
const handleOnComponentOptionChanged = (component, optionName, value) => {
return onComponentOptionChanged(component, optionName, value);
return onComponentOptionChanged(moduleName, component, optionName, value);
};
const handleOnComponentOptionsChanged = (component, options) => {
return onComponentOptionsChanged(component, options);
return onComponentOptionsChanged(moduleName, component, options);
};
const handleComponentClick = (id, component) => {
@ -577,21 +595,26 @@ const EditorComponent = (props) => {
const sideBarDebugger = {
error: (data) => {
debuggerActions.error(data);
debuggerActions.error(data, moduleName);
},
flush: () => {
debuggerActions.flush();
debuggerActions.flush(moduleName);
},
generateErrorLogs: (errors) => debuggerActions.generateErrorLogs(errors),
generateErrorLogs: (errors) => debuggerActions.generateErrorLogs(errors, moduleName),
};
const changeDarkMode = (newMode) => {
useCurrentStateStore.getState().actions.setCurrentState({
globals: {
...currentState.globals,
theme: { name: newMode ? 'dark' : 'light' },
},
});
if (appMode === 'auto') {
useSuperStore
.getState()
.modules[moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
globals: {
...currentState.globals,
theme: { name: newMode ? 'dark' : 'light' },
},
});
}
props.switchDarkMode(newMode);
};
@ -606,7 +629,10 @@ const EditorComponent = (props) => {
};
const setSelectedComponent = (id, component, multiSelect = false) => {
const isAlreadySelected = useEditorStore.getState()?.selectedComponents.find((component) => component.id === id);
const isAlreadySelected = useSuperStore
.getState()
.modules[moduleName].useEditorStore.getState()
?.selectedComponents.find((component) => component.id === id);
if (!isAlreadySelected) {
setSelectedComponents([{ id, component }], multiSelect);
@ -614,7 +640,10 @@ const EditorComponent = (props) => {
};
const onVersionRelease = (versionId) => {
useAppVersionStore.getState().actions.updateReleasedVersionId(versionId);
useSuperStore
.getState()
.modules[moduleName].useAppVersionStore.getState()
.actions.updateReleasedVersionId(versionId);
if (socket instanceof WebSocket && socket?.readyState === WebSocket.OPEN) {
socket.send(
@ -627,9 +656,11 @@ const EditorComponent = (props) => {
};
const computeCanvasBackgroundColor = () => {
const { canvasBackgroundColor } = appDefinition?.globalSettings ?? '#edeff5';
const canvasBackgroundColor = appDefinition?.globalSettings?.canvasBackgroundColor
? appDefinition?.globalSettings?.canvasBackgroundColor
: '#edeff5';
if (['#2f3c4c', '#edeff5'].includes(canvasBackgroundColor)) {
return props.darkMode ? '#2f3c4c' : '#edeff5';
return isAppDarkMode ? '#2f3c4c' : '#edeff5';
}
return canvasBackgroundColor;
};
@ -666,9 +697,16 @@ const EditorComponent = (props) => {
const callBack = async (data, startingPageHandle, versionSwitched = false) => {
setWindowTitle({ page: pageTitles.EDITOR, appName: data.name });
useAppVersionStore.getState().actions.updateEditingVersion(data.editing_version);
useSuperStore
.getState()
.modules[moduleName].useAppVersionStore.getState()
.actions.updateEditingVersion(data.editing_version);
if (!releasedVersionId || !versionSwitched) {
useAppVersionStore.getState().actions.updateReleasedVersionId(data.current_version_id);
useSuperStore
.getState()
.modules[moduleName].useAppVersionStore.getState()
.actions.updateReleasedVersionId(data.current_version_id);
}
const appVersions = await appEnvironmentService.getVersionsByEnvironment(data?.id);
@ -704,8 +742,9 @@ const EditorComponent = (props) => {
};
setCurrentPageId(homePageId);
onAppModeChange(appJson?.globalSettings?.appMode);
useCurrentStateStore.getState().actions.setCurrentState({
useSuperStore.getState().modules[moduleName].useCurrentStateStore.getState().actions.setCurrentState({
page: currentpageData,
});
@ -723,7 +762,10 @@ const EditorComponent = (props) => {
});
}
await useDataSourcesStore.getState().actions.fetchGlobalDataSources(data?.organization_id);
await useSuperStore
.getState()
.modules[moduleName].useDataSourcesStore.getState()
.actions.fetchGlobalDataSources(data?.organization_id);
await fetchDataSources(data.editing_version?.id);
await fetchDataQueries(data.editing_version?.id, true, true);
const currentPageEvents = data.events.filter((event) => event.target === 'page' && event.sourceId === homePageId);
@ -779,11 +821,13 @@ const EditorComponent = (props) => {
});
}
let updatedAppDefinition;
const copyOfAppDefinition = JSON.parse(JSON.stringify(useEditorStore.getState().appDefinition));
const copyOfAppDefinition = JSON.parse(
JSON.stringify(useSuperStore.getState().modules[moduleName].useEditorStore.getState().appDefinition)
);
if (opts?.skipYmapUpdate && opts?.currentSessionId !== currentSessionId) {
updatedAppDefinition = produce(copyOfAppDefinition, (draft) => {
const _currentPageId = useEditorStore.getState().currentPageId;
const _currentPageId = useSuperStore.getState().modules[moduleName].useEditorStore.getState().currentPageId;
if (opts?.componentDeleting) {
const currentPageComponentIds = Object.keys(copyOfAppDefinition.pages[_currentPageId]?.components);
const newComponentIds = Object.keys(newDefinition.pages[_currentPageId]?.components);
@ -921,7 +965,7 @@ const EditorComponent = (props) => {
};
const saveEditingVersion = (isUserSwitchedVersion = false) => {
const editingVersion = useAppVersionStore.getState().editingVersion;
const editingVersion = useSuperStore.getState().modules[moduleName].useAppVersionStore.getState().editingVersion;
if (isVersionReleased && !isUserSwitchedVersion) {
updateEditorState({
isUpdatingEditorStateInProcess: false,
@ -951,7 +995,10 @@ const EditorComponent = (props) => {
...editingVersion,
...{ definition: appDefinition },
};
useAppVersionStore.getState().actions.updateEditingVersion(_editingVersion);
useSuperStore
.getState()
.modules[moduleName].useAppVersionStore.getState()
.actions.updateEditingVersion(_editingVersion);
if (config.ENABLE_MULTIPLAYER_EDITING) {
props.ymap?.set('appDef', {
@ -1124,7 +1171,10 @@ const EditorComponent = (props) => {
const componentDefinitionChanged = (componentDefinition, props) => {
if (isVersionReleased) {
useAppVersionStore.getState().actions.enableReleasedVersionPopupState();
useSuperStore
.getState()
.modules[moduleName].useAppVersionStore.getState()
.actions.enableReleasedVersionPopupState();
return;
}
@ -1182,7 +1232,10 @@ const EditorComponent = (props) => {
componentDeleted: true,
});
} else {
useAppVersionStore.getState().actions.enableReleasedVersionPopupState();
useSuperStore
.getState()
.modules[moduleName].useAppVersionStore.getState()
.actions.enableReleasedVersionPopupState();
}
};
@ -1190,7 +1243,9 @@ const EditorComponent = (props) => {
const gridWidth = (1 * 100) / 43; // width of the canvas grid in percentage
const _appDefinition = _.cloneDeep(appDefinition);
let newComponents = _appDefinition?.pages[currentPageId].components;
const selectedComponents = useEditorStore.getState()?.selectedComponents;
const selectedComponents = useSuperStore
.getState()
.modules[moduleName].useEditorStore.getState()?.selectedComponents;
for (const selectedComponent of selectedComponents) {
let top = newComponents[selectedComponent.id].layouts[currentLayout].top;
@ -1222,7 +1277,7 @@ const EditorComponent = (props) => {
const copyComponents = () =>
cloneComponents(
useEditorStore.getState()?.selectedComponents,
useSuperStore.getState().modules[moduleName].useEditorStore.getState()?.selectedComponents,
appDefinition,
currentPageId,
appDefinitionChanged,
@ -1231,13 +1286,16 @@ const EditorComponent = (props) => {
const cutComponents = () => {
if (isVersionReleased) {
useAppVersionStore.getState().actions.enableReleasedVersionPopupState();
useSuperStore
.getState()
.modules[moduleName].useAppVersionStore.getState()
.actions.enableReleasedVersionPopupState();
return;
}
cloneComponents(
useEditorStore.getState()?.selectedComponents,
useSuperStore.getState().modules[moduleName].useEditorStore.getState()?.selectedComponents,
appDefinition,
currentPageId,
appDefinitionChanged,
@ -1248,11 +1306,14 @@ const EditorComponent = (props) => {
const cloningComponents = () => {
if (isVersionReleased) {
useAppVersionStore.getState().actions.enableReleasedVersionPopupState();
useSuperStore
.getState()
.modules[moduleName].useAppVersionStore.getState()
.actions.enableReleasedVersionPopupState();
return;
}
cloneComponents(
useEditorStore.getState()?.selectedComponents,
useSuperStore.getState().modules[moduleName].useEditorStore.getState()?.selectedComponents,
appDefinition,
currentPageId,
appDefinitionChanged,
@ -1262,7 +1323,7 @@ const EditorComponent = (props) => {
};
const handleEditorEscapeKeyPress = () => {
if (useEditorStore.getState()?.selectedComponents?.length > 0) {
if (useSuperStore.getState().modules[moduleName].useEditorStore.getState()?.selectedComponents?.length > 0) {
updateEditorState({
selectedComponents: [],
});
@ -1270,7 +1331,9 @@ const EditorComponent = (props) => {
};
const removeComponents = () => {
const selectedComponents = useEditorStore.getState()?.selectedComponents;
const selectedComponents = useSuperStore
.getState()
.modules[moduleName].useEditorStore.getState()?.selectedComponents;
if (!isVersionReleased && selectedComponents?.length > 1) {
let newDefinition = cloneDeep(appDefinition);
@ -1286,7 +1349,10 @@ const EditorComponent = (props) => {
});
}
} else if (isVersionReleased) {
useAppVersionStore.getState().actions.enableReleasedVersionPopupState();
useSuperStore
.getState()
.modules[moduleName].useAppVersionStore.getState()
.actions.enableReleasedVersionPopupState();
}
};
@ -1369,11 +1435,14 @@ const EditorComponent = (props) => {
const globals = {
...currentState.globals,
};
useCurrentStateStore.getState().actions.setCurrentState({ globals, page });
useSuperStore
.getState()
.modules[moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({ globals, page });
};
const navigateToPage = (queryParams = [], handle) => {
const appId = useAppDataStore.getState()?.appId;
const appId = useSuperStore.getState().modules[moduleName].useAppDataStore.getState()?.appId;
const queryParamsString = queryParams.map(([key, value]) => `${key}=${value}`).join('&');
props?.navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${handle}?${queryParamsString}`, {
@ -1385,9 +1454,9 @@ const EditorComponent = (props) => {
const switchPage = (pageId, queryParams = []) => {
// This are fetched from store to handle runQueriesOnAppLoad
const currentPageId = useEditorStore.getState().currentPageId;
const appDefinition = useEditorStore.getState().appDefinition;
const pageHandle = getCurrentState().pageHandle;
const currentPageId = useSuperStore.getState().modules[moduleName].useEditorStore.getState().currentPageId;
const appDefinition = useSuperStore.getState().modules[moduleName].useEditorStore.getState().appDefinition;
const pageHandle = getCurrentState(moduleName).pageHandle;
if (currentPageId === pageId && pageHandle === appDefinition?.pages[pageId]?.handle) {
return;
@ -1410,7 +1479,10 @@ const EditorComponent = (props) => {
...currentState.globals,
urlparams: JSON.parse(JSON.stringify(queryString.parse(queryParamsString))),
};
useCurrentStateStore.getState().actions.setCurrentState({ globals, page });
useSuperStore
.getState()
.modules[moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({ globals, page });
setCurrentPageId(pageId);
@ -1639,7 +1711,7 @@ const EditorComponent = (props) => {
const handleCanvasContainerMouseUp = (e) => {
if (
['real-canvas', 'modal'].includes(e.target.className) &&
useEditorStore.getState()?.selectedComponents?.length
useSuperStore.getState().modules[moduleName].useEditorStore.getState()?.selectedComponents?.length
) {
setSelectedComponents(EMPTY_ARRAY);
}
@ -1649,7 +1721,7 @@ const EditorComponent = (props) => {
if (isLoading) {
return (
<div className="apploader">
<div className={cx('apploader', { 'dark-theme theme-dark': props.darkMode })}>
<div className="col col-* editor-center-wrapper">
<div className="editor-center">
<div className="canvas">
@ -1673,7 +1745,7 @@ const EditorComponent = (props) => {
);
}
return (
<div className="editor wrapper">
<div className={`editor wrapper`}>
<Confirm
show={queryConfirmationList?.length > 0}
message={`Do you want to run this query - ${queryConfirmationList[0]?.queryName}?`}
@ -1767,14 +1839,18 @@ const EditorComponent = (props) => {
id="main-editor-canvas"
>
<div
className={`canvas-container align-items-center ${!showLeftSidebar && 'hide-sidebar'}`}
className={cx(
'canvas-container align-items-center',
{ 'dark-theme theme-dark': isAppDarkMode },
{ 'hide-sidebar': !showLeftSidebar }
)}
style={{
transform: `scale(${zoomLevel})`,
borderLeft:
(editorMarginLeft ? editorMarginLeft - 1 : editorMarginLeft) +
`px solid ${computeCanvasBackgroundColor()}`,
height: computeCanvasContainerHeight(),
background: !props.darkMode ? '#EBEBEF' : '#2E3035',
background: !isAppDarkMode ? '#EBEBEF' : '#2E3035',
}}
onMouseUp={handleCanvasContainerMouseUp}
ref={canvasContainerRef}
@ -1821,14 +1897,14 @@ const EditorComponent = (props) => {
</div>
)}
{defaultComponentStateComputed && (
<>
<div>
<Container
canvasWidth={getCanvasWidth()}
socket={socket}
appDefinition={appDefinition}
appDefinitionChanged={appDefinitionChanged}
snapToGrid={true}
darkMode={props.darkMode}
darkMode={isAppDarkMode}
mode={'edit'}
zoomLevel={zoomLevel}
deviceWindowWidth={deviceWindowWidth}
@ -1849,7 +1925,7 @@ const EditorComponent = (props) => {
canvasWidth={getCanvasWidth()}
onDragging={(isDragging) => setIsDragging(isDragging)}
/>
</>
</div>
)}
</div>
</div>
@ -1868,7 +1944,7 @@ const EditorComponent = (props) => {
/>
<ReactTooltip id="tooltip-for-add-query" className="tooltip" />
</div>
<div className="editor-sidebar">
<div className={cx('editor-sidebar', { 'dark-theme theme-dark': props.darkMode })}>
<EditorKeyHooks
moveComponents={moveComponents}
cloneComponents={cloningComponents}
@ -1898,7 +1974,9 @@ const EditorComponent = (props) => {
/>
</div>
{config.COMMENT_FEATURE_ENABLE && showComments && (
<CommentNotifications socket={socket} pageId={currentPageId} />
<div className={cx({ 'dark-theme theme-dark': props.darkMode })}>
<CommentNotifications socket={socket} pageId={currentPageId} />
</div>
)}
</div>
</DndProvider>

View file

@ -1,7 +1,9 @@
import React, { useCallback, memo } from 'react';
import React, { useCallback, memo, useContext } from 'react';
import Selecto from 'react-selecto';
import { useEditorStore, EMPTY_ARRAY } from '@/_stores/editorStore';
import { shallow } from 'zustand/shallow';
import { useSuperStore } from '../_stores/superStore';
import { ModuleContext } from '../_contexts/ModuleContext';
const EditorSelecto = ({
selectionRef,
@ -20,11 +22,19 @@ const EditorSelecto = ({
shallow
);
const moduleName = useContext(ModuleContext);
const onAreaSelectionStart = useCallback(
(e) => {
const isMultiSelect = e.inputEvent.shiftKey || useEditorStore.getState().selectedComponents.length > 0;
const isMultiSelect =
e.inputEvent.shiftKey ||
useSuperStore.getState().modules[moduleName].useEditorStore.getState().selectedComponents.length > 0;
setSelectionInProgress(true);
setSelectedComponents([...(isMultiSelect ? useEditorStore.getState().selectedComponents : EMPTY_ARRAY)]);
setSelectedComponents([
...(isMultiSelect
? useSuperStore.getState().modules[moduleName].useEditorStore.getState().selectedComponents
: EMPTY_ARRAY),
]);
},
[setSelectionInProgress, setSelectedComponents]
);
@ -33,7 +43,7 @@ const EditorSelecto = ({
e.added.forEach((el) => {
el.classList.add('resizer-select');
});
if (useEditorStore.getState().selectionInProgress) {
if (useSuperStore.getState().modules[moduleName].useEditorStore.getState().selectionInProgress) {
e.removed.forEach((el) => {
el.classList.remove('resizer-select');
});
@ -68,7 +78,8 @@ const EditorSelecto = ({
(e) => {
if (selectionDragRef.current) {
e.stop();
useEditorStore.getState().selectionInProgress && setSelectionInProgress(false);
useSuperStore.getState().modules[moduleName].useEditorStore.getState().selectionInProgress &&
setSelectionInProgress(false);
}
},
[setSelectionInProgress, selectionDragRef]
@ -76,7 +87,8 @@ const EditorSelecto = ({
const onAreaSelectionDragEnd = () => {
selectionDragRef.current = false;
useEditorStore.getState().selectionInProgress && setSelectionInProgress(false);
useSuperStore.getState().modules[moduleName].useEditorStore.getState().selectionInProgress &&
setSelectionInProgress(false);
};
return (

View file

@ -0,0 +1,56 @@
import React, { useContext } from 'react';
import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup';
import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
import { useTranslation } from 'react-i18next';
import useAppDarkMode from '@/_hooks/useAppDarkMode';
import { useSuperStore } from '@/_stores/superStore';
import { ModuleContext } from '@/_contexts/ModuleContext';
// import { ModuleContext } from '../_contexts/ModuleContext';
const APP_MODES = [
{ label: 'Auto', value: 'auto' },
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
];
const AppModeToggle = ({ globalSettingsChanged }) => {
const moduleName = useContext(ModuleContext);
const { onAppModeChange, appMode } = useAppDarkMode();
const { t } = useTranslation();
return (
<div className="d-flex align-items-center mb-3">
<span data-cy={`label-maintenance-mode`}>{t('leftSidebar.Settings.appMode', 'App mode')}</span>
<div className="ms-auto position-relative app-mode-switch" style={{ paddingLeft: '0px', width: '158px' }}>
<ToggleGroup
onValueChange={(value) => {
onAppModeChange(value);
let exposedTheme = value;
if (value === 'auto') {
exposedTheme = localStorage.getItem('darkMode') === 'true' ? 'dark' : 'light';
}
useSuperStore
.getState()
.modules[moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
globals: {
...useSuperStore.getState().modules[moduleName].useCurrentStateStore.getState().globals,
theme: { name: exposedTheme },
},
});
globalSettingsChanged({ appMode: value });
}}
defaultValue={appMode}
>
{APP_MODES.map((appMode) => (
<ToggleGroupItem key={appMode.value} value={appMode.value}>
{appMode.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
</div>
);
};
export default AppModeToggle;

View file

@ -15,6 +15,7 @@ import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { useAppDataActions, useAppInfo } from '@/_stores/appDataStore';
import AppModeToggle from './AppModeToggle';
export const GlobalSettings = ({
globalSettings,
@ -42,7 +43,6 @@ export const GlobalSettings = ({
}),
shallow
);
const { app, slug: oldSlug } = useAppInfo();
const coverStyles = {
@ -148,7 +148,7 @@ export const GlobalSettings = ({
/>
)}
<div id="" className={cx({ 'dark-theme': darkMode })}>
<div bsPrefix="global-settings-popover">
<div bsPrefix="global-settings-popover" className="global-settings-panel">
<HeaderSection darkMode={darkMode}>
<HeaderSection.PanelHeader title="Global settings" />
</HeaderSection>
@ -380,7 +380,7 @@ export const GlobalSettings = ({
</div>
</div>
</div>
<AppModeToggle globalSettingsChanged={globalSettingsChanged} />
<div className="d-flex align-items-center global-popover-div-wrap mb-3">
<p className="tj-text-xsm color-slate12 w-full m-auto">Export app</p>
<div>

View file

@ -13,8 +13,8 @@ function HeaderActions({
showToggleLayoutBtn,
showUndoRedoBtn,
showFullWidth,
darkMode,
}) {
const darkMode = localStorage.getItem('darkMode') === 'true';
const { currentLayout, toggleCurrentLayout } = useEditorStore(
(state) => ({
currentLayout: state.currentLayout,

View file

@ -81,7 +81,7 @@ export default function EditorHeader({
const shouldRenderReleaseButton = !!app?.id;
return (
<div className="header" style={{ width: '100%' }}>
<div className={cx('header', { 'dark-theme theme-dark': darkMode })} style={{ width: '100%' }}>
<header className="navbar navbar-expand-md d-print-none">
<div className="container-xl header-container">
<div className="d-flex w-100">
@ -116,6 +116,7 @@ export default function EditorHeader({
handleRedo={handleRedo}
showToggleLayoutBtn
showUndoRedoBtn
darkMode={darkMode}
/>
<div className="d-flex align-items-center">
<div style={{ width: '100px', marginRight: '20px' }}>
@ -153,6 +154,7 @@ export default function EditorHeader({
setAppDefinitionFromVersion={setAppDefinitionFromVersion}
onVersionDelete={onVersionDelete}
isPublic={isPublic ?? false}
darkMode={darkMode}
/>
)}
</div>

View file

@ -86,7 +86,7 @@ export const baseComponentProperties = (
'Additional Actions': Object.keys(AllComponents).filter(
(component) => !SHOW_ADDITIONAL_ACTIONS.includes(component)
),
General: ['Modal', 'TextInput', 'PasswordInput', 'NumberInput', 'Text'],
General: ['Modal', 'TextInput', 'PasswordInput', 'NumberInput', 'Text', 'Table'],
Layout: [],
};
if (component.component.component === 'Listview') {

View file

@ -0,0 +1,109 @@
import React, { useState } from 'react';
import Popover from 'react-bootstrap/Popover';
import { StylesTabElements } from './StylesTabElements';
import { PropertiesTabElements } from './PropertiesTabElements';
export const ColumnPopoverContent = ({
column,
index,
darkMode,
currentState,
onColumnItemChange,
getPopoverFieldSource,
setColumnPopoverRootCloseBlocker,
component,
props,
columnEventChanged,
handleEventManagerPopoverCallback,
}) => {
const [activeTab, setActiveTab] = useState('propertiesTab');
const timeZoneOptions = [
{ name: 'UTC', value: 'Etc/UTC' },
{ name: '-12:00', value: 'Etc/GMT+12' },
{ name: '-11:00', value: 'Etc/GMT+11' },
{ name: '-10:00', value: 'Pacific/Honolulu' },
{ name: '-09:00', value: 'America/Anchorage' },
{ name: '-08:00', value: 'America/Santa_Isabel' },
{ name: '-07:00', value: 'America/Chihuahua' },
{ name: '-06:00', value: 'America/Guatemala' },
{ name: '-05:00', value: 'America/Bogota' },
{ name: '-04:00', value: 'America/Halifax' },
{ name: '-03:30', value: 'America/St_Johns' },
{ name: '-03:00', value: 'America/Sao_Paulo' },
{ name: '-02:00', value: 'Etc/GMT+2' },
{ name: '-01:00', value: 'Atlantic/Cape_Verde' },
{ name: '+00:00', value: 'UTC' },
{ name: '+01:00', value: 'Europe/Berlin' },
{ name: '+02:00', value: 'Africa/Gaborone' },
{ name: '+03:00', value: 'Asia/Baghdad' },
{ name: '+04:00', value: 'Asia/Muscat' },
{ name: '+04:30', value: 'Asia/Kabul' },
{ name: '+05:00', value: 'Asia/Tashkent' },
{ name: '+05:30', value: 'Asia/Colombo' },
{ name: '+05:45', value: 'Asia/Kathmandu' },
{ name: '+06:00', value: 'Asia/Almaty' },
{ name: '+06:30', value: 'Asia/Yangon' },
{ name: '+07:00', value: 'Asia/Bangkok' },
{ name: '+08:00', value: 'Asia/Makassar' },
{ name: '+09:00', value: 'Asia/Seoul' },
{ name: '+09:30', value: 'Australia/Darwin' },
{ name: '+10:00', value: 'Pacific/Chuuk' },
{ name: '+11:00', value: 'Pacific/Pohnpei' },
{ name: '+12:00', value: 'Etc/GMT-12' },
{ name: '+13:00', value: 'Pacific/Auckland' },
];
return (
<>
<Popover.Header>
<div className="d-flex custom-gap-4 align-self-stretch tj-text tj-text-xsm font-weight-500 text-secondary cursor-pointer">
<div
className={`${activeTab === 'propertiesTab' && 'active-column-tab'} column-header-tab`}
onClick={() => {
if (activeTab !== 'propertiesTab') setActiveTab('propertiesTab');
}}
>
Properties
</div>
<div
className={`${activeTab === 'stylesTab' && 'active-column-tab'} column-header-tab`}
onClick={() => {
if (activeTab !== 'stylesTab') setActiveTab('stylesTab');
}}
>
Styles
</div>
</div>
</Popover.Header>
<Popover.Body className={`table-column-popover ${darkMode && 'theme-dark'}`}>
{activeTab === 'propertiesTab' ? (
<PropertiesTabElements
column={column}
index={index}
darkMode={darkMode}
currentState={currentState}
onColumnItemChange={onColumnItemChange}
getPopoverFieldSource={getPopoverFieldSource}
setColumnPopoverRootCloseBlocker={setColumnPopoverRootCloseBlocker}
component={component}
props={props}
columnEventChanged={columnEventChanged}
timeZoneOptions={timeZoneOptions}
handleEventManagerPopoverCallback={handleEventManagerPopoverCallback}
/>
) : (
<StylesTabElements
column={column}
index={index}
darkMode={darkMode}
currentState={currentState}
onColumnItemChange={onColumnItemChange}
getPopoverFieldSource={getPopoverFieldSource}
component={component}
/>
)}
</Popover.Body>
</>
);
};

View file

@ -0,0 +1,389 @@
import React, { useState } from 'react';
import { ProgramaticallyHandleProperties } from '../ProgramaticallyHandleProperties';
import Select from '@/_ui/Select';
import { useTranslation } from 'react-i18next';
import Accordion from '@/_ui/Accordion';
import { resolveReferences } from '@/_helpers/utils';
import styles from '@/_ui/Select/styles';
import FxButton from '../../../../CodeBuilder/Elements/FxButton';
import { CodeHinter } from '../../../../CodeBuilder/CodeHinter';
const TIMEZONE_OPTIONS = [
{ name: 'UTC', value: 'Etc/UTC' },
{ name: '-12:00', value: 'Etc/GMT+12' },
{ name: '-11:00', value: 'Etc/GMT+11' },
{ name: '-10:00', value: 'Pacific/Honolulu' },
{ name: '-09:00', value: 'America/Anchorage' },
{ name: '-08:00', value: 'America/Santa_Isabel' },
{ name: '-07:00', value: 'America/Chihuahua' },
{ name: '-06:00', value: 'America/Guatemala' },
{ name: '-05:00', value: 'America/Bogota' },
{ name: '-04:00', value: 'America/Halifax' },
{ name: '-03:30', value: 'America/St_Johns' },
{ name: '-03:00', value: 'America/Sao_Paulo' },
{ name: '-02:00', value: 'Etc/GMT+2' },
{ name: '-01:00', value: 'Atlantic/Cape_Verde' },
{ name: '+00:00', value: 'UTC' },
{ name: '+01:00', value: 'Europe/Berlin' },
{ name: '+02:00', value: 'Africa/Gaborone' },
{ name: '+03:00', value: 'Asia/Baghdad' },
{ name: '+04:00', value: 'Asia/Muscat' },
{ name: '+04:30', value: 'Asia/Kabul' },
{ name: '+05:00', value: 'Asia/Tashkent' },
{ name: '+05:30', value: 'Asia/Colombo' },
{ name: '+05:45', value: 'Asia/Kathmandu' },
{ name: '+06:00', value: 'Asia/Almaty' },
{ name: '+06:30', value: 'Asia/Yangon' },
{ name: '+07:00', value: 'Asia/Bangkok' },
{ name: '+08:00', value: 'Asia/Makassar' },
{ name: '+09:00', value: 'Asia/Seoul' },
{ name: '+09:30', value: 'Australia/Darwin' },
{ name: '+10:00', value: 'Pacific/Chuuk' },
{ name: '+11:00', value: 'Pacific/Pohnpei' },
{ name: '+12:00', value: 'Etc/GMT-12' },
{ name: '+13:00', value: 'Pacific/Auckland' },
];
const DATE_FORMAT_OPTIONS = [
{
label: 'DD/MM/YYYY',
value: 'DD/MM/YYYY',
},
{
label: 'MM/DD/YYYY',
value: 'MM/DD/YYYY',
},
{
label: 'YYYY/DD/MM',
value: 'YYYY/DD/MM',
},
{
label: 'YYYY/MM/DD',
value: 'YYYY/MM/DD',
},
];
const UNIX_TIMESTAMP_OPTIONS = [
{
label: 's',
value: 'seconds',
},
{
label: 'ms',
value: 'milliseconds',
},
];
const DatepickerProperties = ({ column, index, darkMode, currentState, onColumnItemChange, component }) => {
const { t } = useTranslation();
const items = [];
const [isDateDisplayFormatFxOn, setIsDateDisplayFormatFxOn] = useState(
!column?.notActiveFxActiveFields?.includes('dateFormat') ?? true
);
const [isParseDateFormatFxOn, setIsParseDateFormatFxOn] = useState(
!column?.notActiveFxActiveFields?.includes('parseDateFormat') ?? true
);
items.push(
{
title: 'Date format',
className: 'table-date-picker-formatting-accordion',
children: (
<>
<div className="grey-bg-section mb-2 border">
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px', borderRadius: '6px' }}>
<ProgramaticallyHandleProperties
label="Enable date"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="isDateSelectionEnabled"
props={column}
component={component}
paramType="properties"
paramMeta={{ type: 'toggle', displayName: 'Enable date' }}
/>
</div>
{resolveReferences(column?.isDateSelectionEnabled, currentState) && (
<div
data-cy={`input-date-display-format`}
className="field mb-2 w-100"
onClick={(e) => e.stopPropagation()}
>
<div style={{ padding: '0px 12px' }}>
<div className="d-flex justify-content-between">
<label data-cy={`label-date-display-format`} className="form-label">
{t('widget.Table.dateDisplayformat', 'Date format')}
</label>
<span>
<FxButton
active={isDateDisplayFormatFxOn}
onPress={() => {
let resultFxActiveFields = column?.notActiveFxActiveFields || [];
if (isDateDisplayFormatFxOn) {
resultFxActiveFields.push('dateFormat');
} else {
resultFxActiveFields = resultFxActiveFields.filter((field) => field !== 'dateFormat');
}
setIsDateDisplayFormatFxOn(!isDateDisplayFormatFxOn);
onColumnItemChange(index, 'notActiveFxActiveFields', resultFxActiveFields);
}}
/>
</span>
</div>
{isDateDisplayFormatFxOn ? (
<CodeHinter
initialValue={column?.dateFormat}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
// placeholder={validation?.placeholder ?? ''}
onChange={(value) => onColumnItemChange(index, 'dateFormat', value)}
/>
) : (
<Select
options={DATE_FORMAT_OPTIONS}
value={column?.dateFormat ?? 'DD/MM/YYYY'}
search={true}
closeOnSelect={true}
onChange={(value) => {
onColumnItemChange(index, 'dateFormat', value);
}}
fuzzySearch
placeholder="Select.."
useCustomStyles={true}
styles={styles(darkMode, '100%')}
/>
)}
</div>
</div>
)}
</div>
<div className="grey-bg-section mb-2 border">
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px', borderRadius: '6px' }}>
<ProgramaticallyHandleProperties
label="Enable time"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="isTimeChecked"
props={column}
component={component}
paramType="properties"
paramMeta={{ type: 'toggle', displayName: 'Enable time' }}
/>
</div>
{resolveReferences(column?.isTimeChecked, currentState) && (
<>
{!isDateDisplayFormatFxOn && (
<div className="field mb-2" onClick={(e) => e.stopPropagation()} style={{ padding: '0px 12px' }}>
<label className="form-label">{t('widget.Table.timeFormat', 'Time Format')}</label>
<Select
options={[
{
label: 'HH:mm',
value: 'HH:mm',
},
]}
value={column?.timeFormat ?? 'HH:mm'}
search={true}
closeOnSelect={true}
onChange={(value) => {
onColumnItemChange(index, 'timeFormat', value);
}}
fuzzySearch
placeholder="Select.."
useCustomStyles={true}
styles={styles(darkMode, '100%')}
/>
</div>
)}
<div style={{ padding: '0px 12px' }}>
<ProgramaticallyHandleProperties
label="Enable 24 hr time format"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="isTwentyFourHrFormatEnabled"
props={column}
component={component}
paramType="properties"
paramMeta={{ type: 'toggle', displayName: 'Enable 24 hr time format' }}
/>
</div>
<div
data-cy={`input-display-time-zone`}
className="field mb-2"
onClick={(e) => e.stopPropagation()}
style={{ padding: '0px 12px' }}
>
<label data-cy={`label-display-time-zone`} className="form-label">
Time zone
</label>
<Select
options={TIMEZONE_OPTIONS}
value={column?.timeZoneDisplay ?? ''}
search={true}
closeOnSelect={true}
onChange={(value) => {
onColumnItemChange(index, 'timeZoneDisplay', value);
}}
fuzzySearch
placeholder="Select.."
useCustomStyles={true}
styles={styles(darkMode, '100%')}
/>
</div>
</>
)}
</div>
</>
),
},
{
title: 'Parse format',
children: (
<>
<ProgramaticallyHandleProperties
label="Parse in unix timestamp"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="parseInUnixTimestamp"
props={column}
component={component}
paramType="properties"
paramMeta={{ type: 'toggle', displayName: 'Parse in unix timestamp' }}
/>
{resolveReferences(column?.parseInUnixTimestamp, currentState) ? (
<div className="mt-2">
<div className="field mb-2 tj-app-input">
<label data-cy={`label-date-parse-format`} className="form-label">
{t('widget.Table.unixTimestamp', 'Unix timestamp')}
</label>
<Select
options={UNIX_TIMESTAMP_OPTIONS}
value={column?.unixTimestamp ?? 'seconds'}
search={true}
closeOnSelect={true}
onChange={(value) => {
onColumnItemChange(index, 'unixTimestamp', value);
}}
fuzzySearch
// placeholder="Select.."
useCustomStyles={true}
styles={styles(darkMode, '100%')}
/>
</div>
</div>
) : (
<div className="mt-2">
{resolveReferences(column?.isDateSelectionEnabled, currentState) && (
<div data-cy={`input-parse-timezone`} className="field mb-2">
<div className="d-flex justify-content-between">
<label data-cy={`label-parse-timezone`} className="form-label">
Date
</label>
<span>
<FxButton
active={isParseDateFormatFxOn}
onPress={() => {
let resultFxActiveFields = column?.notActiveFxActiveFields || [];
if (isDateDisplayFormatFxOn) {
resultFxActiveFields.push('parseDateFormat');
} else {
resultFxActiveFields = resultFxActiveFields.filter((field) => field !== 'parseDateFormat');
}
setIsParseDateFormatFxOn(!isParseDateFormatFxOn);
onColumnItemChange(index, 'notActiveFxActiveFields', resultFxActiveFields);
}}
/>
</span>
</div>
{isParseDateFormatFxOn ? (
<CodeHinter
initialValue={column?.parseDateFormat}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
// placeholder={validation?.placeholder ?? ''}
onChange={(value) => onColumnItemChange(index, 'parseDateFormat', value)}
/>
) : (
<Select
options={DATE_FORMAT_OPTIONS}
value={column?.parseDateFormat ?? 'DD/MM/YYYY'}
search={true}
closeOnSelect={true}
onChange={(value) => {
onColumnItemChange(index, 'parseDateFormat', value);
}}
fuzzySearch
placeholder="Select.."
useCustomStyles={true}
styles={styles(darkMode, '100%')}
/>
)}
</div>
)}
{resolveReferences(column?.isTimeChecked, currentState) && (
<>
{!isParseDateFormatFxOn && (
<div className="field mb-2" onClick={(e) => e.stopPropagation()}>
<label className="form-label">{t('widget.Table.timeFormat', 'Time')}</label>
<Select
options={[
{
label: 'HH:mm',
value: 'HH:mm',
},
]}
value={column?.parseTimeFormat ?? 'HH:mm'}
search={true}
closeOnSelect={true}
onChange={(value) => {
onColumnItemChange(index, 'parseTimeFormat', value);
}}
fuzzySearch
placeholder="Select.."
useCustomStyles={true}
styles={styles(darkMode, '100%')}
/>
</div>
)}
<div data-cy={`input-parse-timezone`} className="field mb-2">
<label data-cy={`label-parse-timezone`} className="form-label">
Time zone
</label>
<Select
options={TIMEZONE_OPTIONS}
value={column?.timeZoneValue ?? ''}
search={true}
closeOnSelect={true}
onChange={(value) => {
onColumnItemChange(index, 'timeZoneValue', value);
}}
fuzzySearch
placeholder="Select.."
useCustomStyles={true}
styles={styles(darkMode, '100%')}
/>
</div>
</>
)}
</div>
)}
</>
),
}
);
return <Accordion items={items} className="table-column-date-picker-accordion" />;
};
export default DatepickerProperties;

View file

@ -0,0 +1,60 @@
import React from 'react';
import Icon from '@/_ui/Icon/solidIcons/index';
import { ToolTip } from '@/_components/ToolTip';
export const DEPRECATED_COLUMN_TYPES = [
{ label: 'Default', value: 'default', docLink: '', currentAlternativeColumnLabel: 'String' },
{ label: 'Badge', value: 'badge', currentAlternativeColumnLabel: 'Multiselect' },
{ label: 'Dropdown', value: 'dropdown', currentAlternativeColumnLabel: 'Single select' },
{ label: 'Multiple badges', value: 'badges', currentAlternativeColumnLabel: 'Multiselect' },
{ label: 'Tags', value: 'tags', currentAlternativeColumnLabel: 'Multiselect' },
{ label: 'Radio', value: 'radio', currentAlternativeColumnLabel: 'Single select' },
{ label: 'Multiselect', value: 'multiselect', currentAlternativeColumnLabel: 'Multiselect' },
{ label: 'Toggle switch', value: 'toggle', currentAlternativeColumnLabel: 'Single select' },
];
export const checkIfTableColumnDeprecated = (columnType) => {
return DEPRECATED_COLUMN_TYPES.some((ct) => ct.value === columnType);
};
export const TooltipBody = ({ columnLabel }) => {
return (
<div style={{ padding: '8px 4px', textAlign: 'left' }}>
<div className="font-weight-bold mb-2">Deprecating column type</div>
<div>{`This column type is deprecated and will be removed in a future update. We recommend using the new ${columnLabel} when creating applications moving forward.`}</div>
</div>
);
};
export const DeprecatedColumnTooltip = ({ columnType, children }) => {
const deprecatedColumnType = DEPRECATED_COLUMN_TYPES.find((ct) => ct.value === columnType);
return (
<ToolTip
message={<TooltipBody columnLabel={deprecatedColumnType?.currentAlternativeColumnLabel} />}
show={deprecatedColumnType ?? false}
placement="left"
>
{children}
</ToolTip>
);
};
const DeprecatedColumnTypeMsg = ({ columnType }) => {
const deprecatedColumnType = DEPRECATED_COLUMN_TYPES.find((ct) => ct.value === columnType);
if (!deprecatedColumnType) return null;
return (
<div
className="d-flex mx-3"
style={{ padding: '12px 16px', gap: '6px', backgroundColor: '#FCEEEF', borderRadius: '6px' }}
>
<span>
<Icon name={'warning'} height={16} width={16} fill="#DB4324" />
</span>
<span style={{ color: '#2D343B' }}>
{`This column type is deprecated and will be removed in a future update. We recommend using the new ${deprecatedColumnType.currentAlternativeColumnLabel} when creating applications moving forward.`}
</span>
</div>
);
};
export default DeprecatedColumnTypeMsg;

View file

@ -0,0 +1,326 @@
import React from 'react';
import { resolveReferences } from '@/_helpers/utils';
import { useTranslation } from 'react-i18next';
import { CodeHinter } from '../../../../CodeBuilder/CodeHinter';
import { EventManager } from '../../../EventManager';
import { ProgramaticallyHandleProperties } from '../ProgramaticallyHandleProperties';
import { OptionsList } from '../SelectOptionsList/OptionsList';
import { ValidationProperties } from './ValidationProperties';
import DatepickerProperties from './DatepickerProperties';
import { Option } from '@/Editor/CodeBuilder/Elements/Select';
import DeprecatedColumnTypeMsg from './DeprecatedColumnTypeMsg';
import CustomSelect from '@/_ui/Select';
import defaultStyles from '@/_ui/Select/styles';
import SolidIcon from '@/_ui/Icon/SolidIcons';
export const PropertiesTabElements = ({
column,
index,
darkMode,
currentState,
onColumnItemChange,
getPopoverFieldSource,
setColumnPopoverRootCloseBlocker,
component,
props,
columnEventChanged,
timeZoneOptions,
handleEventManagerPopoverCallback,
}) => {
const { t } = useTranslation();
const customStylesForSelect = {
...defaultStyles(darkMode, '100%'),
};
return (
<>
{column.columnType && <DeprecatedColumnTypeMsg columnType={column.columnType} darkMode={darkMode} />}
<div className="field px-3" data-cy={`dropdown-column-type`} onClick={(e) => e.stopPropagation()}>
<label data-cy={`label-column-type`} className="form-label">
{t('widget.Table.columnType', 'Column type')}
</label>
<CustomSelect
options={[
{ label: 'String', value: 'string' },
{ label: 'Number', value: 'number' },
{ label: 'Text', value: 'text' },
{ label: 'Date Picker', value: 'datepicker' },
{ label: 'Select', value: 'select' },
{ label: 'MultiSelect', value: 'newMultiSelect' },
{ label: 'Boolean', value: 'boolean' },
{ label: 'Image', value: 'image' },
{ label: 'Link', value: 'link' },
// Following column types are deprecated
{ label: 'Default', value: 'default' },
{ label: 'Dropdown', value: 'dropdown' },
{ label: 'Multiselect', value: 'multiselect' },
{ label: 'Toggle switch', value: 'toggle' },
{ label: 'Radio', value: 'radio' },
{ label: 'Badge', value: 'badge' },
{ label: 'Multiple badges', value: 'badges' },
{ label: 'Tags', value: 'tags' },
]}
components={{ DropdownIndicator, Option }}
onChange={(value) => {
onColumnItemChange(index, 'columnType', value);
}}
value={column.columnType}
useCustomStyles={true}
styles={customStylesForSelect}
className={`column-type-table-inspector`}
/>
</div>
<div className="field px-3" data-cy={`input-and-label-column-name`}>
<label data-cy={`label-column-name`} className="form-label">
{t('widget.Table.columnName', 'Column name')}
</label>
<CodeHinter
currentState={currentState}
initialValue={column.name}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={column.name}
onChange={(value) => onColumnItemChange(index, 'name', value)}
componentName={getPopoverFieldSource(column.columnType, 'name')}
popOverCallback={(showing) => {
setColumnPopoverRootCloseBlocker('name', showing);
}}
/>
</div>
<div data-cy={`input-and-label-key`} className="field px-3">
<label className="form-label">{t('widget.Table.key', 'Key')}</label>
<CodeHinter
currentState={currentState}
initialValue={column.key}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={column.name}
onChange={(value) => onColumnItemChange(index, 'key', value)}
componentName={getPopoverFieldSource(column.columnType, 'key')}
popOverCallback={(showing) => {
setColumnPopoverRootCloseBlocker('tableKey', showing);
}}
/>
</div>
<div data-cy={`transformation-field`} className="field px-3">
<label className="form-label">{t('widget.Table.transformationField', 'Transformation')}</label>
<CodeHinter
currentState={currentState}
initialValue={column?.transformation ?? '{{cellValue}}'}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={column.name}
onChange={(value) => onColumnItemChange(index, 'transformation', value)}
componentName={getPopoverFieldSource(column.columnType, 'transformation')}
popOverCallback={(showing) => {
setColumnPopoverRootCloseBlocker('transformation', showing);
}}
enablePreview={false}
/>
</div>
{column.columnType === 'toggle' && (
<div className="px-3">
<EventManager
sourceId={props?.component?.id}
eventSourceType="table_column"
hideEmptyEventsAlert={true}
eventMetaDefinition={{ events: { onChange: { displayName: 'On change' } } }}
currentState={currentState}
dataQueries={props.dataQueries}
components={props.components}
eventsChanged={(events) => columnEventChanged(column, events)}
apps={props.apps}
popOverCallback={(showing) => {
handleEventManagerPopoverCallback(showing);
}}
pages={props.pages}
/>
</div>
)}
{(column.columnType === 'dropdown' ||
column.columnType === 'multiselect' ||
column.columnType === 'badge' ||
column.columnType === 'badges' ||
column.columnType === 'radio') && (
<div>
<div data-cy={`input-and-label-values`} className="field mb-2 px-3">
<label className="form-label">{t('widget.Table.values', 'Values')}</label>
<CodeHinter
currentState={currentState}
initialValue={column.values}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'{{[1, 2, 3]}}'}
onChange={(value) => onColumnItemChange(index, 'values', value)}
componentName={getPopoverFieldSource(column.columnType, 'values')}
popOverCallback={(showing) => {
setColumnPopoverRootCloseBlocker('values', showing);
}}
/>
</div>
<div data-cy={`input-and-label-labels`} className="field mb-2 px-3">
<label className="form-label">{t('widget.Table.labels', 'Labels')}</label>
<CodeHinter
currentState={currentState}
initialValue={column.labels}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'{{["one", "two", "three"]}}'}
onChange={(value) => onColumnItemChange(index, 'labels', value)}
componentName={getPopoverFieldSource(column.columnType, 'labels')}
popOverCallback={(showing) => {
setColumnPopoverRootCloseBlocker('labels', showing);
}}
/>
</div>
</div>
)}
{column.columnType === 'link' && (
<>
<div className="field mb-2 px-3">
<label className="form-label">Display text</label>
<CodeHinter
currentState={currentState}
initialValue={column?.displayText}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'Display text'}
onChange={(value) => onColumnItemChange(index, 'displayText', value)}
componentName={getPopoverFieldSource(column.columnType, 'displayText')}
/>
</div>
<div className="border mx-3" style={{ borderRadius: '6px', overflow: 'hidden' }}>
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
<ProgramaticallyHandleProperties
label="Link target"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="linkTarget"
props={column}
component={component}
paramMeta={{
type: 'toggle',
displayName: 'Open in new tab',
}}
paramType="properties"
/>
</div>
</div>
</>
)}
{column.columnType === 'number' && (
<div className="field mb-2 px-3">
<label className="form-label">{t('widget.Table.decimalPlaces', 'Decimal Places')}</label>
<CodeHinter
currentState={currentState}
initialValue={column?.decimalPlaces}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'{{2}}'}
onChange={(value) => onColumnItemChange(index, 'decimalPlaces', value)}
componentName={getPopoverFieldSource(column.columnType, 'decimalPlaces')}
popOverCallback={(showing) => {
setColumnPopoverRootCloseBlocker('decimalPlaces', showing);
}}
/>
</div>
)}
{!['image', 'link'].includes(column.columnType) && (
<div className="border mx-3" style={{ borderRadius: '6px', overflow: 'hidden' }}>
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
<ProgramaticallyHandleProperties
label="make editable"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="isEditable"
props={column}
component={component}
paramMeta={{ type: 'toggle', displayName: 'Make editable' }}
paramType="properties"
/>
</div>
{resolveReferences(column?.isEditable, currentState) && (
<ValidationProperties
column={column}
index={index}
darkMode={darkMode}
currentState={currentState}
onColumnItemChange={onColumnItemChange}
getPopoverFieldSource={getPopoverFieldSource}
setColumnPopoverRootCloseBlocker={setColumnPopoverRootCloseBlocker}
/>
)}
</div>
)}
<div className="border mx-3" style={{ borderRadius: '6px', overflow: 'hidden', marginTop: '-8px' }}>
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
<ProgramaticallyHandleProperties
label="Visibility"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="columnVisibility"
props={column}
component={component}
paramMeta={{ type: 'toggle', displayName: 'Visibility' }}
paramType="properties"
/>
</div>
</div>
{['select', 'newMultiSelect', 'datepicker'].includes(column.columnType) && <hr className="mx-0 my-2" />}
{column.columnType === 'datepicker' && (
<div className="field" style={{ marginTop: '-24px' }}>
<DatepickerProperties
column={column}
index={index}
darkMode={darkMode}
currentState={currentState}
onColumnItemChange={onColumnItemChange}
component={component}
/>
</div>
)}
{['select', 'newMultiSelect'].includes(column.columnType) && (
<OptionsList
column={column}
props={props}
index={index}
darkMode={darkMode}
currentState={currentState}
getPopoverFieldSource={getPopoverFieldSource}
setColumnPopoverRootCloseBlocker={setColumnPopoverRootCloseBlocker}
component={component}
onColumnItemChange={onColumnItemChange}
/>
)}
</>
);
};
const DropdownIndicator = (props) => {
return (
<div {...props}>
{/* Your custom SVG */}
{props.selectProps.menuIsOpen ? (
<SolidIcon name="arrowUpTriangle" width="16" height="16" fill={'#6A727C'} />
) : (
<SolidIcon name="arrowDownTriangle" width="16" height="16" fill={'#6A727C'} />
)}
</div>
);
};

View file

@ -0,0 +1,214 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CodeHinter } from '../../../../CodeBuilder/CodeHinter';
import { Color } from '../../../Elements/Color';
import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup';
import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
import AlignLeft from '@/_ui/Icon/solidIcons/AlignLeft';
import AlignCenter from '@/_ui/Icon/solidIcons/AlignCenter';
import AlignRight from '@/_ui/Icon/solidIcons/AlignRight';
import { ProgramaticallyHandleProperties } from '../ProgramaticallyHandleProperties';
import { Select } from '@/Editor/CodeBuilder/Elements/Select';
export const StylesTabElements = ({
column,
index,
darkMode,
currentState,
onColumnItemChange,
getPopoverFieldSource,
component,
}) => {
const { t } = useTranslation();
return (
<>
<div className="field d-flex custom-gap-12 align-items-center align-self-stretch justify-content-between px-3">
<label className="d-flex align-items-center" style={{ flex: '1 1 0' }}>
{column.columnType !== 'boolean' && column.columnType !== 'image'
? t('widget.Table.textAlignment', 'Text Alignment')
: 'Alignment'}
</label>
<ToggleGroup
onValueChange={(_value) => onColumnItemChange(index, 'horizontalAlignment', _value)}
defaultValue={column?.horizontalAlignment || 'left'}
style={{ width: '58%' }}
>
<ToggleGroupItem value="left">
<AlignLeft width={14} />
</ToggleGroupItem>
<ToggleGroupItem value="center">
<AlignCenter width={14} />
</ToggleGroupItem>
<ToggleGroupItem value="right">
<AlignRight width={14} />
</ToggleGroupItem>
</ToggleGroup>
</div>
{column.columnType === 'toggle' && (
<div>
<div className="field px-3">
<Color
param={{ name: 'Active color' }}
paramType="properties"
componentMeta={{ properties: { color: { displayName: 'Active color' } } }}
definition={{ value: column.activeColor || '#3c92dc' }}
onChange={(name, value, color) => onColumnItemChange(index, 'activeColor', color)}
shouldFlexDirectionBeRow={true}
/>
</div>
</div>
)}
{column.columnType === 'image' && (
<>
<div data-cy={`input-and-label-border-radius`} className="field px-3">
<label className="form-label">{t('widget.Table.borderRadius', 'Border radius')}</label>
<CodeHinter
currentState={currentState}
initialValue={column.borderRadius}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={''}
onChange={(value) => onColumnItemChange(index, 'borderRadius', value)}
componentName={getPopoverFieldSource(column.columnType, 'borderRadius')}
/>
</div>
<div data-cy={`input-and-label-object-fit`} className="field px-3">
<label className="form-label">{t('widget.Table.imageFit', 'Image fit')}</label>
<Select
className={'select-search'}
meta={{
options: [
{ label: 'Cover', value: 'cover' },
{ label: 'Contain', value: 'contain' },
{ label: 'Fill', value: 'fill' },
],
}}
value={column.objectFit}
search={true}
closeOnSelect={true}
onChange={(value) => {
onColumnItemChange(index, 'objectFit', value);
}}
fuzzySearch
placeholder={t('Select') + '...'}
width={'100%'}
/>
</div>
</>
)}
{column.columnType === 'boolean' && (
<div className="d-flex flex-column custom-gap-16">
<div className="field px-3">
<Color
param={{ name: 'Checked' }}
paramType="properties"
componentMeta={{ properties: { color: { displayName: 'Checked' } } }}
definition={{ value: column?.toggleOnBg ? column.toggleOnBg : darkMode ? '#849DFF' : '#3A5CCC' }}
onChange={(name, value, color) => onColumnItemChange(index, 'toggleOnBg', color)}
shouldFlexDirectionBeRow={true}
/>
</div>
<div className="field px-3">
<Color
param={{ name: 'Unchecked' }}
paramType="properties"
componentMeta={{ properties: { color: { displayName: 'Unchecked' } } }}
definition={{ value: column?.toggleOffBg ? column.toggleOffBg : darkMode ? '#3A3F42' : '#D7DBDF' }}
onChange={(name, value, color) => onColumnItemChange(index, 'toggleOffBg', color)}
shouldFlexDirectionBeRow={true}
/>
</div>
</div>
)}
{['string', 'default', undefined, 'number', 'boolean', 'select', 'text', 'newMultiSelect', 'datepicker'].includes(
column.columnType
) && (
<>
{column.columnType !== 'boolean' && (
<div data-cy={`input-and-label-text-color`} className="field px-3">
<ProgramaticallyHandleProperties
label="Text color"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="textColor"
props={column}
component={component}
paramMeta={{ type: 'color', displayName: 'Text color' }}
paramType="properties"
/>
</div>
)}
<div className="field px-3" data-cy={`input-and-label-cell-background-color`}>
<ProgramaticallyHandleProperties
label="Cell color"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="cellBackgroundColor"
props={column}
component={component}
paramMeta={{ type: 'color', displayName: 'Cell color' }}
paramType="properties"
/>
</div>
</>
)}
{column.columnType === 'link' && (
<>
<div data-cy={`input-and-label-text-color`} className="field px-3">
<ProgramaticallyHandleProperties
label="Text color"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="linkColor"
props={column}
component={component}
paramMeta={{ type: 'color', displayName: 'Text color' }}
paramType="properties"
/>
</div>
<div className="field px-3" data-cy={`input-and-label-cell-background-color`}>
<ProgramaticallyHandleProperties
label="Underline color"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="underlineColor"
props={column}
component={component}
paramMeta={{ type: 'color', displayName: 'Underline color' }}
paramType="properties"
/>
</div>
<div className="d-flex px-3 flex-column custom-gap-16">
<div
data-cy={`input-overflow`}
className="field d-flex custom-gap-12 align-items-center align-self-stretch"
>
<label data-cy={`label-overflow`} className="d-flex align-items-center" style={{ flex: '1 1 0' }}>
Show underline
</label>
<ToggleGroup
onValueChange={(_value) => onColumnItemChange(index, 'underline', _value)}
defaultValue={column.underline || 'hover'}
style={{ flex: '1 1 0' }}
>
<ToggleGroupItem value="hover">Hover</ToggleGroupItem>
<ToggleGroupItem value="always">Always</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
</>
)}
</>
);
};

View file

@ -0,0 +1,252 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CodeHinter } from '../../../../CodeBuilder/CodeHinter';
import ReactDatePicker from 'react-datepicker';
import moment from 'moment';
import Timepicker from '@/ToolJetUI/Timepicker/Timepicker';
import CustomDatePickerHeader from '@/Editor/Components/Table/CustomDatePickerHeader';
import { resolveReferences } from '../../../../../_helpers/utils';
const getDate = (date, format) => {
const dateMomentInstance = date && moment(date, format);
if (dateMomentInstance && dateMomentInstance.isValid()) {
return dateMomentInstance.toDate();
} else {
return null;
}
};
export const ValidationProperties = ({
column,
index,
darkMode,
currentState,
onColumnItemChange,
getPopoverFieldSource,
setColumnPopoverRootCloseBlocker,
}) => {
const { t } = useTranslation();
const columnType = column.columnType;
const getValidationList = (columnType) => {
switch (columnType) {
case 'string':
case undefined:
case 'default':
case 'text': {
const properties = [];
if (column.columnType !== 'text') {
properties.push({
property: 'regex',
dateCy: 'input-and-label-regex',
label: 'Regex',
placeholder: `${/^[\w\s\d]+$/i}`,
});
}
properties.push(
[
{
property: 'minLength',
dateCy: 'input-and-label-min-length',
label: 'Min length',
placeholder: 'Enter min length',
},
{
property: 'maxLength',
dateCy: 'input-and-label-max-length',
label: 'Max length',
placeholder: 'Enter max length',
},
],
{
property: 'customRule',
dateCy: 'input-and-label-custom-rule',
label: 'Custom rule',
placeholder: 'eg. {{ 1 < 2 }}',
}
);
return properties;
}
case 'number':
return [
{ property: 'regex', dateCy: 'input-and-label-regex', label: 'Regex', placeholder: `${/^[\w\s\d]+$/i}` },
[
{
property: 'minValue',
dateCy: 'input-and-label-min-value',
label: 'Min value',
placeholder: 'Enter min value',
},
{
property: 'maxValue',
dateCy: 'input-and-label-max-value',
label: 'Max value',
placeholder: 'Enter max value',
},
],
{
property: 'customRule',
dateCy: 'input-and-label-custom-rule',
label: 'Custom rule',
placeholder: 'eg. {{ 1 < 2 }}',
},
];
case 'dropdown':
case 'select':
case 'newMultiSelect':
return [
{
property: 'customRule',
dateCy: 'input-and-label-custom-rule',
label: 'Custom rule',
placeholder: 'eg. {{ 1 < 2 }}',
},
];
case 'datepicker': {
const isTimeChecked = resolveReferences(column?.isTimeChecked);
let properties = [];
properties.push([
{
property: 'minDate',
dateCy: 'input-and-label-min-date',
label: 'Minimum date',
placeholder: 'MM/DD/YYYY',
fieldType: 'datepicker',
},
{
property: 'maxDate',
dateCy: 'input-and-label-max-date',
label: 'Maximum date',
placeholder: 'MM/DD/YYYY',
fieldType: 'datepicker',
},
]);
if (isTimeChecked) {
properties.push([
{
property: 'minTime',
dateCy: 'input-and-label-min-time',
label: 'Minimum time',
placeholder: 'HH:mm',
fieldType: 'timepicker',
},
{
property: 'maxTime',
dateCy: 'input-and-label-max-time',
label: 'Maximum time',
placeholder: 'HH:mm',
fieldType: 'timepicker',
},
]);
}
properties.push({
property: 'disabledDates',
dateCy: 'input-and-label-custom-rule',
label: 'Disabled dates',
placeholder: '{{[]}}',
});
properties.push({
property: 'customRule',
dateCy: 'input-and-label-custom-rule',
label: 'Custom rule',
placeholder: 'eg. {{ 1 < 2 }}',
});
return properties;
}
default:
return [];
}
};
const validationsList = getValidationList(columnType);
if (validationsList.length < 1) {
return '';
}
const renderAsPerFieldType = (validation) => {
switch (validation.fieldType) {
case 'datepicker':
return (
<div
data-cy={validation.dataCy}
className="field flex-fill inspector-validation-date-picker"
key={validation.property}
>
<label className="form-label">{t(`widget.Table.${validation.property}`, validation.label)}</label>
<ReactDatePicker
selected={getDate(column?.[validation.property], 'MM/DD/YYYY')}
onChange={(date) => onColumnItemChange(index, validation.property, moment(date).format('MM/DD/YYYY'))}
showTimeSelectOnly={validation.showOnlyTime}
placeholderText={validation?.placeholder ?? ''}
renderCustomHeader={(headerProps) => <CustomDatePickerHeader {...headerProps} />}
popperClassName={darkMode && 'theme-dark dark-theme'}
/>
</div>
);
case 'timepicker':
return (
<div
data-cy={validation.dataCy}
className="field flex-fill inspector-validation-date-picker"
key={validation.property}
>
<label className="form-label">{t(`widget.Table.${validation.property}`, validation.label)}</label>
<Timepicker
selected={getDate(column?.[validation.property], 'HH:mm')}
onChange={(date) => onColumnItemChange(index, validation.property, moment(date).format('HH:mm'))}
placeholderText={validation?.placeholder ?? ''}
timeFormat={'HH:mm'}
darkMode={darkMode}
/>
</div>
);
default:
return (
<div data-cy={validation.dataCy} className="field flex-fill" key={validation.property}>
<label className="form-label">{t(`widget.Table.${validation.property}`, validation.label)}</label>
<CodeHinter
currentState={currentState}
initialValue={column?.[validation.property]}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={validation?.placeholder ?? ''}
onChange={(value) => onColumnItemChange(index, validation.property, value)}
componentName={getPopoverFieldSource(column.columnType, validation.property)}
popOverCallback={(showing) => {
setColumnPopoverRootCloseBlocker(validation.property, showing);
}}
/>
</div>
);
}
};
return (
<div className="optional-properties-when-editable-true">
<div className="d-flex flex-column custom-gap-8">
{validationsList.map((validation) => {
if (Array.isArray(validation)) {
return (
<div className="d-flex align-item-start align-self-stretch custom-gap-8" key={validation.property}>
{validation.map((validation) => {
{
return renderAsPerFieldType(validation);
}
})}
</div>
);
} else {
return renderAsPerFieldType(validation);
}
})}
</div>
</div>
);
};

View file

@ -26,6 +26,30 @@ export const ProgramaticallyHandleProperties = ({
return props.columnVisibility;
case 'linkTarget':
return props.linkTarget;
case 'isAllColumnsEditable':
return props?.isAllColumnsEditable;
case 'underlineColor':
return props.underlineColor;
case 'linkColor':
return props.linkColor;
case 'useDynamicOptions':
return props?.useDynamicOptions;
case 'makeDefaultOption':
return props?.[index]?.makeDefaultOption;
case 'textColor':
return props?.textColor;
case 'cellBackgroundColor':
return props?.cellBackgroundColor;
case 'optionsLoadingState':
return props?.optionsLoadingState;
case 'isTimeChecked':
return props?.isTimeChecked;
case 'isTwentyFourHrFormatEnabled':
return props?.isTwentyFourHrFormatEnabled;
case 'parseInUnixTimestamp':
return props?.parseInUnixTimestamp;
case 'isDateSelectionEnabled':
return props?.isDateSelectionEnabled;
default:
return;
}
@ -36,16 +60,41 @@ export const ProgramaticallyHandleProperties = ({
return definitionObj?.value ?? `{{true}}`;
}
if (property === 'linkTarget') {
return definitionObj?.value ?? '_blank';
const value = definitionObj?.value;
if (value === '_self' || value === '{{false}}' || value === '') {
return '{{false}}';
}
return value || '{{true}}';
}
if (property === 'cellBackgroundColor') {
return definitionObj?.value ?? '';
}
if (property === 'textColor') {
return definitionObj?.value ?? '#11181C';
}
if (property === 'underlineColor') {
return definitionObj?.value ?? '#4368E3';
}
if (property === 'underline') {
return definitionObj?.value ?? 'hover';
}
if (property === 'linkColor') {
return definitionObj?.value ?? '#1B1F24';
}
return definitionObj?.value ?? `{{false}}`;
};
const value = getValueBasedOnProperty(property, props);
const param = { name: property };
const definition = { value, fxActive: props.fxActive };
const initialValue = getInitialValue(property, definition);
const param = { name: property === 'makeDefaultOption' ? `options::${property}` : property };
let definition;
let initialValue;
if (Array.isArray(props)) {
definition = { value, fxActive: props?.[index]?.fxActive };
initialValue = getInitialValue(property, definition);
} else {
definition = { value, fxActive: props.fxActive };
initialValue = getInitialValue(property, definition);
}
const options = {};
const calcFxActiveState = (props, property) => {
@ -94,7 +143,7 @@ export const ProgramaticallyHandleProperties = ({
};
return (
<div className={`mb-2 field ${options.className}`} onClick={(e) => e.stopPropagation()}>
<div className={`field ${options.className}`} onClick={(e) => e.stopPropagation()}>
<CodeHinter
enablePreview={true}
initialValue={initialValue}

View file

@ -0,0 +1,346 @@
import React from 'react';
import Accordion from '@/_ui/Accordion';
import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton';
import List from '@/ToolJetUI/List/List';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import NoListItem from '../NoListItem';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
import { ProgramaticallyHandleProperties } from '../ProgramaticallyHandleProperties';
import { resolveReferences } from '@/_helpers/utils';
import { unset } from 'lodash';
export const OptionsList = ({
column,
props,
index,
darkMode,
currentState,
getPopoverFieldSource,
setColumnPopoverRootCloseBlocker,
onColumnItemChange,
component,
}) => {
const items = [];
const recordOptions = (startIndex, endIndex) => {
const columns = props.component.component.definition.properties.columns;
const column = columns.value[index];
const options = column.options;
const [removed] = options.splice(startIndex, 1);
options.splice(endIndex, 0, removed);
column.options = options;
const newColumns = columns.value;
newColumns[index] = column;
props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties', true);
};
const onDragEnd = async ({ source, destination }) => {
if (!destination || source?.index === destination?.index) {
return;
}
await recordOptions(source.index, destination.index);
};
const getItemStyle = (draggableStyle) => {
return {
...draggableStyle,
userSelect: 'none',
position: 'static',
};
};
const createNewOption = () => {
const columns = props.component.component.definition.properties.columns;
const column = columns.value[index];
const options = column.options || [];
options.push({ label: 'one', value: '1' });
column.options = options;
const newColumns = columns.value;
newColumns[index] = column;
props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties', true);
};
const deleteOption = (option, optionIndex) => {
const columns = props.component.component.definition.properties.columns;
const column = columns.value[index];
const options = column.options;
const deletedOption = options.splice(optionIndex, 1);
column.options = options;
const defaultOptionsList = column.defaultOptionsList || [];
const deletedOptionIndexInDefaultOptionsList = defaultOptionsList?.findIndex((defaultOption, index) => {
if (
defaultOption.value === deletedOption.value &&
defaultOption.label === deletedOption.label &&
defaultOption.makeDefaultOption === deletedOption.makeDefaultOption
) {
return index;
} else {
return -1;
}
});
if (deletedOptionIndexInDefaultOptionsList !== -1) {
defaultOptionsList.splice(deletedOptionIndexInDefaultOptionsList, 1);
column.defaultOptionsList = defaultOptionsList;
}
const newColumns = columns.value;
newColumns[index] = column;
props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties', true);
};
const selectPopover = (option, optionIndex) => {
const handleSelectOption = (option, optionIndex, value, index, optionItemChanged) => {
const columns = props.component.component.definition.properties.columns;
const column = columns.value[index];
const options = column.options;
options[optionIndex][optionItemChanged] = value;
column.options = options;
const newColumns = columns.value;
newColumns[index] = column;
props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties', true);
};
const handleDefaultOptionSelection = (optionIndex, property, value) => {
const columns = props.component.component.definition.properties.columns;
const column = columns.value[index];
const options = column.options;
options[optionIndex][property] = value;
column.options = options;
const isValueTruthy = !!resolveReferences(value, currentState);
// This block is responsible for updating list of defaultOptions when makeDefaultOption prop is updated
if (property === 'makeDefaultOption') {
if (column.columnType === 'select') {
// Disable all other default options except the one selected
column.options.forEach((option) => unset(option, 'makeDefaultOption'));
column.options[optionIndex][property] = value;
if (isValueTruthy) {
column.defaultOptionsList = [column.options[optionIndex]];
} else {
column.defaultOptionsList = [];
}
} else {
const defaultOptionsList = column?.defaultOptionsList ?? [];
const indexOfOptionInList = defaultOptionsList.findIndex((option) => {
if (option.value === options[optionIndex].value && option.label === options[optionIndex].label) {
return option;
}
});
if (indexOfOptionInList === -1 && isValueTruthy) {
defaultOptionsList.push(options[optionIndex]);
}
if (indexOfOptionInList !== -1 && !isValueTruthy) {
defaultOptionsList.splice(indexOfOptionInList, 1);
}
column.defaultOptionsList = defaultOptionsList;
}
}
const newColumns = columns.value;
newColumns[index] = column;
props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties', true);
};
return (
<Popover
id="popover-basic"
className={`${darkMode && 'dark-theme'}`}
onClick={(e) => e.stopPropagation()}
style={{ zIndex: 99999, minWidth: 200 }}
>
<Popover.Body>
<div className="field mb-2 tj-app-input" onClick={(e) => e.stopPropagation()}>
<label data-cy={`label-action-button-text`} className="form-label">
Option label
</label>
<CodeHinter
currentState={currentState}
initialValue={option?.label}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={option?.label}
onChange={(value) => handleSelectOption(option, optionIndex, value, index, 'label')}
componentName={getPopoverFieldSource(column.columnType, 'options::label')}
popOverCallback={(showing) => {
setColumnPopoverRootCloseBlocker('options::label', showing);
}}
/>
</div>
<div className="field mb-2 tj-app-input" onClick={(e) => e.stopPropagation()}>
<label data-cy={`label-action-button-text`} className="form-label">
Option value
</label>
<CodeHinter
currentState={currentState}
initialValue={option.value}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={option.value}
onChange={(value) => handleSelectOption(option, optionIndex, value, index, 'value')}
componentName={getPopoverFieldSource(column.columnType, 'options::value')}
popOverCallback={(showing) => {
setColumnPopoverRootCloseBlocker('options::value', showing);
}}
/>
</div>
<ProgramaticallyHandleProperties
label="Make this option as default"
currentState={currentState}
index={optionIndex}
darkMode={darkMode}
callbackFunction={handleDefaultOptionSelection}
property="makeDefaultOption"
props={column.options}
component={component}
paramMeta={{
type: 'toggle',
displayName: 'Make this option as default',
}}
paramType="properties"
/>
</Popover.Body>
</Popover>
);
};
const defaultOptionsValues = () => {
return {
options: [
{ label: 'Reading', value: 'Reading' },
{ label: 'Traveling', value: 'Traveling' },
{ label: 'Photography', value: 'Photography' },
{ label: 'Music', value: 'Music' },
],
};
};
items.push({
title: 'Options',
children: (
<div className="d-flex custom-gap-7 flex-column">
<ProgramaticallyHandleProperties
label="Dynamic option"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="useDynamicOptions"
props={column}
component={component}
paramMeta={{
type: 'toggle',
displayName: 'Dynamic option',
}}
paramType="properties"
/>
{resolveReferences(column?.useDynamicOptions, currentState) ? (
<div className="d-flex custom-gap-7 flex-column">
<CodeHinter
currentState={currentState}
initialValue={column?.dynamicOptions}
theme={darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
onChange={(value) => onColumnItemChange(index, 'dynamicOptions', value)}
componentName={getPopoverFieldSource(column.columnType, 'dynamicOptions')}
popOverCallback={(showing) => {
setColumnPopoverRootCloseBlocker('dynamicOptions', showing);
}}
placeholder={`{{[{ label: 'Reading', value: 'Reading' }, { label: 'Traveling', value: 'Traveling' }, { label: 'Photography', value: 'Photography' }, { label: 'Music', value: 'Music' }]}}`}
/>
<ProgramaticallyHandleProperties
label="Options loading state"
currentState={currentState}
index={index}
darkMode={darkMode}
callbackFunction={onColumnItemChange}
property="optionsLoadingState"
props={column}
component={component}
paramMeta={{
type: 'toggle',
displayName: 'Options loading state',
}}
paramType="properties"
/>
</div>
) : (
<List>
<DragDropContext
onDragEnd={(result) => {
onDragEnd(result);
}}
>
<Droppable droppableId="droppable">
{({ innerRef, droppableProps, placeholder }) => {
const columnHasOptions = column.hasOwnProperty('options');
if (!columnHasOptions) {
const defaultOptions = defaultOptionsValues(columnHasOptions);
Object.assign(column, defaultOptions);
}
return (
<div className="w-100" {...droppableProps} ref={innerRef}>
{column?.options?.map((option, optionIndex) => {
const resolvedItemName = option.label;
return (
<Draggable key={option.label} draggableId={option.label} index={optionIndex}>
{(provided, snapshot) => {
return (
<div
key={optionIndex}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...getItemStyle(provided.draggableProps.style) }}
>
<OverlayTrigger
trigger="click"
placement="top"
rootClose={true}
overlay={selectPopover(option, optionIndex)}
>
<div key={resolvedItemName}>
<List.Item
isDraggable={true}
primaryText={resolvedItemName}
data-cy={`column-${resolvedItemName}`}
enableActionsMenu={false}
onMenuOptionClick={(listItem, menuOptionLabel) => {
if (menuOptionLabel === 'Delete') deleteOption(option, optionIndex);
}}
darkMode={darkMode}
deleteIconOutsideMenu={true}
/>
</div>
</OverlayTrigger>
</div>
);
}}
</Draggable>
);
})}
{placeholder}
</div>
);
}}
</Droppable>
</DragDropContext>
<div>
{column?.options?.length === 0 && <NoListItem text={'There are no columns'} dataCy={`-columns`} />}
<div>
<AddNewButton dataCy={`button-add-column`} onClick={() => createNewOption()}>
{/* {this.props.t('widget.Table.addNewColumn', ' Add new column')} */}
Add new option
</AddNewButton>
</div>
</div>
</List>
)}
</div>
),
});
return <Accordion items={items} className="table-select-column-accordian" />;
};

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,15 @@ import React, { useState } from 'react';
import { SketchPicker } from 'react-color';
import { ToolTip } from './Components/ToolTip';
export const Color = ({ param, definition, onChange, paramType, componentMeta, cyLabel }) => {
export const Color = ({
param,
definition,
onChange,
paramType,
componentMeta,
cyLabel,
shouldFlexDirectionBeRow = false,
}) => {
const [showPicker, setShowPicker] = useState(false);
const coverStyles = {
@ -38,8 +46,10 @@ export const Color = ({ param, definition, onChange, paramType, componentMeta, c
};
return (
<div className="field mb-3">
<ToolTip label={displayName} meta={paramMeta} />
<div
className={`field ${shouldFlexDirectionBeRow && 'd-flex custom-gap-12 align-items-center align-self-stretch'}`}
>
<ToolTip label={displayName} meta={paramMeta} labelClass={shouldFlexDirectionBeRow && 'flex-fill'} />
{showPicker && (
<div>
@ -70,7 +80,7 @@ export const Color = ({ param, definition, onChange, paramType, componentMeta, c
boxShadow: `0px 1px 2px 0px rgba(16, 24, 40, 0.05)`,
}}
></div>
<div style={{ height: '20px' }} className="col">
<div style={{ height: '20px', flex: '1 1 0' }} className="col">
{definition.value}
</div>
</div>

View file

@ -53,6 +53,8 @@ const INSPECTOR_HEADER_OPTIONS = [
},
];
const NEW_REVAMPED_COMPONENTS = ['Text', 'TextInput', 'PasswordInput', 'NumberInput', 'Table'];
export const Inspector = ({
componentDefinitionChanged,
allComponents,
@ -83,7 +85,7 @@ export const Inspector = ({
const [inputRef, setInputFocus] = useFocus();
const [showHeaderActionsMenu, setShowHeaderActionsMenu] = useState(false);
const shouldAddBoxShadow = ['TextInput', 'PasswordInput', 'NumberInput', 'Text'];
const isRevampedComponent = NEW_REVAMPED_COMPONENTS.includes(component.component.component);
const { isVersionReleased } = useAppVersionStore(
(state) => ({
@ -198,6 +200,22 @@ export const Inspector = ({
} else {
allParams[param.name] = value;
}
if (
component.component.component === 'Table' &&
param.name === 'contentWrap' &&
!resolveReferences(value, currentState) &&
newDefinition.properties.columns.value.some((item) => item.columnType === 'image' && item.height !== '')
) {
const updatedColumns = newDefinition.properties.columns.value.map((item) => {
return item.columnType === 'image' ? { ...item, height: '' } : item; // Create a new object for image columns
});
// Update the columns value with the updated columns
newDefinition.properties.columns.value = updatedColumns;
isParamFromTableColumn = true;
}
newDefinition[paramType] = allParams;
newComponent.component.definition = newDefinition;
componentDefinitionChanged(newComponent, {
@ -312,15 +330,7 @@ export const Inspector = ({
);
const stylesTab = (
<div style={{ marginBottom: '6rem' }} className={`${isVersionReleased && 'disabled'}`}>
<div
className={
component.component.component !== 'TextInput' &&
component.component.component !== 'PasswordInput' &&
component.component.component !== 'NumberInput' &&
component.component.component !== 'Text' &&
'p-3'
}
>
<div className={!isRevampedComponent && 'p-3'}>
<Inspector.RenderStyleOptions
componentMeta={componentMeta}
component={component}
@ -330,7 +340,7 @@ export const Inspector = ({
allComponents={allComponents}
/>
</div>
{!shouldAddBoxShadow.includes(component.component.component) && buildGeneralStyle()}
{!isRevampedComponent && buildGeneralStyle()}
</div>
);
@ -468,18 +478,22 @@ const widgetsWithStyleConditions = {
},
],
},
Table: {
conditions: [
{
definition: 'styles',
property: 'contentWrap',
conditionStyles: ['maxRowHeight', 'autoHeight'],
type: 'toggle',
},
],
},
};
const styleGroupedComponentTypes = ['TextInput', 'NumberInput', 'PasswordInput'];
const RenderStyleOptions = ({ componentMeta, component, paramUpdated, dataQueries, currentState, allComponents }) => {
// Initialize an object to group properties by "accordian"
const groupedProperties = {};
if (
component.component.component === 'TextInput' ||
component.component.component === 'PasswordInput' ||
component.component.component === 'NumberInput' ||
component.component.component === 'Text'
) {
if (NEW_REVAMPED_COMPONENTS.includes(component.component.component)) {
// Iterate over the properties in componentMeta.styles
for (const key in componentMeta.styles) {
const property = componentMeta.styles[key];
@ -496,12 +510,7 @@ const RenderStyleOptions = ({ componentMeta, component, paramUpdated, dataQuerie
}
return Object.keys(
component.component.component === 'TextInput' ||
component.component.component === 'PasswordInput' ||
component.component.component === 'NumberInput' ||
component.component.component === 'Text'
? groupedProperties
: componentMeta.styles
NEW_REVAMPED_COMPONENTS.includes(component.component.component) ? groupedProperties : componentMeta.styles
).map((style) => {
const conditionWidget = widgetsWithStyleConditions[component.component.component] ?? null;
const condition = conditionWidget?.conditions.find((condition) => condition.property) ?? {};
@ -525,12 +534,7 @@ const RenderStyleOptions = ({ componentMeta, component, paramUpdated, dataQuerie
const items = [];
if (
component.component.component === 'TextInput' ||
component.component.component === 'PasswordInput' ||
component.component.component === 'NumberInput' ||
component.component.component === 'Text'
) {
if (NEW_REVAMPED_COMPONENTS.includes(component.component.component)) {
items.push({
title: `${style}`,
children: Object.entries(groupedProperties[style]).map(([key, value]) => ({
@ -566,7 +570,14 @@ const RenderStyleOptions = ({ componentMeta, component, paramUpdated, dataQuerie
const resolveConditionalStyle = (definition, condition, currentState) => {
const conditionExistsInDefinition = definition[condition] ?? false;
if (conditionExistsInDefinition) {
return resolveReferences(definition[condition]?.value ?? false, currentState);
switch (condition) {
case 'cellSize': {
const cellSize = resolveReferences(definition[condition]?.value ?? false, currentState) === 'hugContent';
return cellSize;
}
default:
return resolveReferences(definition[condition]?.value ?? false, currentState);
}
}
};

View file

@ -43,19 +43,40 @@ export function renderCustomStyles(
componentConfig.component == 'Listview' ||
componentConfig.component == 'TextInput' ||
componentConfig.component == 'NumberInput' ||
componentConfig.component == 'PasswordInput'
componentConfig.component == 'PasswordInput' ||
componentConfig.component == 'Table'
) {
const paramTypeConfig = componentMeta[paramType] || {};
const paramConfig = paramTypeConfig[param] || {};
const { conditionallyRender = null } = paramConfig;
if (conditionallyRender) {
const { key, value } = conditionallyRender;
if (paramTypeDefinition?.[key] ?? value) {
const resolvedValue = paramTypeDefinition?.[key] && resolveReferences(paramTypeDefinition?.[key], currentState);
const getResolvedValue = (key) => {
return paramTypeDefinition?.[key] && resolveReferences(paramTypeDefinition?.[key], currentState);
};
if (resolvedValue?.value !== value) {
return;
const utilFuncForMultipleChecks = (conditionallyRender) => {
return conditionallyRender.reduce((acc, condition) => {
const { key, value } = condition;
if (paramTypeDefinition?.[key] ?? value) {
const resolvedValue = getResolvedValue(key);
acc.push(resolvedValue?.value !== value);
}
return acc;
}, []);
};
if (conditionallyRender) {
const isConditionallyRenderArray = Array.isArray(conditionallyRender);
if (isConditionallyRenderArray && utilFuncForMultipleChecks(conditionallyRender).includes(true)) {
return;
} else {
const { key, value } = conditionallyRender;
if (paramTypeDefinition?.[key] ?? value) {
const resolvedValue = getResolvedValue(key);
if (resolvedValue?.value !== value) {
return;
}
}
}
}
@ -135,7 +156,7 @@ export function renderElement(
componentMeta={componentMeta}
darkMode={darkMode}
componentName={component.component.name || null}
type={meta.type}
type={meta?.type}
fxActive={definition.fxActive ?? false}
onFxPress={(active) => {
paramUpdated({ name: param, ...component.component.properties[param] }, 'fxActive', active, paramType);

View file

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { useCurrentStateStore } from '@/_stores/currentStateStore';
import { useModuleName } from '@/_contexts/ModuleContext';
import { shallow } from 'zustand/shallow';
import { debuggerActions } from '@/_helpers/appUtils';
import { flow } from 'lodash';
@ -11,6 +12,8 @@ const useDebugger = ({ currentPageId, isDebuggerOpen }) => {
const [unReadErrorCount, setUnReadErrorCount] = useState({ read: 0, unread: 0 });
const [allLog, setAllLog] = useState([]);
const moduleName = useModuleName();
const { errors, succededQuery } = useCurrentStateStore(
(state) => ({
errors: state.errors,
@ -43,7 +46,7 @@ const useDebugger = ({ currentPageId, isDebuggerOpen }) => {
(arr) => arr.filter(([key, value]) => value.data?.status),
Object.fromEntries,
])(errors);
const newErrorLogs = debuggerActions.generateErrorLogs(newError);
const newErrorLogs = debuggerActions.generateErrorLogs(newError, moduleName);
const newPageLevelErrorLogs = newErrorLogs.filter((error) => error.strace === 'page_level');
const newAppLevelErrorLogs = newErrorLogs.filter((error) => error.strace === 'app_level');
if (newErrorLogs) {
@ -64,19 +67,19 @@ const useDebugger = ({ currentPageId, isDebuggerOpen }) => {
};
});
}
debuggerActions.flush();
debuggerActions.flush(moduleName);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ errors })]);
useEffect(() => {
const successQueryLogs = debuggerActions.generateQuerySuccessLogs(succededQuery);
const successQueryLogs = debuggerActions.generateQuerySuccessLogs(succededQuery, moduleName);
if (successQueryLogs?.length) {
setAllLog((prevLogs) => {
const temp = [...successQueryLogs, ...prevLogs];
const sortedDatesDesc = temp.sort((a, b) => moment(b.timestamp).diff(moment(a.timestamp)));
return sortedDatesDesc;
});
debuggerActions.flushAllLog();
debuggerActions.flushAllLog(moduleName);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ succededQuery })]);

View file

@ -3,7 +3,6 @@ import { HeaderSection } from '@/_ui/LeftSidebar';
import JSONTreeViewer from '@/_ui/JSONTreeViewer';
import _ from 'lodash';
import { toast } from 'react-hot-toast';
import { getSvgIcon } from '@/_helpers/appUtils';
import Icon from '@/_ui/Icon/solidIcons/index';
import { useGlobalDataSources } from '@/_stores/dataSourcesStore';
import { useDataQueries } from '@/_stores/dataQueriesStore';
@ -12,6 +11,7 @@ import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { useEditorStore } from '@/_stores/editorStore';
import DataSourceIcon from '@/Editor/QueryManager/Components/DataSourceIcon';
const staticDataSources = [
{ kind: 'tooljetdb', id: 'null', name: 'Tooljet Database' },
@ -104,13 +104,10 @@ export const LeftSidebarInspector = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState, JSON.stringify(dataQueries)]);
const queryIcons = Object.entries(currentState['queries']).map(([key, value]) => {
const queryIcons = dataQueries.map((query) => {
const allDs = [...staticDataSources, ...dataSources];
const icon = allDs.find((ds) => ds.kind === value.kind);
const iconFile = icon?.plugin?.iconFile?.data ?? undefined;
const Icon = () => getSvgIcon(icon?.kind, 16, 16, iconFile ?? undefined);
return { iconName: key, jsx: () => <Icon style={{ height: 16, width: 16, marginRight: 12 }} /> };
const source = allDs.find((ds) => ds.kind === query.kind);
return { iconName: query.name, jsx: () => <DataSourceIcon source={source} height={16} /> };
});
const componentIcons = Object.entries(currentState['components']).map(([key, value]) => {

View file

@ -21,6 +21,7 @@ import useDebugger from './SidebarDebugger/useDebugger';
import { GlobalSettings } from '../Header/GlobalSettings';
import { resolveReferences } from '@/_helpers/utils';
import { useCurrentState } from '@/_stores/currentStateStore';
import cx from 'classnames';
export const LeftSidebar = forwardRef((props, ref) => {
const router = useRouter();
@ -71,9 +72,10 @@ export const LeftSidebar = forwardRef((props, ref) => {
}),
shallow
);
const { showComments } = useEditorStore(
const { showComments, appMode } = useEditorStore(
(state) => ({
showComments: state?.showComments,
appMode: state?.appMode,
}),
shallow
);
@ -84,7 +86,6 @@ export const LeftSidebar = forwardRef((props, ref) => {
currentPageId,
isDebuggerOpen: !!selectedSidebarItem,
});
const sideBarBtnRefs = useRef({});
useEffect(() => {
@ -232,7 +233,7 @@ export const LeftSidebar = forwardRef((props, ref) => {
}, [JSON.stringify(resolveReferences(backgroundFxQuery, currentState))]);
return (
<div className="left-sidebar" data-cy="left-sidebar-inspector">
<div className={cx('left-sidebar', { 'dark-theme theme-dark': darkMode })} data-cy="left-sidebar-inspector">
<LeftSidebarItem
selectedSidebarItem={selectedSidebarItem}
onClick={() => handleSelectedSidebarItem('page')}
@ -314,9 +315,9 @@ export const LeftSidebar = forwardRef((props, ref) => {
/>
</div>
)}
<DarkModeToggle switchDarkMode={switchDarkMode} darkMode={darkMode} tooltipPlacement="right" />
</div>
{/* <LeftSidebarItem icon='support' className='left-sidebar-item' /> */}
</div>
</div>
);

View file

@ -13,9 +13,12 @@ import SolidIcon from '@/_ui/Icon/SolidIcons';
import cx from 'classnames';
import { ToolTip } from '@/_components/ToolTip';
import { TOOLTIP_MESSAGES } from '@/_helpers/constants';
import { useAppDataStore } from '@/_stores/appDataStore';
import { useSuperStore } from '../_stores/superStore';
import { ModuleContext } from '../_contexts/ModuleContext';
class ManageAppUsersComponent extends React.Component {
static contextType = ModuleContext;
constructor(props) {
super(props);
this.isUserAdmin = authenticationService.currentSessionValue?.admin;
@ -109,7 +112,10 @@ class ManageAppUsersComponent extends React.Component {
ischangingVisibility: true,
});
useAppDataStore.getState().actions.updateState({ isPublic: newState });
useSuperStore
.getState()
.modules[this.context].useAppDataStore.getState()
.actions.updateState({ isPublic: newState });
// eslint-disable-next-line no-unused-vars
appsService
@ -165,7 +171,10 @@ class ManageAppUsersComponent extends React.Component {
});
replaceEditorURL(value, this.props.pageHandle);
useAppDataStore.getState().actions.updateState({ slug: value });
useSuperStore
.getState()
.modules[this.context].useAppDataStore.getState()
.actions.updateState({ slug: value });
})
.catch(({ error }) => {
this.setState({

View file

@ -21,10 +21,12 @@ import { shallow } from 'zustand/shallow';
import { Tooltip } from 'react-tooltip';
import { Button } from 'react-bootstrap';
import { cloneDeep } from 'lodash';
import { useModuleName } from '@/_contexts/ModuleContext';
import ParameterList from './ParameterList';
export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, setOptions }, ref) => {
const moduleName = useModuleName();
const { renameQuery } = useDataQueriesActions();
const selectedQuery = useSelectedQuery();
const selectedDataSource = useSelectedDataSource();
@ -57,7 +59,7 @@ export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef, se
return false;
}
const isNewQueryNameAlreadyExists = checkExistingQueryName(newName);
const isNewQueryNameAlreadyExists = checkExistingQueryName(newName, moduleName);
if (isNewQueryNameAlreadyExists) {
toast.error('Query name already exists');
return false;

View file

@ -10,8 +10,10 @@ import { shallow } from 'zustand/shallow';
import Copy from '@/_ui/Icon/solidIcons/Copy';
import DataSourceIcon from '../QueryManager/Components/DataSourceIcon';
import { isQueryRunnable } from '@/_helpers/utils';
import { useModuleName } from '@/_contexts/ModuleContext';
export const QueryCard = ({ dataQuery, darkMode = false, editorRef, appId }) => {
const moduleName = useModuleName();
const selectedQuery = useSelectedQuery();
const { isDeletingQueryInProcess } = useDataQueriesStore();
const { deleteDataQueries, renameQuery, duplicateQuery } = useDataQueriesActions();
@ -42,7 +44,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, editorRef, appId }) =>
if (name === newName) {
return setRenamingQuery(false);
}
const isNewQueryNameAlreadyExists = checkExistingQueryName(newName);
const isNewQueryNameAlreadyExists = checkExistingQueryName(newName, moduleName);
if (newName && !isNewQueryNameAlreadyExists) {
renameQuery(dataQuery?.id, newName, editorRef);
setRenamingQuery(false);

View file

@ -61,7 +61,7 @@ export const QueryDataPane = ({ darkMode, fetchDataQueries, editorRef, appId, to
const filterQueries = (value, queries) => {
if (value) {
const fuse = new Fuse(queries, { keys: ['name'] });
const fuse = new Fuse(queries, { keys: ['name'], shouldSort: true, threshold: 0.3 });
const results = fuse.search(value);
let filterDataQueries = [];
results.every((result) => {

View file

@ -1,15 +1,17 @@
import React, { useState, useRef, useCallback, useEffect } from 'react';
import React, { useState, useRef, useCallback, useEffect, useContext } from 'react';
import { useEventListener } from '@/_hooks/use-event-listener';
import { Tooltip } from 'react-tooltip';
import { QueryDataPane } from './QueryDataPane';
import QueryManager from '../QueryManager/QueryManager';
import useWindowResize from '@/_hooks/useWindowResize';
import { useQueryPanelStore, useQueryPanelActions } from '@/_stores/queryPanelStore';
import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import { useDataQueriesStore, useDataQueries } from '@/_stores/dataQueriesStore';
import Maximize from '@/_ui/Icon/solidIcons/Maximize';
import { cloneDeep, isEmpty, isEqual } from 'lodash';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { ModuleContext } from '../../_contexts/ModuleContext';
import { useSuperStore } from '../../_stores/superStore';
import cx from 'classnames';
const QueryPanel = ({
dataQueriesChanged,
@ -22,6 +24,7 @@ const QueryPanel = ({
onQueryPaneDragging,
handleQueryPaneExpanding,
}) => {
const moduleName = useContext(ModuleContext);
const { updateQueryPanelHeight } = useQueryPanelActions();
const dataQueries = useDataQueries();
const queryManagerPreferences = useRef(JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {});
@ -37,26 +40,28 @@ const QueryPanel = ({
const [windowSize, isWindowResizing] = useWindowResize();
useEffect(() => {
const queryPanelStoreListner = useQueryPanelStore.subscribe(({ selectedQuery }, prevState) => {
if (isEmpty(prevState?.selectedQuery) || isEmpty(selectedQuery)) {
return;
}
const queryPanelStoreListner = useSuperStore
.getState()
.modules[moduleName].useQueryPanelStore.subscribe(({ selectedQuery }, prevState) => {
if (isEmpty(prevState?.selectedQuery) || isEmpty(selectedQuery)) {
return;
}
if (prevState?.selectedQuery?.id !== selectedQuery.id) {
return;
}
if (prevState?.selectedQuery?.id !== selectedQuery.id) {
return;
}
//removing updated_at since this value changes whenever the data is updated in the BE
const formattedQuery = cloneDeep(selectedQuery);
delete formattedQuery.updated_at;
//removing updated_at since this value changes whenever the data is updated in the BE
const formattedQuery = cloneDeep(selectedQuery);
delete formattedQuery.updated_at;
const formattedPrevQuery = cloneDeep(prevState?.selectedQuery || {});
delete formattedPrevQuery.updated_at;
const formattedPrevQuery = cloneDeep(prevState?.selectedQuery || {});
delete formattedPrevQuery.updated_at;
if (!isEqual(formattedQuery, formattedPrevQuery)) {
useDataQueriesStore.getState().actions.saveData(selectedQuery);
}
});
if (!isEqual(formattedQuery, formattedPrevQuery)) {
useSuperStore.getState().modules[moduleName].useDataQueriesStore.getState().actions.saveData(selectedQuery);
}
});
return queryPanelStoreListner;
}, []);
@ -145,7 +150,7 @@ const QueryPanel = ({
}, []);
return (
<>
<div className={cx({ 'dark-theme theme-dark': darkMode })}>
<div
className="query-pane"
style={{
@ -212,7 +217,7 @@ const QueryPanel = ({
</div>
</div>
<Tooltip id="tooltip-for-query-panel-footer-btn" className="tooltip" />
</>
</div>
);
};

View file

@ -1,5 +1,5 @@
/* eslint-disable import/no-named-as-default */
import React, { useCallback, useState, useEffect, useRef } from 'react';
import React, { useCallback, useState, useEffect, useRef, useContext } from 'react';
import { useDrop, useDragLayer } from 'react-dnd';
import { ItemTypes } from './ItemTypes';
import { DraggableBox } from './DraggableBox';
@ -15,9 +15,10 @@ import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { useMounted } from '@/_hooks/use-mount';
import { useEditorStore } from '@/_stores/editorStore';
import { useSuperStore } from '@/_stores/superStore';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import { ModuleContext } from '../_contexts/ModuleContext';
import { isPDFSupported } from '@/_stores/utils';
const NO_OF_GRIDS = 43;
@ -63,6 +64,8 @@ export const SubContainer = ({
Listview: 'listItem',
});
const moduleName = useContext(ModuleContext);
const customResolverVariable = widgetResolvables[parentComponent?.component];
const currentState = useCurrentState();
const { enableReleasedVersionPopupState, isVersionReleased } = useAppVersionStore(
@ -410,7 +413,9 @@ export const SubContainer = ({
let newBoxes = { ...boxes };
const subContainerHeight = canvasBounds.height - 30;
const selectedComponents = useEditorStore.getState().selectedComponents;
const selectedComponents = useSuperStore
.getState()
.modules[moduleName].useEditorStore.getState().selectedComponents;
if (selectedComponents) {
for (const selectedComponent of selectedComponents) {

View file

@ -39,6 +39,8 @@ import { shallow } from 'zustand/shallow';
import { useAppDataActions, useAppDataStore } from '@/_stores/appDataStore';
import { getPreviewQueryParams, redirectToErrorPage } from '@/_helpers/routes';
import { ERROR_TYPES } from '@/_helpers/constants';
import { useSuperStore } from '../_stores/superStore';
import { ModuleContext, useModuleName } from '../_contexts/ModuleContext';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import TooljetLogoIcon from '@/_ui/Icon/solidIcons/TooljetLogoIcon';
import TooljetLogoText from '@/_ui/Icon/solidIcons/TooljetLogoText';
@ -46,8 +48,11 @@ import ViewerSidebarNavigation from './Viewer/ViewerSidebarNavigation';
import MobileHeader from './Viewer/MobileHeader';
import DesktopHeader from './Viewer/DesktopHeader';
import './Viewer/viewer.scss';
import useAppDarkMode from '@/_hooks/useAppDarkMode';
class ViewerComponent extends React.Component {
static contextType = ModuleContext;
constructor(props) {
super(props);
@ -55,7 +60,7 @@ class ViewerComponent extends React.Component {
const slug = this.props.params.slug;
this.subscription = null;
this.props.setEditorOrViewer('viewer');
this.state = {
slug,
deviceWindowWidth,
@ -77,6 +82,7 @@ class ViewerComponent extends React.Component {
navigate: this.props.navigate,
switchPage: this.switchPage,
currentPageId: this.state.currentPageId,
moduleName: this.context,
};
}
@ -87,8 +93,16 @@ class ViewerComponent extends React.Component {
appDefData.homePageId = data.homePageId;
appDefData.showViewerNavigation = data.showViewerNavigation;
}
useAppVersionStore.getState().actions.updateEditingVersion(data.editing_version);
useAppVersionStore.getState().actions.updateReleasedVersionId(data.currentVersionId);
const appMode = data.globalSettings?.appMode || data?.editing_version?.globalSettings?.appMode;
useSuperStore
.getState()
.modules[this.context].useAppVersionStore.getState()
.actions.updateEditingVersion(data.editing_version);
useSuperStore
.getState()
.modules[this.context].useAppVersionStore.getState()
.actions.updateReleasedVersionId(data.currentVersionId);
useSuperStore.getState().modules[this.context].useEditorStore.getState().actions.setAppMode(appMode);
this.setState({
app: data,
isLoading: false,
@ -161,7 +175,7 @@ class ViewerComponent extends React.Component {
const currentPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id ?? homePageId;
const currentPage = pages.find((page) => page.id === currentPageId);
useDataQueriesStore.getState().actions.setDataQueries(dataQueries);
useSuperStore.getState().modules[this.context].useDataQueriesStore.getState().actions.setDataQueries(dataQueries);
this.props.setCurrentState({
queries: queryState,
components: {},
@ -183,7 +197,10 @@ class ViewerComponent extends React.Component {
...variables,
...constants,
});
useEditorStore.getState().actions.toggleCurrentLayout(this.props?.currentLayout == 'mobile' ? 'mobile' : 'desktop');
useSuperStore
.getState()
.modules[this.context].useEditorStore.getState()
.actions.toggleCurrentLayout(this.props?.currentLayout == 'mobile' ? 'mobile' : 'desktop');
this.props.updateState({ events: data.events ?? [] });
this.setState(
{
@ -204,7 +221,7 @@ class ViewerComponent extends React.Component {
() => {
const components = appDefData?.pages[currentPageId]?.components || {};
computeComponentState(components).then(async () => {
computeComponentState(components, this.context).then(async () => {
this.setState({ initialComputationOfStateDone: true, defaultComponentStateComputed: true });
this.runQueries(dataQueries);
@ -274,7 +291,10 @@ class ViewerComponent extends React.Component {
fetchAppVersions = async (appId) => {
const appVersions = await appEnvironmentService.getVersionsByEnvironment(appId);
useAppVersionStore.getState().actions.setAppVersions(appVersions.appVersions);
useSuperStore
.getState()
.modules[this.context].useAppVersionStore.getState()
.actions.setAppVersions(appVersions.appVersions);
};
loadApplicationBySlug = (slug, authentication_failed = false) => {
@ -331,7 +351,10 @@ class ViewerComponent extends React.Component {
};
updateQueryConfirmationList = (queryConfirmationList) =>
useEditorStore.getState().actions.updateQueryConfirmationList(queryConfirmationList);
useSuperStore
.getState()
.modules[this.context].useEditorStore.getState()
.actions.updateQueryConfirmationList(queryConfirmationList);
setupViewer() {
this.subscription = authenticationService.currentSession.subscribe((currentSession) => {
@ -341,7 +364,7 @@ class ViewerComponent extends React.Component {
if (currentSession?.load_app && slug) {
if (currentSession?.group_permissions) {
useAppDataStore.getState().actions.setAppId(appId);
useSuperStore.getState().modules[this.context].useAppDataStore.getState().actions.setAppId(appId);
const currentUser = currentSession.current_user;
const userVars = {
@ -390,7 +413,10 @@ class ViewerComponent extends React.Component {
componentDidMount() {
this.setupViewer();
const isMobileDevice = this.state.deviceWindowWidth < 600;
useEditorStore.getState().actions.toggleCurrentLayout(isMobileDevice ? 'mobile' : 'desktop');
useSuperStore
.getState()
.modules[this.context].useEditorStore.getState()
.actions.toggleCurrentLayout(isMobileDevice ? 'mobile' : 'desktop');
window.addEventListener('message', this.handleMessage);
}
@ -400,8 +426,14 @@ class ViewerComponent extends React.Component {
this.loadApplicationBySlug(this.props.params.slug);
}
if (prevProps.currentLayout !== this.props.currentLayout) {
if (this.props.id && useAppVersionStore.getState()?.editingVersion?.id) {
this.loadApplicationByVersion(this.props.id, useAppVersionStore.getState().editingVersion.id);
if (
this.props.id &&
useSuperStore.getState().modules[this.context].useAppVersionStore.getState()?.editingVersion?.id
) {
this.loadApplicationByVersion(
this.props.id,
useSuperStore.getState().modules[this.context].useAppVersionStore.getState().editingVersion.id
);
}
}
@ -453,7 +485,10 @@ class ViewerComponent extends React.Component {
name: targetPage.name,
},
async () => {
computeComponentState(this.state.appDefinition?.pages[this.state.currentPageId].components).then(async () => {
computeComponentState(
this.state.appDefinition?.pages[this.state.currentPageId].components,
this.context
).then(async () => {
const currentPageEvents = this.state.events.filter(
(event) => event.target === 'page' && event.sourceId === this.state.currentPageId
);
@ -572,6 +607,8 @@ class ViewerComponent extends React.Component {
dataQueries,
canvasWidth,
} = this.state;
const moduleName = this.context;
const currentCanvasWidth = canvasWidth;
const queryConfirmationList = this.props?.queryConfirmationList ?? [];
const canvasMaxWidth = this.computeCanvasMaxWidth();
@ -606,7 +643,12 @@ class ViewerComponent extends React.Component {
);
} else {
return (
<div className={`viewer wrapper ${this.props.currentLayout === 'mobile' ? 'mobile-layout' : ''}`}>
<div
className={cx('viewer wrapper', {
'mobile-layout': this.props.currentLayout,
'theme-dark dark-theme': this.props.darkMode,
})}
>
<Confirm
show={queryConfirmationList.length > 0}
message={'Do you want to run this query?'}
@ -616,6 +658,7 @@ class ViewerComponent extends React.Component {
onCancel={() => onQueryConfirmOrCancel(this.getViewerRef(), queryConfirmationList[0], false, 'view')}
queryConfirmationData={queryConfirmationList[0]}
key={queryConfirmationList[0]?.queryName}
darkMode={this.props.darkMode}
/>
<DndProvider backend={HTML5Backend}>
{this.props.currentLayout !== 'mobile' && (
@ -658,7 +701,6 @@ class ViewerComponent extends React.Component {
<ViewerSidebarNavigation
showHeader={!appDefinition.globalSettings?.hideHeader && isAppLoaded}
isMobileDevice={this.props.currentLayout === 'mobile'}
canvasBackgroundColor={this.computeCanvasBackgroundColor()}
pages={pages}
currentPageId={this.state?.currentPageId ?? this.state.appDefinition?.homePageId}
switchPage={this.switchPage}
@ -712,7 +754,6 @@ class ViewerComponent extends React.Component {
appDefinitionChanged={() => false} // function not relevant in viewer
snapToGrid={true}
appLoading={isLoading}
darkMode={this.props.darkMode}
onEvent={this.handleEvent}
mode="view"
deviceWindowWidth={isMobilePreviewMode ? '450px' : deviceWindowWidth}
@ -724,12 +765,15 @@ class ViewerComponent extends React.Component {
onComponentClick(this, id, component, 'view');
}}
onComponentOptionChanged={(component, optionName, value) => {
return onComponentOptionChanged(component, optionName, value);
return onComponentOptionChanged(this.context, component, optionName, value);
}}
onComponentOptionsChanged={onComponentOptionsChanged}
onComponentOptionsChanged={(...props) =>
onComponentOptionsChanged(this.context, ...props)
}
canvasWidth={this.getCanvasWidth()}
dataQueries={dataQueries}
currentPageId={this.state.currentPageId}
darkMode={this.props.darkMode}
/>
)}
</>
@ -739,7 +783,8 @@ class ViewerComponent extends React.Component {
className="powered-with-tj"
onClick={() => {
const url = `https://tooljet.com/?utm_source=powered_by_banner&utm_medium=${
useAppDataStore.getState()?.metadata?.instance_id
useSuperStore.getState().modules[this.context].useAppDataStore.getState()?.metadata
?.instance_id
}&utm_campaign=self_hosted`;
window.open(url, '_blank');
}}
@ -774,6 +819,7 @@ const withStore = (Component) => (props) => {
shallow
);
const { updateState } = useAppDataActions();
const { isAppDarkMode } = useAppDarkMode();
return (
<Component
{...props}
@ -782,6 +828,7 @@ const withStore = (Component) => (props) => {
currentLayout={currentLayout}
updateState={updateState}
queryConfirmationList={queryConfirmationList}
darkMode={isAppDarkMode}
/>
);
};

View file

@ -10,6 +10,7 @@ import { redirectToDashboard } from '@/_helpers/routes';
import classNames from 'classnames';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import PreviewSettings from './PreviewSettings';
import { useEditorStore } from '@/_stores/editorStore';
const DesktopHeader = ({ showHeader, appName, changeDarkMode, darkMode, setAppDefinitionFromVersion }) => {
const { isVersionReleased, editingVersion } = useAppVersionStore(
@ -19,6 +20,13 @@ const DesktopHeader = ({ showHeader, appName, changeDarkMode, darkMode, setAppDe
}),
shallow
);
const { showDarkModeToggle } = useEditorStore(
(state) => ({
showDarkModeToggle: state.appMode === 'auto' || !state.appMode,
}),
shallow
);
const _renderAppNameAndLogo = () => (
<div
className={classNames('d-flex', 'align-items-center')}
@ -54,9 +62,11 @@ const DesktopHeader = ({ showHeader, appName, changeDarkMode, darkMode, setAppDe
darkMode={darkMode}
/>
)}
<span className="released-version-no-header-dark-mode-icon">
<DarkModeToggle switchDarkMode={changeDarkMode} darkMode={darkMode} />
</span>
{showDarkModeToggle && (
<span className="released-version-no-header-dark-mode-icon">
<DarkModeToggle switchDarkMode={changeDarkMode} darkMode={darkMode} />
</span>
)}
</>
);
}
@ -76,9 +86,11 @@ const DesktopHeader = ({ showHeader, appName, changeDarkMode, darkMode, setAppDe
darkMode={darkMode}
/>
)}
<div className="d-flex align-items-center">
<DarkModeToggle switchDarkMode={changeDarkMode} darkMode={darkMode} />
</div>
{showDarkModeToggle && (
<div className="d-flex align-items-center">
<DarkModeToggle switchDarkMode={changeDarkMode} darkMode={darkMode} />
</div>
)}
</Header>
);
};

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import _, { isEmpty } from 'lodash';
// eslint-disable-next-line import/no-unresolved
import LogoIcon from '@assets/images/rocket.svg';
@ -11,6 +11,7 @@ import classNames from 'classnames';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import PreviewSettings from './PreviewSettings';
import MobileNavigationMenu from './MobileNavigationMenu';
import { useEditorStore } from '@/_stores/editorStore';
const MobileHeader = ({
showHeader,
@ -30,6 +31,12 @@ const MobileHeader = ({
}),
shallow
);
const { showDarkModeToggle } = useEditorStore(
(state) => ({
showDarkModeToggle: state.appMode === 'auto',
}),
shallow
);
// Fetch the version parameter from the query string
const searchParams = new URLSearchParams(window.location.search);
@ -67,6 +74,7 @@ const MobileHeader = ({
darkMode={darkMode}
changeDarkMode={changeDarkMode}
showHeader={showHeader}
showDarkModeToggle={showDarkModeToggle}
/>
);
@ -80,6 +88,7 @@ const MobileHeader = ({
);
const _renderDarkModeBtn = (args) => {
if (!showDarkModeToggle) return null;
const styles = args?.styles ?? {};
return (
<span

View file

@ -6,7 +6,7 @@ import { DarkModeToggle } from '@/_components/DarkModeToggle';
import Header from './Header';
import Cross from '@/_ui/Icon/solidIcons/Cross';
const MobileNavigationMenu = ({ pages, switchPage, currentPageId, darkMode, changeDarkMode }) => {
const MobileNavigationMenu = ({ pages, switchPage, currentPageId, darkMode, changeDarkMode, showDarkModeToggle }) => {
const [hamburgerMenuOpen, setHamburgerMenuOpen] = useState(false);
const handlepageSwitch = (pageId) => {
setHamburgerMenuOpen(false);
@ -102,22 +102,24 @@ const MobileNavigationMenu = ({ pages, switchPage, currentPageId, darkMode, chan
</div>
</div>
</div>
<div>
<hr className="m-0 mb-3" />
<div className="d-flex justify-content-center">
<div
className={`d-flex align-items-center justify-content-center`}
style={{ border: '1px solid var(--slate7)', width: 'calc(100% - 20px)' }}
>
<DarkModeToggle
switchDarkMode={changeDarkMode}
darkMode={darkMode}
showText={true}
tooltipPlacement={'top'}
/>
{showDarkModeToggle && (
<div>
<hr className="m-0 mb-3" />
<div className="d-flex justify-content-center">
<div
className={`d-flex align-items-center justify-content-center`}
style={{ border: '1px solid var(--slate7)', width: 'calc(100% - 20px)' }}
>
<DarkModeToggle
switchDarkMode={changeDarkMode}
darkMode={darkMode}
showText={true}
tooltipPlacement={'top'}
/>
</div>
</div>
</div>
</div>
)}
</MobileMenu>
</>
);

View file

@ -32,6 +32,7 @@ const PreviewSettings = ({ isMobileLayout, setAppDefinitionFromVersion, showHead
onVersionDelete={noop}
isEditable={false}
isViewer
darkMode={darkMode}
/>
)}
</span>
@ -98,12 +99,13 @@ const PreviewSettings = ({ isMobileLayout, setAppDefinitionFromVersion, showHead
onVersionDelete={noop}
isEditable={false}
isViewer
darkMode={darkMode}
/>
)}
</span>
<div className="d-flex p-2 align-items-center">
<span style={{ marginRight: '24px' }}>layout</span>
<HeaderActions showToggleLayoutBtn showFullWidth />
<HeaderActions showToggleLayoutBtn showFullWidth darkMode={darkMode} />
</div>
</Offcanvas.Body>
)}

View file

@ -123,7 +123,7 @@
.canvas-container::-webkit-scrollbar {
width: 0;
// width: 0;
background: transparent;
}

View file

@ -27,7 +27,7 @@ export const WidgetManager = function WidgetManager({ componentTypes, zoomLevel,
function filterComponents(value) {
if (value !== '') {
const fuse = new Fuse(componentTypes, { keys: ['component'] });
const fuse = new Fuse(componentTypes, { keys: ['displayName'], shouldSort: true, threshold: 0.4 });
const results = fuse.search(value);
setFilteredComponents(results.map((result) => result.item));
} else {
@ -81,7 +81,7 @@ export const WidgetManager = function WidgetManager({ componentTypes, zoomLevel,
const otherSection = { title: t('widgetManager.others', 'others'), items: [] };
const allWidgets = [];
const commonItems = ['Table', 'Chart', 'Button', 'Text', 'Datepicker'];
const commonItems = ['Table', 'Button', 'Text', 'TextInput', 'Datepicker', 'Form'];
const formItems = [
'Form',
'TextInput',

View file

@ -33,106 +33,6 @@ export const widgets = [
columns: {
type: 'array',
displayName: 'Table Columns',
// validation: {
// schema: {
// type: 'array',
// element: {
// type: 'union',
// schemas: [
// {
// type: 'object',
// object: {
// columnType: { type: 'string' },
// name: { type: 'string' },
// textWrap: { type: 'string' },
// key: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
// textColor: { type: 'string' },
// regex: { type: 'string' },
// minLength: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
// maxLength: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
// customRule: { type: 'string' },
// },
// },
// {
// type: 'object',
// object: {
// columnType: { type: 'string' },
// name: { type: 'string' },
// key: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
// },
// isEditable: { type: 'boolean' },
// },
// {
// type: 'object',
// object: {
// columnType: { type: 'string' },
// name: { type: 'string' },
// activeColor: { type: 'string' },
// isEditable: { type: 'boolean' },
// },
// },
// {
// type: 'object',
// object: {
// columnType: { type: 'string' },
// name: { type: 'string' },
// key: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
// values: {
// type: 'union',
// schemas: [
// { type: 'array', element: { type: 'string' } },
// { type: 'array', element: { type: 'number' } },
// ],
// },
// labels: {
// type: 'union',
// schemas: [
// { type: 'array', element: { type: 'string' } },
// { type: 'array', element: { type: 'number' } },
// ],
// },
// },
// isEditable: { type: 'boolean' },
// },
// {
// type: 'object',
// object: {
// columnType: { type: 'string' },
// name: { type: 'string' },
// key: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
// values: {
// type: 'union',
// schemas: [
// { type: 'array', element: { type: 'string' } },
// { type: 'array', element: { type: 'number' } },
// ],
// },
// labels: {
// type: 'union',
// schemas: [
// { type: 'array', element: { type: 'string' } },
// { type: 'array', element: { type: 'number' } },
// ],
// },
// },
// isEditable: { type: 'boolean' },
// },
// {
// type: 'object',
// object: {
// columnType: { type: 'string' },
// name: { type: 'string' },
// key: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
// dateFormat: { type: 'string' },
// parseDateFormat: { type: 'string' },
// isTimeChecked: { type: 'boolean' },
// isEditable: { type: 'boolean' },
// },
// },
// ],
// },
// },
// },
},
useDynamicColumn: {
type: 'toggle',
@ -221,7 +121,6 @@ export const widgets = [
{ displayName: 'Client side', value: 'clientSide' },
{ displayName: 'Server side', value: 'serverSide' },
],
// defaultValue: 'clientSide',
validation: {
schema: { type: 'boolean' },
},
@ -236,7 +135,6 @@ export const widgets = [
{ displayName: 'Client side', value: 'clientSide' },
{ displayName: 'Server side', value: 'serverSide' },
],
// defaultValue: 'clientSide',
},
serverSideFilter: {
type: 'clientServerSwitch',
@ -337,13 +235,27 @@ export const widgets = [
schema: { type: 'boolean' },
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
},
},
},
others: {
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop ' },
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
},
defaultSize: {
width: 28.86,
width: 35,
height: 456,
},
events: {
@ -365,57 +277,112 @@ export const widgets = [
validation: {
schema: { type: 'string' },
},
},
actionButtonRadius: {
type: 'code',
displayName: 'Action button radius',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'boolean' }] },
},
accordian: 'Data',
},
tableType: {
type: 'select',
displayName: 'Table type',
displayName: 'Row style',
options: [
{ name: 'Bordered', value: 'table-bordered' },
{ name: 'Regular', value: 'table-classic' },
{ name: 'Bordered', value: 'table-bordered' },
{ name: 'Striped', value: 'table-striped' },
],
validation: {
schema: { type: 'string' },
},
accordian: 'Data',
},
cellSize: {
type: 'select',
displayName: 'Cell size',
displayName: 'Cell height',
options: [
{ name: 'Condensed', value: 'condensed' },
{ name: 'Regular', value: 'regular' },
{ name: 'Condensed', value: 'condensed' },
],
validation: {
schema: { type: 'string' },
},
accordian: 'Data',
},
contentWrap: {
type: 'toggle',
showLabel: false,
toggleLabel: 'Content wrap',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'boolean' }] },
},
accordian: 'Data',
},
maxRowHeight: {
type: 'switch',
displayName: 'Max row height',
validation: { schema: { type: 'string' } },
accordian: 'Data',
options: [
{ displayName: 'Auto', value: 'auto' },
{ displayName: 'Custom', value: 'custom' },
],
conditionallyRender: {
key: 'contentWrap',
value: true,
},
},
maxRowHeightValue: {
type: 'tableRowHeightInput',
showLabel: false,
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'boolean' }] },
},
accordian: 'Data',
conditionallyRender: [
{
key: 'maxRowHeight',
value: 'custom',
},
{
key: 'contentWrap',
value: true,
},
],
},
actionButtonRadius: {
type: 'numberInput',
displayName: 'Button radius',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'boolean' }] },
},
accordian: 'Action button',
},
borderRadius: {
type: 'code',
type: 'numberInput',
displayName: 'Border radius',
validation: {
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
},
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] } },
accordian: 'Container',
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
borderColor: {
type: 'color',
displayName: 'Border',
validation: {
schema: { type: 'boolean' },
schema: { type: 'string' },
defaultValue: false,
},
accordian: 'Container',
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
},
boxShadow: {
type: 'boxShadow',
displayName: 'Box Shadow',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] } },
accordian: 'Container',
},
padding: {
type: 'switch',
displayName: 'Padding',
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] } },
options: [
{ displayName: 'Default', value: 'default' },
{ displayName: 'None', value: 'none' },
],
accordian: 'Container',
},
},
exposedVariables: {
@ -505,7 +472,7 @@ export const widgets = [
loadingState: { value: '{{false}}' },
data: {
value:
"{{ [ \n\t\t{ id: 1, name: 'Sarah', email: 'sarah@example.com'}, \n\t\t{ id: 2, name: 'Lisa', email: 'lisa@example.com'}, \n\t\t{ id: 3, name: 'Sam', email: 'sam@example.com'}, \n\t\t{ id: 4, name: 'Jon', email: 'jon@example.com'} \n] }}",
"{{ [ \n\t\t{ id: 1, name: 'Olivia Nguyen', email: 'olivia.nguyen@example.com', date: '15/05/2022', mobile_number: 9876543210, interest: ['Reading', 'Traveling','Photography'], photo: 'https://reqres.in/img/faces/7-image.jpg' }, \n\t\t{ id: 2, name: 'Liam Patel', email: 'liam.patel@example.com', date: '20/09/2021', mobile_number: 8765432109, interest: ['Cooking','Gardening','Hiking'], photo: 'https://reqres.in/img/faces/5-image.jpg' }, \n\t\t{ id: 3, name: 'Sophia Reyes', email: 'sophia.reyes@example.com', date: '01/01/2023', mobile_number: 7654321098, interest: ['Music','Dancing','Crafting'], photo: 'https://reqres.in/img/faces/3-image.jpg' }, \n\t\t{ id: 4, name: 'Jacob Hernandez', email: 'jacob.hernandez@example.com', date: '10/11/2022', mobile_number: 6543210987, interest: ['Reading', 'Traveling', 'Volunteering'], photo: 'https://reqres.in/img/faces/1-image.jpg' }, \n\t\t{ id: 5, name: 'William Sanchez', email: 'william.sanchez@example.com', date: '07/01/2021', mobile_number: 4321098765, interest: ['Music', 'Dancing', 'Hiking'], photo: 'https://reqres.in/img/faces/4-image.jpg' }, \n\t\t{ id: 6, name: 'Ethan Morales', email: 'ethan.morales@example.com', date: '05/11/2021', mobile_number: 2109876543, interest: ['Cooking', 'Traveling', 'Photography'], photo: 'https://reqres.in/img/faces/6-image.jpg' }, \n\t\t{ id: 7, name: 'Mia Tiana', email: 'mia.tiana@example.com', date: '21/11/2022', mobile_number: 1098705217, interest: ['Music', 'Gardening', 'Hiking'], photo: 'https://reqres.in/img/faces/2-image.jpg' }, \n\t\t{ id: 8, name: 'Lucas Ramirez', email: 'lucas.ramirez@example.com', date: '31/03/2023', mobile_number: 9876543210, interest: ['Reading', 'Dancing', 'Crafting'], photo: 'https://reqres.in/img/faces/9-image.jpg' }, \n\t\t{ id: 9, name: 'Alexander Vela', email: 'alexander.vela@example.com', date: '07/09/2022', mobile_number: 7654321098, interest: ['Music','Gardening','Photography'], photo: 'https://reqres.in/img/faces/8-image.jpg' }, \n\t\t{ id: 10, name: 'Michael Reyes', email: 'michael.reyes@example.com', date: '25/12/2021', mobile_number: 5432109876, interest: ['Cooking','Crafting','Volunteering'], photo: 'https://reqres.in/img/faces/10-image.jpg' } \n] }}",
},
useDynamicColumn: { value: '{{false}}' },
columnData: {
@ -524,6 +491,7 @@ export const widgets = [
showDownloadButton: { value: '{{true}}' },
showFilterButton: { value: '{{true}}' },
autogenerateColumns: { value: true, generateNestedColumns: true },
isAllColumnsEditable: { value: '{{false}}' },
columns: {
value: [
{
@ -531,18 +499,106 @@ export const widgets = [
id: 'e3ecbf7fa52c4d7210a93edb8f43776267a489bad52bd108be9588f790126737',
autogenerated: true,
fxActiveFields: [],
columnSize: 30,
columnType: 'string',
},
{
name: 'photo',
key: 'photo',
id: 'f23b7d134b2e490ea41e3bb8eeb8c8e37472af243bf6b70d5af294482097e3a2',
autogenerated: true,
fxActiveFields: [],
columnType: 'image',
objectFit: 'contain',
borderRadius: '100',
columnSize: 70,
},
{
name: 'name',
id: '5d2a3744a006388aadd012fcc15cc0dbcb5f9130e0fbb64c558561c97118754a',
autogenerated: true,
fxActiveFields: [],
columnSize: 130,
columnType: 'string',
},
{
name: 'email',
id: 'afc9a5091750a1bd4760e38760de3b4be11a43452ae8ae07ce2eebc569fe9a7f',
autogenerated: true,
fxActiveFields: [],
columnSize: 230,
columnType: 'string',
},
{
name: 'date',
id: '27b75c8af9d34d1eaa1f9bb7f8f9f7b0abf1823e799748c8bb57e74f53b2c1dc',
autogenerated: true,
fxActiveFields: [],
columnType: 'datepicker',
isTimeChecked: false,
dateFormat: 'DD/MM/YYYY',
parseDateFormat: 'DD/MM/YYYY',
isDateSelectionEnabled: true,
columnSize: 130,
},
{
name: 'mobile_number',
id: '9c2e3c40572a4aefb8e179ee39a0e1ac9dc2b2e6634be56e1c05be13c3d1de56',
autogenerated: true,
fxActiveFields: [],
columnType: 'number',
columnSize: 140,
},
{
name: 'interest',
key: 'interest',
id: 'f23b7d134b2e490ea41e3bb8eeb8c8e37472af243bf6b70d5af294482097e3a1',
autogenerated: true,
fxActiveFields: [],
columnType: 'newMultiSelect',
columnSize: 300,
options: [
{
label: 'Reading',
value: 'Reading',
},
{
label: 'Traveling',
value: 'Traveling',
},
{
label: 'Photography',
value: 'Photography',
},
{
label: 'Music',
value: 'Music',
},
{
label: 'Cooking',
value: 'Cooking',
},
{
label: 'Crafting',
value: 'Crafting',
},
{
label: 'Voluntering',
value: 'Voluntering',
},
{
label: 'Garndening',
value: 'Garndening',
},
{
label: 'Dancing',
value: 'Dancing',
},
{
label: 'Hiking',
value: 'Hiking',
},
],
},
],
},
@ -556,16 +612,21 @@ export const widgets = [
defaultSelectedRow: { value: '{{{"id":1}}}' },
showAddNewRowButton: { value: '{{true}}' },
allowSelection: { value: '{{true}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
events: [],
styles: {
textColor: { value: '#000' },
actionButtonRadius: { value: '0' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
cellSize: { value: 'regular' },
borderRadius: { value: '4' },
borderRadius: { value: '8' },
tableType: { value: 'table-classic' },
maxRowHeight: { value: 'auto' },
maxRowHeightValue: { value: '80px' },
contentWrap: { value: '{{true}}' },
boxShadow: { value: '0px 0px 0px 0px #00000090' },
padding: { value: 'default' },
},
},
},
@ -707,6 +768,7 @@ export const widgets = [
borderRadius: { value: '{{4}}' },
borderColor: { value: '#375FCF' },
disabledState: { value: '{{false}}' },
padding: { value: 'default' },
},
},
},
@ -1556,7 +1618,6 @@ export const widgets = [
value: 'side',
},
},
backgroundColor: {
type: 'color',
displayName: 'Background',
@ -1700,17 +1761,17 @@ export const widgets = [
},
events: [],
styles: {
textColor: { value: '#11181C' },
borderColor: { value: '#6A727C47' },
textColor: { value: '#1B1F24' },
borderColor: { value: '#CCD1D5' },
accentColor: { value: '#4368E3' },
errTextColor: { value: '#DB4324' },
errTextColor: { value: '#D72D39' },
borderRadius: { value: '{{6}}' },
backgroundColor: { value: '#fff' },
iconColor: { value: '#C1C8CD' },
iconColor: { value: '#CFD3D859' },
direction: { value: 'left' },
width: { value: '{{33}}' },
alignment: { value: 'side' },
color: { value: '#11181C' },
color: { value: '#1B1F24' },
auto: { value: '{{true}}' },
padding: { value: 'default' },
boxShadow: { value: '0px 0px 0px 0px #00000040' },
@ -1989,15 +2050,16 @@ export const widgets = [
styles: {
borderRadius: { value: '{{6}}' },
backgroundColor: { value: '#fff' },
borderColor: { value: '#6A727C47' },
borderColor: { value: '#CCD1D5' },
accentColor: { value: '#4368E3' },
errTextColor: { value: '#DB4324' },
textColor: { value: '#232e3c' },
iconColor: { value: '#C1C8CD' },
errTextColor: { value: '#D72D39' },
textColor: { value: '#1B1F24' },
color: { value: '#1B1F24' },
iconColor: { value: '#CFD3D859' },
direction: { value: 'left' },
width: { value: '{{33}}' },
alignment: { value: 'side' },
color: { value: '#11181C' },
auto: { value: '{{true}}' },
padding: { value: 'default' },
boxShadow: { value: '0px 0px 0px 0px #00000040' },
@ -2273,15 +2335,15 @@ export const widgets = [
styles: {
borderRadius: { value: '{{6}}' },
backgroundColor: { value: '#fff' },
borderColor: { value: '#6A727C47' },
borderColor: { value: '#CCD1D5' },
accentColor: { value: '#4368E3' },
errTextColor: { value: '#DB4324' },
textColor: { value: '#11181C' },
iconColor: { value: '#C1C8CD' },
errTextColor: { value: '#D72D39' },
textColor: { value: '#1B1F24' },
iconColor: { value: '#CFD3D859' },
direction: { value: 'left' },
width: { value: '{{33}}' },
alignment: { value: 'side' },
color: { value: '#11181C' },
color: { value: '#1B1F24' },
auto: { value: '{{true}}' },
padding: { value: 'default' },
boxShadow: { value: '0px 0px 0px 0px #00000040' },

View file

@ -66,6 +66,7 @@ class HomePageComponent extends React.Component {
showTemplateLibraryModal: false,
app: {},
showCreateAppModal: false,
showCreateModuleModal: false,
showCreateAppFromTemplateModal: false,
showImportAppModal: false,
showCloneAppModal: false,
@ -135,11 +136,11 @@ class HomePageComponent extends React.Component {
this.fetchFolders();
};
createApp = async (appName) => {
createApp = async (appName, type) => {
let _self = this;
_self.setState({ creatingApp: true });
try {
const data = await appsService.createApp({ icon: sample(iconList), name: appName });
const data = await appsService.createApp({ icon: sample(iconList), name: appName, type });
const workspaceId = getWorkspaceId();
_self.props.navigate(`/${workspaceId}/apps/${data.id}`);
toast.success('App created successfully!');
@ -544,11 +545,11 @@ class HomePageComponent extends React.Component {
};
openCreateAppModal = () => {
this.setState({ showCreateAppModal: true });
this.setState({ showCreateAppModal: true, showCreateModuleModal: true });
};
closeCreateAppModal = () => {
this.setState({ showCreateAppModal: false });
this.setState({ showCreateAppModal: false, showCreateModuleModal: false });
};
render() {
@ -572,6 +573,7 @@ class HomePageComponent extends React.Component {
appToBeDeleted,
app,
showCreateAppModal,
showCreateModuleModal,
showImportAppModal,
fileContent,
fileName,
@ -581,13 +583,13 @@ class HomePageComponent extends React.Component {
return (
<Layout switchDarkMode={this.props.switchDarkMode} darkMode={this.props.darkMode}>
<div className="wrapper home-page">
{showCreateAppModal && (
{(showCreateAppModal || showCreateModuleModal) && (
<AppModal
closeModal={this.closeCreateAppModal}
processApp={this.createApp}
processApp={(name) => this.createApp(name, showCreateAppModal ? 'front-end' : 'module')}
show={this.openCreateAppModal}
title={'Create app'}
actionButton={'+ Create app'}
title={showCreateAppModal ? 'Create app' : 'Create module'}
actionButton={showCreateAppModal ? '+ Create app' : '+ Create module'}
actionLoadingButton={'Creating'}
/>
)}

View file

@ -3,11 +3,15 @@ import './list.scss';
import ListGroup from 'react-bootstrap/ListGroup';
import { OverlayTrigger, Popover } from 'react-bootstrap';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import classNames from 'classnames';
import Edit from '@/_ui/Icon/bulkIcons/Edit';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import MoreVertical from '@/_ui/Icon/solidIcons/MoreVertical';
import SortableList from '@/_components/SortableList';
import { DeprecatedColumnTooltip } from '@/Editor/Inspector/Components/Table/ColumnManager/DeprecatedColumnTypeMsg';
import Icons from '@/_ui/Icon/solidIcons/index';
function List({ children, ...restProps }) {
return <ListGroup {...restProps}>{children}</ListGroup>;
}
@ -22,11 +26,16 @@ function ListItem({
onMenuOptionClick,
isEditable,
isDraggable,
deleteIconOutsideMenu = false,
showCopyColumnOption = false,
showVisibilityIcon = false,
isColumnVisible = true,
columnType,
isDeprecated,
...restProps
}) {
const [isHovered, setIsHovered] = useState(false);
const [showActionsMenu, setShowActionsMenu] = useState(false);
return (
<div>
<ListGroup.Item
@ -54,8 +63,21 @@ function ListItem({
<Edit width={16} />
</span>
)}
{showVisibilityIcon && !isColumnVisible && (
<span style={{ marginLeft: '8px' }}>
<SolidIcon name="eyedisable" width={16} />
</span>
)}
{isDeprecated && (
<DeprecatedColumnTooltip columnType={columnType}>
<span className={'list-item-deprecated-column-type'}>
<Icons name={'warning'} height={14} width={14} fill="#DB4324" />
</span>
</DeprecatedColumnTooltip>
)}
</div>
<div className="col-auto">
<div className="col-auto d-flex align-items-center custom-gap-4">
<OverlayTrigger
trigger={'click'}
placement={'bottom-end'}
@ -97,12 +119,7 @@ function ListItem({
}}
>
{enableActionsMenu && isHovered && (
<ButtonSolid
variant="tertiary"
size="xs"
className={'list-menu-option-btn'}
// data-cy={'page-menu'}
>
<ButtonSolid variant="tertiary" size="xs" className={'list-menu-option-btn'}>
<span>
<MoreVertical fill={'var(--slate12)'} width={'20'} />
</span>
@ -110,6 +127,38 @@ function ListItem({
)}
</span>
</OverlayTrigger>
{showCopyColumnOption && isHovered && (
<ButtonSolid
variant="ghostBlack"
size="xs"
className={'copy-column-icon'}
// data-cy={'page-menu'}
onClick={(e) => {
e.stopPropagation();
onMenuOptionClick(primaryText, 'copyColumn');
}}
>
<span className="d-flex">
<SolidIcon name="copy" fill={'var(--icons-strong)'} width={16} />
</span>
</ButtonSolid>
)}
{deleteIconOutsideMenu && isHovered && (
<ButtonSolid
variant="danger"
size="xs"
className={'delete-icon-btn'}
// data-cy={'page-menu'}
onClick={(e) => {
e.stopPropagation();
onMenuOptionClick(primaryText, 'Delete');
}}
>
<span className="d-flex">
<Trash fill={'var(--tomato9)'} width={16} />
</span>
</ButtonSolid>
)}
</div>
</div>
</ListGroup.Item>

View file

@ -75,8 +75,41 @@ button:focus:not(:focus-visible) {
}
}
.list-menu-option-btn {
.list-menu-option-btn{
padding: 0px !important;
background-color: var(--base);
border: 1px solid transparent;
}
.delete-icon-btn{
padding: 2px !important;
display: flex;
border: 1px solid var(--tomato7);
background-color: var(--base);
}
.copy-column-icon{
padding: 2px !important;
display: flex;
border: 1px solid var(--borders-default);
background-color: var(--surfaces-surface-01);
box-shadow: 0px 1px 0px 0px var(--elevation-000-box-shadow);
&:hover{
background: var(--borders-strong) !important;
border: 1px solid var(--borders-strong) !important;
}
&:focus{
background-color: var(--surfaces-surface-01) !important;
border: 1px solid var(--borders-default) !important;
box-shadow: 0px 0px 0px 4px var(--interactive-overlays-focus-outline) 0px 0px 0px 2px var(--surfaces-surface-01) !important;
}
&:active{
background-color: var(--borders-strong) !important;
border: 1px solid var(--borders-strong) !important;
}
}
.list-item-deprecated-column-type {
margin-left: 8px;
svg {
margin-bottom: 2px
}
}

View file

@ -0,0 +1,29 @@
import React from 'react';
// eslint-disable-next-line import/no-unresolved
import DatePickerComponent from 'react-datepicker';
import './timepicker.scss';
import cx from 'classnames';
const Timepicker = ({ timeFormat, onChange, selected, maxTime, minTime, darkMode, ...props }) => {
return (
<div className="tj-timepicker">
<DatePickerComponent
{...props}
selected={selected}
onChange={onChange}
showTimeSelect
showTimeSelectOnly
timeIntervals={15}
timeCaption="Time"
dateFormat={timeFormat}
maxTime={maxTime}
timeFormat={timeFormat}
minTime={minTime}
timeInputLabel=""
popperClassName={cx('tj-timepicker-popper', { 'theme-dark dark-theme': darkMode })}
/>
</div>
);
};
export default Timepicker;

View file

@ -0,0 +1,23 @@
.tj-timepicker {
input {
background-color: var(--base);
border: 1px solid var(--slate7) !important;
}
}
.tj-timepicker-popper {
height: 28px !important;
border: 1px solid var(--tj-text-input-widget-border-default);
padding: 10px;
background-color: transparent;
color: var(--text-primary);
.react-datepicker__time-container {
right: 0px;
.react-datepicker__time {
background-color: var(--base);
}
}
}

View file

@ -3,6 +3,7 @@ import { useSpring, animated } from 'react-spring';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Tooltip from 'react-bootstrap/Tooltip';
import { useTranslation } from 'react-i18next';
import classnames from 'classnames';
export const DarkModeToggle = function DarkModeToggle({
darkMode = false,
@ -13,6 +14,7 @@ export const DarkModeToggle = function DarkModeToggle({
const toggleDarkMode = () => {
switchDarkMode(!darkMode);
};
const { t } = useTranslation();
const properties = {
sun: {
@ -51,6 +53,7 @@ export const DarkModeToggle = function DarkModeToggle({
<OverlayTrigger
placement={tooltipPlacement}
delay={{ show: 250, hide: 400 }}
trigger={'hover'}
overlay={
<Tooltip id="button-tooltip">
{darkMode
@ -60,7 +63,7 @@ export const DarkModeToggle = function DarkModeToggle({
}
>
<div
className="unstyled-button dark-theme-toggle-btn sidebar-svg-icon left-sidebar-item "
className={classnames('unstyled-button dark-theme-toggle-btn sidebar-svg-icon left-sidebar-item')}
onClick={toggleDarkMode}
>
<animated.svg

View file

@ -0,0 +1,11 @@
import { createContext, useContext } from 'react';
export const ModuleContext = createContext(null);
export const useModuleName = () => {
const moduleName = useContext(ModuleContext);
if (!moduleName) throw Error('useModuleName can only be used inside a ModuleContext');
return moduleName;
};

View file

@ -34,6 +34,7 @@ import { useAppVersionStore } from '@/_stores/appVersionStore';
import { camelizeKeys } from 'humps';
import { useAppDataStore } from '@/_stores/appDataStore';
import { useEditorStore } from '@/_stores/editorStore';
import { useSuperStore } from '@/_stores/superStore';
const ERROR_TYPES = Object.freeze({
ReferenceError: 'ReferenceError',
@ -61,9 +62,9 @@ export function setCurrentStateAsync(_ref, changes) {
});
}
export function onComponentOptionsChanged(component, options) {
export function onComponentOptionsChanged(moduleName, component, options) {
const componentName = component.name;
const components = getCurrentState().components;
const components = getCurrentState(moduleName).components;
let componentData = components[componentName];
componentData = componentData || {};
@ -71,27 +72,36 @@ export function onComponentOptionsChanged(component, options) {
componentData[option[0]] = option[1];
}
useCurrentStateStore.getState().actions.setCurrentState({
components: { ...components, [componentName]: componentData },
});
useSuperStore
.getState()
.modules[moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
components: { ...components, [componentName]: componentData },
});
return Promise.resolve();
}
export function onComponentOptionChanged(component, option_name, value) {
export function onComponentOptionChanged(moduleName, component, option_name, value) {
const componentName = component.name;
const components = getCurrentState().components;
const components = getCurrentState(moduleName).components;
let componentData = components[componentName];
componentData = componentData || {};
componentData[option_name] = value;
if (option_name !== 'id') {
useCurrentStateStore.getState().actions.setCurrentState({
components: { ...components, [componentName]: componentData },
});
useSuperStore
.getState()
.modules[moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
components: { ...components, [componentName]: componentData },
});
} else if (!componentData?.id) {
useCurrentStateStore.getState().actions.setCurrentState({
components: { ...components, [componentName]: componentData },
});
useSuperStore
.getState()
.modules[moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
components: { ...components, [componentName]: componentData },
});
}
return Promise.resolve();
@ -141,15 +151,15 @@ const evaluatePythonCode = async (options) => {
run: () => actions.runQuery(key),
getData: () => {
return getCurrentState().queries[key].data;
return currentState.queries[key].data;
},
getRawData: () => {
return getCurrentState().queries[key].rawData;
return currentState.queries[key].rawData;
},
getloadingState: () => {
return getCurrentState().queries[key].isLoading;
return currentState.queries[key].isLoading;
},
};
}
@ -223,7 +233,7 @@ export async function runTransformation(
let result = [];
const currentState = getCurrentState() || {};
const currentState = getCurrentState(_ref.moduleName) || {};
if (transformationLanguage === 'python') {
result = await runPythonTransformation(currentState, data, transformation, query, mode);
@ -304,13 +314,13 @@ function showModal(_ref, modal, show) {
const modalMeta = _ref.appDefinition.pages[_ref.currentPageId].components[modalId]; //! NeedToFix
const _components = {
...getCurrentState().components,
...getCurrentState(_ref.moduleName).components,
[modalMeta.component.name]: {
...getCurrentState().components[modalMeta.component.name],
...getCurrentState(_ref.moduleName).components[modalMeta.component.name],
show: show,
},
};
useCurrentStateStore.getState().actions.setCurrentState({
useSuperStore.getState().modules[_ref.moduleName].useCurrentStateStore.getState().actions.setCurrentState({
components: _components,
});
return Promise.resolve();
@ -350,14 +360,19 @@ export const executeAction = debounce(executeActionWithDebounce);
function executeActionWithDebounce(_ref, event, mode, customVariables) {
if (event) {
if (event.runOnlyIf) {
const shouldRun = resolveReferences(event.runOnlyIf, getCurrentState(), undefined, customVariables);
const shouldRun = resolveReferences(
event.runOnlyIf,
getCurrentState(_ref.moduleName),
undefined,
customVariables
);
if (!shouldRun) {
return false;
}
}
switch (event.actionId) {
case 'show-alert': {
const message = resolveReferences(event.message, getCurrentState(), undefined, customVariables);
const message = resolveReferences(event.message, getCurrentState(_ref.moduleName), undefined, customVariables);
switch (event.alertType) {
case 'success':
case 'error':
@ -381,11 +396,15 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
const resolvedParams = {};
if (params) {
Object.keys(params).map(
(param) => (resolvedParams[param] = resolveReferences(params[param], getCurrentState(), undefined))
(param) =>
(resolvedParams[param] = resolveReferences(params[param], getCurrentState(_ref.moduleName), undefined))
);
}
const name =
useDataQueriesStore.getState().dataQueries.find((query) => query.id === queryId)?.name ?? queryName;
useSuperStore
.getState()
.modules[_ref.moduleName].useDataQueriesStore.getState()
.dataQueries.find((query) => query.id === queryId)?.name ?? queryName;
return runQuery(_ref, queryId, name, undefined, mode, resolvedParams);
}
case 'logout': {
@ -393,20 +412,20 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
case 'open-webpage': {
const url = resolveReferences(event.url, getCurrentState(), undefined, customVariables);
const url = resolveReferences(event.url, getCurrentState(_ref.moduleName), undefined, customVariables);
window.open(url, '_blank');
return Promise.resolve();
}
case 'go-to-app': {
const slug = resolveReferences(event.slug, getCurrentState(), undefined, customVariables);
const slug = resolveReferences(event.slug, getCurrentState(_ref.moduleName), undefined, customVariables);
const queryParams = event.queryParams?.reduce(
(result, queryParam) => ({
...result,
...{
[resolveReferences(queryParam[0], getCurrentState())]: resolveReferences(
[resolveReferences(queryParam[0], getCurrentState(_ref.moduleName))]: resolveReferences(
queryParam[1],
getCurrentState(),
getCurrentState(_ref.moduleName),
undefined,
customVariables
),
@ -440,15 +459,20 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
return showModal(_ref, event.modal, false);
case 'copy-to-clipboard': {
const contentToCopy = resolveReferences(event.contentToCopy, getCurrentState(), undefined, customVariables);
const contentToCopy = resolveReferences(
event.contentToCopy,
getCurrentState(_ref.moduleName),
undefined,
customVariables
);
copyToClipboard(contentToCopy);
return Promise.resolve();
}
case 'set-localstorage-value': {
const key = resolveReferences(event.key, getCurrentState(), undefined, customVariables);
const value = resolveReferences(event.value, getCurrentState(), undefined, customVariables);
const key = resolveReferences(event.key, getCurrentState(_ref.moduleName), undefined, customVariables);
const value = resolveReferences(event.value, getCurrentState(_ref.moduleName), undefined, customVariables);
localStorage.setItem(key, value);
return Promise.resolve();
@ -456,9 +480,11 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
case 'generate-file': {
// const fileType = event.fileType;
const data = resolveReferences(event.data, getCurrentState(), undefined, customVariables) ?? [];
const fileName = resolveReferences(event.fileName, getCurrentState(), undefined, customVariables) ?? 'data.txt';
const fileType = resolveReferences(event.fileType, getCurrentState(), undefined, customVariables) ?? 'csv';
const data = resolveReferences(event.data, getCurrentState(_ref.moduleName), undefined, customVariables) ?? [];
const fileName =
resolveReferences(event.fileName, getCurrentState(_ref.moduleName), undefined, customVariables) ?? 'data.txt';
const fileType =
resolveReferences(event.fileType, getCurrentState(_ref.moduleName), undefined, customVariables) ?? 'csv';
const fileData = {
csv: generateCSV,
plaintext: (plaintext) => plaintext,
@ -469,18 +495,21 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
case 'set-table-page': {
setTablePageIndex(event.table, event.pageIndex);
setTablePageIndex(event.table, event.pageIndex, _ref);
break;
}
case 'set-custom-variable': {
const key = resolveReferences(event.key, getCurrentState(), undefined, customVariables);
const value = resolveReferences(event.value, getCurrentState(), undefined, customVariables);
const customAppVariables = { ...getCurrentState().variables };
const key = resolveReferences(event.key, getCurrentState(_ref.moduleName), undefined, customVariables);
const value = resolveReferences(event.value, getCurrentState(_ref.moduleName), undefined, customVariables);
const customAppVariables = { ...getCurrentState(_ref.moduleName).variables };
customAppVariables[key] = value;
return useCurrentStateStore.getState().actions.setCurrentState({
variables: customAppVariables,
});
return useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
variables: customAppVariables,
});
}
case 'get-custom-variable': {
@ -490,27 +519,33 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
case 'unset-custom-variable': {
const key = resolveReferences(event.key, getCurrentState(), undefined, customVariables);
const customAppVariables = { ...getCurrentState().variables };
const key = resolveReferences(event.key, getCurrentState(_ref.moduleName), undefined, customVariables);
const customAppVariables = { ...getCurrentState(_ref.moduleName).variables };
delete customAppVariables[key];
return useCurrentStateStore.getState().actions.setCurrentState({
variables: customAppVariables,
});
return useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
variables: customAppVariables,
});
}
case 'set-page-variable': {
const key = resolveReferences(event.key, getCurrentState(), undefined, customVariables);
const value = resolveReferences(event.value, getCurrentState(), undefined, customVariables);
const key = resolveReferences(event.key, getCurrentState(_ref.moduleName), undefined, customVariables);
const value = resolveReferences(event.value, getCurrentState(_ref.moduleName), undefined, customVariables);
const customPageVariables = {
...getCurrentState().page.variables,
...getCurrentState(_ref.moduleName).page.variables,
[key]: value,
};
return useCurrentStateStore.getState().actions.setCurrentState({
page: {
...getCurrentState().page,
variables: customPageVariables,
},
});
return useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
page: {
...getCurrentState(_ref.moduleName).page,
variables: customPageVariables,
},
});
}
case 'get-page-variable': {
@ -522,18 +557,21 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
case 'unset-page-variable': {
const key = resolveReferences(event.key, getCurrentState(), undefined, customVariables);
const customPageVariables = _.omit(getCurrentState().page.variables, key);
return useCurrentStateStore.getState().actions.setCurrentState({
page: {
...getCurrentState().page,
variables: customPageVariables,
},
});
const key = resolveReferences(event.key, getCurrentState(_ref.moduleName), undefined, customVariables);
const customPageVariables = _.omit(getCurrentState(_ref.moduleName).page.variables, key);
return useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
page: {
...getCurrentState(_ref.moduleName).page,
variables: customPageVariables,
},
});
}
case 'control-component': {
let component = Object.values(getCurrentState()?.components ?? {}).filter(
let component = Object.values(getCurrentState(_ref.moduleName)?.components ?? {}).filter(
(component) => component.id === event.componentId
)[0];
let action = '';
@ -541,8 +579,10 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
// check if component id not found then try to find if its available as child widget else continue
// with normal flow finding action
if (component == undefined) {
component = _ref.appDefinition.pages[getCurrentState()?.page?.id].components[event.componentId].component;
const parent = Object.values(getCurrentState()?.components ?? {}).find(
component =
_ref.appDefinition.pages[getCurrentState(_ref.moduleName)?.page?.id].components[event.componentId]
.component;
const parent = Object.values(getCurrentState(_ref.moduleName)?.components ?? {}).find(
(item) => item.id === component.parent
);
const child = Object.values(parent?.children).find((item) => item.id === event.componentId);
@ -555,7 +595,7 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
}
actionArguments = _.map(event.componentSpecificActionParams, (param) => ({
...param,
value: resolveReferences(param.value, getCurrentState(), undefined, customVariables),
value: resolveReferences(param.value, getCurrentState(_ref.moduleName), undefined, customVariables),
}));
const actionPromise = action && action(...actionArguments.map((argument) => argument.value));
return actionPromise ?? Promise.resolve();
@ -566,7 +606,10 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
// Don't allow switching to disabled page in editor as well as viewer
if (!disabled) {
_ref.switchPage(event.pageId, resolveReferences(event.queryParams, getCurrentState(), [], customVariables));
_ref.switchPage(
event.pageId,
resolveReferences(event.queryParams, getCurrentState(_ref.moduleName), [], customVariables)
);
}
if (_ref.appDefinition.pages[event.pageId]) {
if (disabled) {
@ -580,7 +623,10 @@ function executeActionWithDebounce(_ref, event, mode, customVariables) {
},
},
};
useCurrentStateStore.getState().actions.setErrors(generalProps);
useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setErrors(generalProps);
}
}
@ -603,44 +649,53 @@ export async function onEvent(_ref, eventName, events, options = {}, mode = 'edi
if (eventName === 'onTrigger') {
const { component, queryId, queryName, parameters } = options;
useCurrentStateStore.getState().actions.setCurrentState({
components: {
...getCurrentState().components,
[component.name]: {
...getCurrentState().components[component.name],
useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
components: {
...getCurrentState(_ref.moduleName).components,
[component.name]: {
...getCurrentState(_ref.moduleName).components[component.name],
},
},
},
});
});
runQuery(_ref, queryId, queryName, true, mode, parameters);
}
if (eventName === 'onCalendarEventSelect') {
const { component, calendarEvent } = options;
useCurrentStateStore.getState().actions.setCurrentState({
components: {
...getCurrentState().components,
[component.name]: {
...getCurrentState().components[component.name],
selectedEvent: { ...calendarEvent },
useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
components: {
...getCurrentState(_ref.moduleName).components,
[component.name]: {
...getCurrentState(_ref.moduleName).components[component.name],
selectedEvent: { ...calendarEvent },
},
},
},
});
});
executeActionsForEventId(_ref, 'onCalendarEventSelect', events, mode, customVariables);
}
if (eventName === 'onCalendarSlotSelect') {
const { component, selectedSlots } = options;
useCurrentStateStore.getState().actions.setCurrentState({
components: {
...getCurrentState().components,
[component.name]: {
...getCurrentState().components[component.name],
selectedSlots,
useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
components: {
...getCurrentState(_ref.moduleName).components,
[component.name]: {
...getCurrentState(_ref.moduleName).components[component.name],
selectedSlots,
},
},
},
});
});
executeActionsForEventId(_ref, 'onCalendarSlotSelect', events, mode, customVariables);
}
@ -792,7 +847,7 @@ export function getQueryVariables(options, state) {
export function previewQuery(_ref, query, calledFromQuery = false, userSuppliedParameters = {}) {
let parameters = userSuppliedParameters;
const queryPanelState = useQueryPanelStore.getState();
const queryPanelState = useSuperStore.getState().modules[_ref.moduleName].useQueryPanelStore.getState();
const { queryPreviewData } = queryPanelState;
const { setPreviewLoading, setPreviewData } = queryPanelState.actions;
@ -811,7 +866,7 @@ export function previewQuery(_ref, query, calledFromQuery = false, userSuppliedP
);
}
const queryState = { ...getCurrentState(), parameters };
const queryState = { ...getCurrentState(_ref.moduleName), parameters };
const options = getQueryVariables(query.options, queryState);
return new Promise(function (resolve, reject) {
@ -824,7 +879,7 @@ export function previewQuery(_ref, query, calledFromQuery = false, userSuppliedP
queryExecutionPromise = dataqueryService.preview(
query,
options,
useAppVersionStore.getState().editingVersion?.id
useSuperStore.getState().modules[_ref.moduleName].useAppVersionStore.getState().editingVersion?.id
);
}
@ -903,15 +958,19 @@ export function runQuery(
shouldSetPreviewData = false
) {
let parameters = userSuppliedParameters;
const query = useDataQueriesStore.getState().dataQueries.find((query) => query.id === queryId);
const queryEvents = useAppDataStore
const query = useSuperStore
.getState()
.modules[_ref.moduleName].useDataQueriesStore.getState()
.dataQueries.find((query) => query.id === queryId);
const queryEvents = useSuperStore
.getState()
.modules[_ref.moduleName].useAppDataStore.getState()
.events.filter((event) => event.target === 'data_query' && event.sourceId === queryId);
let dataQuery = {};
// const { setPreviewLoading, setPreviewData } = useQueryPanelStore.getState().actions;
const queryPanelState = useQueryPanelStore.getState();
// const { setPreviewLoading, setPreviewData } = useSuperStore.getState().modules[_ref.moduleName].useQueryPanelStore.getState().actions;
const queryPanelState = useSuperStore.getState().modules[_ref.moduleName].useQueryPanelStore.getState();
const { queryPreviewData } = queryPanelState;
const { setPreviewLoading, setPreviewData } = queryPanelState.actions;
if (shouldSetPreviewData) {
@ -936,12 +995,13 @@ export function runQuery(
);
}
const queryState = { ...getCurrentState(), parameters };
const queryState = { ...getCurrentState(_ref.moduleName), parameters };
const options = getQueryVariables(dataQuery.options, queryState);
if (dataQuery.options?.requestConfirmation) {
const queryConfirmationList = useEditorStore.getState().queryConfirmationList
? [...useEditorStore.getState().queryConfirmationList]
const queryConfirmationList = useSuperStore.getState().modules[_ref.moduleName].useEditorStore.getState()
.queryConfirmationList
? [...useSuperStore.getState().modules[_ref.moduleName].useEditorStore.getState().queryConfirmationList]
: [];
const queryConfirmation = {
@ -963,18 +1023,21 @@ export function runQuery(
// eslint-disable-next-line no-unused-vars
return new Promise(function (resolve, reject) {
useCurrentStateStore.getState().actions.setCurrentState({
queries: {
...getCurrentState().queries,
[queryName]: {
...getCurrentState().queries[queryName],
isLoading: true,
data: [],
rawData: [],
useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
queries: {
...getCurrentState(_ref.moduleName).queries,
[queryName]: {
...getCurrentState(_ref.moduleName).queries[queryName],
isLoading: true,
data: [],
rawData: [],
},
},
},
errors: {},
});
errors: {},
});
let queryExecutionPromise = null;
if (query.kind === 'runjs') {
queryExecutionPromise = executeMultilineJS(_self, query.options.code, query?.id, false, mode, parameters);
@ -1029,33 +1092,39 @@ export function runQuery(
setPreviewData(errorData);
}
// errorData = query.kind === 'runpy' ? data.data : data;
useCurrentStateStore.getState().actions.setErrors({
[queryName]: {
type: 'query',
kind: query.kind,
data: errorData,
options: options,
},
});
useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setErrors({
[queryName]: {
type: 'query',
kind: query.kind,
data: errorData,
options: options,
},
});
useCurrentStateStore.getState().actions.setCurrentState({
queries: {
...getCurrentState().queries,
[queryName]: _.assign(
{
...getCurrentState().queries[queryName],
isLoading: false,
},
query.kind === 'restapi'
? {
request: data.data.requestObject,
response: data.data.responseObject,
responseHeaders: data.data.responseHeaders,
}
: {}
),
},
});
useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
queries: {
...getCurrentState(_ref.moduleName).queries,
[queryName]: _.assign(
{
...getCurrentState(_ref.moduleName).queries[queryName],
isLoading: false,
},
query.kind === 'restapi'
? {
request: data.data.requestObject,
response: data.data.responseObject,
responseHeaders: data.data.responseHeaders,
}
: {}
),
},
});
resolve(data);
onEvent(_self, 'onDataQueryFailure', queryEvents);
if (mode !== 'view') {
@ -1077,23 +1146,29 @@ export function runQuery(
'edit'
);
if (finalData.status === 'failed') {
useCurrentStateStore.getState().actions.setCurrentState({
queries: {
...getCurrentState().queries,
[queryName]: {
...getCurrentState().queries[queryName],
isLoading: false,
useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
queries: {
...getCurrentState(_ref.moduleName).queries,
[queryName]: {
...getCurrentState(_ref.moduleName).queries[queryName],
isLoading: false,
},
},
},
});
});
useCurrentStateStore.getState().actions.setErrors({
[queryName]: {
type: 'transformations',
data: finalData,
options: options,
},
});
useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setErrors({
[queryName]: {
type: 'transformations',
data: finalData,
options: options,
},
});
resolve(finalData);
onEvent(_self, 'onDataQueryFailure', queryEvents);
return;
@ -1111,60 +1186,68 @@ export function runQuery(
duration: notificationDuration,
});
}
useCurrentStateStore.getState().actions.setCurrentState({
queries: {
...getCurrentState().queries,
[queryName]: _.assign(
{
...getCurrentState().queries[queryName],
isLoading: false,
data: finalData,
rawData,
},
query.kind === 'restapi'
? {
request: data.request,
response: data.response,
responseHeaders: data.responseHeaders,
}
: {}
),
},
// Used to generate logs
succededQuery: {
[queryName]: {
type: 'query',
kind: query.kind,
useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
queries: {
...getCurrentState(_ref.moduleName).queries,
[queryName]: _.assign(
{
...getCurrentState(_ref.moduleName).queries[queryName],
isLoading: false,
data: finalData,
rawData,
},
query.kind === 'restapi'
? {
request: data.request,
response: data.response,
responseHeaders: data.responseHeaders,
}
: {}
),
},
},
});
// Used to generate logs
succededQuery: {
[queryName]: {
type: 'query',
kind: query.kind,
},
},
});
resolve({ status: 'ok', data: finalData });
onEvent(_self, 'onDataQuerySuccess', queryEvents, mode);
}
})
.catch(({ error }) => {
if (mode !== 'view') toast.error(error ?? 'Unknown error');
useCurrentStateStore.getState().actions.setCurrentState({
queries: {
...getCurrentState().queries,
[queryName]: {
isLoading: false,
useSuperStore
.getState()
.modules[_ref.moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
queries: {
...getCurrentState(_ref.moduleName).queries,
[queryName]: {
isLoading: false,
},
},
},
});
});
resolve({ status: 'failed', message: error });
});
});
}
export function setTablePageIndex(tableId, index) {
export function setTablePageIndex(tableId, index, _ref) {
if (_.isEmpty(tableId)) {
console.log('No table is associated with this event.');
return Promise.resolve();
}
const table = Object.entries(getCurrentState().components).filter((entry) => entry?.[1]?.id === tableId)?.[0]?.[1];
const table = Object.entries(getCurrentState(_ref.moduleName).components).filter(
(entry) => entry?.[1]?.id === tableId
)?.[0]?.[1];
const newPageIndex = resolveReferences(index, getCurrentState());
table.setPage(newPageIndex ?? 1);
return Promise.resolve();
@ -1186,10 +1269,10 @@ for computing component state. It replaces the previous try-catch block with
a more efficient approach, precomputing the parent component types and using
conditional checks for better performance and error handling.*/
export function computeComponentState(components = {}) {
export function computeComponentState(components = {}, moduleName) {
try {
let componentState = {};
const currentComponents = getCurrentState().components;
const currentComponents = getCurrentState(moduleName).components;
// Precompute parent component types
const parentComponentTypes = {};
@ -1225,14 +1308,17 @@ export function computeComponentState(components = {}) {
}
});
useCurrentStateStore.getState().actions.setCurrentState({
components: {
...componentState,
},
});
useSuperStore
.getState()
.modules[moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
components: {
...componentState,
},
});
return new Promise((resolve) => {
useEditorStore.getState().actions.updateEditorState({
useSuperStore.getState().modules[moduleName].useEditorStore.getState().actions.updateEditorState({
defaultComponentStateComputed: true,
});
resolve();
@ -1256,14 +1342,17 @@ export const getSvgIcon = (key, height = 50, width = 50, iconFile = undefined, s
};
export const debuggerActions = {
error: (errors) => {
useCurrentStateStore.getState().actions.setErrors({
...errors,
});
error: (errors, moduleName) => {
useSuperStore
.getState()
.modules[moduleName].useCurrentStateStore.getState()
.actions.setErrors({
...errors,
});
},
flush: () => {
useCurrentStateStore.getState().actions.setCurrentState({
flush: (moduleName) => {
useSuperStore.getState().modules[moduleName].useCurrentStateStore.getState().actions.setCurrentState({
errors: {},
});
},
@ -1357,8 +1446,8 @@ export const debuggerActions = {
});
return querySuccesslogs;
},
flushAllLog: () => {
useCurrentStateStore.getState().actions.setCurrentState({
flushAllLog: (moduleName) => {
useSuperStore.getState().modules[moduleName].useCurrentStateStore.getState().actions.setCurrentState({
succededQuery: {},
});
},
@ -1399,7 +1488,8 @@ export const cloneComponents = (
currentPageId,
updateAppDefinition,
isCloning = true,
isCut = false
isCut = false,
moduleName
) => {
if (selectedComponents.length < 1) return getSelectedText();
@ -1466,7 +1556,7 @@ export const cloneComponents = (
}
return new Promise((resolve) => {
useEditorStore.getState().actions.updateEditorState({
useSuperStore.getState().modules[moduleName].useEditorStore.getState().actions.updateEditorState({
currentSidebarTab: 2,
});
resolve();
@ -1776,8 +1866,11 @@ function convertMapSet(obj) {
}
}
export const checkExistingQueryName = (newName) =>
useDataQueriesStore.getState().dataQueries.some((query) => query.name === newName);
export const checkExistingQueryName = (newName, moduleName) =>
useSuperStore
.getState()
.modules[moduleName].useDataQueriesStore.getState()
.dataQueries.some((query) => query.name === newName);
export const runQueries = (queries, _ref) => {
queries.forEach((query) => {
@ -1787,30 +1880,33 @@ export const runQueries = (queries, _ref) => {
});
};
export const computeQueryState = (queries) => {
export const computeQueryState = (queries, moduleName) => {
let queryState = {};
queries.forEach((query) => {
if (query.plugin?.plugin_id) {
queryState[query.name] = {
...query.plugin.manifest_file.data?.source?.exposedVariables,
kind: query.plugin.manifest_file.data.source.kind,
...getCurrentState().queries[query.name],
...getCurrentState(moduleName).queries[query.name],
};
} else {
queryState[query.name] = {
...DataSourceTypes.find((source) => source.kind === query.kind)?.exposedVariables,
kind: DataSourceTypes.find((source) => source.kind === query.kind)?.kind,
...getCurrentState()?.queries[query.name],
...getCurrentState(moduleName)?.queries[query.name],
};
}
});
const hasDiffQueryState = !_.isEqual(getCurrentState()?.queries, queryState);
const hasDiffQueryState = !_.isEqual(getCurrentState(moduleName)?.queries, queryState);
if (hasDiffQueryState) {
useCurrentStateStore.getState().actions.setCurrentState({
queries: {
...queryState,
},
});
useSuperStore
.getState()
.modules[moduleName].useCurrentStateStore.getState()
.actions.setCurrentState({
queries: {
...queryState,
},
});
}
};

View file

@ -3,15 +3,15 @@ import moment from 'moment';
import _, { isEmpty } from 'lodash';
import axios from 'axios';
import JSON5 from 'json5';
import { previewQuery, executeAction } from '@/_helpers/appUtils';
import { executeAction } from '@/_helpers/appUtils';
import { toast } from 'react-hot-toast';
import { authenticationService } from '@/_services/authentication.service';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useSuperStore } from '../_stores/superStore';
import { getCurrentState } from '@/_stores/currentStateStore';
import { getWorkspaceIdOrSlugFromURL, getSubpath, returnWorkspaceIdIfNeed } from './routes';
import { getCookie, eraseCookie } from '@/_helpers/cookie';
import { staticDataSources } from '@/Editor/QueryManager/constants';
import { getDateTimeFormat } from '@/Editor/Components/Table/Datepicker';
export function findProp(obj, prop, defval) {
if (typeof defval === 'undefined') defval = null;
@ -413,6 +413,97 @@ export function validateWidget({ validationObject, widgetValue, currentState, cu
};
}
export function validateDates({ validationObject, widgetValue, currentState, customResolveObjects }) {
let isValid = true;
let validationError = null;
const validationDateFormat = validationObject?.dateFormat?.value || 'MM/DD/YYYY';
const validationTimeFormat = validationObject?.timeFormat?.value || 'HH:mm';
const customRule = validationObject?.customRule?.value;
const parsedDateFormat = validationObject?.parseDateFormat?.value;
const isTwentyFourHrFormatEnabled = validationObject?.isTwentyFourHrFormatEnabled?.value ?? false;
const isDateSelectionEnabled = validationObject?.isDateSelectionEnabled?.value ?? true;
const _widgetDateValue = moment(widgetValue, parsedDateFormat);
const _widgetTimeValue = moment(
widgetValue,
getDateTimeFormat(parsedDateFormat, true, isTwentyFourHrFormatEnabled, isDateSelectionEnabled)
).format(validationTimeFormat);
const resolvedMinDate = resolveWidgetFieldValue(
validationObject?.minDate?.value,
currentState,
undefined,
customResolveObjects
);
const resolvedMaxDate = resolveWidgetFieldValue(
validationObject?.maxDate?.value,
currentState,
undefined,
customResolveObjects
);
const resolvedMinTime = resolveWidgetFieldValue(
validationObject?.minTime?.value,
currentState,
undefined,
customResolveObjects
);
const resolvedMaxTime = resolveWidgetFieldValue(
validationObject?.maxTime?.value,
currentState,
undefined,
customResolveObjects
);
// Minimum date validation
if (resolvedMinDate !== undefined && moment(resolvedMinDate).isValid()) {
if (!moment(resolvedMinDate, validationDateFormat).isBefore(moment(_widgetDateValue, validationDateFormat))) {
return {
isValid: false,
validationError: `Minimum date is ${resolvedMinDate}`,
};
}
}
// Maximum date validation
if (resolvedMaxDate !== undefined && moment(resolvedMaxDate).isValid()) {
if (!moment(resolvedMaxDate, validationDateFormat).isAfter(moment(_widgetDateValue, validationDateFormat))) {
return {
isValid: false,
validationError: `Maximum date is ${resolvedMaxDate}`,
};
}
}
// Minimum time validation
if (resolvedMinTime !== undefined && moment(resolvedMinTime, validationTimeFormat, true).isValid()) {
if (!moment(resolvedMinTime, validationTimeFormat).isBefore(moment(_widgetTimeValue, validationTimeFormat))) {
return {
isValid: false,
validationError: `Minimum time is ${resolvedMinTime}`,
};
}
}
// Maximum time validation
if (resolvedMaxTime !== undefined && moment(resolvedMaxTime, validationTimeFormat, true).isValid()) {
if (!moment(resolvedMaxTime, validationTimeFormat).isAfter(moment(_widgetTimeValue, validationTimeFormat))) {
return {
isValid: false,
validationError: `Maximum time is ${resolvedMaxTime}`,
};
}
}
//Custom rule validation
const resolvedCustomRule = resolveWidgetFieldValue(customRule, currentState, false, customResolveObjects);
if (typeof resolvedCustomRule === 'string' && resolvedCustomRule !== '') {
return { isValid: false, validationError: resolvedCustomRule };
}
return {
isValid,
validationError,
};
}
export function validateEmail(email) {
const emailRegex =
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
@ -420,8 +511,16 @@ export function validateEmail(email) {
}
// eslint-disable-next-line no-unused-vars
export async function executeMultilineJS(_ref, code, queryId, isPreview, mode = '', parameters = {}) {
const currentState = getCurrentState();
export async function executeMultilineJS(
_ref,
code,
queryId,
isPreview,
mode = '',
parameters = {},
hasParamSupport = false
) {
const currentState = getCurrentState(_ref.moduleName);
let result = {},
error = null;
@ -432,7 +531,10 @@ export async function executeMultilineJS(_ref, code, queryId, isPreview, mode =
const actions = generateAppActions(_ref, queryId, mode, isPreview);
const queryDetails = useDataQueriesStore.getState().dataQueries.find((q) => q.id === queryId);
const queryDetails = useSuperStore
.getState()
.modules[_ref.moduleName].useDataQueriesStore.getState()
.dataQueries.find((q) => q.id === queryId);
const defaultParams =
queryDetails?.options?.parameters?.reduce(
@ -462,7 +564,10 @@ export async function executeMultilineJS(_ref, code, queryId, isPreview, mode =
params = {};
}
const processedParams = {};
const query = useDataQueriesStore.getState().dataQueries.find((q) => q.name === key);
const query = useSuperStore
.getState()
.modules.modules[_ref.moduleName].useDataQueriesStore.getState()
.dataQueries.find((q) => q.name === key);
query.options.parameters?.forEach((arg) => (processedParams[arg.name] = params[arg.name]));
return actions.runQuery(key, processedParams);
},
@ -591,14 +696,17 @@ export const generateAppActions = (_ref, queryId, mode, isPreview = false) => {
: {};
const runQuery = (queryName = '', parameters) => {
const query = useDataQueriesStore.getState().dataQueries.find((query) => {
const isFound = query.name === queryName;
if (isPreview) {
return isFound;
} else {
return isFound && isQueryRunnable(query);
}
});
const query = useSuperStore
.getState()
.modules[_ref.moduleName].useDataQueriesStore.getState()
.dataQueries.find((query) => {
const isFound = query.name === queryName;
if (isPreview) {
return isFound;
} else {
return isFound && isQueryRunnable(query);
}
});
const processedParams = {};
if (_.isEmpty(query) || queryId === query?.id) {

View file

@ -0,0 +1,46 @@
import { useCallback, useMemo } from 'react';
import { shallow } from 'zustand/shallow';
import { useEditorStore } from '@/_stores/editorStore';
import { useAppDataStore } from '@/_stores/appDataStore';
const useAppDarkMode = () => {
const { appMode, setAppMode } = useEditorStore(
(state) => ({
appMode: state.appMode,
setAppMode: state.actions.setAppMode,
}),
shallow
);
const { isTJDarkMode } = useAppDataStore(
(state) => ({
isTJDarkMode: state.isTJDarkMode,
}),
shallow
);
const handleAppModeChange = useCallback(
(appMode = 'auto') => {
setAppMode(appMode);
},
[setAppMode]
);
const isAppDarkMode = useMemo(() => {
if (appMode === 'light') {
return false;
} else if (appMode === 'dark') {
return true;
} else {
return localStorage.getItem('darkMode') === 'true';
}
}, [appMode, isTJDarkMode]);
return {
onAppModeChange: handleAppModeChange,
appMode,
isAppDarkMode,
};
};
export default useAppDarkMode;

View file

@ -1,153 +1,170 @@
import { appVersionService } from '@/_services';
import { create, zustandDevTools } from './utils';
import { shallow } from 'zustand/shallow';
import { useContext } from 'react';
import { useSuperStore } from './superStore';
import { ModuleContext } from '../_contexts/ModuleContext';
const initialState = {
editingVersion: null,
currentUser: null,
apps: [],
appName: null,
slug: null,
isPublic: null,
isMaintenanceOn: null,
organizationId: null,
currentVersionId: null,
userId: null,
app: {},
components: [],
pages: [],
layouts: [],
events: [],
eventHandlers: [],
appDefinitionDiff: null,
appDiffOptions: {},
isSaving: false,
appId: null,
areOthersOnSameVersionAndPage: false,
appVersionPreviewLink: null,
metadata: null,
eventsUpdatedLoader: false,
eventsCreatedLoader: false,
actionsUpdatedLoader: false,
eventToDeleteLoaderIndex: null,
};
export function createAppDataStore(moduleName) {
const initialState = {
editingVersion: null,
currentUser: null,
apps: [],
appName: null,
slug: null,
isPublic: null,
isMaintenanceOn: null,
organizationId: null,
currentVersionId: null,
userId: null,
app: {},
components: [],
pages: [],
layouts: [],
events: [],
eventHandlers: [],
appDefinitionDiff: null,
appDiffOptions: {},
isSaving: false,
appId: null,
areOthersOnSameVersionAndPage: false,
appVersionPreviewLink: null,
moduleName,
metadata: null,
eventsUpdatedLoader: false,
eventsCreatedLoader: false,
actionsUpdatedLoader: false,
eventToDeleteLoaderIndex: null,
isTJDarkMode: localStorage.getItem('darkMode') === 'true',
};
return create(
zustandDevTools(
(set, get) => ({
...initialState,
actions: {
updateEditingVersion: (version) => set(() => ({ editingVersion: version })),
updateApps: (apps) => set(() => ({ apps: apps })),
updateState: (state) => set((prev) => ({ ...prev, ...state })),
updateAppDefinitionDiff: (appDefinitionDiff) => set(() => ({ appDefinitionDiff: appDefinitionDiff })),
updateAppVersion: (appId, versionId, pageId, appDefinitionDiff, isUserSwitchedVersion = false) => {
return new Promise((resolve, reject) => {
get().actions.setIsSaving(true);
const isComponentCutProcess = get().appDiffOptions?.componentCut === true;
export const useAppDataStore = create(
zustandDevTools(
(set, get) => ({
...initialState,
actions: {
updateEditingVersion: (version) => set(() => ({ editingVersion: version })),
updateApps: (apps) => set(() => ({ apps: apps })),
updateState: (state) => set((prev) => ({ ...prev, ...state })),
updateAppDefinitionDiff: (appDefinitionDiff) => set(() => ({ appDefinitionDiff: appDefinitionDiff })),
updateAppVersion: (appId, versionId, pageId, appDefinitionDiff, isUserSwitchedVersion = false) => {
return new Promise((resolve, reject) => {
useAppDataStore.getState().actions.setIsSaving(true);
const isComponentCutProcess = get().appDiffOptions?.componentCut === true;
appVersionService
.autoSaveApp(
appId,
versionId,
appDefinitionDiff.updateDiff,
appDefinitionDiff.type,
pageId,
appDefinitionDiff.operation,
isUserSwitchedVersion,
isComponentCutProcess
)
.then(() => {
useAppDataStore.getState().actions.setIsSaving(false);
})
.catch((error) => {
useAppDataStore.getState().actions.setIsSaving(false);
reject(error);
})
.finally(() => resolve());
});
},
updateAppVersionEventHandlers: async (events, updateType = 'update', param) => {
useAppDataStore.getState().actions.setIsSaving(true);
if (param === 'actionId') {
set({ actionsUpdatedLoader: true });
}
if (param === 'eventId') {
set({ eventsUpdatedLoader: true });
}
const appId = get().appId;
const versionId = get().currentVersionId;
const response = await appVersionService.saveAppVersionEventHandlers(appId, versionId, events, updateType);
useAppDataStore.getState().actions.setIsSaving(false);
set({ eventsUpdatedLoader: false, actionsUpdatedLoader: false });
const updatedEvents = get().events;
updatedEvents.forEach((e, index) => {
const toUpdate = response.find((r) => r.id === e.id);
if (toUpdate) {
updatedEvents[index] = toUpdate;
appVersionService
.autoSaveApp(
appId,
versionId,
appDefinitionDiff.updateDiff,
appDefinitionDiff.type,
pageId,
appDefinitionDiff.operation,
isUserSwitchedVersion,
isComponentCutProcess
)
.then(() => {
get().actions.setIsSaving(false);
})
.catch((error) => {
get().actions.setIsSaving(false);
reject(error);
})
.finally(() => resolve());
});
},
updateAppVersionEventHandlers: async (events, updateType = 'update', param) => {
get().actions.setIsSaving(true);
if (param === 'actionId') {
set({ actionsUpdatedLoader: true });
}
});
if (param === 'eventId') {
set({ eventsUpdatedLoader: true });
}
const appId = get().appId;
const versionId = get().currentVersionId;
set(() => ({ events: updatedEvents }));
},
const response = await appVersionService.saveAppVersionEventHandlers(appId, versionId, events, updateType);
createAppVersionEventHandlers: async (event) => {
useAppDataStore.getState().actions.setIsSaving(true);
set({ eventsCreatedLoader: true });
get().actions.setIsSaving(false);
set({ eventsUpdatedLoader: false, actionsUpdatedLoader: false });
const updatedEvents = get().events;
const appId = get().appId;
const versionId = get().currentVersionId;
const updatedEvents = get().events;
const response = await appVersionService.createAppVersionEventHandler(appId, versionId, event);
useAppDataStore.getState().actions.setIsSaving(false);
set({ eventsCreatedLoader: false });
updatedEvents.push(response);
set(() => ({ events: updatedEvents }));
},
deleteAppVersionEventHandler: async (eventId) => {
useAppDataStore.getState().actions.setIsSaving(true);
const appId = get().appId;
const versionId = get().currentVersionId;
const updatedEvents = get().events;
const response = await appVersionService.deleteAppVersionEventHandler(appId, versionId, eventId);
useAppDataStore.getState().actions.setIsSaving(false);
set({ eventToDeleteLoaderIndex: null });
if (response?.affected === 1) {
updatedEvents.splice(
updatedEvents.findIndex((e) => e.id === eventId),
1
);
updatedEvents.forEach((e, index) => {
const toUpdate = response.find((r) => r.id === e.id);
if (toUpdate) {
updatedEvents[index] = toUpdate;
}
});
set(() => ({ events: updatedEvents }));
}
},
autoUpdateEventStore: async (versionId) => {
const appId = get().appId;
const response = await appVersionService.findAllEventsWithSourceId(appId, versionId);
},
set(() => ({ events: response }));
createAppVersionEventHandlers: async (event) => {
get().actions.setIsSaving(true);
set({ eventsCreatedLoader: true });
const appId = get().appId;
const versionId = get().currentVersionId;
const updatedEvents = get().events;
const response = await appVersionService.createAppVersionEventHandler(appId, versionId, event);
get().actions.setIsSaving(false);
set({ eventsCreatedLoader: false });
updatedEvents.push(response);
set(() => ({ events: updatedEvents }));
},
deleteAppVersionEventHandler: async (eventId) => {
get().actions.setIsSaving(true);
const appId = get().appId;
const versionId = get().currentVersionId;
const updatedEvents = get().events;
const response = await appVersionService.deleteAppVersionEventHandler(appId, versionId, eventId);
get().actions.setIsSaving(false);
set({ eventToDeleteLoaderIndex: null });
if (response?.affected === 1) {
updatedEvents.splice(
updatedEvents.findIndex((e) => e.id === eventId),
1
);
set(() => ({ events: updatedEvents }));
}
},
autoUpdateEventStore: async (versionId) => {
const appId = get().appId;
const response = await appVersionService.findAllEventsWithSourceId(appId, versionId);
set(() => ({ events: response }));
},
setIsSaving: (isSaving) => set(() => ({ isSaving })),
setAppId: (appId) => set(() => ({ appId })),
setAppPreviewLink: (appVersionPreviewLink) => set(() => ({ appVersionPreviewLink })),
setMetadata: (metadata) => set(() => ({ metadata })),
setEventToDeleteLoaderIndex: (index) => set(() => ({ eventToDeleteLoaderIndex: index })),
updateIsTJDarkMode: (isTJDarkMode) => set({ isTJDarkMode }),
},
setIsSaving: (isSaving) => set(() => ({ isSaving })),
setAppId: (appId) => set(() => ({ appId })),
setAppPreviewLink: (appVersionPreviewLink) => set(() => ({ appVersionPreviewLink })),
setMetadata: (metadata) => set(() => ({ metadata })),
setEventToDeleteLoaderIndex: (index) => set(() => ({ eventToDeleteLoaderIndex: index })),
},
}),
{ name: 'App Data Store' }
)
);
}),
{ name: 'App Data Store' }
)
);
}
export const useAppDataStore = (callback, shallow) => {
const moduleName = useContext(ModuleContext);
if (!moduleName) throw Error('module context not available');
const _useAppDataStore = useSuperStore((state) => state.modules[moduleName].useAppDataStore);
return _useAppDataStore(callback, shallow);
};
export const useEditingVersion = () => useAppDataStore((state) => state.editingVersion, shallow);
export const useIsSaving = () => useAppDataStore((state) => state.isSaving, shallow);

View file

@ -1,33 +1,49 @@
import { create, zustandDevTools } from './utils';
import { useContext } from 'react';
import { useSuperStore } from './superStore';
import { ModuleContext } from '../_contexts/ModuleContext';
const initialState = {
editingVersion: null,
isUserEditingTheVersion: false,
releasedVersionId: null,
isVersionReleased: false,
appVersions: [],
export function createAppVersionStore(moduleName) {
const initialState = {
editingVersion: null,
isUserEditingTheVersion: false,
releasedVersionId: null,
isVersionReleased: false,
appVersions: [],
moduleName,
};
return create(
zustandDevTools(
(set, get) => ({
...initialState,
actions: {
updateEditingVersion: (version) =>
set({ editingVersion: version, isVersionReleased: get().releasedVersionId === version?.id }),
enableReleasedVersionPopupState: () => set({ isUserEditingTheVersion: true }),
disableReleasedVersionPopupState: () => set({ isUserEditingTheVersion: false }),
updateReleasedVersionId: (versionId) =>
set({
releasedVersionId: versionId,
isVersionReleased: get().editingVersion?.id ? get().editingVersion?.id === versionId : false,
}),
setAppVersions: (versions) => set({ appVersions: versions }),
},
}),
{ name: 'App Version Manager Store' }
)
);
}
export const useAppVersionStore = (callback, shallow) => {
const moduleName = useContext(ModuleContext);
if (!moduleName) throw Error('module context not available');
const _useAppVersionStore = useSuperStore((state) => state.modules[moduleName].useAppVersionStore);
return _useAppVersionStore(callback, shallow);
};
export const useAppVersionStore = create(
zustandDevTools(
(set, get) => ({
...initialState,
actions: {
updateEditingVersion: (version) =>
set({ editingVersion: version, isVersionReleased: get().releasedVersionId === version?.id }),
enableReleasedVersionPopupState: () => set({ isUserEditingTheVersion: true }),
disableReleasedVersionPopupState: () => set({ isUserEditingTheVersion: false }),
updateReleasedVersionId: (versionId) =>
set({
releasedVersionId: versionId,
isVersionReleased: get().editingVersion?.id ? get().editingVersion?.id === versionId : false,
}),
setAppVersions: (versions) => set({ appVersions: versions }),
},
}),
{ name: 'App Version Manager Store' }
)
);
export const useAppVersionActions = () => useAppVersionStore((state) => state.actions);
export const useAppVersionState = () => useAppVersionStore((state) => state);

Some files were not shown because too many files have changed in this diff Show more