diff --git a/.version b/.version index 7b5655c811..3a05135cd8 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.35.4 +2.36.0 diff --git a/frontend/.version b/frontend/.version index 7b5655c811..3a05135cd8 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -2.35.4 +2.36.0 diff --git a/frontend/assets/translations/en.json b/frontend/assets/translations/en.json index d02f5fa139..f73c59f4ca 100644 --- a/frontend/assets/translations/en.json +++ b/frontend/assets/translations/en.json @@ -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": { diff --git a/frontend/assets/translations/es.json b/frontend/assets/translations/es.json index 7776a1fb29..5c15ea5f26 100644 --- a/frontend/assets/translations/es.json +++ b/frontend/assets/translations/es.json @@ -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", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5b7a3116b9..ed5bdede1f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 42b905a89c..dc4f945ff3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index f5261122e3..39d719732d 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -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 ( - + ); }; 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 ( <> -
+
{updateAvailable && (

Update available

@@ -181,7 +192,11 @@ class AppComponent extends React.Component { path="/:workspaceId/apps/:slug/:pageHandle?/*" element={ - + this.setState({ isEditorOrViewer: value })} + /> } /> @@ -199,7 +214,11 @@ class AppComponent extends React.Component { path="/applications/:slug/:pageHandle?" element={ - + this.setState({ isEditorOrViewer: value })} + /> } /> @@ -208,7 +227,11 @@ class AppComponent extends React.Component { path="/applications/:slug/versions/:versionId/:pageHandle?" element={ - + this.setState({ isEditorOrViewer: value })} + /> } /> diff --git a/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx b/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx index 6c71dc6a43..6d7af072a1 100644 --- a/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx +++ b/frontend/src/Editor/AppVersionsManager/AppVersionsManager.jsx @@ -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} />
diff --git a/frontend/src/Editor/Box.jsx b/frontend/src/Editor/Box.jsx index bb11eb38d6..495cc962a1 100644 --- a/frontend/src/Editor/Box.jsx +++ b/frontend/src/Editor/Box.jsx @@ -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 = { diff --git a/frontend/src/Editor/CodeBuilder/CodeHinter.jsx b/frontend/src/Editor/CodeBuilder/CodeHinter.jsx index b402f03ec7..b3d0d4c7d2 100644 --- a/frontend/src/Editor/CodeBuilder/CodeHinter.jsx +++ b/frontend/src/Editor/CodeBuilder/CodeHinter.jsx @@ -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 = () => (
- {!['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 { diff --git a/frontend/src/Editor/CodeBuilder/Elements/BoxShadow.jsx b/frontend/src/Editor/CodeBuilder/Elements/BoxShadow.jsx index ec4a41b4c7..1ceb87a1c1 100644 --- a/frontend/src/Editor/CodeBuilder/Elements/BoxShadow.jsx +++ b/frontend/src/Editor/CodeBuilder/Elements/BoxShadow.jsx @@ -29,7 +29,7 @@ export const BoxShadow = ({ value, onChange, cyLabel }) => { const colorPickerStyle = { position: 'absolute', - bottom: '260px', + top: '-220px', }; useEffect(() => { diff --git a/frontend/src/Editor/CodeBuilder/Elements/Color.jsx b/frontend/src/Editor/CodeBuilder/Elements/Color.jsx index 66a3c984f0..1b1b621718 100644 --- a/frontend/src/Editor/CodeBuilder/Elements/Color.jsx +++ b/frontend/src/Editor/CodeBuilder/Elements/Color.jsx @@ -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 }} > - + <>{ColorPicker()} diff --git a/frontend/src/Editor/CodeBuilder/Elements/Select.jsx b/frontend/src/Editor/CodeBuilder/Elements/Select.jsx index cd2d1e170e..a39457c973 100644 --- a/frontend/src/Editor/CodeBuilder/Elements/Select.jsx +++ b/frontend/src/Editor/CodeBuilder/Elements/Select.jsx @@ -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 ( -
- {props.label} - {props.isSelected && ( - - - - )} -
+ +
+ {props.label} + {props.isSelected && ( + + + + )} + {isDeprecated && ( + + + + )} +
+
); }; -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 (
e.stopPropagation()}> { onChange={onChange} width={224} height={32} - styles={selectCustomStyles} + styles={selectCustomStyles(width)} useCustomStyles={true} classNamePrefix="inspector-select" components={{ diff --git a/frontend/src/Editor/CodeBuilder/Elements/Slider.jsx b/frontend/src/Editor/CodeBuilder/Elements/Slider.jsx index c305aa2c80..dd9e1f3547 100644 --- a/frontend/src/Editor/CodeBuilder/Elements/Slider.jsx +++ b/frontend/src/Editor/CodeBuilder/Elements/Slider.jsx @@ -30,7 +30,7 @@ function Slider1({ value, onChange, component }) { }; return ( -
+
{ + 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 ( +
+ + +
+ ); +}; + +export default TableRowHeightInput; diff --git a/frontend/src/Editor/CodeBuilder/Elements/Toggle.jsx b/frontend/src/Editor/CodeBuilder/Elements/Toggle.jsx index b4b9e5621e..9d6a968b18 100644 --- a/frontend/src/Editor/CodeBuilder/Elements/Toggle.jsx +++ b/frontend/src/Editor/CodeBuilder/Elements/Toggle.jsx @@ -1,6 +1,6 @@ import React from 'react'; -export const Toggle = ({ value, onChange, cyLabel }) => { +export const Toggle = ({ value, onChange, cyLabel, meta }) => { return (
@@ -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 && ( + + {meta.toggleLabel} + + )} { - event.preventDefault(); onRecordClicked(index); onRowClicked(index); }} diff --git a/frontend/src/Editor/Components/Modal.jsx b/frontend/src/Editor/Components/Modal.jsx index 0f2a2cec73..be57a1cdc5 100644 --- a/frontend/src/Editor/Components/Modal.jsx +++ b/frontend/src/Editor/Components/Modal.jsx @@ -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 = { diff --git a/frontend/src/Editor/Components/NumberInput.jsx b/frontend/src/Editor/Components/NumberInput.jsx index df2e9140c2..f8f35b1342 100644 --- a/frontend/src/Editor/Components/NumberInput.jsx +++ b/frontend/src/Editor/Components/NumberInput.jsx @@ -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({ <>
{showValidationError && visibility && (
{showValidationError && validationError} diff --git a/frontend/src/Editor/Components/Pagination.jsx b/frontend/src/Editor/Components/Pagination.jsx index 7b23851629..810aab69ad 100644 --- a/frontend/src/Editor/Components/Pagination.jsx +++ b/frontend/src/Editor/Components/Pagination.jsx @@ -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 ( -
+
    ); - case '>>': return ( ); - case '<': return ( ); - case '>': return ( ); - 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 ( -
  • callback(pageNumber)} - className={`page-item ${currentPage === pageNumber ? 'active' : ''}`} - > - {pageNumber} -
  • - ); - }); +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( +
  • callback(i)} className={`page-item ${currentPage === i ? 'active' : ''}`}> + {i} +
  • + ); + } + // 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( +
  • + ... +
  • + ); + + pageNumbers.push( +
  • callback(totalPages)} className={`page-item`}> + {totalPages} +
  • + ); + } + + // If the beginning of the pages is not visible, add ellipsis and remove one number + if (startPage > 1) { + pageNumbers.shift(); + pageNumbers.shift(); + + pageNumbers.unshift( +
  • + ... +
  • + ); + pageNumbers.unshift( +
  • callback(1)} className={`page-item`}> + 1 +
  • + ); + } + } + + return pageNumbers; + }; + + return <>{renderPageNumbers()}; }; Pagination.Operator = Operator; diff --git a/frontend/src/Editor/Components/PasswordInput.jsx b/frontend/src/Editor/Components/PasswordInput.jsx index a6e0cd521f..bcf98d5650 100644 --- a/frontend/src/Editor/Components/PasswordInput.jsx +++ b/frontend/src/Editor/Components/PasswordInput.jsx @@ -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({ <>
    - +
    )} { if (e.key === 'Enter') { @@ -363,11 +374,13 @@ export const PasswordInput = function PasswordInput({
{showValidationError && visibility && (
{showValidationError && validationError} diff --git a/frontend/src/Editor/Components/Table/AddNewRowComponent.jsx b/frontend/src/Editor/Components/Table/AddNewRowComponent.jsx index 3f83af628c..6ea1dd4c0a 100644 --- a/frontend/src/Editor/Components/Table/AddNewRowComponent.jsx +++ b/frontend/src/Editor/Components/Table/AddNewRowComponent.jsx @@ -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({
{headerGroups.map((headerGroup, index) => { @@ -114,7 +115,25 @@ export function AddNewRowComponent({ let cellProps = cell.getCellProps(); const isEditable = true; return ( - @@ -1285,15 +1312,17 @@ export function Table({ `} >
- {column.columnType !== 'selector' && isEditable && ( - - )} + {column.columnType !== 'selector' && + column.columnType !== 'image' && + isEditable && ( + + )}
+ > +
+ ); }} @@ -1372,6 +1408,28 @@ export function Table({ {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 ( { 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({ >
@@ -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 && ( - + {clientSidePagination && !serverSidePagination && `${globalFilteredRows.length} Records`} {serverSidePagination && totalRecords ? `${totalRecords} Records` : ''} @@ -1681,7 +1772,7 @@ export function Table({ { + 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 ( - + {text} {!readOnly && ( - removeTag(text)}> + removeTag(text)}> x )} @@ -41,38 +55,101 @@ export const Tags = ({ value, onChange, readOnly }) => { ); } + const getOverlay = (value, containerWidth) => { + const darkMode = localStorage.getItem('darkMode') === 'true'; + return Array.isArray(value) ? ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {value?.map((tag, index) => { + return ( + + {renderTag(tag)} + + ); + })} +
+ ) : ( +
+ ); + }; + return ( -
{ - e.stopPropagation(); - if (e.key === 'Tab' && !readOnly) { - setShowForm(true); - } - }} + = 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 && ( - setShowForm(true)}> - {'+'} - - )} - - {showForm && ( - - addTag(e.target.value)} - onKeyDown={handleFormKeyDown} - /> - - )} -
+
{ + if (!hovered) setHovered(true); + }} + onMouseOut={() => setHovered(false)} + > + {/* Container for + button */} + {!showForm && !readOnly && ( +
+ + setShowForm(true)}> + {'+'} + + +
+ )} + {/* Container for renderTags */} + {!showForm && ( +
+ {value.map((item, index) => ( + + {renderTag(item)} + + ))} +
+ )} + {/* Input element */} + {showForm && ( +
+ addTag(e.target.value)} + onKeyDown={handleFormKeyDown} + /> +
+ )} +
+ ); }; diff --git a/frontend/src/Editor/Components/Table/Text.jsx b/frontend/src/Editor/Components/Table/Text.jsx new file mode 100644 index 0000000000..8ead00ab0a --- /dev/null +++ b/frontend/src/Editor/Components/Table/Text.jsx @@ -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 = () => ( +
{ + 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 = () => ( +
+ + {cellValue} + +
+ ); + + const getOverlay = () => { + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + style={{ whiteSpace: 'pre-wrap', color: 'var(--text-primary)' }} + > + +
+ ); + }; + + 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 ( + <> +
} + trigger={_showOverlay && ['hover']} + rootClose={true} + show={_showOverlay && showOverlay && !isEditing} + > +
+
{ + if (!hovered) setHovered(true); + }} + onMouseLeave={() => setHovered(false)} + ref={ref} + className={`${!isValid ? 'is-invalid h-100' : ''} ${isEditing ? 'h-100 content-editing' : ''}`} + > + {!isEditable ? _renderNonEditableData() : _renderTextArea()} +
+ {isEditable &&
{validationError}
} +
+ + + ); +}; + +export default Text; diff --git a/frontend/src/Editor/Components/Table/Timepicker.jsx b/frontend/src/Editor/Components/Table/Timepicker.jsx new file mode 100644 index 0000000000..eca29e1314 --- /dev/null +++ b/frontend/src/Editor/Components/Table/Timepicker.jsx @@ -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 ( +
+ { + onChange(moment(value).format('hh:mm')); + }} + enableTime={true} + /> +
+ ); +}; diff --git a/frontend/src/Editor/Components/Table/columns/autogenerateColumns.js b/frontend/src/Editor/Components/Table/columns/autogenerateColumns.js index 1ef59967b1..489fb4a1c7 100644 --- a/frontend/src/Editor/Components/Table/columns/autogenerateColumns.js +++ b/frontend/src/Editor/Components/Table/columns/autogenerateColumns.js @@ -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'; }; diff --git a/frontend/src/Editor/Components/Table/columns/index.jsx b/frontend/src/Editor/Components/Table/columns/index.jsx index f8189ba141..18ef65ec41 100644 --- a/frontend/src/Editor/Components/Table/columns/index.jsx +++ b/frontend/src/Editor/Components/Table/columns/index.jsx @@ -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 ( -
- { - 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()} - /> -
{validationError}
-
- ); - } + const cellTextColor = resolveReferences(column.textColor, currentState, '', { cellValue, rowData }); return ( -
- {String(cellValue)} -
+ ); + + // 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 ( + //
+ //
{ + // 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} + //
+ //
{validationError}
+ //
+ // ); + // } + // return ( + //
+ // {String(cellValue)} + //
+ // ); } 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 ( -
+
{ 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} /> -
{validationError}
+
+
handleIncrement(e)}> + +
+
handleDecrement(e)}> + +
+
+
{validationError}
); } @@ -269,31 +405,29 @@ export default function generateColumnsData({
); } - case 'text': { + case 'text': return ( - + //
+ + //
); - } - 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 ( -
- { - handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original); - }} - fuzzySearch - placeholder={t('globals.select', 'Select') + '...'} - disabled={!isEditable} - className="select-search" - /> -
{validationError}
+
+ {columnType === 'dropdown' && ( + { + 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) && ( + { + 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} + /> + )} +
{validationError}
); } @@ -357,11 +523,11 @@ export default function generateColumnsData({ case 'badges': { return (
-
); } case 'tags': { return ( -
- { - handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original); - }} - readOnly={!isEditable} - /> -
+ { + 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 ( -
+
{cellValue && ( {cellValue} )}
@@ -417,6 +584,7 @@ export default function generateColumnsData({ onChange={(value) => { handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original); }} + containerWidth={width} />
); @@ -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 ( -
+
{ 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 && ( +
{validationError}
+ )}
); } 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 (
- + +
+ ); + } + case 'boolean': { + return ( +
+ + handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original) + } + toggleOnBg={column?.toggleOnBg} + toggleOffBg={column?.toggleOffBg} + />
); } diff --git a/frontend/src/Editor/Components/Table/datepicker.scss b/frontend/src/Editor/Components/Table/datepicker.scss new file mode 100644 index 0000000000..d09a6f59c6 --- /dev/null +++ b/frontend/src/Editor/Components/Table/datepicker.scss @@ -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,'); + /* 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; + } + } + } +} \ No newline at end of file diff --git a/frontend/src/Editor/Components/Table/load-properties-and-styles.js b/frontend/src/Editor/Components/Table/load-properties-and-styles.js index ce4f8ab5b3..a140b068b0 100644 --- a/frontend/src/Editor/Components/Table/load-properties-and-styles.js +++ b/frontend/src/Editor/Components/Table/load-properties-and-styles.js @@ -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, }; } diff --git a/frontend/src/Editor/Components/Text.jsx b/frontend/src/Editor/Components/Text.jsx index 26d8e6f631..61473ba8eb 100644 --- a/frontend/src/Editor/Components/Text.jsx +++ b/frontend/src/Editor/Components/Text.jsx @@ -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'; diff --git a/frontend/src/Editor/Components/TextInput.jsx b/frontend/src/Editor/Components/TextInput.jsx index c729baa3eb..add215f628 100644 --- a/frontend/src/Editor/Components/TextInput.jsx +++ b/frontend/src/Editor/Components/TextInput.jsx @@ -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 = () => ( <>
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({
{showValidationError && visibility && (
{showValidationError && validationError} diff --git a/frontend/src/Editor/Components/tableUtils.js b/frontend/src/Editor/Components/tableUtils.js new file mode 100644 index 0000000000..eed5da6ab2 --- /dev/null +++ b/frontend/src/Editor/Components/tableUtils.js @@ -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; +}; diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx index 4e5e339e06..1fe5b52b83 100644 --- a/frontend/src/Editor/Container.jsx +++ b/frontend/src/Editor/Container.jsx @@ -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; diff --git a/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx b/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx index 22f79e415b..d06384a7bd 100644 --- a/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx +++ b/frontend/src/Editor/DataSourceManager/DataSourceManager.jsx @@ -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; diff --git a/frontend/src/Editor/DraggableBox.jsx b/frontend/src/Editor/DraggableBox.jsx index bed9b7601b..f50c4c19a2 100644 --- a/frontend/src/Editor/DraggableBox.jsx +++ b/frontend/src/Editor/DraggableBox.jsx @@ -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); diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index cc904708a5..6b8eb2ac13 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -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 ( -
+
@@ -1673,7 +1745,7 @@ const EditorComponent = (props) => { ); } return ( -
+
0} message={`Do you want to run this query - ${queryConfirmationList[0]?.queryName}?`} @@ -1767,14 +1839,18 @@ const EditorComponent = (props) => { id="main-editor-canvas" >
{
)} {defaultComponentStateComputed && ( - <> +
{ canvasWidth={getCanvasWidth()} onDragging={(isDragging) => setIsDragging(isDragging)} /> - +
)}
@@ -1868,7 +1944,7 @@ const EditorComponent = (props) => { />
-
+
{ />
{config.COMMENT_FEATURE_ENABLE && showComments && ( - +
+ +
)}
diff --git a/frontend/src/Editor/EditorSelecto.jsx b/frontend/src/Editor/EditorSelecto.jsx index 0f3bd3433d..2a7d177f07 100644 --- a/frontend/src/Editor/EditorSelecto.jsx +++ b/frontend/src/Editor/EditorSelecto.jsx @@ -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 ( diff --git a/frontend/src/Editor/Header/AppModeToggle.jsx b/frontend/src/Editor/Header/AppModeToggle.jsx new file mode 100644 index 0000000000..f66a196287 --- /dev/null +++ b/frontend/src/Editor/Header/AppModeToggle.jsx @@ -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 ( +
+ {t('leftSidebar.Settings.appMode', 'App mode')} +
+ { + 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) => ( + + {appMode.label} + + ))} + +
+
+ ); +}; + +export default AppModeToggle; diff --git a/frontend/src/Editor/Header/GlobalSettings.jsx b/frontend/src/Editor/Header/GlobalSettings.jsx index cb0a95cb29..170c6183bd 100644 --- a/frontend/src/Editor/Header/GlobalSettings.jsx +++ b/frontend/src/Editor/Header/GlobalSettings.jsx @@ -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 = ({ /> )}
-
+
@@ -380,7 +380,7 @@ export const GlobalSettings = ({
- +

Export app

diff --git a/frontend/src/Editor/Header/HeaderActions.jsx b/frontend/src/Editor/Header/HeaderActions.jsx index 314f7a3569..6b8535252b 100644 --- a/frontend/src/Editor/Header/HeaderActions.jsx +++ b/frontend/src/Editor/Header/HeaderActions.jsx @@ -13,8 +13,8 @@ function HeaderActions({ showToggleLayoutBtn, showUndoRedoBtn, showFullWidth, + darkMode, }) { - const darkMode = localStorage.getItem('darkMode') === 'true'; const { currentLayout, toggleCurrentLayout } = useEditorStore( (state) => ({ currentLayout: state.currentLayout, diff --git a/frontend/src/Editor/Header/index.js b/frontend/src/Editor/Header/index.js index 4d9751f207..d155e1b884 100644 --- a/frontend/src/Editor/Header/index.js +++ b/frontend/src/Editor/Header/index.js @@ -81,7 +81,7 @@ export default function EditorHeader({ const shouldRenderReleaseButton = !!app?.id; return ( -
+
@@ -116,6 +116,7 @@ export default function EditorHeader({ handleRedo={handleRedo} showToggleLayoutBtn showUndoRedoBtn + darkMode={darkMode} />
@@ -153,6 +154,7 @@ export default function EditorHeader({ setAppDefinitionFromVersion={setAppDefinitionFromVersion} onVersionDelete={onVersionDelete} isPublic={isPublic ?? false} + darkMode={darkMode} /> )}
diff --git a/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx b/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx index eff2444311..702745372e 100644 --- a/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx +++ b/frontend/src/Editor/Inspector/Components/DefaultComponent.jsx @@ -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') { diff --git a/frontend/src/Editor/Inspector/Components/Table/ColumnManager/ColumnPopover.jsx b/frontend/src/Editor/Inspector/Components/Table/ColumnManager/ColumnPopover.jsx new file mode 100644 index 0000000000..de1da44195 --- /dev/null +++ b/frontend/src/Editor/Inspector/Components/Table/ColumnManager/ColumnPopover.jsx @@ -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 ( + <> + +
+
{ + if (activeTab !== 'propertiesTab') setActiveTab('propertiesTab'); + }} + > + Properties +
+
{ + if (activeTab !== 'stylesTab') setActiveTab('stylesTab'); + }} + > + Styles +
+
+
+ + {activeTab === 'propertiesTab' ? ( + + ) : ( + + )} + + + ); +}; diff --git a/frontend/src/Editor/Inspector/Components/Table/ColumnManager/DatepickerProperties.jsx b/frontend/src/Editor/Inspector/Components/Table/ColumnManager/DatepickerProperties.jsx new file mode 100644 index 0000000000..1bdf01a62a --- /dev/null +++ b/frontend/src/Editor/Inspector/Components/Table/ColumnManager/DatepickerProperties.jsx @@ -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: ( + <> +
+
+ +
+ {resolveReferences(column?.isDateSelectionEnabled, currentState) && ( +
e.stopPropagation()} + > +
+
+ + + { + let resultFxActiveFields = column?.notActiveFxActiveFields || []; + if (isDateDisplayFormatFxOn) { + resultFxActiveFields.push('dateFormat'); + } else { + resultFxActiveFields = resultFxActiveFields.filter((field) => field !== 'dateFormat'); + } + setIsDateDisplayFormatFxOn(!isDateDisplayFormatFxOn); + onColumnItemChange(index, 'notActiveFxActiveFields', resultFxActiveFields); + }} + /> + +
+ {isDateDisplayFormatFxOn ? ( + onColumnItemChange(index, 'dateFormat', value)} + /> + ) : ( + { + onColumnItemChange(index, 'timeFormat', value); + }} + fuzzySearch + placeholder="Select.." + useCustomStyles={true} + styles={styles(darkMode, '100%')} + /> +
+ )} +
+ +
+ +
e.stopPropagation()} + style={{ padding: '0px 12px' }} + > + + { + onColumnItemChange(index, 'unixTimestamp', value); + }} + fuzzySearch + // placeholder="Select.." + useCustomStyles={true} + styles={styles(darkMode, '100%')} + /> +
+
+ ) : ( +
+ {resolveReferences(column?.isDateSelectionEnabled, currentState) && ( +
+
+ + + { + let resultFxActiveFields = column?.notActiveFxActiveFields || []; + if (isDateDisplayFormatFxOn) { + resultFxActiveFields.push('parseDateFormat'); + } else { + resultFxActiveFields = resultFxActiveFields.filter((field) => field !== 'parseDateFormat'); + } + setIsParseDateFormatFxOn(!isParseDateFormatFxOn); + onColumnItemChange(index, 'notActiveFxActiveFields', resultFxActiveFields); + }} + /> + +
+ {isParseDateFormatFxOn ? ( + onColumnItemChange(index, 'parseDateFormat', value)} + /> + ) : ( + { + onColumnItemChange(index, 'parseTimeFormat', value); + }} + fuzzySearch + placeholder="Select.." + useCustomStyles={true} + styles={styles(darkMode, '100%')} + /> +
+ )} +
+ + { + onColumnItemChange(index, 'objectFit', value); + }} + fuzzySearch + placeholder={t('Select') + '...'} + width={'100%'} + /> +
+ + )} + {column.columnType === 'boolean' && ( +
+
+ onColumnItemChange(index, 'toggleOnBg', color)} + shouldFlexDirectionBeRow={true} + /> +
+
+ onColumnItemChange(index, 'toggleOffBg', color)} + shouldFlexDirectionBeRow={true} + /> +
+
+ )} + + {['string', 'default', undefined, 'number', 'boolean', 'select', 'text', 'newMultiSelect', 'datepicker'].includes( + column.columnType + ) && ( + <> + {column.columnType !== 'boolean' && ( +
+ +
+ )} +
+ +
+ + )} + + {column.columnType === 'link' && ( + <> +
+ +
+
+ +
+
+
+ + onColumnItemChange(index, 'underline', _value)} + defaultValue={column.underline || 'hover'} + style={{ flex: '1 1 0' }} + > + Hover + Always + +
+
+ + )} + + ); +}; diff --git a/frontend/src/Editor/Inspector/Components/Table/ColumnManager/ValidationProperties.jsx b/frontend/src/Editor/Inspector/Components/Table/ColumnManager/ValidationProperties.jsx new file mode 100644 index 0000000000..da9ff70245 --- /dev/null +++ b/frontend/src/Editor/Inspector/Components/Table/ColumnManager/ValidationProperties.jsx @@ -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 ( +
+ + onColumnItemChange(index, validation.property, moment(date).format('MM/DD/YYYY'))} + showTimeSelectOnly={validation.showOnlyTime} + placeholderText={validation?.placeholder ?? ''} + renderCustomHeader={(headerProps) => } + popperClassName={darkMode && 'theme-dark dark-theme'} + /> +
+ ); + case 'timepicker': + return ( +
+ + onColumnItemChange(index, validation.property, moment(date).format('HH:mm'))} + placeholderText={validation?.placeholder ?? ''} + timeFormat={'HH:mm'} + darkMode={darkMode} + /> +
+ ); + default: + return ( +
+ + onColumnItemChange(index, validation.property, value)} + componentName={getPopoverFieldSource(column.columnType, validation.property)} + popOverCallback={(showing) => { + setColumnPopoverRootCloseBlocker(validation.property, showing); + }} + /> +
+ ); + } + }; + + return ( +
+
+ {validationsList.map((validation) => { + if (Array.isArray(validation)) { + return ( +
+ {validation.map((validation) => { + { + return renderAsPerFieldType(validation); + } + })} +
+ ); + } else { + return renderAsPerFieldType(validation); + } + })} +
+
+ ); +}; diff --git a/frontend/src/Editor/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx b/frontend/src/Editor/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx index 9b1ff4af85..2dc2ae4dee 100644 --- a/frontend/src/Editor/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx +++ b/frontend/src/Editor/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx @@ -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 ( -
e.stopPropagation()}> +
e.stopPropagation()}> { + 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 ( + e.stopPropagation()} + style={{ zIndex: 99999, minWidth: 200 }} + > + +
e.stopPropagation()}> + + handleSelectOption(option, optionIndex, value, index, 'label')} + componentName={getPopoverFieldSource(column.columnType, 'options::label')} + popOverCallback={(showing) => { + setColumnPopoverRootCloseBlocker('options::label', showing); + }} + /> +
+
e.stopPropagation()}> + + handleSelectOption(option, optionIndex, value, index, 'value')} + componentName={getPopoverFieldSource(column.columnType, 'options::value')} + popOverCallback={(showing) => { + setColumnPopoverRootCloseBlocker('options::value', showing); + }} + /> +
+ +
+
+ ); + }; + + 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: ( +
+ + {resolveReferences(column?.useDynamicOptions, currentState) ? ( +
+ 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' }]}}`} + /> + +
+ ) : ( + + { + onDragEnd(result); + }} + > + + {({ innerRef, droppableProps, placeholder }) => { + const columnHasOptions = column.hasOwnProperty('options'); + if (!columnHasOptions) { + const defaultOptions = defaultOptionsValues(columnHasOptions); + Object.assign(column, defaultOptions); + } + return ( +
+ {column?.options?.map((option, optionIndex) => { + const resolvedItemName = option.label; + return ( + + {(provided, snapshot) => { + return ( +
+ +
+ { + if (menuOptionLabel === 'Delete') deleteOption(option, optionIndex); + }} + darkMode={darkMode} + deleteIconOutsideMenu={true} + /> +
+
+
+ ); + }} +
+ ); + })} + {placeholder} +
+ ); + }} +
+
+
+ {column?.options?.length === 0 && } +
+ createNewOption()}> + {/* {this.props.t('widget.Table.addNewColumn', ' Add new column')} */} + Add new option + +
+
+
+ )} +
+ ), + }); + + return ; +}; diff --git a/frontend/src/Editor/Inspector/Components/Table/Table.jsx b/frontend/src/Editor/Inspector/Components/Table/Table.jsx index c2cb70bdb1..1517400ae7 100644 --- a/frontend/src/Editor/Inspector/Components/Table/Table.jsx +++ b/frontend/src/Editor/Inspector/Components/Table/Table.jsx @@ -10,15 +10,21 @@ import { Color } from '../../Elements/Color'; import SelectSearch from 'react-select-search'; import { v4 as uuidv4 } from 'uuid'; import { EventManager } from '../../EventManager'; -import { CodeHinter } from '../../../CodeBuilder/CodeHinter'; import { withTranslation } from 'react-i18next'; import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton'; import List from '@/ToolJetUI/List/List'; -import { capitalize, has } from 'lodash'; +import { capitalize, has, unset } from 'lodash'; import NoListItem from './NoListItem'; import { ProgramaticallyHandleProperties } from './ProgramaticallyHandleProperties'; -import { useAppDataStore } from '@/_stores/appDataStore'; +import { ColumnPopoverContent } from './ColumnManager/ColumnPopover'; +import { ModuleContext } from '../../../../_contexts/ModuleContext'; +import { useSuperStore } from '@/_stores/superStore'; +import { checkIfTableColumnDeprecated } from './ColumnManager/DeprecatedColumnTypeMsg'; + +const NON_EDITABLE_COLUMNS = ['link', 'image']; class TableComponent extends React.Component { + static contextType = ModuleContext; + constructor(props) { super(props); @@ -45,6 +51,8 @@ class TableComponent extends React.Component { actionPopOverRootClose: true, showPopOver: false, popOverRootCloseBlockers: [], + isAllColumnsEditable: false, + activeColumnPopoverIndex: null, }; } componentDidMount() { @@ -68,9 +76,28 @@ class TableComponent extends React.Component { eventOptionUpdated, components, currentState, + isAllColumnsEditable: this.checkIfAllColumnsAreEditable(this.props.component), }); } + checkIfAllColumnsAreEditable = (component) => { + const isAllColumnsEditable = component.component?.definition?.properties?.columns?.value + ?.filter((column) => !NON_EDITABLE_COLUMNS.includes(column.columnType)) + .every((column) => resolveReferences(column.isEditable, this.props.currentState)); + return isAllColumnsEditable; + }; + + componentDidUpdate(prevProps) { + const prevPropsColumns = prevProps?.component?.component.definition.properties.columns?.value; + const currentPropsColumns = this.props.component.component.definition.properties.columns?.value; + if (prevPropsColumns !== currentPropsColumns) { + const isAllColumnsEditable = currentPropsColumns + .filter((column) => !NON_EDITABLE_COLUMNS.includes(column.columnType)) + .every((column) => resolveReferences(column.isEditable, this.props.currentState)); + this.setState({ isAllColumnsEditable }); + } + } + onActionButtonPropertyChanged = (index, property, value) => { const actions = this.props.component.component.definition.properties.actions; actions.value[index][property] = value; @@ -95,6 +122,12 @@ class TableComponent extends React.Component { this.props.paramUpdated({ name: 'actions' }, 'value', newValues, 'properties', true); }; + handleToggleColumnPopover = (index) => { + this.setState({ + activeColumnPopoverIndex: index, + }); + }; + actionButtonEventOptionUpdated = (event, option, value, extraData) => { const actions = this.props.component.component.definition.properties.actions; const index = extraData.index; @@ -134,651 +167,55 @@ class TableComponent extends React.Component { } columnPopover = (column, index) => { - const timeZoneOptions = [ - { name: 'UTC', value: 'Etc/UTC' }, - { name: '-12:00', value: 'Etc/GMT+12' }, - { name: '-11:00', value: 'Etc/GMT+11' }, - { name: '-10:00', value: 'Pacific/Honolulu' }, - { name: '-09:00', value: 'America/Anchorage' }, - { name: '-08:00', value: 'America/Santa_Isabel' }, - { name: '-07:00', value: 'America/Chihuahua' }, - { name: '-06:00', value: 'America/Guatemala' }, - { name: '-05:00', value: 'America/Bogota' }, - { name: '-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 ( - -
- - { - this.onColumnItemChange(index, 'columnType', value); - }} - fuzzySearch - placeholder={this.props.t('globals.select', 'Select') + '...'} - /> -
-
- - this.onColumnItemChange(index, 'name', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'name')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('name', showing); - }} - /> -
- {(column.columnType === 'string' || column.columnType === undefined || column.columnType === 'default') && ( -
- - { - this.onColumnItemChange(index, 'textWrap', value); - }} - fuzzySearch - placeholder={this.props.t('globals.select', 'Select') + '...'} - /> -
- )} -
- - this.onColumnItemChange(index, 'key', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'key')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('tableKey', showing); - }} - /> -
- -
- - this.onColumnItemChange(index, 'transformation', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'transformation')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('transformation', showing); - }} - enablePreview={false} - /> -
- -
- - { - this.onColumnItemChange(index, 'horizontalAlignment', value); - }} - fuzzySearch - placeholder={this.props.t('globals.select', 'Select') + '...'} - /> -
- - {(column.columnType === 'string' || column.columnType === undefined || column.columnType === 'default') && ( -
-
- - this.onColumnItemChange(index, 'textColor', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'textColor')} - fieldMeta={column} - component={this.state.component} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('textColor', showing); - }} - /> -
-
- - this.onColumnItemChange(index, 'cellBackgroundColor', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'cellBackgroundColor')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('cellBackgroundColor', showing); - }} - /> -
- - {resolveReferences(column.isEditable, this.state.currentState) && ( -
-
- {this.props.t('widget.Table.validation', 'Validation')} -
-
- - this.onColumnItemChange(index, 'regex', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'regex')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('regex', showing); - }} - /> -
-
- - this.onColumnItemChange(index, 'minLength', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'minLength')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('minLength', showing); - }} - /> -
-
- - this.onColumnItemChange(index, 'maxLength', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'maxLength')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('maxLength', showing); - }} - /> -
-
- - this.onColumnItemChange(index, 'customRule', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'customRule')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('customRule', showing); - }} - /> -
-
- )} -
- )} - - {column.columnType === 'number' && resolveReferences(column.isEditable, this.state.currentState) && ( -
-
- {this.props.t('widget.Table.validation', 'Validation')} -
-
- - this.onColumnItemChange(index, 'minValue', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'minValue')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('minValue', showing); - }} - /> -
-
- - this.onColumnItemChange(index, 'maxValue', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'maxValue')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('maxValue', showing); - }} - /> -
-
- )} - - {column.columnType === 'toggle' && ( -
-
- this.onColumnItemChange(index, 'activeColor', color)} - /> -
- this.columnEventChanged(column, events)} - apps={this.props.apps} - popOverCallback={(showing) => { - this.setState({ actionPopOverRootClose: !showing }); - this.setState({ showPopOver: showing }); - }} - pages={this.props.pages} - /> -
- )} - - {(column.columnType === 'dropdown' || - column.columnType === 'multiselect' || - column.columnType === 'badge' || - column.columnType === 'badges' || - column.columnType === 'radio') && ( -
-
- - this.onColumnItemChange(index, 'values', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'values')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('values', showing); - }} - /> -
-
- - this.onColumnItemChange(index, 'labels', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'labels')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('labels', showing); - }} - /> -
-
- )} - - {column.columnType === 'dropdown' && ( - <> - {resolveReferences(column.isEditable, this.state.currentState) && ( -
-
- {this.props.t('widget.Table.validation', 'Validation')} -
-
- - this.onColumnItemChange(index, 'customRule', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'customRule')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('customRule', showing); - }} - /> -
-
- )} - - )} - - {column.columnType === 'datepicker' && ( -
- -
- this.onColumnItemChange(index, 'dateFormat', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'dateFormat')} - popOverCallback={(showing) => { - this.setColumnPopoverRootCloseBlocker('dateFormat', showing); - }} - /> -
- -
- { - e.stopPropagation(); - this.onColumnItemChange(index, 'parseDateFormat', e.target.value); - }} - defaultValue={column.parseDateFormat} - placeholder={'DD-MM-YYYY'} - /> -
- -
- { - this.onColumnItemChange(index, 'timeZoneValue', value); - }} - fuzzySearch - placeholder="Select.." - /> -
- -
- { - this.onColumnItemChange(index, 'timeZoneDisplay', value); - }} - fuzzySearch - placeholder="Select.." - /> -
-
-
- { - this.onColumnItemChange(index, 'isTimeChecked', !column.isTimeChecked); - }} - checked={column.isTimeChecked} - /> - - {this.props.t('widget.Table.showTime', 'show time')} - -
-
-
- )} - {column.columnType === 'image' && ( - <> -
- - this.onColumnItemChange(index, 'borderRadius', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'borderRadius')} - /> -
-
- - this.onColumnItemChange(index, 'width', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'width')} - /> -
-
- - this.onColumnItemChange(index, 'height', value)} - componentName={this.getPopoverFieldSource(column.columnType, 'height')} - /> -
-
- - { - this.onColumnItemChange(index, 'objectFit', value); - }} - fuzzySearch - placeholder={this.props.t('Select') + '...'} - /> -
- - )} - {column.columnType === 'link' && ( -
- -
- )} - - {!['image', 'link'].includes(column.columnType) && ( - - )} - - -
+
); }; deleteEvents = (ref, eventTarget) => { - const events = useAppDataStore.getState().events.filter((event) => event.target === eventTarget); + const events = useSuperStore + .getState() + .modules[this.context].useAppDataStore.getState() + .events.filter((event) => event.target === eventTarget); const toDelete = events?.filter((e) => e.event?.ref === ref.ref); return new Promise.all( toDelete?.forEach((e) => { - return useAppDataStore.getState().actions.deleteAppVersionEventHandler(e.id); + return useSuperStore + .getState() + .modules[this.context].useAppDataStore.getState() + .actions.deleteAppVersionEventHandler(e.id); }) ); }; + handleEventManagerPopoverCallback = (showing) => { + this.setState({ actionPopOverRootClose: !showing }); + this.setState({ showPopOver: showing }); + }; actionPopOver = (action, index) => { const dummyComponentForActionButton = { component: { @@ -792,8 +229,8 @@ class TableComponent extends React.Component { return ( - -
+ +
@@ -808,7 +245,7 @@ class TableComponent extends React.Component { value={action.buttonText} />
-
+
@@ -927,7 +364,13 @@ class TableComponent extends React.Component { addNewColumn = () => { const columns = this.props.component.component.definition.properties.columns; const newValue = columns.value; - newValue.push({ name: this.generateNewColumnName(columns.value), id: uuidv4(), fxActiveFields: [] }); + newValue.push({ + name: this.generateNewColumnName(columns.value), + id: uuidv4(), + isEditable: this.state?.isAllColumnsEditable, + fxActiveFields: [], + columnType: 'string', + }); this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties', true); }; @@ -948,12 +391,39 @@ class TableComponent extends React.Component { onColumnItemChange = (index, item, value) => { const columns = this.props.component.component.definition.properties.columns; const column = columns.value[index]; + const isAllColumnsEditable = this.state.isAllColumnsEditable; + if (item === 'columnType' && (value === 'select' || value === 'newMultiSelect')) { + column?.options?.length > 0 && column.options.forEach((option) => unset(option, 'makeDefaultOption')); + column.defaultOptionsList = []; + } column[item] = value; const newColumns = columns.value; newColumns[index] = column; + if (NON_EDITABLE_COLUMNS.includes(newColumns[index].columnType)) { + newColumns[index].isEditable = '{{false}}'; + } + + if (item === 'columnType' && !NON_EDITABLE_COLUMNS.includes(value) && isAllColumnsEditable) { + newColumns[index].isEditable = '{{true}}'; + } + this.props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties', true); + + // When any of the column is not editable, we need to disable "make all columns editable" toggle + if (item === 'isEditable' && !resolveReferences(value) && isAllColumnsEditable) { + this.setState({ isAllColumnsEditable: false }); + } + // Check if all columns are editable and also if we have disabled "make all columns editable" toggle, if yes then enable it + if (item === 'isEditable' && resolveReferences(value) && !isAllColumnsEditable) { + const _isAllColumnsEditable = newColumns + .filter((column) => !NON_EDITABLE_COLUMNS.includes(column.columnType)) + .every((column) => resolveReferences(column.isEditable)); + if (_isAllColumnsEditable) { + this.setState({ isAllColumnsEditable: true }); + } + } }; getItemStyle = (isDragging, draggableStyle) => ({ @@ -995,9 +465,33 @@ class TableComponent extends React.Component { getPopoverFieldSource = (column, field) => `component/${this.props.component.component.name}/${column ?? 'default'}::${field}`; + handleMakeAllColumnsEditable = (value) => { + const columns = resolveReferences( + this.props.component.component.definition.properties.columns, + this.props.currentState + ); + + this.setState({ isAllColumnsEditable: resolveReferences(value) }); + + const newValue = columns.value.map((column) => ({ + ...column, + isEditable: !NON_EDITABLE_COLUMNS.includes(column.columnType) ? value : '{{false}}', + })); + + this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties', true); + }; + + duplicateColumn = (index) => { + const columns = this.props.component.component.definition.properties?.columns ?? []; + const newColumns = columns.value; + let columnToBeDuplicated = newColumns?.[index]; + columnToBeDuplicated = { ...columnToBeDuplicated, id: uuidv4() }; + newColumns.push(columnToBeDuplicated); + this.props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties', true); + }; + render() { const { dataQueries, component, paramUpdated, componentMeta, components, currentState, darkMode } = this.props; - const columns = component.component.definition.properties.columns; const actions = component.component.definition.properties.actions || { value: [] }; if (!component.component.definition.properties.displaySearchBox) @@ -1032,6 +526,7 @@ class TableComponent extends React.Component { ? resolveReferences(component.component.definition.properties.allowSelection?.value, currentState) : resolveReferences(component.component.definition.properties.highlightSelectedRow.value, currentState) || resolveReferences(component.component.definition.properties.showBulkSelector.value, currentState); + const renderCustomElement = (param, paramType = 'properties') => { return renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState); }; @@ -1068,9 +563,53 @@ class TableComponent extends React.Component { > {({ innerRef, droppableProps, placeholder }) => ( -
+
{columns.value.map((item, index) => { const resolvedItemName = resolveReferences(item.name, this.state.currentState); + const isEditable = resolveReferences(item.isEditable, this.state.currentState); + const columnVisibility = item?.columnVisibility ?? true; + const getSecondaryText = (text) => { + switch (text) { + case undefined: + return ''; + case 'string': + return 'String'; + case 'default': + return 'Default'; + case 'number': + return 'Number'; + case 'text': + return 'Text'; + case 'badge': + return 'Badge'; + case 'badges': + return 'Badges'; + case 'tags': + return 'Tags'; + case 'dropdown': + return 'Dropdown'; + case 'link': + return 'Link'; + case 'radio': + return 'Radio'; + case 'multiselect': + return 'Multiselect deprecated'; + case 'toggle': + return 'Toggle'; + case 'datepicker': + return 'Datepicker'; + case 'image': + return 'Image'; + case 'boolean': + return 'Boolean'; + case 'select': + return 'Select'; + case 'newMultiSelect': + return 'Multiselect'; + default: + capitalize(text ?? ''); + } + }; return ( {(provided, snapshot) => ( @@ -1086,26 +625,45 @@ class TableComponent extends React.Component { placement="left" rootClose={this.state.popOverRootCloseBlockers.length === 0} overlay={this.columnPopover(item, index)} + onToggle={(show) => { + if (show) { + this.handleToggleColumnPopover(index); + } else { + this.handleToggleColumnPopover(null); + } + }} > -
+
{ - if (menuOptionLabel === 'Delete') + if (menuOptionLabel === 'Delete') { this.removeColumn(index, `${item.name}-${index}`); + } else if (menuOptionLabel === 'copyColumn') { + this.duplicateColumn(index); + } }} darkMode={darkMode} - menuActions={[ - { - label: 'Delete', - icon: '', - }, - ]} + // menuActions={[ + // { + // label: 'Delete', + // icon: '', + // }, + // ]} + deleteIconOutsideMenu={true} + showCopyColumnOption={true} + showVisibilityIcon={true} + isColumnVisible={resolveReferences(columnVisibility, this.state.currentState)} + className={`${ + this.state.activeColumnPopoverIndex === index && 'active-column-list' + }`} + columnType={item?.columnType} + isDeprecated={checkIfTableColumnDeprecated(item?.columnType)} />
@@ -1119,13 +677,26 @@ class TableComponent extends React.Component { )} -
+
{columns?.value?.length === 0 && } -
+
{this.props.t('widget.Table.addNewColumn', ' Add new column')}
+ { + this.handleMakeAllColumnsEditable(value); + }} + property="isAllColumnsEditable" + props={{ isAllColumnsEditable: this.state.isAllColumnsEditable }} + component={this.props.component} + paramMeta={{ type: 'toggle', displayName: 'Make all columns editable' }} + paramType="properties" + />
)} @@ -1176,6 +747,8 @@ class TableComponent extends React.Component { 'hideColumnSelectorButton', 'loadingState', 'showBulkUpdateActions', + 'visibility', + 'disabledState', ]; items.push({ diff --git a/frontend/src/Editor/Inspector/Elements/Color.jsx b/frontend/src/Editor/Inspector/Elements/Color.jsx index c92cb27cdb..661898079b 100644 --- a/frontend/src/Editor/Inspector/Elements/Color.jsx +++ b/frontend/src/Editor/Inspector/Elements/Color.jsx @@ -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 ( -
- +
+ {showPicker && (
@@ -70,7 +80,7 @@ export const Color = ({ param, definition, onChange, paramType, componentMeta, c boxShadow: `0px 1px 2px 0px rgba(16, 24, 40, 0.05)`, }} >
-
+
{definition.value}
diff --git a/frontend/src/Editor/Inspector/Inspector.jsx b/frontend/src/Editor/Inspector/Inspector.jsx index 936f2014aa..7a0175a30a 100644 --- a/frontend/src/Editor/Inspector/Inspector.jsx +++ b/frontend/src/Editor/Inspector/Inspector.jsx @@ -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 = (
-
+
- {!shouldAddBoxShadow.includes(component.component.component) && buildGeneralStyle()} + {!isRevampedComponent && buildGeneralStyle()}
); @@ -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); + } } }; diff --git a/frontend/src/Editor/Inspector/Utils.js b/frontend/src/Editor/Inspector/Utils.js index 56b36741e3..aad23c71c6 100644 --- a/frontend/src/Editor/Inspector/Utils.js +++ b/frontend/src/Editor/Inspector/Utils.js @@ -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); diff --git a/frontend/src/Editor/LeftSidebar/SidebarDebugger/useDebugger.js b/frontend/src/Editor/LeftSidebar/SidebarDebugger/useDebugger.js index 75f9124fa4..024b3ad6fe 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarDebugger/useDebugger.js +++ b/frontend/src/Editor/LeftSidebar/SidebarDebugger/useDebugger.js @@ -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 })]); diff --git a/frontend/src/Editor/LeftSidebar/SidebarInspector.jsx b/frontend/src/Editor/LeftSidebar/SidebarInspector.jsx index 0edb6c8bf6..7233fcbf72 100644 --- a/frontend/src/Editor/LeftSidebar/SidebarInspector.jsx +++ b/frontend/src/Editor/LeftSidebar/SidebarInspector.jsx @@ -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: () => }; + const source = allDs.find((ds) => ds.kind === query.kind); + return { iconName: query.name, jsx: () => }; }); const componentIcons = Object.entries(currentState['components']).map(([key, value]) => { diff --git a/frontend/src/Editor/LeftSidebar/index.jsx b/frontend/src/Editor/LeftSidebar/index.jsx index e6adb30771..120e6e9b40 100644 --- a/frontend/src/Editor/LeftSidebar/index.jsx +++ b/frontend/src/Editor/LeftSidebar/index.jsx @@ -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 ( -
+
handleSelectedSidebarItem('page')} @@ -314,9 +315,9 @@ export const LeftSidebar = forwardRef((props, ref) => { />
)} +
- {/* */}
); diff --git a/frontend/src/Editor/ManageAppUsers.jsx b/frontend/src/Editor/ManageAppUsers.jsx index 8c0d8f32cb..129da02bde 100644 --- a/frontend/src/Editor/ManageAppUsers.jsx +++ b/frontend/src/Editor/ManageAppUsers.jsx @@ -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({ diff --git a/frontend/src/Editor/QueryManager/Components/QueryManagerHeader.jsx b/frontend/src/Editor/QueryManager/Components/QueryManagerHeader.jsx index dce7260229..bb8c05ab28 100644 --- a/frontend/src/Editor/QueryManager/Components/QueryManagerHeader.jsx +++ b/frontend/src/Editor/QueryManager/Components/QueryManagerHeader.jsx @@ -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; diff --git a/frontend/src/Editor/QueryPanel/QueryCard.jsx b/frontend/src/Editor/QueryPanel/QueryCard.jsx index 1b954f693c..efa32645a4 100644 --- a/frontend/src/Editor/QueryPanel/QueryCard.jsx +++ b/frontend/src/Editor/QueryPanel/QueryCard.jsx @@ -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); diff --git a/frontend/src/Editor/QueryPanel/QueryDataPane.jsx b/frontend/src/Editor/QueryPanel/QueryDataPane.jsx index 990f813af7..7c8fe55c4d 100644 --- a/frontend/src/Editor/QueryPanel/QueryDataPane.jsx +++ b/frontend/src/Editor/QueryPanel/QueryDataPane.jsx @@ -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) => { diff --git a/frontend/src/Editor/QueryPanel/QueryPanel.jsx b/frontend/src/Editor/QueryPanel/QueryPanel.jsx index 9f102c6f1d..61f29c84fc 100644 --- a/frontend/src/Editor/QueryPanel/QueryPanel.jsx +++ b/frontend/src/Editor/QueryPanel/QueryPanel.jsx @@ -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 ( - <> +
- +
); }; diff --git a/frontend/src/Editor/SubContainer.jsx b/frontend/src/Editor/SubContainer.jsx index 45ee2dc834..0b75970bce 100644 --- a/frontend/src/Editor/SubContainer.jsx +++ b/frontend/src/Editor/SubContainer.jsx @@ -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) { diff --git a/frontend/src/Editor/Viewer.jsx b/frontend/src/Editor/Viewer.jsx index 9695e5cd8f..6177292189 100644 --- a/frontend/src/Editor/Viewer.jsx +++ b/frontend/src/Editor/Viewer.jsx @@ -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 ( -
+
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} /> {this.props.currentLayout !== 'mobile' && ( @@ -658,7 +701,6 @@ class ViewerComponent extends React.Component { 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 ( (props) => { currentLayout={currentLayout} updateState={updateState} queryConfirmationList={queryConfirmationList} + darkMode={isAppDarkMode} /> ); }; diff --git a/frontend/src/Editor/Viewer/DesktopHeader.jsx b/frontend/src/Editor/Viewer/DesktopHeader.jsx index 3a4182304e..9e40fb3b07 100644 --- a/frontend/src/Editor/Viewer/DesktopHeader.jsx +++ b/frontend/src/Editor/Viewer/DesktopHeader.jsx @@ -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 = () => (
)} - - - + {showDarkModeToggle && ( + + + + )} ); } @@ -76,9 +86,11 @@ const DesktopHeader = ({ showHeader, appName, changeDarkMode, darkMode, setAppDe darkMode={darkMode} /> )} -
- -
+ {showDarkModeToggle && ( +
+ +
+ )}
); }; diff --git a/frontend/src/Editor/Viewer/MobileHeader.jsx b/frontend/src/Editor/Viewer/MobileHeader.jsx index 662e349749..129cf8b3ce 100644 --- a/frontend/src/Editor/Viewer/MobileHeader.jsx +++ b/frontend/src/Editor/Viewer/MobileHeader.jsx @@ -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 ( { +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
-
-
-
-
- + {showDarkModeToggle && ( +
+
+
+
+ +
-
+ )} ); diff --git a/frontend/src/Editor/Viewer/PreviewSettings.jsx b/frontend/src/Editor/Viewer/PreviewSettings.jsx index 3ec6acbf29..1035472e09 100644 --- a/frontend/src/Editor/Viewer/PreviewSettings.jsx +++ b/frontend/src/Editor/Viewer/PreviewSettings.jsx @@ -32,6 +32,7 @@ const PreviewSettings = ({ isMobileLayout, setAppDefinitionFromVersion, showHead onVersionDelete={noop} isEditable={false} isViewer + darkMode={darkMode} /> )} @@ -98,12 +99,13 @@ const PreviewSettings = ({ isMobileLayout, setAppDefinitionFromVersion, showHead onVersionDelete={noop} isEditable={false} isViewer + darkMode={darkMode} /> )}
layout - +
)} diff --git a/frontend/src/Editor/Viewer/viewer.scss b/frontend/src/Editor/Viewer/viewer.scss index 7d25e623df..6c3e03c088 100644 --- a/frontend/src/Editor/Viewer/viewer.scss +++ b/frontend/src/Editor/Viewer/viewer.scss @@ -123,7 +123,7 @@ .canvas-container::-webkit-scrollbar { - width: 0; + // width: 0; background: transparent; } diff --git a/frontend/src/Editor/WidgetManager.jsx b/frontend/src/Editor/WidgetManager.jsx index a8ed78fb5d..1bf3c515dc 100644 --- a/frontend/src/Editor/WidgetManager.jsx +++ b/frontend/src/Editor/WidgetManager.jsx @@ -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', diff --git a/frontend/src/Editor/WidgetManager/widgetConfig.js b/frontend/src/Editor/WidgetManager/widgetConfig.js index fc2c6b77ee..4212985baa 100644 --- a/frontend/src/Editor/WidgetManager/widgetConfig.js +++ b/frontend/src/Editor/WidgetManager/widgetConfig.js @@ -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' }, diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index 3b6d6bdaf5..30aaac8e83 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -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 (
- {showCreateAppModal && ( + {(showCreateAppModal || showCreateModuleModal) && ( 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'} /> )} diff --git a/frontend/src/ToolJetUI/List/List.jsx b/frontend/src/ToolJetUI/List/List.jsx index 2579c50bd9..702121f311 100644 --- a/frontend/src/ToolJetUI/List/List.jsx +++ b/frontend/src/ToolJetUI/List/List.jsx @@ -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 {children}; } @@ -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 (
)} + {showVisibilityIcon && !isColumnVisible && ( + + + + )} + + {isDeprecated && ( + + + + + + )}
-
+
{enableActionsMenu && isHovered && ( - + @@ -110,6 +127,38 @@ function ListItem({ )} + {showCopyColumnOption && isHovered && ( + { + e.stopPropagation(); + onMenuOptionClick(primaryText, 'copyColumn'); + }} + > + + + + + )} + {deleteIconOutsideMenu && isHovered && ( + { + e.stopPropagation(); + onMenuOptionClick(primaryText, 'Delete'); + }} + > + + + + + )}
diff --git a/frontend/src/ToolJetUI/List/list.scss b/frontend/src/ToolJetUI/List/list.scss index 3253fb8b10..43fc26ffeb 100644 --- a/frontend/src/ToolJetUI/List/list.scss +++ b/frontend/src/ToolJetUI/List/list.scss @@ -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 + } } \ No newline at end of file diff --git a/frontend/src/ToolJetUI/Timepicker/Timepicker.jsx b/frontend/src/ToolJetUI/Timepicker/Timepicker.jsx new file mode 100644 index 0000000000..65a3c9b2dd --- /dev/null +++ b/frontend/src/ToolJetUI/Timepicker/Timepicker.jsx @@ -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 ( +
+ +
+ ); +}; + +export default Timepicker; diff --git a/frontend/src/ToolJetUI/Timepicker/timepicker.scss b/frontend/src/ToolJetUI/Timepicker/timepicker.scss new file mode 100644 index 0000000000..8d940ae21a --- /dev/null +++ b/frontend/src/ToolJetUI/Timepicker/timepicker.scss @@ -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); + } + } +} + diff --git a/frontend/src/_components/DarkModeToggle.jsx b/frontend/src/_components/DarkModeToggle.jsx index d797fa7a17..ae7fa16fd3 100644 --- a/frontend/src/_components/DarkModeToggle.jsx +++ b/frontend/src/_components/DarkModeToggle.jsx @@ -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({ {darkMode @@ -60,7 +63,7 @@ export const DarkModeToggle = function DarkModeToggle({ } >
{ + const moduleName = useContext(ModuleContext); + + if (!moduleName) throw Error('useModuleName can only be used inside a ModuleContext'); + + return moduleName; +}; diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index a22768b5e8..64531a2de2 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -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, + }, + }); } }; diff --git a/frontend/src/_helpers/utils.js b/frontend/src/_helpers/utils.js index 1fb8257879..688ade0663 100644 --- a/frontend/src/_helpers/utils.js +++ b/frontend/src/_helpers/utils.js @@ -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) { diff --git a/frontend/src/_hooks/useAppDarkMode.js b/frontend/src/_hooks/useAppDarkMode.js new file mode 100644 index 0000000000..c89fb204ed --- /dev/null +++ b/frontend/src/_hooks/useAppDarkMode.js @@ -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; diff --git a/frontend/src/_stores/appDataStore.js b/frontend/src/_stores/appDataStore.js index fb50c8f9e4..dc458b7f53 100644 --- a/frontend/src/_stores/appDataStore.js +++ b/frontend/src/_stores/appDataStore.js @@ -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); diff --git a/frontend/src/_stores/appVersionStore.js b/frontend/src/_stores/appVersionStore.js index 1824c28e87..d84c38c1d8 100644 --- a/frontend/src/_stores/appVersionStore.js +++ b/frontend/src/_stores/appVersionStore.js @@ -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); diff --git a/frontend/src/_stores/currentStateStore.js b/frontend/src/_stores/currentStateStore.js index 6bcb2ff579..223890bd98 100644 --- a/frontend/src/_stores/currentStateStore.js +++ b/frontend/src/_stores/currentStateStore.js @@ -1,41 +1,57 @@ import { shallow } from 'zustand/shallow'; import { create, zustandDevTools } from './utils'; import { omit } from 'lodash'; +import { useContext } from 'react'; +import { useSuperStore } from './superStore'; +import { ModuleContext } from '../_contexts/ModuleContext'; -const initialState = { - queries: {}, - components: {}, - globals: { - theme: { name: 'light' }, - urlparams: null, - }, - errors: {}, - variables: {}, - client: {}, - server: {}, - page: { - handle: '', +export function createCurrentStateStore(moduleName) { + const initialState = { + queries: {}, + components: {}, + globals: { + theme: { name: 'light' }, + urlparams: null, + }, + errors: {}, variables: {}, - }, - succededQuery: {}, -}; + client: {}, + server: {}, + page: { + handle: '', + variables: {}, + }, + succededQuery: {}, + moduleName, + }; -export const useCurrentStateStore = create( - zustandDevTools( - (set, get) => ({ - ...initialState, - actions: { - setCurrentState: (currentState) => { - set({ ...currentState }, false, { type: 'SET_CURRENT_STATE', currentState }); + return create( + zustandDevTools( + (set, get) => ({ + ...initialState, + actions: { + setCurrentState: (currentState) => { + set({ ...currentState }, false, { type: 'SET_CURRENT_STATE', currentState }); + }, + setErrors: (error) => { + set({ errors: { ...get().errors, ...error } }, false, { type: 'SET_ERRORS', error }); + }, }, - setErrors: (error) => { - set({ errors: { ...get().errors, ...error } }, false, { type: 'SET_ERRORS', error }); - }, - }, - }), - { name: 'Current State' } - ) -); + }), + { name: 'Current State' } + ) + ); +} + +export const useCurrentStateStore = (callback, shallow) => { + const moduleName = useContext(ModuleContext); + + if (!moduleName) throw Error('module context not available'); + + const _useCurrentStateStore = useSuperStore((state) => state.modules[moduleName].useCurrentStateStore); + + return _useCurrentStateStore(callback, shallow); +}; export const useCurrentState = () => // Omitting 'actions' here because we don't want to expose it to user @@ -55,6 +71,6 @@ export const useCurrentState = () => }; }, shallow); -export const getCurrentState = () => { - return omit(useCurrentStateStore.getState(), 'actions'); +export const getCurrentState = (moduleName) => { + return omit(useSuperStore.getState().modules[moduleName].useCurrentStateStore.getState(), 'actions'); }; diff --git a/frontend/src/_stores/dataQueriesStore.js b/frontend/src/_stores/dataQueriesStore.js index e6c6a6feba..538109c5da 100644 --- a/frontend/src/_stores/dataQueriesStore.js +++ b/frontend/src/_stores/dataQueriesStore.js @@ -3,398 +3,455 @@ import { getDefaultOptions } from './storeHelper'; import { dataqueryService } from '@/_services'; // import debounce from 'lodash/debounce'; import { useAppDataStore } from '@/_stores/appDataStore'; -import { useQueryPanelStore } from '@/_stores/queryPanelStore'; -import { useAppVersionStore } from '@/_stores/appVersionStore'; import { runQueries } from '@/_helpers/appUtils'; import { v4 as uuidv4 } from 'uuid'; import { toast } from 'react-hot-toast'; import _, { isEmpty, throttle } from 'lodash'; -import { useEditorStore } from './editorStore'; +import { useSuperStore } from './superStore'; import { shallow } from 'zustand/shallow'; +import { useContext } from 'react'; +import { ModuleContext } from '../_contexts/ModuleContext'; import { getCurrentState, useCurrentStateStore } from './currentStateStore'; -const initialState = { - dataQueries: [], - sortBy: 'updated_at', - sortOrder: 'desc', - loadingDataQueries: true, - isDeletingQueryInProcess: false, - /** TODO: Below two params are primarily used only for websocket invocation post update. Can be removed onece websocket logic is revamped */ - // isCreatingQueryInProcess: false, - creatingQueryInProcessId: null, - isUpdatingQueryInProcess: false, - /** When a 'Create Data Query' operation is in progress, rename/update API calls are cached in the variable. */ - queuedActions: {}, -}; +export function createDataQueriesStore(moduleName) { + const initialState = { + dataQueries: [], + sortBy: 'updated_at', + sortOrder: 'desc', + loadingDataQueries: true, + isDeletingQueryInProcess: false, + /** TODO: Below two params are primarily used only for websocket invocation post update. Can be removed onece websocket logic is revamped */ + // isCreatingQueryInProcess: false, + creatingQueryInProcessId: null, + isUpdatingQueryInProcess: false, + /** When a 'Create Data Query' operation is in progress, rename/update API calls are cached in the variable. */ + queuedActions: {}, + moduleName, // TODOS: change this + }; -export const useDataQueriesStore = create( - zustandDevTools( - (set, get) => ({ - ...initialState, - actions: { - // TODO: Remove editor state while changing currentState - fetchDataQueries: async (appVersionId, selectFirstQuery = false, runQueriesOnAppLoad = false, ref) => { - set({ loadingDataQueries: true }); - const data = await dataqueryService.getAll(appVersionId); - set((state) => ({ - dataQueries: sortByAttribute(data.data_queries, state.sortBy, state.sortOrder), - loadingDataQueries: false, - })); + return create( + zustandDevTools( + (set, get) => ({ + ...initialState, + actions: { + // TODO: Remove editor state while changing currentState + fetchDataQueries: async (appVersionId, selectFirstQuery = false, runQueriesOnAppLoad = false, ref) => { + set({ loadingDataQueries: true }); + const data = await dataqueryService.getAll(appVersionId); + set((state) => ({ + dataQueries: sortByAttribute(data.data_queries, state.sortBy, state.sortOrder), + loadingDataQueries: false, + })); - if (data.data_queries.length !== 0) { - const queryConfirmationList = []; - const updatedQueries = {}; - const currentQueries = useCurrentStateStore.getState().queries; + if (data.data_queries.length !== 0) { + const queryConfirmationList = []; + const updatedQueries = {}; + const currentQueries = useSuperStore + .getState() + .modules[get().moduleName].useCurrentStateStore.getState().queries; - data.data_queries.forEach(({ id, name, options }) => { - updatedQueries[name] = _.merge(currentQueries[name], { id: id, isLoading: false, data: [], rawData: [] }); + data.data_queries.forEach(({ id, name, options }) => { + updatedQueries[name] = _.merge(currentQueries[name], { + id: id, + isLoading: false, + data: [], + rawData: [], + }); + if (options && options?.requestConfirmation && options?.runOnPageLoad) { + queryConfirmationList.push({ queryId: id, queryName: name }); + } + }); - if (options && options?.requestConfirmation && options?.runOnPageLoad) { - queryConfirmationList.push({ queryId: id, queryName: name }); + if (queryConfirmationList.length !== 0) { + useSuperStore + .getState() + .modules[get().moduleName].useEditorStore.getState() + .actions.updateQueryConfirmationList(queryConfirmationList); } - }); - if (queryConfirmationList.length !== 0) { - useEditorStore.getState().actions.updateQueryConfirmationList(queryConfirmationList); + useSuperStore + .getState() + .modules[get().moduleName].useCurrentStateStore.getState() + .actions.setCurrentState({ + ...getCurrentState(get().moduleName), + queries: updatedQueries, + }); } - useCurrentStateStore.getState().actions.setCurrentState({ - ...getCurrentState(), - queries: updatedQueries, - }); - } + // Compute query state to be added in the current state + const { actions, selectedQuery } = useSuperStore + .getState() + .modules[get().moduleName].useQueryPanelStore.getState(); + if (selectFirstQuery) { + actions.setSelectedQuery(data.data_queries[0]?.id, data.data_queries[0]); + } else if (selectedQuery?.id) { + const query = data.data_queries.find((query) => query.id === selectedQuery?.id); + actions.setSelectedQuery(query?.id); + } - // Compute query state to be added in the current state - const { actions, selectedQuery } = useQueryPanelStore.getState(); - if (selectFirstQuery) { - actions.setSelectedQuery(data.data_queries[0]?.id, data.data_queries[0]); - } else if (selectedQuery?.id) { - const query = data.data_queries.find((query) => query.id === selectedQuery?.id); - actions.setSelectedQuery(query?.id); - } + // Runs query on loading application + if (runQueriesOnAppLoad) runQueries(data.data_queries, ref); + }, + setDataQueries: (dataQueries) => set({ dataQueries }), + deleteDataQueries: (queryId) => { + set({ isDeletingQueryInProcess: true }); + useSuperStore.getState().modules[get().moduleName].useAppDataStore.getState().actions.setIsSaving(true); + dataqueryService + .del(queryId) + .then(() => { + const { actions } = useSuperStore.getState().modules[get().moduleName].useQueryPanelStore.getState(); + const { dataQueries } = useSuperStore + .getState() + .modules[get().moduleName].useDataQueriesStore.getState(); + const newSelectedQuery = dataQueries.find((query) => query.id !== queryId); + actions.setSelectedQuery(newSelectedQuery?.id || null); + if (!newSelectedQuery?.id) { + actions.setSelectedDataSource(null); + } + set((state) => ({ + isDeletingQueryInProcess: false, + dataQueries: state.dataQueries.filter((query) => query.id !== queryId), + })); - // Runs query on loading application - if (runQueriesOnAppLoad) runQueries(data.data_queries, ref); - }, - setDataQueries: (dataQueries) => set({ dataQueries }), - deleteDataQueries: (queryId) => { - set({ isDeletingQueryInProcess: true }); - useAppDataStore.getState().actions.setIsSaving(true); - dataqueryService - .del(queryId) - .then(() => { - const { actions } = useQueryPanelStore.getState(); - const { dataQueries } = useDataQueriesStore.getState(); - const newSelectedQuery = dataQueries.find((query) => query.id !== queryId); - actions.setSelectedQuery(newSelectedQuery?.id || null); - if (!newSelectedQuery?.id) { - actions.setSelectedDataSource(null); - } - set((state) => ({ - isDeletingQueryInProcess: false, - dataQueries: state.dataQueries.filter((query) => query.id !== queryId), - })); + const currentQueries = useSuperStore + .getState() + .modules[get().moduleName].useCurrentStateStore.getState().queries; - const currentQueries = useCurrentStateStore.getState().queries; - - useCurrentStateStore.getState().actions.setCurrentState({ - ...getCurrentState(), - queries: Object.keys(currentQueries).reduce((acc, key) => { - if (currentQueries[key].id !== queryId) { - acc[key] = currentQueries[key]; - } - return acc; - }, {}), - }); - }) - .catch(() => { - set({ - isDeletingQueryInProcess: false, - }); - }) - .finally(() => useAppDataStore.getState().actions.setIsSaving(false)); - }, - updateDataQuery: (options) => { - set({ isUpdatingQueryInProcess: true }); - const { actions, selectedQuery } = useQueryPanelStore.getState(); - set((state) => ({ - isUpdatingQueryInProcess: false, - dataQueries: state.dataQueries.map((query) => { - if (query.id === selectedQuery.id) { - return { - ...query, - options: { ...options }, - }; - } - return query; - }), - })); - actions.setSelectedQuery(selectedQuery.id); - }, - // createDataQuery: (appId, appVersionId, options, kind, name, selectedDataSource, shouldRunQuery) => { - createDataQuery: (selectedDataSource, shouldRunQuery) => { - const appVersionId = useAppVersionStore.getState().editingVersion?.id; - const appId = useAppDataStore.getState().appId; - const { options, name } = getDefaultOptions(selectedDataSource); - const kind = selectedDataSource.kind; - const tempId = uuidv4(); - set({ creatingQueryInProcessId: tempId }); - const { actions, selectedQuery } = useQueryPanelStore.getState(); - const dataSourceId = selectedDataSource?.id !== 'null' ? selectedDataSource?.id : null; - const pluginId = selectedDataSource.pluginId || selectedDataSource.plugin_id; - useAppDataStore.getState().actions.setIsSaving(true); - const { dataQueries } = useDataQueriesStore.getState(); - const currDataQueries = [...dataQueries]; - set(() => ({ - dataQueries: [ - { - ...selectedQuery, - data_source_id: dataSourceId, - app_version_id: appVersionId, - options, - name, - kind, - id: tempId, - plugin: selectedDataSource.plugin, - }, - ...currDataQueries, - ], - })); - actions.setSelectedQuery(tempId); - actions.setNameInputFocussed(true); - dataqueryService - .create(appId, appVersionId, name, kind, options, dataSourceId, pluginId) - .then((data) => { - set((state) => ({ - creatingQueryInProcessId: null, - dataQueries: state.dataQueries.map((query) => { - if (query.id === tempId) { - return { - ...query, - id: data.id, - data_source_id: dataSourceId, - }; - } - return query; - }), - })); - actions.setSelectedQuery(data.id, data); - if (shouldRunQuery) actions.setQueryToBeRun(data); - - /** Checks if there is an API call cached. If yes execute it */ - if (!isEmpty(get()?.queuedActions?.renameQuery)) { - get().actions.renameQuery(data.id, get().queuedActions.renameQuery); - set({ queuedActions: { ...get().queuedActions, renameQuery: undefined } }); - } - - if (!isEmpty(get()?.queuedActions?.saveData)) { - get().actions.saveData({ ...get().queuedActions.saveData, id: data.id }); - set({ queuedActions: { ...get().queuedActions, saveData: undefined } }); - } - - const currentQueries = useCurrentStateStore.getState().queries; - - useCurrentStateStore.getState().actions.setCurrentState({ - ...getCurrentState(), - queries: { - ...currentQueries, - [data.name]: { - id: data.id, - isLoading: false, - data: [], - rawData: [], - }, - }, - }); - }) - .catch((error) => { - set((state) => ({ - creatingQueryInProcessId: null, - dataQueries: state.dataQueries.filter((query) => query.id !== tempId), - })); - actions.setSelectedQuery(null); - toast.error(`Failed to create query: ${error.message}`); - }) - .finally(() => useAppDataStore.getState().actions.setIsSaving(false)); - }, - renameQuery: (id, newName) => { - /** If query creation in progress, skips call and pushes the update to queue */ - if (get().creatingQueryInProcessId === id) { - set({ queuedActions: { ...get().queuedActions, renameQuery: newName } }); - return; - } - useAppDataStore.getState().actions.setIsSaving(true); - /** - * Seting name to store before api call for instant UI update and better UX. - * Name is again set to state post api call to handle if renaming fails in backend. - * */ - set((state) => ({ - dataQueries: state.dataQueries.map((query) => (query.id === id ? { ...query, name: newName } : query)), - })); - dataqueryService - .update(id, newName) - .then((data) => { - set((state) => ({ - dataQueries: state.dataQueries.map((query) => { - if (query.id === id) { - return { ...query, name: newName, updated_at: data.updated_at }; - } - return query; - }), - })); - useQueryPanelStore.getState().actions.setSelectedQuery(id); - - const currentQueries = useCurrentStateStore.getState().queries; - - const queryName = Object.keys(currentQueries).find((key) => currentQueries[key].id === id); - - const { [queryName]: _, ...rest } = currentQueries; - - useCurrentStateStore.getState().actions.setCurrentState({ - ...getCurrentState(), - queries: { - ...rest, - [newName]: { - ...currentQueries[queryName], - name: newName, - }, - }, - }); - }) - .finally(() => useAppDataStore.getState().actions.setIsSaving(false)); - }, - changeDataQuery: (newDataSource) => { - const { selectedQuery } = useQueryPanelStore.getState(); - set({ - isUpdatingQueryInProcess: true, - }); - useAppDataStore.getState().actions.setIsSaving(true); - dataqueryService - .changeQueryDataSource(selectedQuery?.id, newDataSource.id) - .then(() => { - set((state) => ({ - isUpdatingQueryInProcess: false, - dataQueries: state.dataQueries.map((query) => { - if (query?.id === selectedQuery?.id) { - return { ...query, dataSourceId: newDataSource?.id, data_source_id: newDataSource?.id }; - } - return query; - }), - })); - useQueryPanelStore.getState().actions.setSelectedQuery(selectedQuery.id); - useQueryPanelStore.getState().actions.setSelectedDataSource(newDataSource); - }) - .catch(() => { - set({ - isUpdatingQueryInProcess: false, - }); - }) - .finally(() => useAppDataStore.getState().actions.setIsSaving(false)); - }, - duplicateQuery: (id, appId) => { - set({ creatingQueryInProcessId: uuidv4() }); - const { actions } = useQueryPanelStore.getState(); - const { dataQueries } = useDataQueriesStore.getState(); - const queryToClone = { ...dataQueries.find((query) => query.id === id) }; - let newName = queryToClone.name + '_copy'; - const names = dataQueries.map(({ name }) => name); - let count = 0; - while (names.includes(newName)) { - count++; - newName = queryToClone.name + '_copy' + count.toString(); - } - queryToClone.name = newName; - - useAppDataStore.getState().actions.setIsSaving(true); - dataqueryService - .create( - appId, - queryToClone.app_version_id, - queryToClone.name, - queryToClone.kind, - queryToClone.options, - queryToClone.data_source_id, - queryToClone.pluginId - ) - .then((data) => { - set((state) => ({ - creatingQueryInProcessId: null, - dataQueries: [{ ...data, data_source_id: queryToClone.data_source_id }, ...state.dataQueries], - })); - actions.setSelectedQuery(data.id, { ...data, data_source_id: queryToClone.data_source_id }); - - const dataQueryEvents = useAppDataStore - .getState() - .events?.filter((event) => event.target === 'data_query' && event.sourceId === queryToClone.id); - - if (dataQueryEvents?.length === 0) return; - - return Promise.all( - dataQueryEvents.map((event) => { - const newEvent = { - event: { - ...event?.event, - }, - eventType: event?.target, - attachedTo: data.id, - index: event?.index, - }; - useAppDataStore.getState().actions?.createAppVersionEventHandlers(newEvent); - }) + useSuperStore + .getState() + .modules[get().moduleName].useCurrentStateStore.getState() + .actions.setCurrentState({ + ...getCurrentState(get().moduleName), + queries: Object.keys(currentQueries).reduce((acc, key) => { + if (currentQueries[key].id !== queryId) { + acc[key] = currentQueries[key]; + } + return acc; + }, {}), + }); + }) + .catch(() => { + set({ + isDeletingQueryInProcess: false, + }); + }) + .finally(() => + useSuperStore.getState().modules[get().moduleName].useAppDataStore.getState().actions.setIsSaving(false) ); - }) - .catch((error) => { - console.error('error', error); - set({ - creatingQueryInProcessId: null, + }, + updateDataQuery: (options) => { + set({ isUpdatingQueryInProcess: true }); + const { actions, selectedQuery } = useSuperStore + .getState() + .modules[get().moduleName].useQueryPanelStore.getState(); + set((state) => ({ + isUpdatingQueryInProcess: false, + dataQueries: state.dataQueries.map((query) => { + if (query.id === selectedQuery.id) { + return { + ...query, + options: { ...options }, + }; + } + return query; + }), + })); + actions.setSelectedQuery(selectedQuery.id); + }, + // createDataQuery: (appId, appVersionId, options, kind, name, selectedDataSource, shouldRunQuery) => { + createDataQuery: (selectedDataSource, shouldRunQuery) => { + const appVersionId = useSuperStore.getState().modules[get().moduleName].useAppVersionStore.getState() + .editingVersion?.id; + const appId = useSuperStore.getState().modules[get().moduleName].useAppDataStore.getState().appId; + const { options, name } = getDefaultOptions(selectedDataSource, get().moduleName); + const kind = selectedDataSource.kind; + const tempId = uuidv4(); + set({ creatingQueryInProcessId: tempId }); + const { actions, selectedQuery } = useSuperStore + .getState() + .modules[get().moduleName].useQueryPanelStore.getState(); + const dataSourceId = selectedDataSource?.id !== 'null' ? selectedDataSource?.id : null; + const pluginId = selectedDataSource.pluginId || selectedDataSource.plugin_id; + useSuperStore.getState().modules[get().moduleName].useAppDataStore.getState().actions.setIsSaving(true); + const { dataQueries } = useSuperStore.getState().modules[get().moduleName].useDataQueriesStore.getState(); + const currDataQueries = [...dataQueries]; + set(() => ({ + dataQueries: [ + { + ...selectedQuery, + data_source_id: dataSourceId, + app_version_id: appVersionId, + options, + name, + kind, + id: tempId, + plugin: selectedDataSource.plugin, + }, + ...currDataQueries, + ], + })); + actions.setSelectedQuery(tempId); + actions.setNameInputFocussed(true); + dataqueryService + .create(appId, appVersionId, name, kind, options, dataSourceId, pluginId) + .then((data) => { + set((state) => ({ + creatingQueryInProcessId: null, + dataQueries: state.dataQueries.map((query) => { + if (query.id === tempId) { + return { + ...query, + id: data.id, + data_source_id: dataSourceId, + }; + } + return query; + }), + })); + actions.setSelectedQuery(data.id, data); + if (shouldRunQuery) actions.setQueryToBeRun(data); + + /** Checks if there is an API call cached. If yes execute it */ + if (!isEmpty(get()?.queuedActions?.renameQuery)) { + get().actions.renameQuery(data.id, get().queuedActions.renameQuery); + set({ queuedActions: { ...get().queuedActions, renameQuery: undefined } }); + } + + if (!isEmpty(get()?.queuedActions?.saveData)) { + get().actions.saveData({ ...get().queuedActions.saveData, id: data.id }); + set({ queuedActions: { ...get().queuedActions, saveData: undefined } }); + } + + const currentQueries = useSuperStore + .getState() + .modules[get().moduleName].useCurrentStateStore.getState().queries; + + useSuperStore + .getState() + .modules[get().moduleName].useCurrentStateStore.getState() + .actions.setCurrentState({ + ...getCurrentState(get().moduleName), + queries: { + ...currentQueries, + [data.name]: { + id: data.id, + isLoading: false, + data: [], + rawData: [], + }, + }, + }); + }) + .catch((error) => { + set((state) => ({ + creatingQueryInProcessId: null, + dataQueries: state.dataQueries.filter((query) => query.id !== tempId), + })); + actions.setSelectedQuery(null); + toast.error(`Failed to create query: ${error.message}`); + }) + .finally(() => + useSuperStore.getState().modules[get().moduleName].useAppDataStore.getState().actions.setIsSaving(false) + ); + }, + renameQuery: (id, newName) => { + /** If query creation in progress, skips call and pushes the update to queue */ + if (get().creatingQueryInProcessId === id) { + set({ queuedActions: { ...get().queuedActions, renameQuery: newName } }); + return; + } + useSuperStore.getState().modules[get().moduleName].useAppDataStore.getState().actions.setIsSaving(true); + /** + * Seting name to store before api call for instant UI update and better UX. + * Name is again set to state post api call to handle if renaming fails in backend. + * */ + set((state) => ({ + dataQueries: state.dataQueries.map((query) => (query.id === id ? { ...query, name: newName } : query)), + })); + dataqueryService + .update(id, newName) + .then((data) => { + set((state) => ({ + dataQueries: state.dataQueries.map((query) => { + if (query.id === id) { + return { ...query, name: newName, updated_at: data.updated_at }; + } + return query; + }), + })); + useSuperStore + .getState() + .modules[get().moduleName].useQueryPanelStore.getState() + .actions.setSelectedQuery(id); + + const currentQueries = useSuperStore + .getState() + .modules[get().moduleName].useCurrentStateStore.getState().queries; + + const queryName = Object.keys(currentQueries).find((key) => currentQueries[key].id === id); + + const { [queryName]: _, ...rest } = currentQueries; + + useSuperStore + .getState() + .modules[get().moduleName].useCurrentStateStore.getState() + .actions.setCurrentState({ + ...getCurrentState(get().moduleName), + queries: { + ...rest, + [newName]: { + ...currentQueries[queryName], + name: newName, + }, + }, + }); + }) + .finally(() => + useSuperStore.getState().modules[get().moduleName].useAppDataStore.getState().actions.setIsSaving(false) + ); + }, + changeDataQuery: (newDataSource) => { + const { selectedQuery } = useSuperStore.getState().modules[get().moduleName].useQueryPanelStore.getState(); + set({ + isUpdatingQueryInProcess: true, + }); + useSuperStore.getState().modules[get().moduleName].useAppDataStore.getState().actions.setIsSaving(true); + dataqueryService + .changeQueryDataSource(selectedQuery?.id, newDataSource.id) + .then(() => { + set((state) => ({ + isUpdatingQueryInProcess: false, + dataQueries: state.dataQueries.map((query) => { + if (query?.id === selectedQuery?.id) { + return { ...query, dataSourceId: newDataSource?.id, data_source_id: newDataSource?.id }; + } + return query; + }), + })); + useSuperStore + .getState() + .modules[get().moduleName].useQueryPanelStore.getState() + .actions.setSelectedQuery(selectedQuery.id); + useSuperStore + .getState() + .modules[get().moduleName].useQueryPanelStore.getState() + .actions.setSelectedDataSource(newDataSource); + }) + .catch(() => { + set({ + isUpdatingQueryInProcess: false, + }); + }) + .finally(() => + useSuperStore.getState().modules[get().moduleName].useAppDataStore.getState().actions.setIsSaving(false) + ); + }, + duplicateQuery: (id, appId) => { + set({ creatingQueryInProcessId: uuidv4() }); + const { actions } = useSuperStore.getState().modules[get().moduleName].useQueryPanelStore.getState(); + const { dataQueries } = useSuperStore.getState().modules[get().moduleName].useDataQueriesStore.getState(); + const queryToClone = { ...dataQueries.find((query) => query.id === id) }; + let newName = queryToClone.name + '_copy'; + const names = dataQueries.map(({ name }) => name); + let count = 0; + while (names.includes(newName)) { + count++; + newName = queryToClone.name + '_copy' + count.toString(); + } + queryToClone.name = newName; + + useSuperStore.getState().modules[get().moduleName].useAppDataStore.getState().actions.setIsSaving(true); + dataqueryService + .create( + appId, + queryToClone.app_version_id, + queryToClone.name, + queryToClone.kind, + queryToClone.options, + queryToClone.data_source_id, + queryToClone.pluginId + ) + .then((data) => { + set((state) => ({ + creatingQueryInProcessId: null, + dataQueries: [{ ...data, data_source_id: queryToClone.data_source_id }, ...state.dataQueries], + })); + actions.setSelectedQuery(data.id, { ...data, data_source_id: queryToClone.data_source_id }); + + const dataQueryEvents = useAppDataStore + .getState() + .events?.filter((event) => event.target === 'data_query' && event.sourceId === queryToClone.id); + + if (dataQueryEvents?.length === 0) return; + + return Promise.all( + dataQueryEvents.map((event) => { + const newEvent = { + event: { + ...event?.event, + }, + eventType: event?.target, + attachedTo: data.id, + index: event?.index, + }; + useSuperStore + .getState() + .modules[get().moduleName].useAppDataStore.getState() + .actions?.createAppVersionEventHandlers(newEvent); + }) + ); }); - }) - .finally(() => useAppDataStore.getState().actions.setIsSaving(false)); + }, + + // createDataQuery: (appId, appVersionId, options, kind, name, selectedDataSource, shouldRunQuery) => { + + saveData: throttle((newValues) => { + /** If query creation in progress, skips call and pushes the update to queue */ + if (get().creatingQueryInProcessId && get().creatingQueryInProcessId === newValues.id) { + set({ queuedActions: { ...get().queuedActions, saveData: newValues } }); + return; + } + useSuperStore.getState().modules[get().moduleName].useAppDataStore.getState().actions.setIsSaving(true); + set({ isUpdatingQueryInProcess: true }); + dataqueryService + .update(newValues?.id, newValues?.name, newValues?.options) + .then((data) => { + localStorage.removeItem('transformation'); + set((state) => ({ + dataQueries: state.dataQueries.map((query) => { + if (query.id === newValues?.id) { + return { ...query, updated_at: data.updated_at }; + } + return query; + }), + isUpdatingQueryInProcess: false, + })); + }) + .catch(() => { + set({ + isUpdatingQueryInProcess: false, + }); + }) + .finally(() => + useSuperStore.getState().modules[get().moduleName].useAppDataStore.getState().actions.setIsSaving(false) + ); + }, 500), + sortDataQueries: (sortBy, sortOrder) => { + set(({ dataQueries, sortOrder: currSortOrder }) => { + const newSortOrder = sortOrder ? sortOrder : currSortOrder === 'asc' ? 'desc' : 'asc'; + return { + sortBy, + sortOrder: newSortOrder, + dataQueries: sortByAttribute(dataQueries, sortBy, newSortOrder), + }; + }); + }, }, - saveData: throttle((newValues) => { - /** If query creation in progress, skips call and pushes the update to queue */ - if (get().creatingQueryInProcessId && get().creatingQueryInProcessId === newValues.id) { - set({ queuedActions: { ...get().queuedActions, saveData: newValues } }); - return; - } - useAppDataStore.getState().actions.setIsSaving(true); - set({ isUpdatingQueryInProcess: true }); - dataqueryService - .update(newValues?.id, newValues?.name, newValues?.options) - .then((data) => { - localStorage.removeItem('transformation'); - set((state) => ({ - dataQueries: state.dataQueries.map((query) => { - if (query.id === newValues?.id) { - return { ...query, updated_at: data.updated_at }; - } - return query; - }), - isUpdatingQueryInProcess: false, - })); - }) - .catch(() => { - set({ - isUpdatingQueryInProcess: false, - }); - }) - .finally(() => useAppDataStore.getState().actions.setIsSaving(false)); - }, 500), - sortDataQueries: (sortBy, sortOrder) => { - set(({ dataQueries, sortOrder: currSortOrder }) => { - const newSortOrder = sortOrder ? sortOrder : currSortOrder === 'asc' ? 'desc' : 'asc'; - return { - sortBy, - sortOrder: newSortOrder, - dataQueries: sortByAttribute(dataQueries, sortBy, newSortOrder), - }; - }); - }, - }, - }), - { name: 'Data Queries Store' } - ) -); + }), + { name: 'Data Queries Store' } + ) + ); +} const sortByAttribute = (data, sortBy, order) => { if (order === 'asc') { @@ -413,6 +470,19 @@ const sortByAttribute = (data, sortBy, order) => { } }; +export const useDataQueriesStore = (callback, shallow) => { + const moduleName = useContext(ModuleContext); + + if (!moduleName) + throw Error( + 'useDataQueriesStore can only be called inside Module context. (hint: Wrap with ModuleContext.Provider)' + ); + + const _useDataQueriesStore = useSuperStore((state) => state.modules[moduleName].useDataQueriesStore); + + return _useDataQueriesStore(callback, shallow); +}; + export const useDataQueries = () => useDataQueriesStore((state) => state.dataQueries, shallow); export const useDataQueriesActions = () => useDataQueriesStore((state) => state.actions); export const useQueryCreationLoading = () => useDataQueriesStore((state) => !!state.creatingQueryInProcessId, shallow); diff --git a/frontend/src/_stores/dataSourcesStore.js b/frontend/src/_stores/dataSourcesStore.js index 0b02b6040c..60d26686c0 100644 --- a/frontend/src/_stores/dataSourcesStore.js +++ b/frontend/src/_stores/dataSourcesStore.js @@ -1,53 +1,69 @@ import { create, zustandDevTools } from './utils'; import { datasourceService, globalDatasourceService } from '@/_services'; +import { useContext } from 'react'; +import { useSuperStore } from './superStore'; +import { ModuleContext } from '../_contexts/ModuleContext'; -const initialState = { - dataSources: [], - loadingDataSources: true, - globalDataSources: [], - globalDataSourceStatus: { - isSaving: false, - isEditing: false, - unSavedModalVisible: false, - action: null, - saveAction: null, - }, +export function createDataSourcesStore(moduleName) { + const initialState = { + dataSources: [], + loadingDataSources: true, + globalDataSources: [], + globalDataSourceStatus: { + isSaving: false, + isEditing: false, + unSavedModalVisible: false, + action: null, + saveAction: null, + }, + moduleName, + }; + + return create( + zustandDevTools( + (set, get) => ({ + ...initialState, + actions: { + fetchDataSources: (appId) => { + set({ loadingDataSources: true }); + datasourceService.getAll(appId).then((data) => { + set({ + dataSources: data.data_sources, + loadingDataSources: false, + }); + }); + }, + fetchGlobalDataSources: (organizationId) => { + globalDatasourceService.getAll(organizationId).then((data) => { + set({ + globalDataSources: data.data_sources, + }); + }); + }, + setGlobalDataSourceStatus: (status) => + set({ + globalDataSourceStatus: { + ...get().globalDataSourceStatus, + ...status, + }, + }), + }, + }), + { name: 'Data Source Store' } + ) + ); +} + +export const useDataSourcesStore = (callback, shallow) => { + const moduleName = useContext(ModuleContext); + + if (!moduleName) throw Error('module context not available'); + + const _useDataSourcesStore = useSuperStore((state) => state.modules[moduleName].useDataSourcesStore); + + return _useDataSourcesStore(callback, shallow); }; -export const useDataSourcesStore = create( - zustandDevTools( - (set, get) => ({ - ...initialState, - actions: { - fetchDataSources: (appId) => { - set({ loadingDataSources: true }); - datasourceService.getAll(appId).then((data) => { - set({ - dataSources: data.data_sources, - loadingDataSources: false, - }); - }); - }, - fetchGlobalDataSources: (organizationId) => { - globalDatasourceService.getAll(organizationId).then((data) => { - set({ - globalDataSources: data.data_sources, - }); - }); - }, - setGlobalDataSourceStatus: (status) => - set({ - globalDataSourceStatus: { - ...get().globalDataSourceStatus, - ...status, - }, - }), - }, - }), - { name: 'Data Source Store' } - ) -); - export const useDataSources = () => useDataSourcesStore((state) => state.dataSources); export const useGlobalDataSources = () => useDataSourcesStore((state) => state.globalDataSources); export const useLoadingDataSources = () => useDataSourcesStore((state) => state.loadingDataSources); diff --git a/frontend/src/_stores/editorStore.js b/frontend/src/_stores/editorStore.js index adb55a9eef..16e63b254e 100644 --- a/frontend/src/_stores/editorStore.js +++ b/frontend/src/_stores/editorStore.js @@ -1,5 +1,8 @@ import { create } from './utils'; import { v4 as uuid } from 'uuid'; +import { useContext } from 'react'; +import { useSuperStore } from './superStore'; +import { ModuleContext } from '../_contexts/ModuleContext'; const STORE_NAME = 'Editor'; export const EMPTY_ARRAY = []; @@ -14,84 +17,99 @@ const ACTIONS = { SET_IS_EDITOR_ACTIVE: 'SET_IS_EDITOR_ACTIVE', }; -const initialState = { - currentLayout: 'desktop', - showComments: false, - hoveredComponent: '', - selectionInProgress: false, - selectedComponents: EMPTY_ARRAY, - isEditorActive: false, - selectedComponent: null, - scrollOptions: { - container: null, - throttleTime: 0, - threshold: 0, - }, - canUndo: false, - canRedo: false, - currentVersion: {}, - noOfVersionsSupported: 100, - appDefinition: {}, - isUpdatingEditorStateInProcess: false, - saveError: false, - isLoading: true, - defaultComponentStateComputed: false, - showLeftSidebar: true, - queryConfirmationList: [], - currentPageId: null, - currentSessionId: uuid(), -}; - -export const useEditorStore = create( - //Redux Dev tools for this store are disabled since its freezing chrome tab - (set, get) => ({ - ...initialState, - actions: { - setShowComments: (showComments) => - set({ showComments }, false, { - type: ACTIONS.SET_HOVERED_COMPONENT, - showComments, - }), - toggleComments: () => - set({ showComments: !get().showComments }, false, { - type: ACTIONS.TOGGLE_COMMENTS, - }), - toggleCurrentLayout: (currentLayout) => - set({ currentLayout }, false, { - type: ACTIONS.TOGGLE_CURRENT_LAYOUT, - currentLayout, - }), - setIsEditorActive: (isEditorActive) => set(() => ({ isEditorActive })), - updateEditorState: (state) => set((prev) => ({ ...prev, ...state })), - updateQueryConfirmationList: (queryConfirmationList) => set({ queryConfirmationList }), - setHoveredComponent: (hoveredComponent) => - set({ hoveredComponent }, false, { - type: ACTIONS.SET_HOVERED_COMPONENT, - hoveredComponent, - }), - setSelectionInProgress: (isSelectionInProgress) => { - set( - { - isSelectionInProgress, - }, - false, - { type: ACTIONS.SET_SELECTION_IN_PROGRESS } - ); - }, - setSelectedComponents: (selectedComponents, isMulti = false) => { - const newSelectedComponents = isMulti - ? [...get().selectedComponents, ...selectedComponents] - : selectedComponents; - - set({ - selectedComponents: newSelectedComponents, - }); - }, - setCurrentPageId: (currentPageId) => set({ currentPageId }), +export function createEditorStore(moduleName) { + const initialState = { + currentLayout: 'desktop', + showComments: false, + hoveredComponent: '', + selectionInProgress: false, + selectedComponents: [], + isEditorActive: false, + selectedComponent: null, + scrollOptions: { + container: null, + throttleTime: 0, + threshold: 0, }, - }), - { name: STORE_NAME } -); + canUndo: false, + canRedo: false, + currentVersion: {}, + noOfVersionsSupported: 100, + appDefinition: {}, + isUpdatingEditorStateInProcess: false, + saveError: false, + isLoading: true, + defaultComponentStateComputed: false, + showLeftSidebar: true, + queryConfirmationList: [], + currentPageId: null, + currentSessionId: uuid(), + moduleName, + appMode: 'auto', + }; + + return create( + //Redux Dev tools for this store are disabled since its freezing chrome tab + (set, get) => ({ + ...initialState, + actions: { + setShowComments: (showComments) => + set({ showComments }, false, { + type: ACTIONS.SET_HOVERED_COMPONENT, + showComments, + }), + toggleComments: () => + set({ showComments: !get().showComments }, false, { + type: ACTIONS.TOGGLE_COMMENTS, + }), + toggleCurrentLayout: (currentLayout) => + set({ currentLayout }, false, { + type: ACTIONS.TOGGLE_CURRENT_LAYOUT, + currentLayout, + }), + setIsEditorActive: (isEditorActive) => set(() => ({ isEditorActive })), + updateEditorState: (state) => set((prev) => ({ ...prev, ...state })), + updateQueryConfirmationList: (queryConfirmationList) => set({ queryConfirmationList }), + setHoveredComponent: (hoveredComponent) => + set({ hoveredComponent }, false, { + type: ACTIONS.SET_HOVERED_COMPONENT, + hoveredComponent, + }), + setSelectionInProgress: (isSelectionInProgress) => { + set( + { + isSelectionInProgress, + }, + false, + { type: ACTIONS.SET_SELECTION_IN_PROGRESS } + ); + }, + setSelectedComponents: (selectedComponents, isMulti = false) => { + const newSelectedComponents = isMulti + ? [...get().selectedComponents, ...selectedComponents] + : selectedComponents; + + set({ + selectedComponents: newSelectedComponents, + }); + }, + setCurrentPageId: (currentPageId) => set({ currentPageId }), + setAppMode: (appMode) => set({ appMode }), + }, + }), + { name: STORE_NAME } + ); +} + +export const useEditorStore = (callback, shallow) => { + const moduleName = useContext(ModuleContext); + + if (!moduleName) throw Error('module context not available'); + + const _useEditorStore = useSuperStore((state) => state.modules[moduleName].useEditorStore); + + return _useEditorStore(callback, shallow); +}; export const useEditorActions = () => useEditorStore((state) => state.actions); export const useEditorState = () => useEditorStore((state) => state); diff --git a/frontend/src/_stores/queryPanelStore.js b/frontend/src/_stores/queryPanelStore.js index 83187b80e5..d0b1dba7bf 100644 --- a/frontend/src/_stores/queryPanelStore.js +++ b/frontend/src/_stores/queryPanelStore.js @@ -1,45 +1,67 @@ import { create, zustandDevTools } from './utils'; +import { useContext } from 'react'; +import { ModuleContext } from '../_contexts/ModuleContext'; import { useDataQueriesStore } from '@/_stores/dataQueriesStore'; +import { useSuperStore } from '@/_stores/superStore'; -const queryManagerPreferences = JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {}; -const initialState = { - queryPanelHeight: queryManagerPreferences?.isExpanded ? queryManagerPreferences?.queryPanelHeight : 95 ?? 70, - selectedQuery: null, - selectedDataSource: null, - queryToBeRun: null, - previewLoading: false, - queryPreviewData: '', - showCreateQuery: false, - nameInputFocussed: false, -}; +export function createQueryPanelStore(moduleName) { + const queryManagerPreferences = JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {}; + const initialState = { + queryPanelHeight: queryManagerPreferences?.isExpanded ? queryManagerPreferences?.queryPanelHeight : 95 ?? 70, + selectedQuery: null, + selectedDataSource: null, + queryToBeRun: null, + previewLoading: false, + queryPreviewData: '', + showCreateQuery: false, + nameInputFocussed: false, + moduleName, + }; -export const useQueryPanelStore = create( - zustandDevTools( - (set) => ({ - ...initialState, - actions: { - updateQueryPanelHeight: (newHeight) => set(() => ({ queryPanelHeight: newHeight })), - setSelectedQuery: (queryId) => { - set(() => { - if (queryId === null) { - return { selectedQuery: null }; - } - const query = useDataQueriesStore.getState().dataQueries.find((query) => query.id === queryId); - return { selectedQuery: query }; - }); + return create( + zustandDevTools( + (set, get) => ({ + ...initialState, + actions: { + updateQueryPanelHeight: (newHeight) => set(() => ({ queryPanelHeight: newHeight })), + setSelectedQuery: (queryId) => { + set(() => { + if (queryId === null) { + return { selectedQuery: null }; + } + const query = useSuperStore + .getState() + .modules[get().moduleName].useDataQueriesStore.getState() + .dataQueries.find((query) => query.id === queryId); + return { selectedQuery: query }; + }); + }, + setSelectedDataSource: (dataSource = null) => set({ selectedDataSource: dataSource }), + setQueryToBeRun: (query) => set({ queryToBeRun: query }), + setPreviewLoading: (status) => set({ previewLoading: status }), + setPreviewData: (data) => set({ queryPreviewData: data }), + setShowCreateQuery: (showCreateQuery) => set({ showCreateQuery }), + setNameInputFocussed: (nameInputFocussed) => set({ nameInputFocussed }), }, - setSelectedDataSource: (dataSource = null) => set({ selectedDataSource: dataSource }), - setQueryToBeRun: (query) => set({ queryToBeRun: query }), - setPreviewLoading: (status) => set({ previewLoading: status }), - setPreviewData: (data) => set({ queryPreviewData: data }), - setShowCreateQuery: (showCreateQuery) => set({ showCreateQuery }), - setNameInputFocussed: (nameInputFocussed) => set({ nameInputFocussed }), - }, - }), - { name: 'Query Panel Store' } - ) -); + }), + { name: 'Query Panel Store' } + ) + ); +} + +export const useQueryPanelStore = (callback, shallow) => { + const moduleName = useContext(ModuleContext); + + if (!moduleName) + throw Error( + 'useQueryPanelStore can only be called inside Module context. (hint: Wrap with ModuleContext.Provider)' + ); + + const _useQueryPanelStore = useSuperStore((state) => state.modules[moduleName].useQueryPanelStore); + + return _useQueryPanelStore(callback, shallow); +}; export const usePanelHeight = () => useQueryPanelStore((state) => state.queryPanelHeight); export const useSelectedQuery = () => useQueryPanelStore((state) => state.selectedQuery); diff --git a/frontend/src/_stores/storeHelper.js b/frontend/src/_stores/storeHelper.js index 5d8c1e5d5a..2ab350c74d 100644 --- a/frontend/src/_stores/storeHelper.js +++ b/frontend/src/_stores/storeHelper.js @@ -1,9 +1,9 @@ import { schemaUnavailableOptions } from '@/Editor/QueryManager/constants'; import { allOperations } from '@tooljet/plugins/client'; import { capitalize } from 'lodash'; -import { useDataQueriesStore } from '@/_stores/dataQueriesStore'; +import { useSuperStore } from './superStore'; -export const getDefaultOptions = (source) => { +export const getDefaultOptions = (source, moduleName) => { const isSchemaUnavailable = Object.keys(schemaUnavailableOptions).includes(source.kind); let options = {}; @@ -36,11 +36,11 @@ export const getDefaultOptions = (source) => { } } - return { options, name: computeQueryName(source.kind) }; + return { options, name: computeQueryName(source.kind, moduleName) }; }; -const computeQueryName = (kind) => { - const dataQueries = useDataQueriesStore.getState().dataQueries; +const computeQueryName = (kind, moduleName) => { + const dataQueries = useSuperStore.getState().modules[moduleName].useDataQueriesStore.getState().dataQueries; const currentQueriesForKind = dataQueries.filter((query) => query.kind === kind); let currentNumber = currentQueriesForKind.length + 1; diff --git a/frontend/src/_stores/superStore.js b/frontend/src/_stores/superStore.js new file mode 100644 index 0000000000..f0f4e80eda --- /dev/null +++ b/frontend/src/_stores/superStore.js @@ -0,0 +1,44 @@ +import { createDataQueriesStore } from './dataQueriesStore'; +import { createEditorStore } from './editorStore'; +import { createQueryPanelStore } from './queryPanelStore'; +import { createDataSourcesStore } from './dataSourcesStore'; +import { createCurrentStateStore } from './currentStateStore'; +import { create } from './utils'; +import { createAppVersionStore } from './appVersionStore'; +import { createAppDataStore } from './appDataStore'; +import { omit } from 'lodash'; + +const generateModule = (moduleName) => ({ + useEditorStore: createEditorStore(moduleName), + useQueryPanelStore: createQueryPanelStore(moduleName), + useDataQueriesStore: createDataQueriesStore(moduleName), + useDataSourcesStore: createDataSourcesStore(moduleName), + useCurrentStateStore: createCurrentStateStore(moduleName), + useAppVersionStore: createAppVersionStore(moduleName), + useAppDataStore: createAppDataStore(moduleName), +}); + +const mainModule = generateModule('#main'); + +export const useSuperStore = create((set, get) => ({ + modules: { + '#main': mainModule, + }, + + createModule: (moduleName) => { + const modules = get().modules; + const newModules = { + ...modules, + [moduleName]: generateModule(moduleName), + }; + + set(() => ({ modules: newModules })); + return true; + }, + + destroyModule: (moduleName) => { + const modules = get().modules; + const newModules = omit(modules, moduleName); + set(() => ({ modules: newModules })); + }, +})); diff --git a/frontend/src/_styles/custom.scss b/frontend/src/_styles/custom.scss index 8a1bb53ac9..5d996f40d3 100644 --- a/frontend/src/_styles/custom.scss +++ b/frontend/src/_styles/custom.scss @@ -110,15 +110,6 @@ body { .tags.row,.radio-row{ justify-content: left; } - &.has-datepicker{ - .td-container{ - .w-100.h-100{ - div{ - justify-content: left !important; - } - } - } - } } .table-text-align-right{ input{ @@ -130,15 +121,6 @@ body { .tags.row,.radio.row{ justify-content: right; } - &.has-datepicker{ - .td-container{ - .w-100.h-100{ - div{ - justify-content: right !important; - } - } - } - } } .table-text-align-center{ input{ diff --git a/frontend/src/_styles/designtheme.scss b/frontend/src/_styles/designtheme.scss index 7fda3ce450..e5e5926375 100644 --- a/frontend/src/_styles/designtheme.scss +++ b/frontend/src/_styles/designtheme.scss @@ -61,24 +61,91 @@ @import "@radix-ui/colors/goldDark.css"; @import "@radix-ui/colors/bronzeDark.css"; -:root { +:root, .light-theme { --base: #FCFCFD; --base-black: #18181A; --text-black-000: #000000; --slate12: #313739; - --tj-text-input-widget-border-default: #6A727C47; - --tj-text-input-widget-field-default: #FFFFFF; - --tj-text-input-widget-hover: #F1F3F5; - --tj-text-input-widget-error: #DB4324; - --tj-text-input-widget-disabled: #DFE3E6; - --tj-text-input-widget-border-clicked: #3E63DD; --controls-switch-tab: #fff; --controls-switch-tag: #CCD1D54D; --text-disabled: #889099; --text-default: #1B1F24; + // new design theme + --primary-brand: #4368E3; + --primary-accent-strong: #4368E3; + --primary-accent-subtle: rgba(67, 104, 227, 30%); + --primary-white: #FFFFFF; + --primary-black: #1B1F24; + + // text + --text-primary: #1B1F24; + --text-placeholder: #6A727C; + --text-disbled: #ACB2B9; + --text-on-solid: #FFFFFF; + + + // status + --status-error-strong: #D72D39; + --status-error-subtle: rgba(215, 45, 57, 30%); + --status-warning-strong: #BF4F03; + --status-warning-subtle: rgba(191, 79, 3, 30%); + --status-success-strong: #1E823B; + --status-success-subtle: rgba(30, 130, 59, 30%); + + //icon + --icons-strong: #6A727C; + --icons-default: #6A727C; + --icons-weak-disabled: #6A727C; + --icons-disabled-on-white: #6A727C; + --icons-on-solid: #6A727C; + + // borders + --borders-strong: #ACB2B9; + --borders-default: #CCD1D5; + --borders-weak-disabled: #CCD1D5; + --borders-disabled-on-white: #E4E7EB; + --borders-disabled-on-white-dimmed: rgba(246, 248, 250, 80%); + + // interactive overlays + --interactive-overlays-fill-hover: rgba(27, 31, 36, 8%); + --interactive-overlays-fill-pressed: rgba(27, 31, 36, 10%); + --interactive-overlays-border-hover: rgba(27, 31, 36, 12%); + --interactive-overlays-border-pressed: rgba(27, 31, 36, 16%); + --interactive-overlays-focus-outline: #4368E3; + --interactive-overlays-focus-inner-shadow: #FFFFFF; + --interactive-overlays-column-resize: #1B1F244D; + + //interactive + --interactive-default: #CCD1D54D; + --interactive-hover:#ACB2B959; + + + + //surfaces + --surfaces-app-bg-default: #F6F6F6; + --surfaces-surface-01: #FFFFFF; + --surfaces-surface-02: #F6F8FA; + --surfaces-surface-03: #E4E7EB; + + // utilities + --utilities-scrollbar: rgba(106, 114, 124, 12%); + + //box shadows + --elevation-000-box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.10); + --elevation-200-box-shadow: 0px 2px 4px 0px rgba(48, 50, 51, 0.10), 0px 0px 1px 0px rgba(48, 50, 51, 0.05); + --elevation-300-box-shadow: 0px 4px 8px 0px rgba(48, 50, 51, 0.10), 0px 0px 1px 0px rgba(48, 50, 51, 0.05); + --elevation-400-box-shadow: 0px 8px 16px 0px rgba(48, 50, 51, 0.10), 0px 0px 1px 0px rgba(48, 50, 51, 0.05); + --elevation-500-box-shadow: 0px 16px 24px 0px rgba(48, 50, 51, 0.09), 0px 0px 1px 0px rgba(48, 50, 51, 0.05); + --elevation-600-box-shadow: 0px 24px 40px 0px rgba(48, 50, 51, 0.08), 0px 0px 1px 0px rgba(48, 50, 51, 0.05); + --elevation-700-box-shadow: 0px 32px 50px 0px rgba(48, 50, 51, 0.08), 0px 0px 1px 0px rgba(48, 50, 51, 0.05); + --elevation-100-box-shadow: 0px 1px 1px 0px rgba(48, 50, 51, 0.10), 0px 0px 1px 0px rgba(48, 50, 51, 0.05); + + --interactive-overlay-border-pressed: #1B1F2429; + --interactive-overlay-fill-pressed: #1B1F241A; + --interactive-overlay-column-resize: #1B1F244D; } .dark-theme { @@ -88,16 +155,85 @@ --text-black-000: #ffffff; --slate1: #1f2936; - --tj-text-input-widget-border-default: #6D757D7A; - --tj-text-input-widget-field-default: #313538; - --tj-text-input-widget-hover: #292D2F; - --tj-text-input-widget-error: #EC5E41; - --tj-text-input-widget-disabled: #2B2F3199; - --tj-text-input-widget-border-clicked: #3E63DD; --controls-switch-tab: #2B3036; --controls-switch-tag: #121518; --text-disabled: #6D757D; --text-default: #FAFCFF; + + // new design theme + --primary-brand: #4A6DD9; + --primary-accent-strong: #4A6DD9; + --primary-accent-subtle: rgba(74, 109, 217, 30%); + --primary-white: #FAFCFF; + --primary-black: #121518; + + // text + --text-primary: #CFD3D8; + --text-placeholder: #858C94; + --text-disbled: #545B64; + --text-on-solid: #FAFCFF; + + + // status + --status-error-strong: #D03F43; + --status-error-subtle: rgba(208, 63, 67, 30%); + --status-warning-strong: #BA5722; + --status-warning-subtle: rgba(186, 87, 34, 30%); + --status-success-strong: #318344; + --status-success-subtle: rgba(49, 131, 68, 30%); + + //icon + --icons-strong: rgba(207, 211, 216, 90%); + --icons-default: rgba(207, 211, 216, 65%); + --icons-weak-disabled: rgba(207, 211, 216, 35%); + --icons-disabled-on-white: rgba(207, 211, 216, 20%); + --icons-on-solid: #FAFCFF; + + // borders + --borders-strong: #545B64; + --borders-default: #3C434B; + --borders-weak-disabled: #2B3036; + --borders-disabled-on-white: rgba(43, 48, 54, 20%); + --borders-disabled-on-white-dimmed: rgba(43, 48, 54, 20%); + + + // interactive overlays + + --interactive-overlays-fill-hover: rgba(255, 255, 255, 12%); + --interactive-overlays-fill-pressed: rgba(255, 255, 255, 20%); + --interactive-overlays-border-hover: rgba(255, 255, 255, 20%); + --interactive-overlays-border-pressed: rgba(255, 255, 255, 28%); + --interactive-overlays-focus-outline: #4A6DD9; + --interactive-overlays-focus-inner-shadow: #121518; + --interactive-overlays-column-resize: #FFFFFF80; + + //interactive + --interactive-default: #A1A7AE1F; + --interactive-hover:#A1A7AE29; + + + //surfaces + --surfaces-app-bg-default: #121518; + --surfaces-surface-01: #1E2226; + --surfaces-surface-02: #2B3036; + --surfaces-surface-03: #3C434B; + + // utilities + --utilities-scrollbar: rgba(109, 117, 125, 16%); + + //box-shadow + --elevation-000-box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.40); + --elevation-100-box-shadow: 0px 1px 1px 0px #000, 0px 0px 1px 0px rgba(0, 0, 0, 0.90); + --elevation-200-box-shadow: 0px 2px 4px 0px #000, 0px 0px 1px 0px rgba(0, 0, 0, 0.90); + --elevation-300-box-shadow: 0px 4px 8px 0px #000, 0px 0px 1px 0px rgba(0, 0, 0, 0.90); + --elevation-400-box-shadow: 0px 8px 16px 0px #000, 0px 0px 1px 0px rgba(0, 0, 0, 0.90); + --elevation-500-box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.99), 0px 0px 1px 0px rgba(0, 0, 0, 0.90); + --elevation-600-box-shadow: 0px 24px 40px 0px rgba(0, 0, 0, 0.98), 0px 0px 1px 0px rgba(0, 0, 0, 0.90); + --elevation-700-box-shadow: 0px 32px 50px 0px rgba(0, 0, 0, 0.98), 0px 0px 1px 0px rgba(0, 0, 0, 0.90); + + --interactive-overlay-border-pressed: #FFFFFF47; + --interactive-overlay-fill-pressed: #FFFFFF33; + --interactive-overlay-column-resize: #FFFFFF80; } \ No newline at end of file diff --git a/frontend/src/_styles/dropdown-custom.scss b/frontend/src/_styles/dropdown-custom.scss index a5f32919c2..751148c134 100644 --- a/frontend/src/_styles/dropdown-custom.scss +++ b/frontend/src/_styles/dropdown-custom.scss @@ -19,7 +19,7 @@ } .react-select__single-value { - color: var(--slate12) !important; + color: var(--slate12) ; } .react-select__menu { @@ -101,4 +101,41 @@ .react-select__menu-portal { z-index: 9999 !important; -} \ No newline at end of file +} + +// following is the styles for table select column type menu list and options styles. If its same for all the select elements in the editor, then we can make it common and not specific for table select +.table-select-custom-menu-list{ + .react-select__menu-list{ + padding: 2px; + // this is needed otherwise :active state doesn't look nice, gap is required + display: flex; + flex-direction: column; + gap: 4px !important; + background-color: var(--base) !important; + overflow-y: auto; + } + .react-select__option{ + display: flex; + justify-content: space-between; + padding: 8px 12px; + align-self: stretch; + align-items: center; + color: var(--slate12) !important; + border-radius: 6px; + /* Paragraph/Extrasmall/Regular */ + font-family: 'IBM Plex Sans'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 166.667% */ + &.react-select__option--is-selected{ + color: var(--indigo9) !important; + } + &:active{ + background: var(--base) !important; + box-shadow: 0px 0px 0px 4px var(--slate6); + color : var(--slate12) !important; + } + } +} + diff --git a/frontend/src/_styles/editor/react-select-search.scss b/frontend/src/_styles/editor/react-select-search.scss index e22c2cc830..707c59e8a4 100644 --- a/frontend/src/_styles/editor/react-select-search.scss +++ b/frontend/src/_styles/editor/react-select-search.scss @@ -45,13 +45,17 @@ } .popover.popover-dark-themed .select-search-container, -.main-wrapper.theme-dark .select-search-container { - --select-search-background: #272822; - --select-search-border: #333c48; - --select-search-selected: #89b4fa; - --select-search-text: #fff; - --select-search-subtle-text: #a6adc8; - --select-search-highlight: #1e1e2e; +.main-wrapper { + .theme-dark { + .select-search-container { + --select-search-background: #272822; + --select-search-border: #333c48; + --select-search-selected: #89b4fa; + --select-search-text: #fff; + --select-search-subtle-text: #a6adc8; + --select-search-highlight: #1e1e2e; + } + } } .select-search-container *, diff --git a/frontend/src/_styles/left-sidebar.scss b/frontend/src/_styles/left-sidebar.scss index eb09912d85..a6935ebfc8 100644 --- a/frontend/src/_styles/left-sidebar.scss +++ b/frontend/src/_styles/left-sidebar.scss @@ -79,6 +79,8 @@ max-width: 350px; } + + .queryData { &.close { max-height: 0; @@ -158,6 +160,22 @@ } } +.global-settings-panel { + .app-mode-switch { + .ToggleGroup { + width : 100%; + } + .ToggleGroupItem { + font-size: 12px; + } + .ToggleGroupItem[data-state='on'] { + .toggle-item { + font-size: 12px; + } + } + } +} + .debugger { .leftsidebar-panel-header { border-bottom: none; diff --git a/frontend/src/_styles/table-component.scss b/frontend/src/_styles/table-component.scss index afc811bfb0..fd75c2c319 100644 --- a/frontend/src/_styles/table-component.scss +++ b/frontend/src/_styles/table-component.scss @@ -1,88 +1,114 @@ @import "./typography.scss"; @import "./designtheme.scss"; -.jet-table.table-component{ - padding: 8px; - &.jet-table { - background-color: var(--base) !important; - box-shadow: 0px 2px 4px -2px rgba(16, 24, 40, 0.06), 0px 4px 8px -2px rgba(16, 24, 40, 0.10); - } - .table-filters{ - border: 1px solid var(--slate3, #F1F3F5) !important; - .card-footer{ - background-color: inherit !important; - } - } - .card-footer,.card{ - background-color:var(--base) !important; - } - .card{ - border: 1px solid var(--slate3) !important; - } - .card-footer{ - padding: 0.75rem; - } - .card-header{ - border-bottom: 0 !important; - } - .table-clear-icon{ - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.00) !important; - background: var(--indigo3, #F0F4FF) !important; - } - .table-global-search{ - &:hover{ - border-radius: 6px; - border: 1px solid var(--slate8, #C1C8CD) !important; - background: var(--slate4, #ECEEF0) !important; - } - &:focus-visible{ - border-radius: 6px; - border: 1px solid var(--slate7, #D7DBDF) !important; - background: var(--base-base, #FFF) !important; - /* Focus rings/Gray/light */ - box-shadow: 0px 0px 0px 4px var(--slate6) !important; - } - &:focus-within{ - border-radius: 6px; - border: 1px solid var(--indigo9, #3E63DD) !important; - background: var(--indigo2, #F8FAFF) !important; +.jet-table.table-component { + padding: 8px; - /* Selected ring/Indigo/light */ - box-shadow: 0px 0px 0px 1px var(--indigo6) !important; + &.jet-table { + background-color: var(--surfaces-surface-01); + box-shadow: var(--elevation-200-box-shadow); + border: 1px solid var(--borders-weak-disabled); + } + + .table-filters { + border: 1px solid var(--slate3, #F1F3F5) !important; + + .card-footer { + background-color: inherit !important; + } + } + + .card-footer, + .card { + background-color: var(--surfaces-surface-01); + } + + .card { + border: 1px solid var(--slate3) !important; + } + + .card-footer { + padding: 0.75rem; + } + + .card-header { + border-bottom: 0 !important; + } + + // .table-clear-icon{ + // border-radius: 4px; + // border: 1px solid rgba(255, 255, 255, 0.00) !important; + // background: var(--indigo3, #F0F4FF) !important; + // } + .table-global-search { + border: 1px solid var(--borders-default) !important; + + input { + color: var(--text-primary) !important; + } + + ; + + &:hover { + border-radius: 6px; + border: 1px solid var(--slate8, #C1C8CD) !important; + background: var(surfaces/surface-01) !important; + } + + &:focus-visible { + border-radius: 6px; + border: 2px solid var(--primary-accent-strong) !important; + // background: var(--base-base, #FFF) !important; + /* Focus rings/Gray/light */ + // box-shadow: 0px 0px 0px 4px var(--slate6) !important; + } + + &:focus-within { + border-radius: 6px; + border: 2px solid var(--primary-accent-strong) !important; + /* Selected ring/Indigo/light */ + // box-shadow: 0px 0px 0px 1px var(--indigo6) !important; + } + } + + .tr { + height: 32px; + + .th { + .thead-editable-icon-header-text-wrapper { + max-width: 100%; + min-width: 70%; + width: 100%; + } + + .header-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } } - .tr{ - height: 32px; - .th{ - .thead-editable-icon-header-text-wrapper{ - max-width: 100%; - min-width: 70%; - width: 100%; - } - .header-text{ - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - - } - .table-row{ - border-top: 0 !important; - border-bottom: 1px solid inherit; - } - .table-bordered > :not(caption) > * > *{ - border-width: 0 1px 0 0; - } + + } + + .table-row { + border-top: 0 !important; + border-bottom: 1px solid inherit; + } + + .table-bordered> :not(caption)>*>* { + border-width: 0 1px 0 0; + } } + .jet-data-table { &::-webkit-scrollbar-thumb { - background-color: var(--slate4) !important; /* Color of the scrollbar thumb */ - border-radius: 8px; /* Rounded corners of the thumb */ + background-color: var(--slate4) !important; + /* Color of the scrollbar thumb */ + border-radius: 8px; + /* Rounded corners of the thumb */ } - + thead { z-index: 2; } @@ -91,19 +117,34 @@ display: flex !important; } - .table-row:hover, - .table-row:focus { - background: var(--slate4, #ECEEF0) !important; - } - .table-row.selected { --tblr-table-accent-bg: var(--indigo3, #F0F4FF) !important; background: var(--indigo3, #F0F4FF) !important; } td { - min-height: 40px; overflow-x: initial; + padding: 8px 12px !important; + overflow-y: hidden; + + &.has-number { + padding: 0px 12px 0px 12px !important; + } + + &.has-number.isEditable { + padding: 0px 0px 0px 12px !important; + } + + &.has-dropdown, + &.has-multiselect, + &.has-badge, + &.has-badges { + overflow-y: visible !important; + } + + // &.has-number { + // padding: 0 !important; + // } .text-container { padding: 0; @@ -111,22 +152,29 @@ border: 0; height: 100%; outline: none; + -webkit-user-modify: read-write-plaintext-only; + } + + textarea { + resize: none; } } td.regular { - min-height: 46px; + white-space: nowrap; + /* Prevent text wrapping */ + } td.condensed { - min-height: 36px; + white-space: nowrap; + /* Prevent text wrapping */ } .has-dropdown, .has-multiselect, - .has-text, .has-datepicker, - .has-actions { + .has-text .has-actions { padding: 0 5px; } @@ -135,9 +183,13 @@ margin: 0; } + .overflow-x-hidden { + overflow-x: hidden !important; + } + .wrap-wrapper { white-space: normal !important; - word-break: break-all; + word-break: break-word; } .scroll-wrapper { @@ -149,6 +201,7 @@ } .td { + // padding: 6px 12px; .text-container:focus-visible, .text-container:focus, @@ -158,6 +211,12 @@ height: 100%; } + + &:not(:focus-within):hover { + background: linear-gradient(0deg, var(--interactive-overlays-fill-hover), var(--interactive-overlays-fill-hover)), + } + + display: flex !important; .td-container { @@ -172,16 +231,15 @@ td { .text-container:focus { - position: sticky; - height: 120px; + position: absolute; + height: 100%; overflow-y: scroll; - margin-top: -10px; - padding: 10px; - margin-left: -9px; - background: white; + top: 0; + left: 0; + right: 0; + background: var(--surfaces-surface-01) !important; box-shadow: rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, rgba(15, 15, 15, 0.1) 0px 3px 6px, rgba(15, 15, 15, 0.2) 0px 9px 24px; - white-space: initial; } .text-container:focus-visible, @@ -242,6 +300,14 @@ // } } +.jet-data-table>table:not(.table-resizing) { + + .table-row:hover, + .table-row:focus { + background: var(--slate4, #ECEEF0) !important; + } +} + .jet-data-table::-webkit-scrollbar { background: transparent; } @@ -256,12 +322,17 @@ } .jet-data-table { - .th:first-child,td:first-child{ + + .th:first-child, + td:first-child { border-left: none; } - .th:last-child,td:last-child{ + + .th:last-child, + td:last-child { border-right: none; } + overflow: hidden; .form-check { @@ -297,6 +368,7 @@ thead { width: 100%; + } .select-search:not(.is-loading):not(.select-search--multiple) .select-search__value::after { @@ -353,7 +425,7 @@ } } - .th{ + .th { .resizer { display: inline-block; height: 100%; @@ -365,9 +437,36 @@ touch-action: none; width: 30px; cursor: ew-resize !important; + border: 0 !important; // overriding the border added in theme.scss + + .table-column-resize-handle { + display: none; + width: 10px; + height: 100%; + transform: translateX(50%); + + &::after { + content: ''; + display: block; + height: 10px; + width: 4px; + background: var(--interactive-overlay-column-resize); + top: 11px; + position: absolute; + left: 3px; + border-radius: 10px; + } + } + + &:hover { + .table-column-resize-handle { + display: block !important; + } + } } - &:last-child{ - .resizer{ + + &:last-child { + .resizer { transform: translateX(0%); } } @@ -380,6 +479,7 @@ tr { flex-grow: 1; + th:last-child { flex: 1 1 auto !important; } @@ -396,17 +496,18 @@ right: 0; height: 300px; z-index: 100; - background: var(--slate1, #FBFCFD) !important; + background-color: var(--surfaces-surface-01) !important; overflow: hidden; } -.table-add-new-row{ +.table-add-new-row { padding: 8px; + color: var(--slate12) !important; + + .th { + background-color: var(--slate3) !important; color: var(--slate12) !important; - .th{ - background-color: var(--slate3) !important; - color: var(--slate12) !important; - } + } } /* @@ -414,101 +515,125 @@ */ // download pop-up in the table widget .table-widget-download-popup { - background-color: var(--base) !important; - color: var(--slate12) !important; - padding: 4px; - border-radius: 6px; - width: fit-content; - border: 1px solid var(--slate3) !important; + background-color: var(--surfaces-surface-01) !important; + color: var(--slate12) !important; + padding: 4px; + border-radius: 6px; + width: fit-content; + border: 1px solid var(--slate3) !important; + display: flex; + flex: 1 0 0; + flex-direction: column; + align-items: flex-start; + + .popover-body { + color: inherit; + border: none; + background-color: inherit; + } + + .table-download-option { display: flex; flex-direction: column; - align-items: flex-start; - .popover-body{ - color: inherit; - border: none; - } - - .table-download-option{ + flex: 1 0 0; + border-radius: 6px; + + span { display: flex; + padding: 10px 14px !important; + align-items: center; + gap: 37px; flex: 1 0 0; - flex-direction: column; - span{ - display: flex; - padding: 10px 14px !important; - align-items: center; - gap: 37px; - flex: 1 0 0; - border-radius: 6px; - &:hover{ - background-color: var(--slate3, #F1F3F5) !important; - } - &:focus-visible{ - background: var(--slate1, #FBFCFD) !important; - box-shadow: 0px 0px 0px 4px var(--slate6) !important; - } - - } - } -} -// hidecolumn popover in table footer -.dropdown-table-column-hide-common { - width: 100%; - color : var(--slate12) !important; - height: 100% !important; - .dropdown-item{ - display: flex; - height: 36px; - width: 100%; - padding: 10px 14px; border-radius: 6px; - gap: 6px; - input{ - width: 16px; - height: 16px; + + &:hover { + background-color: var(--slate3, #F1F3F5) !important; } - } - overflow-wrap: wrap; - .dropdown-item{ - &:hover{ - background-color: var(--slate3, #F1F3F5) !important; - } - &:focus{ + + &:focus-visible { background: var(--slate1, #FBFCFD) !important; box-shadow: 0px 0px 0px 4px var(--slate6) !important; } } -} -.popover:has(.dropdown-table-column-hide-common){ - border : 1px solid var(--slate3) !important; - background-color: var(--base) !important; - padding: 4px; - border-radius: 6px; + } } -.jet-table.table-component{ - .pagination-container{ - input{ - color :var(--slate12) !important; +.popover:has(.dropdown-table-column-hide-common) { + border: 1px solid var(--slate3) !important; + background-color: var(--surfaces-surface-01) !important; + padding: 4px; + border-radius: 6px; +} + +// hidecolumn popover in table footer +.dropdown-table-column-hide-common { + width: 100%; + color: var(--slate12) !important; + height: 100% !important; + + .dropdown-item { + display: flex; + height: 36px; + width: 100%; + padding: 10px 14px; + border-radius: 6px; + gap: 6px; + + input { + width: 16px; + height: 16px; + } + } + + overflow-wrap: wrap; + + .dropdown-item { + &:hover { + background-color: var(--slate3, #F1F3F5) !important; + } + + &:focus { + background: var(--slate1, #FBFCFD) !important; + box-shadow: 0px 0px 0px 4px var(--slate6) !important; + } + } +} + +.popover:has(.dropdown-table-column-hide-common) { + border: 1px solid var(--slate3) !important; + background-color: var(--base) !important; + padding: 4px; + border-radius: 6px; +} + +.jet-table.table-component { + .pagination-container { + input { + color: var(--slate12) !important; border: 1px solid var(--slate7, #D7DBDF) !important; background: var(--base, #FFF) !important; - &:hover{ + + &:hover { border: 1px solid var(--slate8, #C1C8CD) !important; background: var(--slate1, #FBFCFD) !important; } - &:focus-visible{ + + &:focus-visible { border: 1px solid var(--slate7, #D7DBDF) !important; background: var(--base, #FFF) !important; /* Focus rings/Gray/light */ box-shadow: 0px 0px 0px 4px var(--slate6) !important; } - &:active{ + + &:active { border: 1px solid var(--indigo9, #3E63DD) !important; background: var(--indigo2, #F8FAFF) !important; /* Focus rings/Indigo/light */ box-shadow: 0px 0px 0px 4px var(--indigo6) !important; } } - .total-page-number{ + + .total-page-number { color: var(--slate11) !important } } @@ -523,56 +648,68 @@ .jet-table-image-column { margin: 0 auto; } -.jet-table{ - .tr{ - .th{ - background: var(--slate3, #F1F3F5) !important; + +.jet-table { + .tr { + .th { + background: var(--surfaces-surface-02); border-bottom: 1px solid var(--slate5, #E6E8EB) !important; border-top: 0 !important; padding: 6px 12px; align-items: center; - color: var(--slate12) !important; - font-weight: 400 !important; - font-size: 12px !important; + color: var(--text-primary) !important; + font-weight: 500 !important; + font-size: 14px !important; width: 100%; - &:hover{ - background: var(--slate4, #ECEEF0) !important; + text-transform: capitalize; + + &:hover { + + background: linear-gradient(0deg, var(--surfaces-surface-02), var(--surfaces-surface-02)), linear-gradient(0deg, var(--interactive-overlays-fill-hover), var(--interactive-overlays-fill-hover)) !important; } - &:focus-within{ + + &:focus-within { outline: 0; background: var(--slate4, #ECEEF0) !important; box-shadow: 0px 0px 0px 4px var(--slate6) !important; z-index: 999; margin: 4px 0; - + } - &:first-child:focus-within{ + + &:first-child:focus-within { outline: 0; - border-left: 4px solid var(--slate6); + border-left: 4px solid var(--slate6); } - &:last-child:focus-within{ + + &:last-child:focus-within { outline: 0; - border-right: 4px solid var(--slate6); + border-right: 4px solid var(--slate6); } - div:focus-visible{ + + div:focus-visible { outline: 0 !important; } - .header-text{ + + .header-text { outline: 0; } } - .th.resizing-column{ + + .th.resizing-column { color: var(--slate9) !important; - background-color: var(--slate4, #ECEEF0) !important; + background-color: var(--slate4, #ECEEF0) !important; } - + } - .warning-no-data{ + + .warning-no-data { padding: 12px; border-radius: 6px; background-color: var(--slate3, #F1F3F5) !important; gap: 10px; - .warning-svg-wrapper{ + + .warning-svg-wrapper { display: flex; height: 20px; width: 20px; @@ -581,62 +718,735 @@ align-items: center; background: inherit; } - .warning-no-data-text{ + + .warning-no-data-text { color: var(--slate11) !important; /* Paragraph/Small/Medium */ font-size: 14px; font-style: normal; font-weight: 500; - line-height: 20px; /* 142.857% */ + line-height: 20px; + /* 142.857% */ } } + .warning-no-data-text { + color: var(--slate11) !important; + /* Paragraph/Small/Medium */ + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; + /* 142.857% */ + } + + } -.table-striped > tbody > tr:nth-of-type(even){ - background-color: var(--slate2) ; - --tblr-table-accent-bg : var(--slate2) ; + +.table-striped>tbody>tr:nth-of-type(even) { + background-color: var(--slate2); + --tblr-table-accent-bg: var(--slate2); } -.resizing-column{ - background-color: var(--slate4, #ECEEF0) !important; - border-right: 4px solid var(--slate5, #E6E8EB) !important; + +.table-striped>tbody>tr:nth-of-type(even):hover { + background-color: var(--interactive-overlays-fill-hover); + --tblr-table-accent-bg: var(--interactive-overlays-fill-hover); } -.table-striped{ - .resizing-column{ - background-color: var(--slate4, #ECEEF0) !important; - --tblr-table-accent-bg : var(--slate4,#ECEEF0) ; - border-right: 4px solid var(--slate5, #E6E8EB) !important; + +.resizing-column { + border-right: 1px solid var(--interactive-overlay-border-pressed) !important; +} + +.table-striped { + .resizing-column { + background-color: var(--slate4, #ECEEF0) !important; + --tblr-table-accent-bg: var(--slate4, #ECEEF0); + border-right: 1px solid var(--interactive-overlay-border-pressed) !important; } } -.loading-spinner-table-component{ - transform-origin:center; - animation:spinner_AtaB .75s infinite linear; - @keyframes spinner_AtaB{100%{transform:rotate(360deg)}}; - svg{ + +.loading-spinner-table-component { + transform-origin: center; + animation: spinner_AtaB .75s infinite linear; + + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg) + } + } + + ; + + svg { fill: var(--indigo6) !important; - + } } -.jet-data-table .has-actions{ - padding: 6px 12px; - .justify-content-start{ + +.jet-data-table .has-actions { + padding: 0 12px; + + .justify-content-start { justify-content: center !important; } } -.jet-table.table-component{ - .isResizing{ + +.jet-table.table-component { + .isResizing { cursor: ew-resize !important; } - .filter-applied-state{ - top: 0; - right: 0; - height: fit-content; - .filter-applied-svg{ - position: absolute; - top: -5px; - right: -5px; - } - } + + .filter-applied-state { + top: 0; + right: 0; + height: fit-content; + + .filter-applied-svg { + position: absolute; + top: -5px; + right: -5px; + } + } } -.th:has(.resizer:hover){ - border-right:4px solid var(--slate5, #ECEEF0) !important; + +// boolean column type css +.boolean-switch { + position: relative; + display: inline-block; + width: 28px; + height: 16px; + padding: 2px; + + input { + opacity: 0; + width: 0; + height: 0; + } + + .boolean-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--slate7); + -webkit-transition: .4s; + transition: .4s; + border-radius: 12px; + padding: 2px; + } + + .boolean-slider:before { + position: absolute; + content: ""; + width: 12px; + height: 12px; + left: 2px; + bottom: 2px; + background-color: var(--base); + -webkit-transition: .4s; + transition: .4s; + border-radius: 50%; + } + + input:checked+.boolean-slider { + background-color: var(--indigo10); + } + + input:focus+.boolean-slider { + box-shadow: 0 0 1px var(--indigo10); + } + + input:checked+.boolean-slider:before { + -webkit-transform: translateX(12px); + -ms-transform: translateX(12px); + transform: translateX(12px); + } +} + + + +.jet-data-table { + .table { + .td { + + // overflow: hidden; + &:hover { + // border: 1px solid var(--slate8); + background-color: var(--slate4); + } + + .invalid-feedback { + height: 22px; + } + + &:has(.invalid-feedback):hover { + border: 1px solid var(--tomato10); + + } + + &:focus-within { + border: 1px solid var(--interactive-overlays-focus-outline); + background-color: var(--surfaces-surface-01) !important; + box-shadow: var(--elevation-200-box-shadow); + + &:has(.long-text-input) { + border: 0; + padding: 0 !important; + overflow: visible !important; + } + } + + .table-column-type-div-element, + .table-column-type-input-element { + &:focus-visible { + border: none !important; + outline: none !important; + } + } + + .table-column-type-input-element { + color: var(--slate12); + } + + .table-column-type-div-element { + display: flex; + align-items: center; + } + + &.has-textarea { + overflow: hidden; + + .table-column-type-div-element { + display: flex; + height: 100%; + } + } + + } + + //for select column type + .td.has-dropdown, + td.has-multiselect { + .select-search-input { + //old dropdown + border: 0; + background: transparent; + } + } + + .td.has-badge, + .td.has-tags, + .td.has-badge, + .td.has-link, + .td.has-radio, + .td.has-dropdown, + .td.has-multiselect, + td.has-toggle { + background-color: inherit; + } + + .td.has-select { + .react-select__control { + border: none !important; + background: inherit !important; + } + + .table-select-search { + height: 100%; + display: flex; + align-items: center; + width: 100%; + + .react-select__control { + height: 100%; + display: flex; + align-items: center; + width: 100%; + + .react-select__value-container { + display: flex; + align-items: center; + padding: 0px; + } + + .react-select__value-container--is-multi { + flex-flow: wrap; + gap: 4px; + max-height: 100%; + } + } + + .react-select__indicators { + display: flex; + align-items: center; + height: 100%; + } + + .react-select__dropdown-indicator { + svg { + background-color: var(--slate7) !important; + padding: 2px !important; + border-radius: 4px !important; + + path { + fill: var(--slate11) !important + } + } + } + } + + &:focus-within { + border: 1px solid var(--indigo9) !important; + background-color: var(--indigo1) !important; + + .table-select-search { + .react-select__indicators { + // display: none !important; + } + } + } + } + } +} + +.table-select-column-type-search-box-wrapper { + border-bottom: 1px solid var(--slate4); + display: flex; + padding: 12px 8px; + flex-direction: column; + align-items: flex-start; + gap: 5px; + align-self: stretch; + height: 40px; + justify-content: center; + display: flex; + flex-direction: row; + align-items: center; + background-color: var(--surfaces-surface-01) !important; + + .table-select-column-type-search-box { + width: 100%; + box-sizing: 'border-box'; + border: none; + border-radius: 0; + background: transparent; + color: var(--slate12); + gap: 16px; + } +} + +.table-column-popover#popover-basic-2 { + z-index: 9999 !important; + min-width: 280px; + max-height: 85vh !important; + + .popover-body { + display: flex; + flex-direction: column; + padding: 16px 0px 16px !important; + gap: 16px !important; + align-self: stretch; + background-color: var(--slate1) !important; + border-radius: 0; + + .optional-properties-when-editable-true { + // background-color: var(--slate3); + padding: 12px; + // border-radius: 6px; + display: flex; + flex-direction: column; + gap: 10px; + } + + label { + @extend .tj-text; + @extend .tj-text-xsm; + font-weight: 400; + color: var(--slate12) !important; + padding-bottom: 2px; + } + + .ToggleGroupItem { + flex: 1 1 0; + } + + .table-column-date-picker-accordion { + + .accordion-button { + .accordion-title-text { + text-transform: none !important + } + } + + .grey-bg-section { + // background-color: var(--slate3); + // padding: 12px 16px; + border-radius: 6px; + display: flex; + flex-direction: column; + gap: 10px; + } + } + } + + .popover-header { + padding: 4px 0 0 0 !important; + background-color: var(--slate3); + + .active-column-tab { + border-bottom: 1px solid var(--indigo9); + color: var(--indigo9) + } + + .column-header-tab { + display: flex; + padding: 6px 8px; + justify-content: center; + align-items: center; + gap: 4px; + flex: 1 0 0; + } + } +} + +.table-action-popover { + .form-label { + margin-bottom: 2px !important; + } + + .form-control { + margin-bottom: 0 !important; + } +} + +.table-select-column-accordian { + padding: 0px 16px 0px; + margin-top: -24px; + + .accordion-button { + padding: 16px 0px !important; + } + + .accordion-body { + padding: 0 !important; + } + + .accordion-item { + border-width: 0px 0px 0px 0px !important; + border: solid var(--slate5); + } + + .list-group-item { + margin-bottom: 4px !important; + } +} + +.table-select-custom-menu-list { + .react-select__menu-list { + display: flex; + flex-direction: column; + + .option-wrapper { + align-items: center; + padding: 12px 8px; + + &:hover { + background: var(--interactive-overlays-fill-hover) !important; + border-radius: 6px; + } + + &:active { + background: var(--surfaces-surface-01) !important; + box-shadow: 0px 0px 0px 4px #DFE3E6; + + } + + .form-check { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0 !important; + + input[type=checkbox] { + width: 18px !important; + height: 18px !important; + } + + input[type=checkbox]:checked { + background-color: var(--indigo9) !important; + + } + } + } + + background-color: var(--surfaces-surface-01) !important; + } + + box-shadow: var(--elevation-400-box-shadow) !important; + +} + +.table-custom-select-badge-badges { + .select-search-value { + width: 90%; + + div { + display: flex; + gap: 6px !important; + + } + } + + &.content--wrap { + .select-search-value { + height: fit-content; + + div { + flex-flow: wrap; + } + + } + } + + &.content--overflow-hidden { + .select-search-value { + div { + overflow: hidden; + } + + } + } + + &.content--wrap-content--overflow-hidden { + // overflow: hidden; + height: 100%; + max-height: 100%; + + .select-search-container.select-search-is-multiple { + max-height: 100% !important; + height: 100% !important; + + .select-search-value { + display: flex; + align-items: center; + overflow: hidden !important; + max-height: 100% !important; + height: 100% !important; + + div { + display: flex; + max-height: 100%; + overflow: hidden; + flex-flow: wrap; + } + } + } + } + + .select-search-options { + overflow: hidden; + } +} + +.table-tags { + flex-direction: row-reverse; + + .tag-wrapper { + width: 100px; + /* Adjust width as needed */ + } + + .add-tag-button { + width: 30px; + /* Adjust width as needed */ + } + + input[type="text"] { + width: 100px; + /* Adjust width as needed */ + } + +} + +.table-radio-column-cell { + overflow: hidden; + margin-top: 10px; + + div { + height: 100%; + display: flex; + flex-flow: wrap; + gap: 3px; + } +} + +.table-column-lists { + .list-group-item { + margin-bottom: 0 !important; + background-color: var(--interactive-default) !important; + } + + .active-column-list { + background-color: var(--interactive-hover) !important; + } + + .list-group-item:hover { + background-color: var(--interactive-hover) !important; + } + +} + +.overlay-radio-table { + + // margin-top: 10px; + span { + font-family: 'IBM Plex Sans'; + line-height: 20px; + font-size: 14px; + font-weight: 400; + text-align: left; + color: var(--text-primary); + + } +} + + +.long-text-input { + height: 100%; + // display: flex; + // align-items: center; + + &:focus { + width: 100%; + // min-height: 100px; + // height: 100%; + /* Set a minimum height */ + // max-height: 200px; + resize: vertical; + /* Allow vertical resizing */ + overflow-y: auto; + /* Enable vertical scrolling */ + position: absolute; + padding: 0.75rem !important; + outline: none !important; + border: 1px solid var(--interactive-overlays-focus-outline) !important; + background-color: var(--surfaces-surface-01) !important; + z-index: 99999 !important; + align-items: flex-start !important; + } +} + +.table> :not(caption)>*>* { + padding: 0; +} + +.overlay-cell-table { + box-shadow: var(--elevation-400-box-shadow) !important; + background: var(--surfaces-surface-01) !important; + display: inline-flex !important; + flex-wrap: wrap !important; + gap: 10px !important; + padding: 16px !important; + border-radius: 6px !important; + word-wrap: break-word !important; + height: fit-content !important; +} + +.has-number { + .arror-container { + display: none; + } + + &:hover, + &:focus-within { + .arror-container { + display: flex !important; + border-left: 0.5px solid var(--borders-default) !important; + position: absolute; + right: 0px; + top: 0px; + bottom: 0px; + flex-direction: column; + justify-content: center; + + .numberinput-up-arrow-table { + right: 1px !important; + top: 1px !important; + } + + .numberinput-down-arrow-table { + right: 1px !important; + bottom: 1px !important; + } + } + } +} + +.column-type-table-inspector { + .react-select__indicators { + padding: 0 8px; + } + +} + +.has-select, +.has-datepicker { + .cell-icon-display { + visibility: hidden !important; + } + + &:hover, + &:focus-within { + .cell-icon-display { + visibility: visible !important; + } + } +} + +.table-link-hover:hover { + text-decoration: underline; +} + +.react-datepicker__time-list-item, +.react-datepicker__time-list-item--selected { + margin-bottom: 3px !important; +} + +.react-datepicker__time-box { + display: flex; + + ul { + padding: 0px 10px 0px 10px !important; + } +} + +.is-invalid.content-editing~.invalid-feedback { + padding: 0px 12px 8px; + ; +} + +.table.table-striped> :not(caption)>*>* { + border-bottom: 0; + /* Remove border bottom if .table-striped class is present */ +} + +.th:has(.resizer:hover) { + border-right: 1px solid var(--interactive-overlay-border-pressed) !important; +} + +.arror-container { + position: absolute; + right: 0px; +} + +.table-add-new-row { + .long-text-input { + max-height: max-content !important; + } +} + +.table-column-type-input-element[type="number"] { + -moz-appearance: textfield !important; } \ No newline at end of file diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index 46c4126406..3a2a34f4e0 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -276,6 +276,7 @@ button { border-width: 0px 0px 1px 0px; } + .accordion-item, .accordion-button { background-color: inherit; @@ -724,7 +725,7 @@ button { } .canvas-area { - background: #F9F9FB; + // background: #F9F9FB; margin: 0px auto; .resizer { @@ -775,7 +776,7 @@ button { .canvas-area { width: 1280px; - background: #F9F9FB; + // background: #F9F9FB; margin: 0px auto; background-size: 80px 80px; background-repeat: repeat; @@ -2903,8 +2904,7 @@ input:focus-visible { } .sketch-picker { - position: absolute; - left: -192px; + position: relative; top: 0px; border-radius: 6px !important; border: 1px solid var(--slate5, #E6E8EB) !important; @@ -2929,6 +2929,7 @@ input:focus-visible { .boxshadow-picker { .sketch-picker { left: -209px !important; + position: absolute !important; } } @@ -3097,7 +3098,7 @@ input:focus-visible { } .main .canvas-container .canvas-area { - background: #2f3c4c; + // background: #2f3c4c; } @@ -5316,7 +5317,7 @@ div#driver-page-overlay { .sso-card-wrapper { background: var(--base); min-height: 100%; - //height: calc(100vh - 156px); + // height: calc(100vh - 156px) !important; display: grid; grid-template-rows: auto 1fr auto; @@ -5378,6 +5379,7 @@ div#driver-page-overlay { margin-bottom: 4px !important; color: var(--slate12); } + .card-footer { display: flex; justify-content: flex-end; @@ -5390,13 +5392,15 @@ div#driver-page-overlay { align-Self: 'stretch'; height: 88px; } + .card-body { height: 467px; padding: 24px; - .form-group{ - .tj-app-input{ - .form-control{ - &:disabled{ + + .form-group { + .tj-app-input { + .form-control { + &:disabled { background: var(--slate3) !important; } } @@ -7590,9 +7594,9 @@ tbody { } } -.jet-data-table td .textarea-dark-theme.text-container:focus { - background-color: transparent !important; -} +// .jet-data-table td .textarea-dark-theme.text-container:focus { +// background-color: transparent !important; +// } .tooljet-logo-loader { height: 100vh; @@ -12393,6 +12397,7 @@ tbody { .sketch-picker { left: 70px !important; top: 207px; + position: absolute !important; } } @@ -12448,6 +12453,8 @@ tbody { .sketch-picker { left: 7px; width: 170px !important; + position: absolute !important; + } } @@ -12477,7 +12484,9 @@ tbody { .custom-gap-2 { gap: 2px } - +.custom-gap-3{ + gap: 3px; +} .custom-gap-4 { gap: 4px; } @@ -12489,12 +12498,21 @@ tbody { .custom-gap-12 { gap: 12px } +.custom-gap-16{ + gap: 16px; +} +.text-black-000{ + color: var(--text-black-000) !important; +} .overflow-tooltip { .tooltip-inner { max-width: 100%; } } +.tooltip-inner { + border-radius: 8px; +} #inspector-tabpane-properties { .accordion { @@ -12503,7 +12521,9 @@ tbody { } } } - +.custom-gap-7{ + gap:7px +} .card-table { overflow: visible; } @@ -12662,37 +12682,41 @@ tbody { } .modal-custom-height { - height: 700px !important; /* Set the desired width */ + height: 700px !important; + /* Set the desired width */ } + .tj-text-input-widget { - border: 1px solid var(--tj-text-input-widget-border-default); - background-color: var(--tj-text-input-widget-field-default); + border: 1px solid var(--borders-default); + background-color: var(--surfaces-surface-01); width: 100%; padding: 0px; z-index: 2; + &:disabled { + background-color: var(--surfaces-surface-03); + } &:hover:not(:focus) { border: 1px solid var(--tblr-input-border-color-darker) !important; } &.is-invalid { - border: 1px solid var(--tj-text-input-widget-error) !important; // For example, a red border for invalid input + border: 1px solid var(--status-error-strong) !important; // For example, a red border for invalid input } &:focus { outline: none !important; - border: 1px solid var(--tj-text-input-widget-border-clicked); + border: 1px solid var(--primary-accent-strong); } &:active { - // border: 1px solid var(--tj-text-input-widget-border-clicked) !important; outline: none !important; } &::placeholder { - color: #7E868C; + color: var(--text-placeholder) !important; } } @@ -12749,3 +12773,84 @@ tbody { -moz-appearance: textfield !important; } } + +.inspector-validation-date-picker { + .react-datepicker-wrapper { + input { + width: 100%; + border: 1px solid var(--slate7); + padding: 5px 5px; + background-color: var(--base); + color: var(--slate12) + } +} + + .dark-theme { + .react-datepicker__day { + color: white; + } + } +} + +.canvas-container { + scrollbar-color: transparent; + scrollbar-width: thin; + &::-webkit-scrollbar { + background-color: transparent; + width: 6px; + scrollbar-width: thin; + } + &::-webkit-scrollbar-track { + background-color: transparent; + scrollbar-width: thin; + } + &::-webkit-scrollbar-thumb { + background-color: transparent; + } + &:hover{ + scrollbar-color: #6a727c4d; + &::-webkit-scrollbar-thumb { + background-color: #6a727c4d !important; + } + } +} + +.jet-listview { + &:hover{ + scrollbar-color: #6a727c4d; + &::-webkit-scrollbar-thumb { + background-color: #6a727c4d !important; + } + } +} + +.dark-theme { + .canvas-container { + &:hover { + scrollbar-color: #6a727c4d; + + &::-webkit-scrollbar-thumb { + background-color: #6a727c4d !important; + } + } + } + + .jet-listview { + &:hover { + scrollbar-color: #6a727c4d; + + &::-webkit-scrollbar-thumb { + background-color: #6a727c4d !important; + } + } + } +} +.number-input-arrow { + &:hover { + background-color: var(--interactive-overlays-fill-hover) !important; + } + + &:active { + background-color: var(--interactive-overlays-fill-pressed) !important; + } +} \ No newline at end of file diff --git a/frontend/src/_ui/Accordion/AccordionItem.js b/frontend/src/_ui/Accordion/AccordionItem.js index 2e518f1c63..275bed5e6c 100644 --- a/frontend/src/_ui/Accordion/AccordionItem.js +++ b/frontend/src/_ui/Accordion/AccordionItem.js @@ -40,7 +40,7 @@ const AccordionItem = ({ open = true, index, title, children }) => { aria-expanded="false" data-cy={`widget-accordion-${title.toLowerCase()}`} > - {title} + {title}
{ +const Accordion = ({ items, className = '' }) => { return ( -
+
{items.map(({ title, isOpen, children }, index) => { // eslint-disable-next-line react/no-children-prop return ; diff --git a/frontend/src/_ui/AppButton/AppButton.scss b/frontend/src/_ui/AppButton/AppButton.scss index b6ef57a7fe..f55306e1c6 100644 --- a/frontend/src/_ui/AppButton/AppButton.scss +++ b/frontend/src/_ui/AppButton/AppButton.scss @@ -218,17 +218,14 @@ color: var(--slate12); border: none; background: transparent; - + &:hover { background: var(--slate4, #ECEEF0) !important; color: var(--slate11); - border: 1px solid rgba(255, 255, 255, 0.00) !important; } &:focus-visible { color: var(--slate11) !important; - // background: var(--base); - // border: none; box-shadow: 0px 0px 0px 4px var(--slate6) !important; outline: none; border: 1px solid rgba(255, 255, 255, 0.00) !important; diff --git a/frontend/src/_ui/Icon/solidIcons/AlignCenter.jsx b/frontend/src/_ui/Icon/solidIcons/AlignCenter.jsx index e9f0fe29c7..243548de6c 100644 --- a/frontend/src/_ui/Icon/solidIcons/AlignCenter.jsx +++ b/frontend/src/_ui/Icon/solidIcons/AlignCenter.jsx @@ -2,11 +2,18 @@ import React from 'react'; const AlignCenter = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => { return ( - + diff --git a/frontend/src/_ui/Icon/solidIcons/AlignLeft.jsx b/frontend/src/_ui/Icon/solidIcons/AlignLeft.jsx index 61f3133d4d..19e5c22550 100644 --- a/frontend/src/_ui/Icon/solidIcons/AlignLeft.jsx +++ b/frontend/src/_ui/Icon/solidIcons/AlignLeft.jsx @@ -2,11 +2,18 @@ import React from 'react'; const AlignLeft = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => { return ( - + diff --git a/frontend/src/_ui/Icon/solidIcons/AlignRight.jsx b/frontend/src/_ui/Icon/solidIcons/AlignRight.jsx index b3b1ae51e5..56325e6f1e 100644 --- a/frontend/src/_ui/Icon/solidIcons/AlignRight.jsx +++ b/frontend/src/_ui/Icon/solidIcons/AlignRight.jsx @@ -12,7 +12,7 @@ const AlignRight = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = diff --git a/frontend/src/_ui/Icon/solidIcons/ArrowDownTriangle.jsx b/frontend/src/_ui/Icon/solidIcons/ArrowDownTriangle.jsx new file mode 100644 index 0000000000..b592d3e0e5 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/ArrowDownTriangle.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const ArrowDownTriangle = ({ fill = '#C1C8CD', width = '24', className = '', viewBox = '0 0 24 24' }) => ( + + + +); + +export default ArrowDownTriangle; diff --git a/frontend/src/_ui/Icon/solidIcons/ArrowUpTriangle.jsx b/frontend/src/_ui/Icon/solidIcons/ArrowUpTriangle.jsx new file mode 100644 index 0000000000..846d7ac451 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/ArrowUpTriangle.jsx @@ -0,0 +1,21 @@ +import React from 'react'; + +const ArrowUpTriangle = ({ fill = '#C1C8CD', width = '24', className = '', viewBox = '0 0 24 24' }) => ( + + + +); + +export default ArrowUpTriangle; diff --git a/frontend/src/_ui/Icon/solidIcons/CheveronLeftDouble.jsx b/frontend/src/_ui/Icon/solidIcons/CheveronLeftDouble.jsx index 8cfbcba7dc..f3ca958f61 100644 --- a/frontend/src/_ui/Icon/solidIcons/CheveronLeftDouble.jsx +++ b/frontend/src/_ui/Icon/solidIcons/CheveronLeftDouble.jsx @@ -1,5 +1,5 @@ import React from 'react'; -const CheveronLeftDouble = ({ fill = '#C1C8CD', width = '24', className = 'manish', viewBox = '0 0 24 24' }) => ( +const CheveronLeftDouble = ({ fill = '#C1C8CD', width = '24', className = '', viewBox = '0 0 24 24' }) => ( ( +const RemoveRectangle = ({ fill = '#C1C8CD', width = '24', className = '', viewBox = '0 0 24 24' }) => ( ( + + + +); + +export default TriangleDownCenter; diff --git a/frontend/src/_ui/Icon/solidIcons/TriangleUpCenter.jsx b/frontend/src/_ui/Icon/solidIcons/TriangleUpCenter.jsx new file mode 100644 index 0000000000..684f83f1c4 --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/TriangleUpCenter.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const TriangleUpCenter = ({ fill = '#C1C8CD', width = '24', className = '', viewBox = '0 0 24 24', style }) => ( + + + +); + +export default TriangleUpCenter; diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js index da972e6e07..1f206a7404 100644 --- a/frontend/src/_ui/Icon/solidIcons/index.js +++ b/frontend/src/_ui/Icon/solidIcons/index.js @@ -142,6 +142,8 @@ import Check from './Check.jsx'; import Editable from './Editable.jsx'; import Save from './Save.jsx'; import Cross from './Cross.jsx'; +import ArrowUpTriangle from './ArrowUpTriangle.jsx'; +import ArrowDownTriangle from './ArrowDownTriangle.jsx'; import EnterButtonIcon from './EnterButtonIcon.jsx'; import WorkspaceConstants from './WorkspaceConstants.jsx'; import ArrowBackDown from './ArrowBackDown.jsx'; @@ -158,6 +160,8 @@ import Uppercase from './Uppercase.jsx'; import Lowercase from './Lowercase.jsx'; import Capitalize from './Capitalize.jsx'; import Oblique from './Oblique.jsx'; +import TriangleUpCenter from './TriangleUpCenter.jsx'; +import TriangleDownCenter from './TriangleDownCenter.jsx'; const Icon = (props) => { switch (props.name) { @@ -465,6 +469,10 @@ const Icon = (props) => { return ; case 'cross': return ; + case 'arrowUpTriangle': + return ; + case 'arrowDownTriangle': + return ; case 'underline': return ; case 'overline': @@ -479,6 +487,10 @@ const Icon = (props) => { return ; case 'oblique': return ; + case 'TriangleUpCenter': + return ; + case 'TriangleDownCenter': + return ; default: return ; } diff --git a/frontend/src/_ui/Icon/solidIcons/new-export-1682542240466.json b/frontend/src/_ui/Icon/solidIcons/new-export-1682542240466.json deleted file mode 100644 index 0e283d4824..0000000000 --- a/frontend/src/_ui/Icon/solidIcons/new-export-1682542240466.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "tooljet_database": [ - { - "id": "a7426184-4d4d-4191-9f74-d668cc779ef9", - "table_name": "new", - "schema": { - "columns": [ - { - "column_name": "id", - "data_type": "integer", - "column_default": "nextval('\"a7426184-4d4d-4191-9f74-d668cc779ef9_id_seq\"'::regclass)", - "character_maximum_length": null, - "numeric_precision": 32, - "is_nullable": "NO", - "constraint_type": "PRIMARY KEY", - "keytype": "PRIMARY KEY" - } - ] - } - } - ], - "tooljet_version": "2.4.9" -} \ No newline at end of file diff --git a/frontend/src/_ui/JSONTreeViewer/JSONNodeValue.jsx b/frontend/src/_ui/JSONTreeViewer/JSONNodeValue.jsx index bb75df2c12..ddcf368fd3 100644 --- a/frontend/src/_ui/JSONTreeViewer/JSONNodeValue.jsx +++ b/frontend/src/_ui/JSONTreeViewer/JSONNodeValue.jsx @@ -1,6 +1,8 @@ import React from 'react'; +import useAppDarkMode from '@/_hooks/useAppDarkMode'; const JSONTreeValueNode = ({ data, type }) => { + const { appMode } = useAppDarkMode(); if (type === 'Function') { return; // const functionString = `${data.toString().split('{')[0].trim()}{...}`; @@ -23,13 +25,29 @@ const JSONTreeValueNode = ({ data, type }) => { const clsForUndefinedOrNull = (type === 'Undefined' || type === 'Null') && 'badge badge-secondary'; return ( - - {value} - + <> + + {value} + + {appMode === 'auto' && (data === 'light' || data === 'dark') && ( + + auto + + )} + ); }; diff --git a/frontend/src/_ui/Label.jsx b/frontend/src/_ui/Label.jsx index 0a4f91d0cc..58ca13645b 100644 --- a/frontend/src/_ui/Label.jsx +++ b/frontend/src/_ui/Label.jsx @@ -1,12 +1,11 @@ import React from 'react'; -function Label({ label, width, labelRef, darkMode, color, defaultAlignment, direction, auto, isMandatory, _width }) { +function Label({ label, width, labelRef, color, defaultAlignment, direction, auto, isMandatory, _width }) { return ( <> {label && (width > 0 || auto) && (
+
{ + const nonEditableContent = (isTruthyValue) => { + return isTruthyValue ? ( + + ) : ( + + ); + }; + + const getCustomBgStyles = (value, toggleOnBg, toggleOffBg) => { + if (value && toggleOnBg) { + return { backgroundColor: toggleOnBg }; + } + if (!value && toggleOffBg) { + return { backgroundColor: toggleOffBg }; + } + return {}; + }; + + const editableContent = (isEditable, value, onChange) => { + return ( + + ); + }; + + return ( +
+ {isEditable ? editableContent(isEditable, value, onChange) : nonEditableContent(value)} +
+ ); +}; diff --git a/frontend/src/Editor/Components/Table/CustomDatePickerHeader.jsx b/frontend/src/Editor/Components/Table/CustomDatePickerHeader.jsx new file mode 100644 index 0000000000..d072e9f1d8 --- /dev/null +++ b/frontend/src/Editor/Components/Table/CustomDatePickerHeader.jsx @@ -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 ( + <> +
+ +
+ + +
+ + +
+ + ); +}; + +export default CustomDatePickerHeader; diff --git a/frontend/src/Editor/Components/Table/CustomDropdown.jsx b/frontend/src/Editor/Components/Table/CustomDropdown.jsx new file mode 100644 index 0000000000..2478e6638f --- /dev/null +++ b/frontend/src/Editor/Components/Table/CustomDropdown.jsx @@ -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 ( +
+ {arrayOfValueProps.map((value, index) => ( + + {value} + + ))} +
+ ); + } else if (valueProps) { + const stringifyValue = String(valueProps.value); + const arrayOfValueProps = stringifyValue.includes(',') ? stringifyValue.split(', ') : stringifyValue.split(' '); + return ( +
+ {arrayOfValueProps.map((value, index) => ( + + {value} + + ))} +
+ ); + } + } + 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) ? ( +
+ {labels?.map((label) => { + return ( + + {label} + + ); + })} +
+ ) : ( +
+ ); + }; + + return ( + = 1 && ['hover'] + } + rootClose={true} + show={ + multiple && + elem && + (elem?.clientHeight < elem?.scrollHeight || elem?.clientWidth < elem?.scrollWidth) && + value?.length >= 1 && + showOverlay + } + > +
{ + if (!hovered) setHovered(true); + }} + onMouseLeave={() => setHovered(false)} + > + +
+
+ ); +}; diff --git a/frontend/src/Editor/Components/Table/CustomSelect.jsx b/frontend/src/Editor/Components/Table/CustomSelect.jsx index 71196b65b9..0c7f992974 100644 --- a/frontend/src/Editor/Components/Table/CustomSelect.jsx +++ b/frontend/src/Editor/Components/Table/CustomSelect.jsx @@ -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) => ( - - {value} - - )); + 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) => ( - - {value} - - )); + }, [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) => , + 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 ( -
- +
+ ) + } + trigger={isMulti && !isFocused && valueContainerHeight > containerHeight && ['hover', 'focus']} //container width -24 -16 gives that select container size + rootClose={true} + > +
+ + 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 + /> +
+ + {optionsLoadingState ? ( +
+
+ +
+
+ ) : ( + children + )} +
+
+ ); +}; + +const CustomMultiSelectOption = ({ innerRef, innerProps, children, isSelected, ...props }) => { + return ( +
+ {props.isMulti ? ( + e.stopPropagation()} key="" value={children} /> + ) : ( +
+ e.stopPropagation()} key="" value={children} /> +
+ )} + {children} +
+ ); +}; + +const MultiValueRemove = (props) => { + const { innerProps } = props; + return
; +}; +const CustomMultiValueContainer = (props) => { + return ( +
+ {props.children} +
+ ); +}; + +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) ? ( +
+ {value?.map((option) => { + return ( + + {getLabel(option)} + + ); + })} +
+ ) : ( +
+ ); +}; + +const DropdownIndicator = (props) => { + return ( +
+ {/* Your custom SVG */} + {props.selectProps.menuIsOpen ? ( + + ) : ( + + )}
); }; diff --git a/frontend/src/Editor/Components/Table/Datepicker.jsx b/frontend/src/Editor/Components/Table/Datepicker.jsx index 4138bea58a..bc12fe3d8e 100644 --- a/frontend/src/Editor/Components/Table/Datepicker.jsx +++ b/frontend/src/Editor/Components/Table/Datepicker.jsx @@ -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 ( +
+ { + e.stopPropagation(); + }} + className={cx('table-column-datepicker-input text-truncate', { + 'pointer-events-none': readOnly, + })} + value={value} + onClick={onClick} + ref={dateInputRef} + style={styles} + /> + {!readOnly && ( + + + + )} +
+ ); +}); + +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 (
- 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={ + + } + showTimeSelect={isTimeChecked} + showTimeSelectOnly={!isDateSelectionEnabled && isTimeChecked} + showMonthDropdown + showYearDropdown + dropdownMode="select" + excludeDates={excludedDates} + showPopperArrow={false} + renderCustomHeader={(headerProps) => } + shouldCloseOnSelect + readOnly={readOnly} + popperProps={{ strategy: 'fixed' }} + timeIntervals={15} + timeFormat={isTwentyFourHrFormatEnabled ? 'HH:mm' : 'h:mm aa'} />
); diff --git a/frontend/src/Editor/Components/Table/Filter.jsx b/frontend/src/Editor/Components/Table/Filter.jsx index fd1664606f..c158d852b7 100644 --- a/frontend/src/Editor/Components/Table/Filter.jsx +++ b/frontend/src/Editor/Components/Table/Filter.jsx @@ -172,7 +172,7 @@ export function Filter(props) { } return ( -
+

@@ -207,6 +207,7 @@ export function Filter(props) { className={`${darkMode ? 'select-search-dark' : 'select-search'} mb-0`} styles={selectStyles('100%')} useCustomStyles={true} + darkMode={darkMode} />

@@ -222,6 +223,7 @@ export function Filter(props) { styles={selectStyles('100%')} dataCy={`select-coloumn-dropdown-${index ?? ''}`} useCustomStyles={true} + darkMode={darkMode} />
diff --git a/frontend/src/Editor/Components/Table/GenerateEachCellValue.jsx b/frontend/src/Editor/Components/Table/GenerateEachCellValue.jsx index 485fb773e7..62419b512f 100644 --- a/frontend/src/Editor/Components/Table/GenerateEachCellValue.jsx +++ b/frontend/src/Editor/Components/Table/GenerateEachCellValue.jsx @@ -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 ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + style={{ whiteSpace: 'pre-wrap', color: 'var(--text-primary)' }} + > + +
+ ); + }; + 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) => `${match}`); } } - return ( -
{ - 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 ? ( -
+ + const _renderCellWhenHighlighted = () => { + return ( +
{ + if (!hovered) setHovered(true); + }} + onMouseOut={() => setHovered(false)} + > + {cellValue === null ? ( + + ) : (
-
- {validationData.validationError} +
+ )} +
+ {validationData.validationError}
+
+ ); + }; + + const _renderNullCell = () => { + if (isEditable) { + if (!isNullCellClicked && !updateCellValue.current) { + return ; + } else return cellRender; + } else { + return ; + } + }; + + 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 ( +
+ {!isColumnTypeAction && columnTypeAllowToRenderMarkElement.includes(columnType) && showHighlightedCells ? ( +
} + trigger={_showOverlay && ['hover']} + rootClose={true} + show={_showOverlay && showOverlay} + > + {_renderCellWhenHighlighted()} + + ) : cellValue === null ? ( + _renderNullCell() ) : ( cellRender )} diff --git a/frontend/src/Editor/Components/Table/GlobalFilter.jsx b/frontend/src/Editor/Components/Table/GlobalFilter.jsx index 92f4a1d8fe..4296a1787e 100644 --- a/frontend/src/Editor/Components/Table/GlobalFilter.jsx +++ b/frontend/src/Editor/Components/Table/GlobalFilter.jsx @@ -24,14 +24,14 @@ export const GlobalFilter = ({ return (
- + { setValue(e.target.value); @@ -45,14 +45,14 @@ export const GlobalFilter = ({ />
{ setGlobalFilter(undefined); setValue(''); onComponentOptionChanged(component, 'searchText', ''); }} > - +
diff --git a/frontend/src/Editor/Components/Table/Link.jsx b/frontend/src/Editor/Components/Table/Link.jsx index 67083e6b05..0c05ac0800 100644 --- a/frontend/src/Editor/Components/Table/Link.jsx +++ b/frontend/src/Editor/Components/Table/Link.jsx @@ -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 ( -
+ ); diff --git a/frontend/src/Editor/Components/Table/NullRenderer/NullRenderer.jsx b/frontend/src/Editor/Components/Table/NullRenderer/NullRenderer.jsx new file mode 100644 index 0000000000..09def4f993 --- /dev/null +++ b/frontend/src/Editor/Components/Table/NullRenderer/NullRenderer.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import './nullRenderer.scss'; +import classNames from 'classnames'; + +const NullRenderer = ({ darkMode }) => { + return ( +
+ NULL +
+ ); +}; + +export default NullRenderer; diff --git a/frontend/src/Editor/Components/Table/NullRenderer/nullRenderer.scss b/frontend/src/Editor/Components/Table/NullRenderer/nullRenderer.scss new file mode 100644 index 0000000000..ac423d85a2 --- /dev/null +++ b/frontend/src/Editor/Components/Table/NullRenderer/nullRenderer.scss @@ -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 + } + } +} \ No newline at end of file diff --git a/frontend/src/Editor/Components/Table/Pagination.jsx b/frontend/src/Editor/Components/Table/Pagination.jsx index 0de949c4d9..c2a519c711 100644 --- a/frontend/src/Editor/Components/Table/Pagination.jsx +++ b/frontend/src/Editor/Components/Table/Pagination.jsx @@ -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) => { diff --git a/frontend/src/Editor/Components/Table/Radio.jsx b/frontend/src/Editor/Components/Table/Radio.jsx index 747c54ff92..56b35512b1 100644 --- a/frontend/src/Editor/Components/Table/Radio.jsx +++ b/frontend/src/Editor/Components/Table/Radio.jsx @@ -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 ( -
-
- {options.map((option, index) => ( - - ))} + 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) => ( + + )); + }; + + const getOverlay = (options, containerWidth) => { + return Array.isArray(options) ? ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {renderOptions(options)}
-
+ ) : ( +
+ ); + }; + + return ( + +
{ + if (!hovered) setHovered(true); + }} + onMouseOut={() => setHovered(false)} + > +
{renderOptions(options)}
+
+
); }; diff --git a/frontend/src/Editor/Components/Table/String.jsx b/frontend/src/Editor/Components/Table/String.jsx new file mode 100644 index 0000000000..95226ef938 --- /dev/null +++ b/frontend/src/Editor/Components/Table/String.jsx @@ -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 = () => ( +
{ + 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(); + }} + > + {cellValue} +
+ ); + + const getOverlay = () => { + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + style={{ color: 'var(--text-primary)' }} + > + + {cellValue} + +
+ ); + }; + + const _showOverlay = + ref?.current && + (ref?.current?.clientWidth < ref?.current?.children[0]?.offsetWidth || + ref?.current?.clientHeight < ref?.current?.children[0]?.offsetHeight); + + return ( + <> +
} + trigger={_showOverlay && ['hover', 'focus']} + rootClose={true} + show={_showOverlay && showOverlay && !isEditing} + > + {!isEditable ? ( +
{ + if (!hovered) setHovered(true); + }} + onMouseLeave={() => { + setHovered(false); + }} + ref={ref} + > + + {cellValue} + +
+ ) : ( +
+
{ + if (!hovered) setHovered(true); + }} + onMouseLeave={() => setHovered(false)} + className={`${!isValid ? 'is-invalid h-100' : ''} ${isEditing ? 'h-100 content-editing' : ''} h-100`} + > + {_renderString()} +
+
{validationError}
+
+ )} + + + ); +}; + +export default String; diff --git a/frontend/src/Editor/Components/Table/Table.jsx b/frontend/src/Editor/Components/Table/Table.jsx index 19cb66c7ef..1986aedcc7 100644 --- a/frontend/src/Editor/Components/Table/Table.jsx +++ b/frontend/src/Editor/Components/Table/Table.jsx @@ -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 ( -
+
); @@ -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({
{ 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} >