Feature: Add JSON datatype to ToolJet Database (#2492)

* feat: jsonb datatype support in tooljet database inprogress

* feat: jsonb support added for tooljet database column operations as well as create and edit table

* feat: added support to query JSONB values in JOIN operation

* added dto validation for join operation

* Added json data type in tjdb

* single line editor bug fixed

* Basic UI implementation for create table drawer for jsonb column type

* removed the console

* Added the sanitization for json default value in the dto

* Added jsonb svg for jsonb column type

* Updated UI for created column form

* Updated UI for edit column form

* Change the UI for create row

* Updated UI for edit row form

* Show dummy {...} value on table cell

* Setting up the codehinter in tjdb dashboard

* Created codehinter wrapper for tjdb table celljsonb data type

* Codehineter for tjdb cell

* Made changes in tjdb column and row drawers

* removed unwanted code

* Added maximum height for codehinter wrapper in each drawers

Avoided keydown event in create column drawer

* Set max height to codehinter for tjdb table cell

* Added jsonb path option for list row operation [rebase]

* Added filtering out column with json datatype in udpate rows operation

* Added filter option with jsonpath in delete row operation

* Made changes for join tables

* added json path in jon filter condition

* fix: parsing the jsonb default values in view table api

* Made on change and initial value value changes for all tjdb dashboard changes

* updated intial value and component name for codehinter in all drawers for tooljetdb

* Table cell edit codehinter initial value

* Updated codehinter onchange and initial value

* Added json path field for join sort section

* TJDB query manager updates for jsonb column type

* Tjdb dashboard bug fixes

* fix: joins jsonbpath expression can be sent without single quotes

* Added error validation for JSON in codehinter

* Removed console

* Added codehinter wrapper for tjdb cell

* Made default functional

* Made set to null functionality working

* Toggle functionality for default value

* Toggle null functionality

* Clean code

* create row form added handle disable input click

* cutom-footer css and add new data css

* Fixed tooltip

* Updated tooltip for join codehinters in query manager

* fix: jsonb column values validation in server side

* Made the initial value empty string if value is undefined

* active tab in edit row form bug

* Error state in tjdb hinter inside cell

* code mirror breaks, on the initial render

* Added placeholder and adjusted icon size in dropdown

* Fixed: Opening the cell with keyboard nav shows ‘enter to save’.

* bug fixed

* json icon alignment in the dropdown fixed

* In create and edit table, codehinter text is vertcially centered aligned

* Create row and update row info message for jsonb column type fixed

* SHowing popover when clicked on the jsonb cell

* Showing unique constraint in disable state for jsonb

* bugfix: bulkupload in tjdb for jsonb datatype should accept different json format

* Bug fixed for cellhinter and row form

* Avoided flickering in column form

* removed console

* zindex issue for codehinter in react portal for table cell

* Single line editor file changes removed

* row form bug fixed

* single quotes string escaped as it needs to be inserted as default value in JSONB column

* Bug fixed

* Edit and create row active tab bug fixed

* If value is empty, then dont show error in the codehinter

* fix: error handling for invalid jsonpath in join expression

* Handled null and Null edge cases for edit and create row

* Removed console

* Bug fix for save chages button in edit and create row drawer

* Error toast message for invalid syntax

* removed debounce time

* Copied all query manager and codehinter changes from editor to appbuilder directory

* Updated imports of codehinter in tjdb forms inside dashboard

* Discarded all changes made to editor directory

* Removed console

* Fixed flickering effect in the edit and create row drawer in TJDB dashboard

* Added focused state to codehinter in tjdb drawer, updated font size for the same

* Updated error message

* bug fixed : Pop up icon not visible inside codehinter

* Fixed : border issue in tjdb codehinter

* bugfix: array is not allowed to insert in JSONB column (#2734)

* Error message in tjdb drawers

* Error state for table schema

* Error message for cell hinter wrapper

* fixed : space between {} and table name in popup

* Spacing issue in create and update column UI in query manager fixed

* Show tooltip in fk column dropdown

* Bug fixed: edit drawer on first render cutom value is always empty

* Bug fixed

* Reverting react portal changes

* bug fixed for error copyright

* z index issue fixed for tableschema

* reverting back the changes for table form from last commit

* Bug fix: Flickering issue in table schema for codehinter input

* Bug fixed : toggle bwtween null and default value

* Handled spacing between the component name in react portal

* added new schema definition as we have introduced jsonb datatype default values can have Object and Array

* fix: removed the support for error code mapping for undefined function error  because database has different error text for it based on the scenario like jsonb aggregates etc

---------

Co-authored-by: Ganesh Kumar <ganesh8056234@gmail.com>
Co-authored-by: Ganesh Kumar <40178541+ganesh8056@users.noreply.github.com>
Co-authored-by: Akshay <akshaysasidharan93@gmail.com>
This commit is contained in:
Manish Kushare 2024-11-29 16:13:24 +05:30 committed by Akshay Sasidharan
parent 3b7577262d
commit e1d7fbb2bb
58 changed files with 2948 additions and 648 deletions

View file

@ -1,7 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useResolveStore } from '@/_stores/resolverStore';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
import './styles.scss';
import SingleLineCodeEditor from './SingleLineCodeEditor';
@ -11,12 +10,14 @@ import Tooltip from 'react-bootstrap/Tooltip';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import { isNumber } from 'lodash';
import { Alert } from '@/_ui/Alert/Alert';
import TJDBCodeEditor from './TJDBHinter';
const CODE_EDITOR_TYPE = {
fxEditor: SingleLineCodeEditor.EditorBridge,
basic: SingleLineCodeEditor,
multiline: MultiLineCodeEditor,
extendedSingleLine: SingleLineCodeEditor,
tjdbHinter: TJDBCodeEditor,
};
const CodeHinter = ({ type = 'basic', initialValue, componentName, disabled, ...restProps }) => {
@ -93,7 +94,7 @@ const PopupIcon = ({ callback, icon, tip, position, isMultiEditor = false }) =>
overlay={<Tooltip id="button-tooltip">{tip}</Tooltip>}
>
<img
// style={{ zIndex: 10000 }}
style={{ zIndex: 10000 }}
className="svg-icon m-2 popup-btn"
src={`assets/images/icons/${icon}.svg`}
width={size}

View file

@ -0,0 +1,196 @@
/* eslint-disable import/no-unresolved */
import React, { useEffect, useLayoutEffect } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { okaidia } from '@uiw/codemirror-theme-okaidia';
import { githubLight } from '@uiw/codemirror-theme-github';
import ErrorBoundary from '@/_ui/ErrorBoundary';
import CodeHinter from './CodeHinter';
import _, { initial, noop } from 'lodash';
import { handleLowPriorityWork } from '@/_helpers/editorHelpers';
import { useMounted } from '@/_hooks/use-mount';
import toast from 'react-hot-toast';
import SolidIcon from '@/_ui/Icon/SolidIcons';
const langSupport = Object.freeze({
javascript: javascript(),
});
const TJDBCodeEditor = (props) => {
const {
darkMode,
initialValue,
lang,
className,
onChange,
componentName,
placeholder,
portalProps,
paramLabel = '',
readOnly = false,
editable = true,
footerComponent = () => noop,
errorCallback = () => noop,
showErrorMessage = false,
reset = false,
defaultValue = null,
shouldUpdateToNullVal = false,
columnName = '',
} = props;
const mounted = useMounted();
const [currentValue, setCurrentValue] = React.useState(() => initialValue);
const [errorState, setErrorState] = React.useState(false);
const [error, setError] = React.useState(null);
const theme = darkMode ? okaidia : githubLight;
const langExtention = langSupport[lang] ?? null;
// eslint-disable-next-line react-hooks/exhaustive-deps
const { handleTogglePopupExapand, isOpen, setIsOpen, forceUpdate } = portalProps;
let cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : props.cyLabel;
const handleOnChange = (value) => {
if (value === '') {
setErrorState(false);
setError(null);
setCurrentValue(value);
return;
}
try {
// Try to parse the value as JSON
const parsedValue = JSON.parse(value);
if (!_.isObject(parsedValue)) {
setErrorState(true);
setError('Expected a JSON object');
throw new Error('');
} else {
setErrorState(false);
setError(null);
}
} catch (err) {
// If JSON parsing fails, it's not valid JSON
setErrorState(true);
setError('Invalid JSON');
}
setCurrentValue(value);
};
useEffect(() => {
//hack : this use effect is for edit row drawer, on initial render, custom value was always ""
if (!currentValue) setCurrentValue(initialValue);
}, [initialValue]);
useLayoutEffect(() => {
if (mounted && reset && defaultValue) {
handleLowPriorityWork(() => setCurrentValue(JSON.stringify(defaultValue)));
}
}, [reset, defaultValue]);
useLayoutEffect(() => {
if (!mounted) return;
if (shouldUpdateToNullVal) {
handleLowPriorityWork(() => setCurrentValue('null'));
} else {
!reset && handleLowPriorityWork(() => setCurrentValue(initialValue));
}
}, [shouldUpdateToNullVal]);
useEffect(() => {
if (reset || shouldUpdateToNullVal) return;
onChange(currentValue);
}, [currentValue]);
useEffect(() => {
errorCallback(errorState);
}, [errorState]);
const setupConfig = {
lineNumbers: false,
syntaxHighlighting: true,
bracketMatching: true,
foldGutter: false,
highlightActiveLine: false,
autocompletion: false,
highlightActiveLineGutter: false,
completionKeymap: false,
searchKeymap: false,
};
return (
<div
className="cm-codehinter position-relative"
style={{
width: '100%',
height: isOpen ? '350p' : 'auto',
}}
>
<div className={`cm-codehinter ${className} ${darkMode && 'cm-codehinter-dark-themed'}`}>
<CodeHinter.PopupIcon
callback={handleTogglePopupExapand}
icon="portal-open"
tip="Pop out code editor into a new window"
isMultiEditor={false}
/>
<CodeHinter.Portal
isCopilotEnabled={false}
isOpen={isOpen}
callback={setIsOpen}
componentName={componentName}
key={componentName}
forceUpdate={forceUpdate}
optionalProps={{ styles: { height: 300 }, cls: '' }}
darkMode={darkMode}
selectors={{ className: 'preview-block-portal tjdb-portal-codehinter' }}
dragResizePortal={true}
callgpt={null}
>
<ErrorBoundary>
<div className={`${errorState && 'tjdb-hinter-error'}`} data-cy={`${cyLabel}-input-field`}>
<CodeMirror
value={currentValue}
placeholder={placeholder}
height={isOpen ? '350px' : '32px'}
maxHeight={'350px'}
width="100%"
theme={theme}
extensions={[langExtention]}
onChange={handleOnChange}
basicSetup={setupConfig}
style={{
overflowY: 'auto',
borderRadius: '4px',
}}
indentWithTab={true}
readOnly={readOnly}
editable={editable}
/>
</div>
<div
className={`codehinter-error-container ${showErrorMessage && errorState ? 'd-block' : 'd-none'}`}
style={{
height: errorState ? 'auto' : '0px',
}}
>
<span className="mx-2">
{' '}
<SolidIcon name="warning" width="16px" fill={'var(--tomato9)'} />
</span>
<span>Invalid JSON syntax</span>
</div>
<div className={` ${isOpen ? 'd-block footer-component' : 'd-none'}`}>{footerComponent()}</div>
</ErrorBoundary>
</CodeHinter.Portal>
</div>
</div>
);
};
export default TJDBCodeEditor;

View file

@ -583,4 +583,21 @@
.disabled-pointerevents {
pointer-events: none;
}
.portal-container:has(.tjdb-portal-codehinter){
z-index: 100000 !important;
}
.tjdb-codehinter-wrapper-drawer{
.codehinter-error-container{
background: transparent;
font-size: 12px;
color: var(--tomato9);
}
}
.portal-container:has(.tjdb-hinter-error){
.codehinter-error-container{
display: none !important;
}
}

View file

@ -1,11 +1,12 @@
import React, { useState, useEffect, useContext } from 'react';
import React, { useState, useEffect, useContext, useRef } from 'react';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import { v4 as uuidv4 } from 'uuid';
import { isEmpty } from 'lodash';
import _, { isEmpty } from 'lodash';
import { useMounted } from '@/_hooks/use-mount';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import RenderColumnUI from './RenderColumnUI';
import { NoCondition } from './NoConditionUI';
import cx from 'classnames';
export const CreateRow = React.memo(({ optionchanged, options, darkMode }) => {
const mounted = useMounted();
@ -50,7 +51,11 @@ export const CreateRow = React.memo(({ optionchanged, options, darkMode }) => {
Columns
</label>
<div className={`field-container flex-grow-1 ${!isEmpty(columnOptions) && 'minw-400-w-400'}`}>
<div
className={`field-container flex-grow-1 d-flex custom-gap-6 flex-column ${
!isEmpty(columnOptions) && 'minw-400-w-400'
}`}
>
{isEmpty(columnOptions) && <NoCondition text="There are no columns" />}
{!isEmpty(columnOptions) &&
Object.entries(columnOptions).map(([key, value]) => (
@ -72,7 +77,7 @@ export const CreateRow = React.memo(({ optionchanged, options, darkMode }) => {
variant="ghostBlue"
size="sm"
onClick={addNewColumnOptionsPair}
className={isEmpty(columnOptions) ? '' : 'mt-2'}
className="d-flex justify-content-start width-fit-content"
>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -99,11 +104,14 @@ const RenderColumnOptions = ({
darkMode,
removeColumnOptionsPair,
}) => {
const filteredColumns = columns.filter(({ column_default }) => !column_default?.startsWith('nextval('));
const filteredColumns = columns.filter(({ column_default }) =>
_.isObject(column_default) ? true : !column_default?.startsWith('nextval(')
);
const existingColumnOption = Object.values ? Object.values(columnOptions) : [];
let displayColumns = filteredColumns.map(({ accessor }) => ({
let displayColumns = filteredColumns.map(({ accessor, dataType }) => ({
value: accessor,
label: accessor,
icon: dataType,
}));
if (existingColumnOption.length > 0) {
@ -119,7 +127,6 @@ const RenderColumnOptions = ({
};
const newColumnOptions = { ...columnOptions, [id]: updatedOption };
handleColumnOptionChange(newColumnOptions);
};
@ -133,6 +140,7 @@ const RenderColumnOptions = ({
handleColumnOptionChange(newColumnOptions);
};
const currentColumnType = columns?.find((columnDetails) => columnDetails.accessor === column)?.dataType;
return (
<RenderColumnUI
@ -144,6 +152,7 @@ const RenderColumnOptions = ({
handleValueChange={handleValueChange}
removeColumnOptionsPair={removeColumnOptionsPair}
id={id}
currentColumnType={currentColumnType}
/>
);
};

View file

@ -115,16 +115,21 @@ const RenderFilterFields = ({
updateFilterOptionsChanged,
deleteRowsOptions,
darkMode,
jsonpath = '',
}) => {
let displayColumns = columns.map(({ accessor }) => ({
let displayColumns = columns.map(({ accessor, dataType }) => ({
value: accessor,
label: accessor,
icon: dataType,
}));
operator = operators.find((val) => val.value === operator);
const handleColumnChange = (selectedOption) => {
updateFilterOptionsChanged({ ...deleteRowsOptions?.where_filters[id], ...{ column: selectedOption.value } });
updateFilterOptionsChanged({
...deleteRowsOptions?.where_filters[id],
...{ column: selectedOption.value, columnDataType: selectedOption?.dataType || '' },
});
};
const handleOperatorChange = (selectedOption) => {
@ -135,6 +140,15 @@ const RenderFilterFields = ({
updateFilterOptionsChanged({ ...deleteRowsOptions?.where_filters[id], ...{ value: newValue } });
};
const handleJsonPathChange = (value) => {
updateFilterOptionsChanged({
...deleteRowsOptions?.where_filters[id],
jsonpath: value,
});
};
const isSelectedColumnJsonbType = columns.find((col) => col.accessor === column)?.dataType === 'jsonb';
return (
<RenderFilterSectionUI
column={column}
@ -149,6 +163,9 @@ const RenderFilterFields = ({
handleValueChange={handleValueChange}
removeFilterConditionPair={removeFilterConditionPair}
id={id}
isSelectedColumnJsonbType={isSelectedColumnJsonbType}
handleJsonPathChange={handleJsonPathChange}
jsonpath={jsonpath}
/>
);
};

View file

@ -14,6 +14,8 @@ import { getPrivateRoute } from '@/_helpers/routes';
import { useNavigate } from 'react-router-dom';
import useConfirm from './Confirm';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { ToolTip } from '@/_components';
const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
const { selectedTableId, tables, joinOptions, findTableDetails, tableForeignKeyInfo } =
@ -406,7 +408,11 @@ const JoinOn = ({
const rightFieldTableDetails = (rightFieldTable && findTableDetails(rightFieldTable)) || {};
const leftFieldOptions = leftFieldTableDetails?.table_name
? tableInfo[leftFieldTableDetails.table_name]?.map((col) => ({ label: col.Header, value: col.Header })) ?? []
? tableInfo[leftFieldTableDetails.table_name]?.map((col) => ({
label: col.Header,
value: col.Header,
icon: col.dataType,
})) ?? []
: [];
const selectedLeftField = leftFieldTableDetails?.table_name
? tableInfo[leftFieldTableDetails.table_name]?.find((col) => col.Header === leftFieldColumn) ?? []
@ -420,9 +426,17 @@ const JoinOn = ({
}
return true;
})
.map((col) => ({ label: col.Header, value: col.Header })) || []
.map((col) => ({
label: col.Header,
value: col.Header,
icon: col.dataType,
})) || []
: [];
const selectedRightField = rightFieldTableDetails?.table_name
? tableInfo[rightFieldTableDetails.table_name]?.find((col) => col.Header === rightFieldColumn) ?? []
: {};
const _operators = [{ label: '=', value: '=' }];
const groupOperators = [
@ -480,7 +494,7 @@ const JoinOn = ({
</Col>
<Col sm="4" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0"
buttonClasses="border"
showPlaceHolder
options={leftFieldOptions}
darkMode={darkMode}
@ -504,8 +518,45 @@ const JoinOn = ({
});
}}
/>
{selectedLeftField?.dataType === 'jsonb' && (
<div className="tjdb-codehinter-jsonpath">
<ToolTip
message={
condition?.leftField?.jsonpath
? condition.leftField.jsonpath
: 'Access nested JSON fields by using -> for JSON object and ->> for text'
}
tooltipClassName="tjdb-table-tooltip"
placement="top"
trigger={['hover', 'focus']}
width="160px"
>
<span>
<CodeHinter
type="basic"
initialValue={condition?.leftField?.jsonpath || ''}
lang="javascript"
onChange={(value) => {
onChange &&
onChange({
...condition,
leftField: {
...condition.leftField,
jsonpath: value,
},
});
}}
enablePreview={false}
height="30"
placeholder="->>key"
componentName={condition?.leftField?.columnName ? `{}${condition.leftField.columnName}` : ''}
/>
</span>
</ToolTip>
</div>
)}
</Col>
<Col sm="1" className="p-0">
<Col sm="1" className="p-0 ">
{/* <DropDownSelect
options={operators}
darkMode={darkMode}
@ -517,14 +568,19 @@ const JoinOn = ({
{/* Above line is commented and value is hardcoded as below */}
<div style={{ height: '30px', borderRadius: 0 }} className="tj-small-btn px-2 text-center border border-end-0">
<div
style={{ height: '30px', borderRadius: 0 }}
className="tj-small-btn px-2 text-center border border-start-0 border-end-0"
>
{operator}
</div>
</Col>
<Col sm="5" className="p-0 d-flex">
<div className="flex-grow-1">
<DropDownSelect
buttonClasses={`border ${index === 0 && 'rounded-end'}`}
buttonClasses={`border ${
index === 0 && !selectedRightField?.dataType === 'jsonb' && 'rounded-end'
} overflow-hidden `}
showPlaceHolder
options={rightFieldOptions}
emptyError={
@ -548,6 +604,44 @@ const JoinOn = ({
});
}}
/>
{selectedRightField?.dataType === 'jsonb' && (
<div className="tjdb-codehinter-jsonpath">
<ToolTip
message={
condition?.rightField?.jsonpath
? condition.rightField.jsonpath
: 'Access nested JSON fields by using -> for JSON object and ->> for text'
}
tooltipClassName="tjdb-table-tooltip"
placement="top"
trigger={['hover', 'focus']}
width="160px"
>
<span>
<CodeHinter
type="basic"
inEditor={true}
initialValue={condition?.rightField?.jsonpath || ''}
lang="javascript"
onChange={(value) => {
onChange &&
onChange({
...condition,
rightField: {
...condition.rightField,
jsonpath: value,
},
});
}}
enablePreview={false}
height="30"
placeholder="->>key"
componentName={condition?.rightField?.columnName ? `{}${condition.rightField.columnName}` : ''}
/>
</span>
</ToolTip>
</div>
)}
</div>
{index > 0 && (
<ButtonSolid

View file

@ -1,9 +1,19 @@
import React, { useContext } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import DropDownSelect from './DropDownSelect';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import RightArrow from '@/_ui/Icon/solidIcons/RightArrow';
import DownArrow from '@/_ui/Icon/solidIcons/DownArrow';
import InfomrationCirlce from '@/_ui/Icon/solidIcons/InformationCircle';
import { ToolTip } from '@/_components/ToolTip';
import { v4 as uuidv4 } from 'uuid';
import _ from 'lodash';
import { NoCondition } from './NoConditionUI';
export default function JoinSelect({ darkMode }) {
const { joinOptions, tableInfo, joinTableOptions, joinTableOptionsChange, findTableDetails } =
@ -83,62 +93,103 @@ export default function JoinSelect({ darkMode }) {
setJoinSelectOptions(newSelectFields);
};
const handleJSonChange = (value, colName, table) => {
const selectedJsonColumns = [...joinSelectOptions];
const indexToBeChanged = selectedJsonColumns.findIndex((col) => col.name === colName && col.table === table);
if (indexToBeChanged !== -1) {
selectedJsonColumns[indexToBeChanged] = { ...selectedJsonColumns[indexToBeChanged], jsonpath: value };
}
setJoinSelectOptions(selectedJsonColumns);
};
return (
<Container fluid className="p-0">
<Container fluid className="p-0 d-flex flex-column custom-gap-8">
{tables.length ? (
tables.map((table) => {
const respectiveTableSelectedOptions = joinSelectOptions.filter((val) => val?.table === table);
const respectiveTableOptions = tableOptions[table] ?? [];
const tableDetails = findTableDetails(table);
const allOptionOfTableWithDataType = [];
const tableJsonbColumntypes = tableInfo[tableDetails?.table_name]?.reduce((acc, col) => {
if (col?.dataType === 'jsonb') {
acc.push(col.accessor);
allOptionOfTableWithDataType.push({ label: col.accessor, value: col.accessor, icon: col.dataType });
} else {
allOptionOfTableWithDataType.push({ label: col.accessor, value: col.accessor, icon: col.dataType });
}
return acc;
}, []);
const selectedJsonbColumns = respectiveTableSelectedOptions?.filter((col) =>
tableJsonbColumntypes?.includes(col.name)
);
return (
<Row key={table} className="mb-2 mx-0">
<Col sm="3" className="p-0">
<div
style={{
height: '30px',
borderRadius: 0,
}}
className="tj-small-btn px-2 border border-end-0 rounded-start"
>
{findTableDetails(table)?.table_name ?? ''}
</div>
</Col>
<Col sm="9" className="p-0">
<DropDownSelect
buttonClasses="border rounded-end"
highlightSelected={false}
showPlaceHolder
options={[
{ label: 'Select All', value: 'SELECT ALL' },
...(tableOptions[table]?.sort((a, b) => {
const aChecked = joinSelectOptions.some((item) => item.name === a.value && item.table === table);
const bChecked = joinSelectOptions.some((item) => item.name === b.value && item.table === table);
if (aChecked && !bChecked) {
return -1;
}
if (!aChecked && bChecked) {
return 1;
}
return 0;
}) ?? []),
]}
darkMode={darkMode}
isMulti
onChange={(values) => handleChange(values, table)}
value={[
...(respectiveTableOptions?.length === respectiveTableSelectedOptions?.length &&
respectiveTableSelectedOptions?.length !== 0
? [
{
label: 'Select All',
value: 'SELECT ALL',
},
]
: []),
...respectiveTableSelectedOptions.map((column) => ({ value: column?.name, label: column?.name })),
]}
/>
</Col>
</Row>
<div key={table}>
<Row className="mb-2 mx-0">
<Col sm="3" className="p-0">
<div
style={{
height: '30px',
borderRadius: 0,
}}
className="tj-small-btn px-2 border border-end-0 rounded-start"
>
{findTableDetails(table)?.table_name ?? ''}
</div>
</Col>
<Col sm="9" className="p-0">
<DropDownSelect
buttonClasses="border rounded-end"
highlightSelected={false}
showPlaceHolder
options={[
{ label: 'Select All', value: 'SELECT ALL' },
...(allOptionOfTableWithDataType?.sort((a, b) => {
const aChecked = joinSelectOptions.some(
(item) => item.name === a.value && item.table === table
);
const bChecked = joinSelectOptions.some(
(item) => item.name === b.value && item.table === table
);
if (aChecked && !bChecked) {
return -1;
}
if (!aChecked && bChecked) {
return 1;
}
return 0;
}) ?? []),
]}
darkMode={darkMode}
isMulti
onChange={(values) => handleChange(values, table)}
value={[
...(respectiveTableOptions?.length === respectiveTableSelectedOptions?.length &&
respectiveTableSelectedOptions?.length !== 0
? [
{
label: 'Select All',
value: 'SELECT ALL',
},
]
: []),
...respectiveTableSelectedOptions.map((column) => ({ value: column?.name, label: column?.name })),
]}
/>
</Col>
</Row>
<JsonBfieldsForSelect
selectedJsonbColumns={selectedJsonbColumns}
handleJSonChange={handleJSonChange}
table={table}
// removeJsonPathColPair={removeJsonPathColPair}
/>
</div>
);
})
) : (
@ -159,3 +210,177 @@ export default function JoinSelect({ darkMode }) {
</Container>
);
}
const JsonBfieldsForSelect = ({ selectedJsonbColumns, handleJSonChange, table }) => {
const [jsonPaths, setJsonPaths] = useState({});
const isInitialized = useRef(false);
useEffect(() => {
// Check if selectedJsonbColumns has data and if initialization has not already occurred
if (!isInitialized.current && selectedJsonbColumns?.length > 0) {
const jsonPathsToUpdate = selectedJsonbColumns.reduce((acc, col) => {
const uuid = uuidv4();
acc[uuid] = {
name: col.name,
jsonpath: col?.jsonpath || '',
id: uuid,
table: col.table,
};
return acc;
}, {});
setJsonPaths(jsonPathsToUpdate);
isInitialized.current = true; // Prevent further re-runs
}
}, [selectedJsonbColumns]); // Dependency array to track changes
const handleRemove = (id, colName, colTable) => {
const jsonpathsToUpdate = { ...jsonPaths };
delete jsonpathsToUpdate[id];
handleJSonChange('', colName, colTable);
setJsonPaths(jsonpathsToUpdate);
};
const handleColumnChange = (id, selectedOption) => {
const jsonpathsToUpdate = { ...jsonPaths };
jsonpathsToUpdate[id] = { ...jsonpathsToUpdate[id], name: selectedOption.value };
setJsonPaths(jsonpathsToUpdate);
handleJSonChange(jsonpathsToUpdate[id].jsonpath, jsonpathsToUpdate[id].name, jsonpathsToUpdate[id].table);
};
const addNewColumnOptionsPair = () => {
const jsonpathsToUpdate = { ...jsonPaths };
const uuid = uuidv4();
jsonpathsToUpdate[uuid] = {
name: '',
jsonpath: '',
id: uuid,
table: table,
};
setJsonPaths(jsonpathsToUpdate);
};
const handleJSonPathChange = (value, colName, tableId, id) => {
const jsonpathsToUpdate = { ...jsonPaths };
jsonpathsToUpdate[id] = { ...jsonpathsToUpdate[id], jsonpath: value };
handleJSonChange(value, colName, tableId);
};
const preSelectedOptions = Object.values(jsonPaths).map((col) => col.name);
const options = selectedJsonbColumns
.filter((col) => !preSelectedOptions.includes(col.name)) // Filter out columns
.map((col) => ({ label: col.name, value: col.name, table: col.table, icon: 'jsonb' })); // Transform each filtered column
const isJsonbColumnSelected = _.isEmpty(selectedJsonbColumns);
return (
<div className="d-flex flex-column custom-gap-4 w-100">
<div>
{isJsonbColumnSelected ? (
<RightArrow fill="var(--slate9)" width="14" />
) : (
<DownArrow fill="var(--slate9)" width="14" />
)}
<span>Access nested JSON field</span>
<ToolTip
message="Use -> for JSON object and ->> for text"
tooltipClassName="tjdb-table-tooltip"
placement="top"
trigger={['hover', 'focus']}
>
<span>
<InfomrationCirlce fill="var(--slate9)" width="14" />
</span>
</ToolTip>
</div>
{!isJsonbColumnSelected && (
<div className="d-flex flex-column custom-gap-4" style={{ padding: '0 28px' }}>
{Object.entries(jsonPaths).map(([key, colDetails]) => {
return (
<div className="p-0 field custom-gap-4 flex-grow-1" key={key}>
<Row className="mx-0">
<Col sm="4" className="p-0">
<DropDownSelect
useMenuPortal={true}
showPlaceHolder
placeholder="Select column"
value={{ label: colDetails.name, value: colDetails.name }}
options={options}
onChange={(selectedOption) => handleColumnChange(colDetails.id, selectedOption)}
// darkMode={darkMode}
buttonClasses="border border-end-0 rounded-start overflow-hidden"
/>
</Col>
<Col sm="8" className="p-0 d-flex tjdb-codhinter-wrapper">
<ToolTip
message={
colDetails?.jsonpath
? colDetails.jsonpath
: 'Access nested JSON fields by using -> for JSON object and ->> for text'
}
tooltipClassName="tjdb-table-tooltip"
placement="top"
trigger={['hover', 'focus']}
width="160px"
>
<div className="w-100">
<CodeHinter
type="basic"
initialValue={colDetails?.jsonpath || ''}
onChange={(value) => {
handleJSonPathChange(value, colDetails.name, colDetails.table, colDetails.id);
}}
enablePreview={false}
height="30"
placeholder="->>'key'"
componentName={colDetails?.name ? `{}${colDetails?.name}` : ''}
/>
</div>
</ToolTip>
<ButtonSolid
size="sm"
variant="ghostBlack"
className="px-1 rounded-0 border rounded-end"
customStyles={{
height: '30px',
}}
onClick={() => handleRemove(colDetails.id, colDetails.name, colDetails.table)}
>
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
</ButtonSolid>
</Col>
</Row>
</div>
);
})}
{_.isEmpty(jsonPaths) && <NoCondition text="There are no columns added" />}
<ToolTip
message={'There are no more JSON type columns'}
tooltipClassName="tjdb-table-tooltip"
placement="top"
trigger={['hover', 'focus']}
width="160px"
show={_.isEmpty(options)}
>
<ButtonSolid
variant="ghostBlue"
size="sm"
onClick={addNewColumnOptionsPair}
className={`cursor-pointer fit-content mt-2}`}
disabled={_.isEmpty(options)}
>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.34554 10.0207C5.15665 10.0207 4.99832 9.95678 4.87054 9.829C4.74276 9.70123 4.67887 9.54289 4.67887 9.354V5.854H1.17887C0.989985 5.854 0.831651 5.79011 0.703874 5.66234C0.576096 5.53456 0.512207 5.37623 0.512207 5.18734C0.512207 4.99845 0.576096 4.84012 0.703874 4.71234C0.831651 4.58456 0.989985 4.52067 1.17887 4.52067H4.67887V1.02067C4.67887 0.831782 4.74276 0.673448 4.87054 0.54567C4.99832 0.417893 5.15665 0.354004 5.34554 0.354004C5.53443 0.354004 5.69276 0.417893 5.82054 0.54567C5.94832 0.673448 6.01221 0.831782 6.01221 1.02067V4.52067H9.51221C9.7011 4.52067 9.85943 4.58456 9.98721 4.71234C10.115 4.84012 10.1789 4.99845 10.1789 5.18734C10.1789 5.37623 10.115 5.53456 9.98721 5.66234C9.85943 5.79011 9.7011 5.854 9.51221 5.854H6.01221V9.354C6.01221 9.54289 5.94832 9.70123 5.82054 9.829C5.69276 9.95678 5.53443 10.0207 5.34554 10.0207Z"
fill="#466BF2"
/>
</svg>
&nbsp; Add column
</ButtonSolid>
</ToolTip>
</div>
)}
</div>
);
};

View file

@ -7,6 +7,8 @@ import Trash from '@/_ui/Icon/solidIcons/Trash';
import AddRectangle from '@/_ui/Icon/bulkIcons/AddRectangle';
import { isEmpty } from 'lodash';
import { NoCondition } from './NoConditionUI';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { ToolTip } from '@/_components';
export default function JoinSort({ darkMode }) {
const { tableInfo, joinOrderByOptions, setJoinOrderByOptions, joinOptions, findTableDetails } =
@ -41,6 +43,7 @@ export default function JoinSort({ darkMode }) {
label: columns.Header,
value: columns.Header + '_' + tableId,
table: tableId,
icon: columns.dataType,
})) || [],
};
tableList.push(tableDetailsForDropDown);
@ -59,11 +62,16 @@ export default function JoinSort({ darkMode }) {
) : (
joinOrderByOptions.map((options, i) => {
const tableDetails = options?.table ? findTableDetails(options?.table) : '';
const isColumnJsonbType =
tableInfo[tableDetails?.table_name]?.find((col) => col.accessor === options?.columnName).dataType ===
'jsonb';
return (
<Row className="mb-2 mx-0 " key={i}>
<Col sm="6" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0 rounded-start overflow-hidden"
buttonClasses={`border ${
isColumnJsonbType ? 'rounded-top-left' : 'rounded rounded-top-right-0 rounded-bottom-right-0'
} overflow-hidden`}
showPlaceHolder
options={tableList}
darkMode={darkMode}
@ -89,11 +97,51 @@ export default function JoinSort({ darkMode }) {
);
}}
/>
{isColumnJsonbType && (
<div className="tjdb-codehinter-jsonpath">
<ToolTip
message={
options?.jsonpath
? options.jsonpath
: 'Access nested JSON fields by using -> for JSON object and ->> for text'
}
tooltipClassName="tjdb-table-tooltip"
placement="top"
trigger={['hover', 'focus']}
width="160px"
>
<span>
<CodeHinter
type="basic"
initialValue={options?.jsonpath || ''}
lang="javascript"
onChange={(value) => {
setJoinOrderByOptions(
joinOrderByOptions.map((sortBy, index) => {
if (i === index) {
return {
...sortBy,
jsonpath: value,
};
}
return sortBy;
})
);
}}
enablePreview={false}
height="30"
placeholder="->>'key'"
componentName={options?.columnName ? `{}${options.columnName}` : ''}
/>
</span>
</ToolTip>
</div>
)}
</Col>
<Col sm="6" className="p-0 d-flex">
<div className="flex-grow-1">
<DropDownSelect
buttonClasses="border border-end-0 overflow-hidden"
buttonClasses="border border-start-0 border-end-0 overflow-hidden"
showPlaceHolder
options={sortbyConstants}
darkMode={darkMode}

View file

@ -13,6 +13,7 @@ import { filterOperatorOptions, nullOperatorOptions } from './util';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { AggregateFilter } from './AggregateUI';
import { NoCondition } from './NoConditionUI';
import { ToolTip } from '@/_components';
export const JoinTable = React.memo(({ darkMode }) => {
return (
@ -319,6 +320,23 @@ const RenderFilterSection = ({ darkMode }) => {
}),
operator: valueToUpdate.operator,
};
case 'Jsonpath': {
return valueToUpdate.isLeftSideCondition
? {
...conditionDetail,
leftField: {
...conditionDetail.leftField,
jsonpath: valueToUpdate.jsonpath,
},
}
: {
...conditionDetail,
rightField: {
...conditionDetail.rightField,
jsonpath: valueToUpdate.jsonpath,
},
};
}
default:
return conditionDetail;
}
@ -362,6 +380,8 @@ const RenderFilterSection = ({ darkMode }) => {
label: columns.Header,
value: columns.Header + '-' + tableId,
table: tableId,
icon: columns?.dataType,
// columnDataType: columns?.dataType,
})) || [],
};
tableList.push(tableDetailsForDropDown);
@ -376,6 +396,10 @@ const RenderFilterSection = ({ darkMode }) => {
const filterComponents = conditionsList.map((conditionDetail, index) => {
const { operator = '', leftField = {}, rightField = {} } = conditionDetail;
const LeftSideTableDetails = leftField?.table ? findTableDetails(leftField?.table) : '';
const isSelectedColumnJsonb =
leftField?.table &&
tableInfo[LeftSideTableDetails?.table_name]?.find((col) => col.accessor === leftField?.columnName)?.dataType ===
'jsonb';
return (
<Row className="mb-2 mx-0" key={index}>
<Col sm="2" className="p-0">
@ -406,7 +430,7 @@ const RenderFilterSection = ({ darkMode }) => {
borderRadius: 0,
height: '30px',
}}
className="tj-small-btn px-2 rounded-start border border-end-0"
className="tj-small-btn px-2 rounded-start border"
>
{conditions?.operator}
</div>
@ -414,7 +438,7 @@ const RenderFilterSection = ({ darkMode }) => {
</Col>
<Col sm="3" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0"
buttonClasses="border"
showPlaceHolder
onChange={(newValue) =>
updateFilterConditionEntry('Column', index, {
@ -433,10 +457,43 @@ const RenderFilterSection = ({ darkMode }) => {
options={tableList}
darkMode={darkMode}
/>
{isSelectedColumnJsonb && (
<div className="tjdb-codehinter-jsonpath">
<ToolTip
message={
leftField?.jsonpath
? leftField.jsonpath
: 'Access nested JSON fields by using -> for JSON object and ->> for text'
}
tooltipClassName="tjdb-table-tooltip"
placement="top"
trigger={['hover', 'focus']}
width="160px"
>
<span>
<CodeHinter
type="basic"
initialValue={leftField?.jsonpath || ''}
lang="javascript"
onChange={(value) => {
updateFilterConditionEntry('Jsonpath', index, {
jsonpath: value,
isLeftSideCondition: true,
});
}}
enablePreview={false}
height="30"
placeholder="->>'key'"
componentName={leftField?.columnName ? `{}${leftField.columnName}` : ''}
/>
</span>
</ToolTip>
</div>
)}
</Col>
<Col sm="2" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0"
buttonClasses="border border-start-0 border-end-0"
showPlaceHolder
onChange={(change) => updateFilterConditionEntry('Operator', index, { operator: change?.value })}
value={filterOperatorOptions.find((op) => op.value === operator)}

View file

@ -211,6 +211,7 @@ const RenderSortFields = ({
columns,
updateSortOptionsChanged,
darkMode,
jsonpath = '',
}) => {
const orders = [
{ value: 'asc', label: 'Ascending' },
@ -220,9 +221,10 @@ const RenderSortFields = ({
order = orders.find((val) => val.value === order);
const existingColumnOptions = Object.values(listRowsOptions?.order_filters).map((item) => item.column);
let displayColumns = columns.map(({ accessor }) => ({
let displayColumns = columns.map(({ accessor, dataType }) => ({
value: accessor,
label: accessor,
icon: dataType,
}));
if (existingColumnOptions.length > 0) {
@ -232,13 +234,25 @@ const RenderSortFields = ({
}
const handleColumnChange = (selectedOption) => {
updateSortOptionsChanged({ ...listRowsOptions?.order_filters[id], ...{ column: selectedOption.value } });
updateSortOptionsChanged({
...listRowsOptions?.order_filters[id],
...{ column: selectedOption.value },
});
};
const handleDirectionChange = (selectedOption) => {
updateSortOptionsChanged({ ...listRowsOptions?.order_filters[id], ...{ order: selectedOption.value } });
};
const handleJsonPathChange = (value) => {
updateSortOptionsChanged({
...listRowsOptions?.order_filters[id],
jsonpath: value,
});
};
const isSelectedColumnJsonbType = columns.find((col) => col.accessor === column)?.dataType === 'jsonb';
return (
<RenderSortUI
column={column}
@ -250,6 +264,9 @@ const RenderSortFields = ({
handleDirectionChange={handleDirectionChange}
removeSortConditionPair={removeSortConditionPair}
id={id}
isSelectedColumnJsonbType={isSelectedColumnJsonbType}
handleJsonPathChange={handleJsonPathChange}
jsonpath={jsonpath}
/>
);
};
@ -264,16 +281,20 @@ const RenderFilterFields = ({
updateFilterOptionsChanged,
removeFilterConditionPair,
darkMode,
jsonpath = '',
}) => {
let displayColumns = columns.map(({ accessor }) => ({
let displayColumns = columns.map(({ accessor, dataType }) => ({
value: accessor,
label: accessor,
icon: dataType,
}));
console.log('manish ---> inside list', { operator, operators });
operator = operators.find((val) => val.value === operator);
const handleColumnChange = (selectedOption) => {
updateFilterOptionsChanged({ ...listRowsOptions?.where_filters[id], ...{ column: selectedOption.value } });
updateFilterOptionsChanged({
...listRowsOptions?.where_filters[id],
...{ column: selectedOption.value },
});
};
const handleOperatorChange = (selectedOption) => {
@ -284,6 +305,15 @@ const RenderFilterFields = ({
updateFilterOptionsChanged({ ...listRowsOptions?.where_filters[id], ...{ value: newValue } });
};
const handleJsonPathChange = (value) => {
updateFilterOptionsChanged({
...listRowsOptions?.where_filters[id],
jsonpath: value,
});
};
const isSelectedColumnJsonbType = columns.find((col) => col.accessor === column)?.dataType === 'jsonb';
return (
<RenderFilterSectionUI
column={column}
@ -298,6 +328,9 @@ const RenderFilterFields = ({
handleValueChange={handleValueChange}
removeFilterConditionPair={removeFilterConditionPair}
id={id}
isSelectedColumnJsonbType={isSelectedColumnJsonbType}
handleJsonPathChange={handleJsonPathChange}
jsonpath={jsonpath}
/>
);
};

View file

@ -1,4 +1,5 @@
import CodeHinter from '@/AppBuilder/CodeEditor';
import { resolveReferences } from '@/Editor/CodeEditor/utils';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import React from 'react';
@ -14,12 +15,14 @@ const RenderColumnUI = ({
handleValueChange,
removeColumnOptionsPair,
id,
currentColumnType = '',
}) => {
column = typeof column === 'object' && column !== null ? column : { label: column, value: column };
const isJSonTypeColumn = currentColumnType === 'jsonb';
return (
<div className="">
<div className="" key={id}>
<Container fluid className="p-0">
<Row className="mb-2 mx-0">
<Row className="mx-0">
<Col sm="6" className="p-0">
<DropDownSelect
useMenuPortal={true}
@ -38,7 +41,15 @@ const RenderColumnUI = ({
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
onChange={(newValue) => {
if (isJSonTypeColumn) {
const [_, __, resolvedValue] = resolveReferences(`{{${newValue}}}`);
handleValueChange(resolvedValue);
} else {
handleValueChange(newValue);
}
}}
{...(isJSonTypeColumn && { lang: 'javascript' })}
/>
<ButtonSolid
size="sm"
@ -53,6 +64,7 @@ const RenderColumnUI = ({
</ButtonSolid>
</Col>
</Row>
{isJSonTypeColumn && <span>Use SQL mode to update values in nested JSON field </span>}
</Container>
</div>
);

View file

@ -1,4 +1,5 @@
import CodeHinter from '@/AppBuilder/CodeEditor';
import { ToolTip } from '@/_components';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import React from 'react';
@ -18,6 +19,9 @@ const RenderFilterSectionUI = ({
handleValueChange,
removeFilterConditionPair,
id,
isSelectedColumnJsonbType = false,
handleJsonPathChange,
jsonpath = '',
}) => {
column = typeof column === 'object' && column !== null ? column : { label: column, value: column };
operator = typeof operator === 'object' && operator !== null ? operator : { label: operator, value: operator };
@ -34,11 +38,40 @@ const RenderFilterSectionUI = ({
options={displayColumns}
onChange={handleColumnChange}
// width={'auto'}
buttonClasses="border border-end-0 rounded-start overflow-hidden"
buttonClasses={`border ${
isSelectedColumnJsonbType ? 'border-top-left-rounded' : 'rounded-start'
} overflow-hidden`}
showPlaceHolder
darkMode={darkMode}
isMulti={false}
/>
{isSelectedColumnJsonbType && (
<div className="tjdb-codehinter-jsonpath">
<ToolTip
message={
jsonpath ? jsonpath : 'Access nested JSON fields by using -> for JSON object and ->> for text'
}
tooltipClassName="tjdb-table-tooltip"
placement="top"
trigger={['hover', 'focus']}
width="160px"
>
<span>
<CodeHinter
type="basic"
initialValue={jsonpath}
lang="javascript"
onChange={(value) => {
handleJsonPathChange(value);
}}
enablePreview={false}
height="30"
placeholder="->>key"
/>
</span>
</ToolTip>
</div>
)}
</Col>
<Col sm="4" className="p-0">
@ -49,7 +82,7 @@ const RenderFilterSectionUI = ({
options={operators}
onChange={handleOperatorChange}
// width={'auto'}
buttonClasses="border border-end-0 overflow-hidden"
buttonClasses="border border-start-0 border-end-0 overflow-hidden"
showPlaceHolder
darkMode={darkMode}
/>

View file

@ -1,3 +1,5 @@
import CodeHinter from '@/AppBuilder/CodeEditor';
import { ToolTip } from '@/_components';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import React from 'react';
@ -14,6 +16,9 @@ const RenderSortUI = ({
handleDirectionChange,
removeSortConditionPair,
id,
isSelectedColumnJsonbType = false,
handleJsonPathChange,
jsonpath = '',
}) => {
column = typeof column === 'object' && column !== null ? column : { label: column, value: column };
order = typeof order === 'object' && order !== null ? order : { label: order, value: order };
@ -24,7 +29,9 @@ const RenderSortUI = ({
<Row className="mb-2 mx-0 ">
<Col sm="6" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0 rounded-start overflow-hidden"
buttonClasses={`border ${
isSelectedColumnJsonbType ? 'border-top-left-rounded' : 'rounded-start'
} overflow-hidden`}
useMenuPortal={true}
placeholder="Select column"
value={column}
@ -34,11 +41,38 @@ const RenderSortUI = ({
width="auto"
darkMode={darkMode}
/>
{isSelectedColumnJsonbType && (
<div className="tjdb-codehinter-jsonpath">
<ToolTip
message={
jsonpath ? jsonpath : 'Access nested JSON fields by using -> for JSON object and ->> for text'
}
tooltipClassName="tjdb-table-tooltip"
placement="top"
trigger={['hover', 'focus']}
width="160px"
>
<span>
<CodeHinter
type="basic"
initialValue={jsonpath}
lang="javascript"
onChange={(value) => {
handleJsonPathChange(value);
}}
enablePreview={false}
height="30"
placeholder="->>key"
/>
</span>
</ToolTip>
</div>
)}
</Col>
<Col sm="6" className="p-0 d-flex">
<div className="flex-grow-1">
<DropDownSelect
buttonClasses="border border-end-0 overflow-hidden"
buttonClasses="border border-start-0 border-end-0 overflow-hidden"
useMenuPortal={true}
placeholder="Select direction"
value={order}

View file

@ -447,7 +447,8 @@ function DataSourceSelect({
show={
(foreignKeyAccess && props.data.dataType === 'serial') ||
props.data.dataType === 'boolean' ||
props.data.dataType === 'timestamp with time zone'
props.data.dataType === 'timestamp with time zone' ||
props.data.dataType === 'jsonb'
}
>
<div
@ -480,13 +481,7 @@ function DataSourceSelect({
(isValidElement(props.data.icon) ? (
props.data.icon
) : (
<SolidIcon
name={props.data.icon}
style={{ height: 16, width: 16 }}
width={20}
height={17}
viewBox=""
/>
<SolidIcon name={props.data.icon} width={16} />
))}
<ToolTip

View file

@ -2,12 +2,13 @@ import React, { useContext } from 'react';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import { operators } from '@/TooljetDatabase/constants';
import { v4 as uuidv4 } from 'uuid';
import { isEmpty } from 'lodash';
import _, { isEmpty } from 'lodash';
import { isOperatorOptions } from './util';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import RenderFilterSectionUI from './RenderFilterSectionUI';
import RenderColumnUI from './RenderColumnUI';
import { NoCondition } from './NoConditionUI';
import cx from 'classnames';
export const UpdateRows = React.memo(({ darkMode }) => {
const { columns, updateRowsOptions, handleUpdateRowsOptionsChange } = useContext(TooljetDatabaseContext);
@ -118,7 +119,11 @@ export const UpdateRows = React.memo(({ darkMode }) => {
<label className="form-label flex-shrink-0" data-cy="label-column-filter">
Columns
</label>
<div className={`field-container flex-grow-1 ${!isEmpty(updateRowsOptions?.columns) && 'minw-400-w-400'}`}>
<div
className={`field-container flex-grow-1 d-flex custom-gap-6 flex-column ${
!isEmpty(updateRowsOptions?.columns) && 'minw-400-w-400'
}`}
>
{isEmpty(updateRowsOptions?.columns) && <NoCondition text="There are no columns" />}
{!isEmpty(updateRowsOptions?.columns) &&
Object.entries(updateRowsOptions?.columns).map(([key, value]) => {
@ -142,7 +147,7 @@ export const UpdateRows = React.memo(({ darkMode }) => {
variant="ghostBlue"
size="sm"
onClick={addNewColumnOptionsPair}
className={`cursor-pointer fit-content ${isEmpty(updateRowsOptions?.columns) ? '' : 'mt-2'}`}
className="d-flex justify-content-start width-fit-content cursor-pointer"
>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -169,16 +174,21 @@ const RenderFilterFields = ({
updateRowsOptions,
darkMode,
removeFilterConditionPair,
jsonpath = '',
}) => {
let displayColumns = columns.map(({ accessor }) => ({
let displayColumns = columns.map(({ accessor, dataType }) => ({
value: accessor,
label: accessor,
icon: dataType,
}));
operator = operators.find((val) => val.value === operator);
const handleColumnChange = (selectedOption) => {
updateFilterOptionsChanged({ ...updateRowsOptions?.where_filters[id], ...{ column: selectedOption.value } });
updateFilterOptionsChanged({
...updateRowsOptions?.where_filters[id],
...{ column: selectedOption.value },
});
};
const handleOperatorChange = (selectedOption) => {
@ -189,6 +199,15 @@ const RenderFilterFields = ({
updateFilterOptionsChanged({ ...updateRowsOptions?.where_filters[id], ...{ value: newValue } });
};
const handleJsonPathChange = (value) => {
updateFilterOptionsChanged({
...updateRowsOptions?.where_filters[id],
jsonpath: value,
});
};
const isSelectedColumnJsonbType = columns.find((col) => col.accessor === column)?.dataType === 'jsonb';
return (
<RenderFilterSectionUI
column={column}
@ -203,6 +222,9 @@ const RenderFilterFields = ({
handleValueChange={handleValueChange}
removeFilterConditionPair={removeFilterConditionPair}
id={id}
handleJsonPathChange={handleJsonPathChange}
isSelectedColumnJsonbType={isSelectedColumnJsonbType}
jsonpath={jsonpath}
/>
);
};
@ -217,13 +239,19 @@ const RenderColumnOptions = ({
darkMode,
removeColumnOptionsPair,
}) => {
const filteredColumns = columns.filter(({ column_default }) => !column_default?.startsWith('nextval('));
const filteredColumns = columns.filter(({ column_default }) =>
_.isObject(column_default) ? true : !column_default?.startsWith('nextval(')
);
const existingColumnOptions = Object.values(updateRowsOptions?.columns).map(({ column }) => column);
let displayColumns = filteredColumns.map(({ accessor }) => ({
let displayColumns = filteredColumns.map(({ accessor, dataType }) => ({
value: accessor,
label: accessor,
icon: dataType,
}));
const currentColumnType = columns?.find((columnDetails) => columnDetails.accessor === column)?.dataType;
if (existingColumnOptions.length > 0) {
displayColumns = displayColumns.filter(
({ value }) => !existingColumnOptions.map((item) => item !== column && item).includes(value)
@ -238,7 +266,6 @@ const RenderColumnOptions = ({
};
const newColumnOptions = { ...columnOptions, [id]: updatedOption };
handleColumnOptionChange(newColumnOptions);
};
@ -264,6 +291,7 @@ const RenderColumnOptions = ({
handleValueChange={handleValueChange}
removeColumnOptionsPair={removeColumnOptionsPair}
id={id}
currentColumnType={currentColumnType}
/>
);
};

View file

@ -30,4 +30,29 @@
font-weight: 500;
line-height: 20px;
}
}
}
.tjdb-codehinter-border-none{
.cm-editor{
border: none !important;
}
}
.tjdb-codehinter-jsonpath{
.cm-editor{
border-radius: 0 0 4px 4px !important;
border-top: 0 !important;
height: 30px ;
min-height: 30px;
}
}
.border-top-left-rounded{
border-top-left-radius: 4px;
}
.border-top-right-rounded{
border-top-right-radius: 4px;
}
.custom-gap-6{
gap: 6px;
}

View file

@ -447,7 +447,8 @@ function DataSourceSelect({
show={
(foreignKeyAccess && props.data.dataType === 'serial') ||
props.data.dataType === 'boolean' ||
props.data.dataType === 'timestamp with time zone'
props.data.dataType === 'timestamp with time zone' ||
props.data.dataType === 'jsonb'
}
>
<div

View file

@ -21,6 +21,8 @@ import Skeleton from 'react-loading-skeleton';
import DateTimePicker from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker';
import { getLocalTimeZone, timeZonesWithOffsets } from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/util';
import defaultStyles from '@/_ui/Select/styles';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { resolveReferences } from '@/AppBuilder/CodeEditor/utils';
const ColumnForm = ({
onCreate,
@ -57,6 +59,7 @@ const ColumnForm = ({
label: '',
});
const isTimestamp = dataType?.value === 'timestamp with time zone';
const isJsonbColumnType = dataType?.value === 'jsonb';
const tzOptions = useMemo(() => timeZonesWithOffsets(), []);
@ -240,6 +243,37 @@ const ColumnForm = ({
setOnDeletePopup(true);
};
const [disabledSaveButton, setDisabledSaveButton] = useState(true);
useEffect(() => {
setDisabledSaveButton(columnName === '');
}, [columnName]);
const handleInputError = (bool = false) => {
setDisabledSaveButton(bool);
};
const codehinterCallback = React.useCallback(() => {
return (
<CodeHinter
type="tjdbHinter"
inEditor={false}
initialValue={defaultValue ? JSON.stringify(defaultValue) : ''}
lang="javascript"
onChange={(value) => {
const [_, __, resolvedValue] = resolveReferences(`{{${value}}}`);
setDefaultValue(resolvedValue);
}}
componentName={`{} ${columnName}`}
errorCallback={handleInputError}
lineNumbers={false}
placeholder="{}"
columnName={columnName}
showErrorMessage={true}
/>
);
}, [defaultValue]);
return (
<div className="drawer-card-wrapper ">
<div className="drawer-card-title ">
@ -326,6 +360,10 @@ const ColumnForm = ({
isClearable={true}
isPlaceholderEnabled={true}
/>
) : isJsonbColumnType ? (
<div className="tjdb-codehinter-wrapper-drawer" onKeyDown={(e) => e.stopPropagation()}>
{codehinterCallback()}
</div>
) : !foreignKeyDetails?.length > 0 && !isForeignKey ? (
<input
value={defaultValue}
@ -387,7 +425,6 @@ const ColumnForm = ({
</span>
) : null}
</div>
<div className="row mb-3">
<ToolTip
message={
@ -397,6 +434,8 @@ const ColumnForm = ({
? 'Foreign key relation cannot be created for boolean type column'
: dataType?.value === 'timestamp with time zone'
? 'Foreign key relation cannot be created with this data type'
: isJsonbColumnType
? 'Foreign key relation cannot be created for jsonb type column'
: 'Fill in column details to create a foreign key relation'
}
placement="top"
@ -404,7 +443,7 @@ const ColumnForm = ({
show={
isEmpty(dataType) ||
isEmpty(columnName) ||
['boolean', 'serial', 'timestamp with time zone'].includes(dataType?.value)
['boolean', 'serial', 'timestamp with time zone', 'jsonb'].includes(dataType?.value)
}
>
<div className="col-1">
@ -425,7 +464,7 @@ const ColumnForm = ({
disabled={
isEmpty(dataType) ||
isEmpty(columnName) ||
['serial', 'boolean', 'timestamp with time zone'].includes(dataType?.value)
['serial', 'boolean', 'timestamp with time zone', 'jsonb'].includes(dataType?.value)
}
/>
</label>
@ -460,7 +499,6 @@ const ColumnForm = ({
)} */}
</div>
</div>
<Drawer
isOpen={isForeignKeyDraweOpen}
position="right"
@ -499,7 +537,6 @@ const ColumnForm = ({
initiator="ForeignKeyTableForm"
/>
</Drawer>
<div className="row mb-3">
<div className="col-1">
<label className={`form-switch`}>
@ -521,8 +558,21 @@ const ColumnForm = ({
</p>
</div>
</div>
{!['boolean', 'timestamp with time zone'].includes(dataType?.value) && (
<div className="row mb-3">
<div className="row mb-3">
<ToolTip
message={
dataType?.value === 'boolean'
? 'Unique constraint cannot be added for boolean type column'
: dataType?.value === 'timestamp with time zone'
? 'Unique constraint cannot be added for this type column'
: isJsonbColumnType
? 'Unique constraint cannot be added for JSON type column'
: ''
}
placement="top"
tooltipClassName="tootip-table"
show={['boolean', 'timestamp with time zone', 'jsonb'].includes(dataType?.value)}
>
<div className="col-1">
<label className={`form-switch`}>
<input
@ -532,18 +582,18 @@ const ColumnForm = ({
onChange={(e) => {
setIsUniqueConstraint(e.target.checked);
}}
disabled={dataType?.value === 'serial'}
disabled={['serial', 'boolean', 'timestamp with time zone', 'jsonb'].includes(dataType?.value)}
/>
</label>
</div>
<div className="col d-flex flex-column">
<p className="m-0 p-0 fw-500 tj-switch-text">{'UNIQUE'}</p>
<p className="fw-400 secondary-text tj-text-xsm">
This constraint restricts entry of duplicate values in this column.
</p>
</div>
</ToolTip>
<div className="col d-flex flex-column">
<p className="m-0 p-0 fw-500 tj-switch-text">{'UNIQUE'}</p>
<p className="fw-400 secondary-text tj-text-xsm">
This constraint restricts entry of duplicate values in this column.
</p>
</div>
)}
</div>
</div>
<DrawerFooter
fetching={fetching}
@ -552,7 +602,8 @@ const ColumnForm = ({
shouldDisableCreateBtn={
isEmpty(columnName) ||
isEmpty(dataType) ||
(isNotNull === true && rows.length > 0 && isEmpty(defaultValue) && dataType?.value !== 'serial')
(isNotNull === true && rows.length > 0 && isEmpty(defaultValue) && dataType?.value !== 'serial') ||
disabledSaveButton
}
showToolTipForFkOnReadDocsSection={true}
initiator={initiator}

View file

@ -22,6 +22,7 @@ const ColumnsForm = ({
createForeignKeyInEdit = false,
selectedTable,
setForeignKeys,
handleInputError,
}) => {
const [columnSelection, setColumnSelection] = useState({ index: 0, value: '', configurations: {} });
const [hoveredColumn, setHoveredColumn] = useState(null);
@ -117,6 +118,7 @@ const ColumnsForm = ({
indexHover={hoveredColumn}
foreignKeyDetails={foreignKeyDetails}
existingForeignKeyDetails={existingForeignKeyDetails} // foreignKeys from context state
handleInputError={handleInputError}
/>
<div className="d-flex mb-2 mt-2 border-none" style={{ maxHeight: '32px' }}>

View file

@ -29,6 +29,8 @@ import Skeleton from 'react-loading-skeleton';
import Tick from '@/_ui/Icon/bulkIcons/Tick';
import DateTimePicker from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker';
import { getLocalTimeZone, timeZonesWithOffsets } from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/util';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { resolveReferences } from '@/AppBuilder/CodeEditor/utils';
const ColumnForm = ({
onClose,
@ -75,6 +77,7 @@ const ColumnForm = ({
const [onDelete, setOnDelete] = useState([]);
const [onUpdate, setOnUpdate] = useState([]);
const isTimestamp = dataType === 'timestamp with time zone';
const isJsonbColumnType = dataType === 'jsonb';
const { Option } = components;
// this is for DropDownDetails component which is react select
@ -462,6 +465,37 @@ const ColumnForm = ({
return matchingColumn;
}
const [disabledSaveButton, setDisabledSaveButton] = useState(true);
useEffect(() => {
setDisabledSaveButton(columnName === '');
}, [columnName]);
const handleInputError = (bool = false) => {
setDisabledSaveButton(bool);
};
const codehinterCallback = React.useCallback(() => {
return (
<CodeHinter
type="tjdbHinter"
inEditor={false}
initialValue={defaultValue ? JSON.stringify(defaultValue) : ''}
lang="javascript"
onChange={(value) => {
const [_, __, resolvedValue] = resolveReferences(`{{${value}}}`);
setDefaultValue(resolvedValue);
}}
componentName={`{} ${columnName}`}
errorCallback={handleInputError}
lineNumbers={false}
placeholder="{}"
columnName={columnName}
showErrorMessage={true}
/>
);
}, [defaultValue]);
return (
<>
<div className="drawer-card-wrapper ">
@ -586,7 +620,6 @@ const ColumnForm = ({
/>
</div>
)}
<div className="mb-3 tj-app-input">
<div className="form-label" data-cy="default-value-input-field-label">
Default value
@ -606,6 +639,10 @@ const ColumnForm = ({
isClearable={true}
isPlaceholderEnabled={true}
/>
) : isJsonbColumnType ? (
<div className="tjdb-codehinter-wrapper-drawer" onKeyDown={(e) => e.stopPropagation()}>
{codehinterCallback()}
</div>
) : !isMatchingForeignKeyColumn(selectedColumn?.Header) ? (
<input
value={selectedColumn?.dataType !== 'serial' ? defaultValue : null}
@ -679,9 +716,7 @@ const ColumnForm = ({
</span>
) : null}
</div>
{/* foreign key toggle */}
<div className="row mb-3">
<ToolTip
message={
@ -691,6 +726,8 @@ const ColumnForm = ({
? 'Foreign key relation cannot be created for boolean type column'
: dataType === 'timestamp with time zone'
? 'Foreign key relation cannot be created for this data type'
: dataType === 'jsonb'
? 'Foreign key relation cannot be created for JSON data type'
: 'Fill in column details to create a foreign key relation'
}
placement="top"
@ -698,7 +735,7 @@ const ColumnForm = ({
show={
isEmpty(dataType) ||
isEmpty(columnName) ||
['boolean', 'serial', 'timestamp with time zone'].includes(dataType)
['boolean', 'serial', 'timestamp with time zone', 'jsonb'].includes(dataType)
}
>
<div className="col-1">
@ -721,7 +758,7 @@ const ColumnForm = ({
dataType?.value === 'serial' ||
isEmpty(dataType) ||
isEmpty(columnName) ||
['boolean', 'serial', 'timestamp with time zone'].includes(dataType)
['boolean', 'serial', 'timestamp with time zone', 'jsonb'].includes(dataType)
}
/>
</label>
@ -799,9 +836,7 @@ const ColumnForm = ({
initiator="ForeignKeyTableForm"
/>
</Drawer>
{/* <ForeignKeyRelation tableName={selectedTable.table_name} columns={columns} /> */}
<ToolTip
message={
selectedColumn.constraints_type.is_primary_key === true
@ -844,63 +879,70 @@ const ColumnForm = ({
</div>
</div>
</ToolTip>
{!['boolean', 'timestamp with time zone'].includes(dataType) && (
<ToolTip
message={
selectedColumn.constraints_type.is_primary_key === true
? 'Primary key values must be unique'
: selectedColumn.dataType === 'serial' &&
(selectedColumn.constraints_type.is_primary_key !== true ||
selectedColumn.constraints_type.is_primary_key === true)
? 'Serial data type value must be unique'
: null
}
placement="top"
tooltipClassName="tooltip-table-edit-column"
style={toolTipPlacementStyle}
show={
selectedColumn.constraints_type?.is_primary_key === true ||
(selectedColumn.dataType === 'serial' &&
<ToolTip
message={
selectedColumn.constraints_type.is_primary_key === true
? 'Primary key values must be unique'
: selectedColumn.dataType === 'serial' &&
(selectedColumn.constraints_type.is_primary_key !== true ||
selectedColumn.constraints_type.is_primary_key === true))
}
>
<div className="row mb-1">
<div className="col-1">
<label className={`form-switch`}>
<input
className="form-check-input"
type="checkbox"
checked={
!isUniqueConstraint && selectedColumn?.constraints_type?.is_primary_key
? true
: isUniqueConstraint
}
onChange={(e) => {
setIsUniqueConstraint(e.target.checked);
}}
disabled={
selectedColumn?.dataType === 'serial' || selectedColumn?.constraints_type?.is_primary_key
}
/>
</label>
</div>
<div className="col d-flex flex-column">
<p className="m-0 p-0 fw-500 tj-switch-text">{'UNIQUE'}</p>
<p className="fw-400 secondary-text tj-text-xsm tj-switch-text">
This constraint restricts entry of duplicate values in this column.
</p>
</div>
selectedColumn.constraints_type.is_primary_key === true)
? 'Serial data type value must be unique'
: selectedColumn.dataType === 'boolean'
? 'Unique constraint cannot be added for boolean type column'
: selectedColumn.dataType === 'timestamp with time zone'
? 'Unique constraint cannot be added for this type column'
: selectedColumn.dataType === 'jsonb'
? 'Unique constraint cannot be added for JSON type column'
: null
}
placement="top"
tooltipClassName="tooltip-table-edit-column"
style={toolTipPlacementStyle}
show={
selectedColumn.constraints_type?.is_primary_key === true ||
(selectedColumn.dataType === 'serial' &&
(selectedColumn.constraints_type.is_primary_key !== true ||
selectedColumn.constraints_type.is_primary_key === true)) ||
['boolean', 'timestamp with time zone', 'jsonb'].includes(selectedColumn.dataType)
}
>
<div className="row mb-1">
<div className="col-1">
<label className={`form-switch`}>
<input
className="form-check-input"
type="checkbox"
checked={
!isUniqueConstraint && selectedColumn?.constraints_type?.is_primary_key
? true
: isUniqueConstraint
}
onChange={(e) => {
setIsUniqueConstraint(e.target.checked);
}}
disabled={
['serial', 'boolean', 'timestamp with time zone', 'jsonb'].includes(selectedColumn?.dataType) ||
selectedColumn?.constraints_type?.is_primary_key
}
/>
</label>
</div>
</ToolTip>
)}
<div className="col d-flex flex-column">
<p className="m-0 p-0 fw-500 tj-switch-text">{'UNIQUE'}</p>
<p className="fw-400 secondary-text tj-text-xsm tj-switch-text">
This constraint restricts entry of duplicate values in this column.
</p>
</div>
</div>
</ToolTip>
</div>
<DrawerFooter
isEditMode={true}
fetching={fetching}
onClose={onClose}
onEdit={handleEdit}
shouldDisableCreateBtn={columnName === ''}
shouldDisableCreateBtn={disabledSaveButton}
showToolTipForFkOnReadDocsSection={true}
initiator={initiator}
/>

View file

@ -20,6 +20,33 @@ import ArrowRight from '../Icons/ArrowRight.svg';
import Skeleton from 'react-loading-skeleton';
import DateTimePicker from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker';
import { getLocalTimeZone, getUTCOffset } from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/util';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { resolveReferences } from '@/AppBuilder/CodeEditor/utils';
const compareValueInObject = (currentValue, defaultValue) => {
try {
let cv = currentValue;
let defaultVal = defaultValue;
// Step 1: Parse cv until it's fully converted to an object
while (typeof cv === 'string') {
cv = JSON.parse(cv);
}
// Step 2: Use Lodash's isEqual for a deep comparison
return _.isEqual(cv, defaultVal);
} catch (error) {
return false;
}
};
const transformJSONValue = (value) => {
if (typeof value === 'string') {
return JSON.stringify(JSON.parse(value));
} else {
return JSON.stringify(value);
}
};
const EditRowForm = ({
onEdit,
@ -39,6 +66,12 @@ const EditRowForm = ({
const [inputValues, setInputValues] = useState([]);
const [errorMap, setErrorMap] = useState({});
const [disabledSaveButton, setDisabledSaveButton] = useState(false);
const handleInputError = (bool = false) => {
setDisabledSaveButton(bool);
};
useEffect(() => {
toast.dismiss();
}, []);
@ -46,9 +79,17 @@ const EditRowForm = ({
useEffect(() => {
if (currentValue) {
const keysWithNullValues = Object.keys(currentValue).filter((key) => currentValue[key] === null);
const keysWithDefaultValues = Object.keys(currentValue).filter(
(key, index) => currentValue[key]?.toString() === columns[index].column_default
);
const keysWithDefaultValues = Object.keys(currentValue).filter((key, index) => {
if (columns[index].dataType === 'jsonb') {
try {
return compareValueInObject(currentValue[key], columns[index].column_default);
} catch (error) {
return false;
}
}
return currentValue[key]?.toString() === columns[index].column_default;
});
setActiveTab((prevActiveTabs) => {
const newActiveTabs = [...prevActiveTabs];
keysWithNullValues.forEach((key) => {
@ -57,20 +98,36 @@ const EditRowForm = ({
newActiveTabs[index] = 'Null';
}
});
keysWithDefaultValues.forEach((key) => {
const index = Object.keys(currentValue).indexOf(key);
if (currentValue[key]?.toString() === columns[index].column_default) {
const compareCondition =
columns[index].dataType === 'jsonb'
? compareValueInObject(currentValue[key], columns[index].column_default)
: currentValue[key]?.toString() === columns[index].column_default;
if (compareCondition) {
newActiveTabs[index] = 'Default';
}
});
return newActiveTabs;
});
const initialInputValues = currentValue
? Object.keys(currentValue).map((key, index) => {
const value =
currentValue[key] === null ? null : currentValue[key] === currentValue[key] ? currentValue[key] : '';
const isJsonDataType = columns[index].dataType === 'jsonb';
let isJsonbCurrentAndDefaultValueEqual = false;
if (isJsonDataType) {
isJsonbCurrentAndDefaultValueEqual = compareValueInObject(
currentValue[key],
columns[index].column_default
);
}
const value = currentValue[key] === null ? null : currentValue[key] ? currentValue[key] : '';
const disabledValue =
currentValue[key] === null || currentValue[key]?.toString() === columns[index].column_default
currentValue[key] === null ||
(isJsonDataType
? isJsonbCurrentAndDefaultValueEqual
: currentValue[key]?.toString() === columns[index].column_default)
? true
: false;
return { value: value, disabled: disabledValue, label: value };
@ -136,12 +193,18 @@ const EditRowForm = ({
newInputValues[index] = { value: defaultValue, disabled: true, label: defaultValue };
} else if (defaultValue && tabData === 'Default' && dataType === 'boolean') {
newInputValues[index] = { value: actualDefaultVal, disabled: true, label: actualDefaultVal };
} else if (defaultValue && tabData === 'Default' && dataType === 'jsonb') {
const [_, __, resolvedValue] = resolveReferences(`{{${defaultValue}}}`);
newInputValues[index] = { value: resolvedValue, disabled: false, label: resolvedValue };
} else if (nullValue && tabData === 'Null' && dataType !== 'boolean') {
newInputValues[index] = { value: null, disabled: true, label: null };
} else if (nullValue && tabData === 'Null' && dataType === 'boolean') {
newInputValues[index] = { value: null, disabled: true, label: null };
} else if (tabData === 'Custom' && customVal.length > 0) {
} else if (tabData === 'Custom' && customVal?.length > 0 && dataType !== 'jsonb') {
newInputValues[index] = { value: customVal, disabled: false, label: customVal };
} else if (tabData === 'Custom' && customVal?.length > 0 && dataType === 'jsonb') {
const [_, __, resolvedValue] = resolveReferences(`{{${customVal}}}`);
newInputValues[index] = { value: resolvedValue, disabled: false, label: resolvedValue };
} else if (tabData === 'Custom' && customVal.length <= 0) {
newInputValues[index] = { value: '', disabled: false, label: '' };
} else {
@ -165,6 +228,20 @@ const EditRowForm = ({
? null
: null,
});
} else if (dataType === 'jsonb') {
setRowData({
...rowData,
[columnName]:
newInputValues[index].value === null
? null
: compareValueInObject(newInputValues[index].value, defaultValue)
? defaultValue
: _.isEqual(newInputValues[index].value, currentValue)
? currentValue
: currentValue === null && customVal === ''
? ''
: null,
});
} else {
setRowData({
...rowData,
@ -451,6 +528,108 @@ const EditRowForm = ({
)}
</div>
);
case 'jsonb':
return (
<div style={{ position: 'relative' }} onKeyDown={(e) => e.stopPropagation()}>
{inputValues[index]?.value === null ? (
<div
style={{
display: 'flex',
alignItems: 'center',
position: 'relative',
backgroundColor: 'transparent',
width: '100%',
border: '1px solid var(--slate7)',
padding: '5px 5px',
borderRadius: '6px',
}}
className={'null-container'}
tabindex="0"
>
<span
style={{
position: 'static',
backgroundColor: 'transparent',
}}
className={'null-tag'}
>
Null
</span>
</div>
) : activeTab[index] === 'Default' ? (
<div
style={{
display: 'flex',
alignItems: 'center',
position: 'relative',
backgroundColor: 'transparent',
width: '100%',
border: '1px solid var(--slate7)',
padding: '5px 5px',
borderRadius: '6px',
overflow: 'hidden',
height: '36px',
maxHeight: '36px',
fontSize: '12px',
}}
tabindex="0"
className="truncate"
>
{transformJSONValue(column_default)}
</div>
) : (
<div className="tjdb-codehinter-wrapper-drawer" onKeyDown={(e) => e.stopPropagation()}>
<CodeHinter
type="tjdbHinter"
inEditor={false}
initialValue={inputValues[index]?.value ? transformJSONValue(inputValues[index]?.value) : ''}
lang="javascript"
onChange={(value) => {
if (value === 'Null') {
handleInputChange(index, value, columnName);
} else {
const [_, __, resolvedValue] = resolveReferences(`{{${value}}}`);
handleInputChange(index, resolvedValue, columnName);
}
}}
componentName={`{} ${columnName}`}
errorCallback={handleInputError}
lineNumbers={false}
placeholder="{}"
columnName={columnName}
showErrorMessage={true}
/>
</div>
)}
{(inputValues[index]?.disabled || shouldInputBeDisabled) && (
<div
onClick={() => {
handleDisabledInputClick(
index,
'Custom',
column_default,
isNullable,
columnName,
dataType,
currentValue[columnName]
);
handleTabClick(index, 'Custom', column_default, isNullable, columnName, dataType);
}}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 4,
cursor: 'pointer',
backgroundColor: 'transparent',
}}
/>
)}
</div>
);
default:
break;
@ -724,7 +903,9 @@ const EditRowForm = ({
fetching={fetching}
onClose={onClose}
onEdit={handleSubmit}
shouldDisableCreateBtn={Object.values(matchingObject).includes('') || (isSubset && isSubsetForCharacter)}
shouldDisableCreateBtn={
Object.values(matchingObject).includes('') || (isSubset && isSubsetForCharacter) || disabledSaveButton
}
initiator={initiator}
/>
)}

View file

@ -15,7 +15,34 @@ import './styles.scss';
import Skeleton from 'react-loading-skeleton';
import DateTimePicker from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker';
import { getLocalTimeZone, getUTCOffset } from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/util';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { resolveReferences } from '@/AppBuilder/CodeEditor/utils';
import _ from 'lodash';
const compareValueInObject = (currentValue, defaultValue) => {
try {
let cv = currentValue;
let defaultVal = defaultValue;
// Step 1: Parse cv until it's fully converted to an object
while (typeof cv === 'string') {
cv = JSON.parse(cv);
}
// Step 2: Use Lodash's isEqual for a deep comparison
return _.isEqual(cv, defaultVal);
} catch (error) {
return false;
}
};
const transformJSONValue = (value) => {
if (typeof value === 'string') {
return JSON.stringify(JSON.parse(value));
} else {
return JSON.stringify(value);
}
};
const RowForm = ({
onCreate,
onClose,
@ -30,6 +57,13 @@ const RowForm = ({
const inputRefs = useRef({});
const primaryKeyColumns = [];
const nonPrimaryKeyColumns = [];
const [disabledSaveButton, setDisabledSaveButton] = useState(false);
const handleInputError = (bool = false) => {
setDisabledSaveButton(bool);
};
columns.forEach((column) => {
if (column?.constraints_type?.is_primary_key) {
primaryKeyColumns.push({ ...column });
@ -121,7 +155,8 @@ const RowForm = ({
return matchingColumn;
}
const handleDisabledInputClick = (index, columnName) => {
const handleDisabledInputClick = (index, columnName, defaultValue = '', nullValue = '', dataType = '') => {
//index, columnName, 'Custom', defaultValue, null, dataType
if (inputRefs.current[columnName]) {
setTimeout(() => {
inputRefs.current[columnName].focus();
@ -137,6 +172,7 @@ const RowForm = ({
if (isCurrentlyDisabled) {
setData((prevData) => ({ ...prevData, [columnName]: newInputValues[index].value }));
}
handleTabClick(index, 'Custom', defaultValue, nullValue, columnName, dataType);
};
const handleTabClick = (index, tabData, defaultValue, nullValue, columnName, dataType) => {
@ -155,6 +191,9 @@ const RowForm = ({
disabled: true,
label: defaultValue,
};
} else if (defaultValue && tabData === 'Default' && dataType === 'jsonb') {
const [_, __, resolvedValue] = resolveReferences(`{{${defaultValue}}}`);
newInputValues[index] = { value: resolvedValue, disabled: false, label: resolvedValue };
} else if (nullValue && tabData === 'Null' && dataType !== 'boolean') {
newInputValues[index] = { value: null, checkboxValue: false, disabled: true, label: null };
} else if (nullValue && tabData === 'Null' && dataType === 'boolean') {
@ -164,6 +203,8 @@ const RowForm = ({
} else if (tabData === 'Custom' && dataType === 'timestamp with time zone') {
if (oldActiveTab[index] === 'Custom') return;
newInputValues[index] = { value: new Date().toISOString(), checkboxValue: false, disabled: false, label: '' };
} else if (tabData === 'Custom' && dataType === 'jsonb') {
newInputValues[index] = { value: '', checkboxValue: false, disabled: false, label: '' };
} else {
newInputValues[index] = { value: '', checkboxValue: false, disabled: false, label: '' };
}
@ -177,6 +218,16 @@ const RowForm = ({
...data,
[accessor]: inputValuesArr[index].checkboxValue === null ? null : inputValuesArr[index].checkboxValue,
});
} else if (dataType === 'jsonb') {
setData({
...data,
[accessor]:
inputValuesArr[index].value === null
? null
: compareValueInObject(inputValuesArr[index].value, defaultVal)
? defaultVal
: inputValuesArr[index].value,
});
} else {
setData({
...data,
@ -194,13 +245,13 @@ const RowForm = ({
const newInputValues = [...inputValues];
const isNull = value === null || value === 'Null';
newInputValues[index] = {
value: value === 'Null' ? null : value,
value: isNull ? null : value,
checkboxValue: inputValues[index].checkboxValue,
disabled: isNull,
label: value === 'Null' ? null : value,
label: isNull ? null : value,
};
setInputValues(newInputValues);
setData({ ...data, [columnName]: value === 'Null' ? null : value });
setData({ ...data, [columnName]: isNull ? null : value });
if (isNull) {
const newActiveTabs = [...activeTab];
newActiveTabs[index] = 'Null';
@ -416,7 +467,7 @@ const RowForm = ({
)}
{inputValues[index]?.disabled && (
<div
onClick={() => handleDisabledInputClick(index, columnName)}
onClick={() => handleDisabledInputClick(index, columnName, defaultValue, isNullable, dataType)}
style={{
position: 'absolute',
top: 0,
@ -479,7 +530,7 @@ const RowForm = ({
/>
{inputValues[index]?.disabled && (
<div
onClick={() => handleDisabledInputClick(index, columnName)}
onClick={() => handleDisabledInputClick(index, columnName, defaultValue, isNullable, dataType)}
style={{
position: 'absolute',
top: 0,
@ -495,6 +546,100 @@ const RowForm = ({
</div>
);
case 'jsonb': {
return (
<div style={{ position: 'relative' }}>
{inputValues[index]?.value === null ? (
<div
style={{
display: 'flex',
alignItems: 'center',
position: 'relative',
backgroundColor: 'transparent',
width: '100%',
border: '1px solid var(--slate7)',
padding: '5px 5px',
borderRadius: '6px',
}}
className={'null-container'}
tabindex="0"
>
<span
style={{
position: 'static',
backgroundColor: 'transparent',
}}
className={'null-tag'}
>
Null
</span>
</div>
) : activeTab[index] === 'Default' ? (
<div
style={{
display: 'flex',
alignItems: 'center',
position: 'relative',
backgroundColor: 'transparent',
width: '100%',
border: '1px solid var(--slate7)',
padding: '5px 5px',
borderRadius: '6px',
overflow: 'hidden',
height: '36px',
maxHeight: '36px',
fontSize: '12px',
}}
tabindex="0"
className="truncate"
>
{transformJSONValue(defaultValue)}
</div>
) : (
<div className="tjdb-codehinter-wrapper-drawer" onKeyDown={(e) => e.stopPropagation()}>
<CodeHinter
type="tjdbHinter"
inEditor={false}
initialValue={inputValues[index]?.value ? transformJSONValue(inputValues[index]?.value) : ''}
lang="javascript"
onChange={(value) => {
if (value === 'Null') {
handleInputChange(index, value, columnName);
} else {
const [_, __, resolvedValue] = resolveReferences(`{{${value}}}`);
handleInputChange(index, resolvedValue, columnName);
}
}}
componentName={`{} ${columnName}`}
errorCallback={handleInputError}
lineNumbers={false}
placeholder="{}"
columnName={columnName}
showErrorMessage={true}
/>
</div>
)}
{inputValues[index]?.disabled && (
<div
onClick={() => {
handleDisabledInputClick(index, columnName, defaultValue, isNullable, dataType);
}}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 1,
cursor: 'pointer',
backgroundColor: 'transparent',
}}
/>
)}
</div>
);
}
default:
break;
}
@ -686,7 +831,7 @@ const RowForm = ({
fetching={fetching}
onClose={onClose}
onCreate={handleSubmit}
shouldDisableCreateBtn={Object.values(matchingObject).includes('')}
shouldDisableCreateBtn={Object.values(matchingObject).includes('') || disabledSaveButton}
initiator={initiator}
/>
</div>

View file

@ -35,6 +35,12 @@ const TableForm = ({
const selectedTableColumnDetails = Object.values(selectedTableColumns);
const darkMode = localStorage.getItem('darkMode') === 'true';
//Following state and handleInputError is to disable footer if JSON value is invalid for JSON column type
const [disabledCreateButton, setDisabledCreateButton] = useState(false);
const handleInputError = (bool = false) => {
setDisabledCreateButton(bool);
};
const [fetching, setFetching] = useState(false);
const [showModal, setShowModal] = useState(false);
const [createForeignKeyInEdit, setCreateForeignKeyInEdit] = useState(false);
@ -165,6 +171,10 @@ const TableForm = ({
toast.error('Column names cannot be empty');
return;
}
if (disabledCreateButton) {
toast.error('Invalid JSON syntax for JSONB type column');
return;
}
const checkingValues = isEmpty(foreignKeyDetails) ? false : true;
@ -190,6 +200,11 @@ const TableForm = ({
const handleEdit = async () => {
if (!validateTableName()) return;
if (disabledCreateButton) {
toast.error('Invalid JSON syntax for JSONB type column');
return;
}
setFetching(true);
const { error } = await tooljetDatabaseService.renameTable(
organizationId,
@ -319,6 +334,7 @@ const TableForm = ({
createForeignKeyInEdit={createForeignKeyInEdit}
selectedTable={selectedTable}
setForeignKeys={setForeignKeys}
handleInputError={handleInputError}
/>
</div>
<DrawerFooter

View file

@ -82,7 +82,8 @@ function SourceKeyRelation({
isDisabled:
item?.data_type === 'serial' ||
item?.data_type === 'boolean' ||
item?.data_type === 'timestamp with time zone'
item?.data_type === 'timestamp with time zone' ||
item?.data_type === 'jsonb'
? true
: false,
};

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect, useMemo, memo } from 'react';
import { UniqueConstraintPopOver } from '../Table/ActionsPopover/UniqueConstraintPopOver';
import cx from 'classnames';
import { ToolTip } from '@/_components/ToolTip';
@ -21,6 +21,42 @@ import {
getLocalTimeZone,
timeZonesWithOffsets,
} from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/util';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { resolveReferences } from '@/AppBuilder/CodeEditor/utils';
import _ from 'lodash';
function areEqual(prevProps, nextProps) {
return _.isEqual(prevProps.defaultValue, nextProps.defaultValue);
}
const MeomoizedCodehinter = memo(({ columnDetails, index, handleInputError, setColumns, defaultValue }) => {
return (
<CodeHinter
type="tjdbHinter"
inEditor={false}
initialValue={columnDetails[index]?.column_default ? JSON.stringify(columnDetails[index].column_default) : ''}
lang="javascript"
onChange={(value) => {
const [_, __, resolvedValue] = resolveReferences(`{{${value}}}`);
setColumns((prevColumns) => {
console.log('prevCol', { prevColumns });
const updatedColumns = { ...prevColumns };
updatedColumns[index] = {
...updatedColumns[index], // Preserve other properties like `column_name`
column_default: resolvedValue,
};
return updatedColumns;
});
}}
componentName={`{} ${columnDetails[index].column_name}`}
errorCallback={handleInputError}
lineNumbers={false}
placeholder="{}"
height="36"
columnName={columnDetails[index]?.column_name}
/>
);
}, areEqual);
function TableSchema({
columns,
@ -35,6 +71,7 @@ function TableSchema({
foreignKeyDetails,
setForeignKeyDetails,
existingForeignKeyDetails,
handleInputError,
}) {
const [referencedColumnDetails, setReferencedColumnDetails] = useState([]);
const [previousColumnNames, setPreviousColumnNames] = useState([]);
@ -155,452 +192,482 @@ function TableSchema({
return (
<div className="column-schema-container">
{Object.keys(columnDetails).map((index) => (
<div
key={index}
className={`list-group-item mb-1 mt-2 table-schema ${index == indexHover ? 'foreignKey-hover' : ''}`}
>
<div className="table-schema-row">
{/* <div className="col-1">
{Object.keys(columnDetails).map((index) => {
return (
<div
key={index}
className={`list-group-item mb-1 mt-2 table-schema ${index == indexHover ? 'foreignKey-hover' : ''}`}
>
<div className="table-schema-row">
{/* <div className="col-1">
<DragIcon />
</div> */}
<div className="m-0 pe-0 ps-1 columnName" data-cy="column-name-input-field">
<input
onChange={(e) => {
e.persist();
const prevColumns = { ...columnDetails };
setForeignKeyDetails((prevState) => {
return prevState.map((item) => {
return {
...item,
column_names: item.column_names.map((col) => {
return col === previousColumnNames[index] ? e.target.value : col;
}),
};
<div className="m-0 pe-0 ps-1 columnName" data-cy="column-name-input-field">
<input
onChange={(e) => {
e.persist();
const prevColumns = _.cloneDeep(columnDetails);
setForeignKeyDetails((prevState) => {
return prevState.map((item) => {
return {
...item,
column_names: item.column_names.map((col) => {
return col === previousColumnNames[index] ? e.target.value : col;
}),
};
});
});
});
prevColumns[index].column_name = e.target.value;
setColumns(prevColumns);
}}
value={columnDetails[index].column_name}
type="text"
className="form-control"
placeholder="Enter name"
data-cy={`name-input-field-${columnDetails[index].column_name}`}
// disabled={columns[index]?.constraints_type?.is_primary_key === true}
/>
</div>
<ToolTip
message={
foreignKeyDetails.some((item) => item.column_names[0] === columnDetails[index]?.column_name) ? (
<div>
<span>Foreign key relation</span>
<div className="d-flex align-item-center justify-content-between mt-2 custom-tooltip-style">
<span>
{
foreignKeyDetails.find((item) => item.column_names[0] === columnDetails[index]?.column_name)
?.column_names[0]
}
</span>
<ArrowRight />
<span>{`${
foreignKeyDetails.find((item) => item.column_names[0] === columnDetails[index]?.column_name)
?.referenced_table_name
}.${
foreignKeyDetails.find((item) => item.column_names[0] === columnDetails[index]?.column_name)
?.referenced_column_names[0]
}`}</span>
</div>
</div>
) : columnDetails[index]?.data_type === 'boolean' ? (
'Foreign key relation cannot be created for boolean type column'
) : columnDetails[index]?.data_type === 'serial' ? (
'Foreign key relation cannot be created for serial type column'
) : (
'No foreign key relation'
)
}
placement="top"
tooltipClassName="tootip-table"
>
<div
className={cx({
'foreign-key-relation-active': foreignKeyDetails?.some(
(item) => item.column_names[0] === columnDetails[index]?.column_name
),
'foreign-key-relation': foreignKeyDetails?.some(
(item) => item.column_names[0] !== columnDetails[index]?.column_name
),
})}
>
<ForeignKeyRelation width="13" height="13" />
</div>
</ToolTip>
<ToolTip
message={
columnDetails[index]?.constraints_type?.is_primary_key === true
? 'Primary key data type cannot be modified'
: columnDetails[index]?.data_type === 'timestamp with time zone'
? 'Date with time'
: null
}
placement="top"
tooltipClassName="tootip-table"
style={getToolTipPlacementStyle(index, isEditMode, columnDetails)}
show={
(isEditMode && columnDetails[index]?.constraints_type?.is_primary_key === true ? true : false) ||
columnDetails[index]?.data_type === 'timestamp with time zone'
}
>
<div className="p-0 datatype-dropdown" data-cy="type-dropdown-field">
<Select
width="120px"
height="36px"
//useMenuPortal={false}
value={columnDetails[index]?.dataTypeDetails}
defaultValue={columnDetails[index]?.constraints_type?.is_primary_key === true ? serialDataType : null}
options={dataTypes}
onChange={(value) => {
setColumnSelection((prevState) => ({
...prevState,
index: index,
value: value.value,
}));
const prevColumns = { ...columnDetails };
prevColumns[index].data_type = value ? value.value : null;
isEditMode &&
(prevColumns[index].column_default = value.value === 'serial' ? 'Auto-generated' : null);
prevColumns[index].dataTypeDetails = value;
const columnConstraints = prevColumns[index]?.constraints_type ?? {};
columnConstraints.is_not_null =
value.value === 'serial' ||
(prevColumns[index].constraints_type?.is_primary_key &&
prevColumns[index]?.data_type !== 'serial');
columnConstraints.is_unique = prevColumns[index].constraints_type?.is_primary_key
? true
: value?.value === 'boolean'
? false
: false;
columnConstraints.is_primary_key = value.value === 'boolean' && false;
// columnConstraints.is_primary_key = value.value === 'serial' && true;
prevColumns[index].constraints_type = { ...columnConstraints };
prevColumns[index].column_name = e.target.value;
setColumns(prevColumns);
}}
components={{
Option: CustomSelectOption,
IndicatorSeparator: () => null,
}}
styles={customStyles}
formatOptionLabel={formatOptionLabel}
placeholder={
columnDetails[index].data_type === 'serial' ? (
<div>
<span style={{ marginRight: '5px' }}>
<Serial width="16" />
</span>
<span>{columns[0]?.data_type}</span>
</div>
) : (
<div>
<span style={{ marginRight: '3px' }}>
<SelectIcon width="17" />
</span>
<span style={{ color: '#889096' }}>Select...</span>
</div>
)
}
onMenuOpen={() => {
setColumnSelection((prevState) => ({
...prevState,
index: index,
value: columnDetails[index]?.data_type,
}));
}}
onMenuClose={() => {
setColumnSelection({ index: 0, value: '' });
}}
isDisabled={
isEditMode && columnDetails[index]?.constraints_type?.is_primary_key === true ? true : false
}
value={columnDetails[index].column_name}
type="text"
className="form-control"
placeholder="Enter name"
data-cy={`name-input-field-${columnDetails[index].column_name}`}
// disabled={columns[index]?.constraints_type?.is_primary_key === true}
/>
</div>
</ToolTip>
{checkMatchingColumnNamesInForeignKey(foreignKeyDetails, columnDetails[index].column_name) ? (
<DropDownSelect
buttonClasses="border border-end-1 foreignKeyAcces-container"
showPlaceHolder={true}
options={referenceTableDetails}
darkMode={darkMode}
emptyError={
<div className="dd-select-alert-error m-2 d-flex align-items-center">
<Information />
No data found
</div>
}
loader={
<>
<Skeleton height={18} width={176} className="skeleton" style={{ margin: '15px 50px 7px 7px' }} />
<Skeleton height={18} width={212} className="skeleton" style={{ margin: '7px 14px 7px 7px' }} />
<Skeleton height={18} width={176} className="skeleton" style={{ margin: '7px 50px 15px 7px' }} />
</>
}
isLoading={true}
value={
columnDetails[index].column_default !== null
? { value: columnDetails[index].column_default, label: columnDetails[index].column_default }
: defaultValue[index]
}
// foreignKeyAccessInRowForm={true}
disabled={
(columnDetails[index].data_type === 'serial' &&
columnDetails[index]?.constraints_type?.is_primary_key === true) ||
columnDetails[index].data_type === 'serial'
}
topPlaceHolder={
(columnDetails[index].data_type === 'serial' &&
columnDetails[index]?.constraints_type?.is_primary_key === true) ||
columnDetails[index].data_type === 'serial'
? 'Auto-generated'
: 'Null'
}
onChange={(value) => {
setDefaultValue((prevState) => {
const newState = [...prevState];
newState[index].value = value.value === 'Null' ? null : value.value;
newState[index].label = value.value === 'Null' ? null : value.value;
return newState;
});
const prevColumns = { ...columnDetails };
prevColumns[index].column_default = value.value;
setColumns(prevColumns);
}}
onAdd={true}
addBtnLabel={'Open referenced table'}
foreignKeys={foreignKeyDetails}
setReferencedColumnDetails={setReferencedColumnDetails}
scrollEventForColumnValues={true}
cellColumnName={columnDetails[index].column_name}
columnDataType={columnDetails[index].data_type}
isEditTable={isEditMode}
isCreateTable={!isEditMode}
/>
) : (
<ToolTip
message={
columnDetails[index]?.data_type === 'serial'
? 'Serial data type values cannot be modified'
: columnDetails[index]?.data_type === 'timestamp with time zone' &&
columnDetails[index]?.column_default
? convertDateToTimeZoneFormatted(
columnDetails[index].column_default,
columnDetails[index]?.configurations?.timezone || getLocalTimeZone()
)
foreignKeyDetails.some((item) => item.column_names[0] === columnDetails[index]?.column_name) ? (
<div>
<span>Foreign key relation</span>
<div className="d-flex align-item-center justify-content-between mt-2 custom-tooltip-style">
<span>
{
foreignKeyDetails.find((item) => item.column_names[0] === columnDetails[index]?.column_name)
?.column_names[0]
}
</span>
<ArrowRight />
<span>{`${
foreignKeyDetails.find((item) => item.column_names[0] === columnDetails[index]?.column_name)
?.referenced_table_name
}.${
foreignKeyDetails.find((item) => item.column_names[0] === columnDetails[index]?.column_name)
?.referenced_column_names[0]
}`}</span>
</div>
</div>
) : columnDetails[index]?.data_type === 'boolean' ? (
'Foreign key relation cannot be created for boolean type column'
) : columnDetails[index]?.data_type === 'serial' ? (
'Foreign key relation cannot be created for serial type column'
) : columnDetails[index]?.data_type === 'jsonb' ? (
'JSON cannot have foreign key relation'
) : (
'No foreign key relation'
)
}
placement="top"
tooltipClassName="tootip-table"
>
<div
className={cx({
'foreign-key-relation-active': foreignKeyDetails?.some(
(item) => item.column_names[0] === columnDetails[index]?.column_name
),
'foreign-key-relation': foreignKeyDetails?.some(
(item) => item.column_names[0] !== columnDetails[index]?.column_name
),
})}
>
<ForeignKeyRelation width="13" height="13" />
</div>
</ToolTip>
<ToolTip
message={
columnDetails[index]?.constraints_type?.is_primary_key === true
? 'Primary key data type cannot be modified'
: columnDetails[index]?.data_type === 'timestamp with time zone'
? 'Date with time'
: null
}
placement="top"
tooltipClassName="tootip-table"
style={getToolTipPlacementStyle(index, isEditMode, columnDetails)}
show={
columnDetails[index]?.data_type === 'serial' ||
(columnDetails[index]?.data_type === 'timestamp with time zone' &&
!!columnDetails[index]?.column_default)
(isEditMode && columnDetails[index]?.constraints_type?.is_primary_key === true ? true : false) ||
columnDetails[index]?.data_type === 'timestamp with time zone'
}
>
<div className="m-0" data-cy="column-default-input-field">
{columnDetails[index].data_type === 'timestamp with time zone' ? (
<div style={{ width: '125px' }}>
<DateTimePicker
timestamp={columnDetails[index].column_default}
timezone={columnDetails[index]?.configurations?.timezone || getLocalTimeZone()}
setTimestamp={(value, isTimeSelect = false) => {
const prevColumns = { ...columnDetails };
columnDetails[index].isOpenOnStart =
isTimeSelect && !!prevColumns[index]?.column_default === false && !!value === true;
prevColumns[index].column_default = value;
setColumns(prevColumns);
}}
isOpenOnStart={columnDetails[index]?.isOpenOnStart}
isClearable={true}
isPlaceholderEnabled={true}
// format="dd/MM/yyyy"
/>
</div>
) : (
<input
onChange={(e) => {
e.persist();
const prevColumns = { ...columnDetails };
prevColumns[index].column_default = e.target.value;
setColumns(prevColumns);
}}
value={
columnDetails[index].data_type === 'serial'
? 'Auto-generated'
: // : checkDefaultValue(columnDetails[index].column_default)
// ? null
columnDetails[index].column_default
}
type="text"
className="form-control defaultValue"
data-cy="default-input-field"
placeholder={
(columnDetails[index].data_type === 'serial' &&
columnDetails[index]?.constraints_type?.is_primary_key === true) ||
columnDetails[index].data_type === 'serial'
? 'Auto-generated'
: 'Enter value'
}
disabled={
(columnDetails[index].data_type === 'serial' &&
columnDetails[index]?.constraints_type?.is_primary_key === true) ||
columnDetails[index].data_type === 'serial'
}
/>
)}
<div className="p-0 datatype-dropdown" data-cy="type-dropdown-field">
<Select
width="120px"
height="36px"
//useMenuPortal={false}
value={columnDetails[index]?.dataTypeDetails}
defaultValue={
columnDetails[index]?.constraints_type?.is_primary_key === true ? serialDataType : null
}
options={dataTypes}
onChange={(value) => {
setColumnSelection((prevState) => ({
...prevState,
index: index,
value: value.value,
}));
const prevColumns = { ...columnDetails };
prevColumns[index].data_type = value ? value.value : null;
isEditMode &&
(prevColumns[index].column_default = value.value === 'serial' ? 'Auto-generated' : null);
prevColumns[index].dataTypeDetails = value;
const columnConstraints = prevColumns[index]?.constraints_type ?? {};
columnConstraints.is_not_null =
value.value === 'serial' ||
(prevColumns[index].constraints_type?.is_primary_key &&
prevColumns[index]?.data_type !== 'serial');
columnConstraints.is_unique = prevColumns[index].constraints_type?.is_primary_key
? true
: value?.value === 'boolean'
? false
: false;
columnConstraints.is_primary_key = value.value === 'boolean' && false;
// columnConstraints.is_primary_key = value.value === 'serial' && true;
prevColumns[index].constraints_type = { ...columnConstraints };
setColumns(prevColumns);
}}
components={{
Option: CustomSelectOption,
IndicatorSeparator: () => null,
}}
styles={customStyles}
formatOptionLabel={formatOptionLabel}
placeholder={
columnDetails[index].data_type === 'serial' ? (
<div>
<span style={{ marginRight: '5px' }}>
<Serial width="16" />
</span>
<span>{columns[0]?.data_type}</span>
</div>
) : (
<div>
<span style={{ marginRight: '3px' }}>
<SelectIcon width="17" />
</span>
<span style={{ color: '#889096' }}>Select...</span>
</div>
)
}
onMenuOpen={() => {
setColumnSelection((prevState) => ({
...prevState,
index: index,
value: columnDetails[index]?.data_type,
}));
}}
onMenuClose={() => {
setColumnSelection({ index: 0, value: '' });
}}
isDisabled={
isEditMode && columnDetails[index]?.constraints_type?.is_primary_key === true ? true : false
}
/>
</div>
</ToolTip>
)}
<ToolTip
message={
columnDetails[index]?.data_type === 'boolean'
? 'Boolean type column cannot be a primary key'
: columnDetails[index]?.data_type === 'timestamp with time zone'
? ' Primary key cannot be created with this column type'
: 'There must be atleast one Primary key'
}
placement="top"
tooltipClassName="tootip-table"
show={
(primaryKeyLength === 1 && columnDetails[index]?.constraints_type?.is_primary_key === true) ||
['boolean', 'timestamp with time zone'].includes(columnDetails[index]?.data_type)
}
>
<div className="primary-check">
<IndeterminateCheckbox
checked={
columnDetails[index]?.constraints_type?.is_primary_key &&
['boolean', 'timestamp with time zone'].includes(columnDetails[index]?.data_type)
? false
: columnDetails[index]?.constraints_type?.is_primary_key
? true
: false
{checkMatchingColumnNamesInForeignKey(foreignKeyDetails, columnDetails[index].column_name) ? (
<DropDownSelect
buttonClasses="border border-end-1 foreignKeyAcces-container"
showPlaceHolder={true}
options={referenceTableDetails}
darkMode={darkMode}
emptyError={
<div className="dd-select-alert-error m-2 d-flex align-items-center">
<Information />
No data found
</div>
}
onChange={(e) => {
loader={
<>
<Skeleton height={18} width={176} className="skeleton" style={{ margin: '15px 50px 7px 7px' }} />
<Skeleton height={18} width={212} className="skeleton" style={{ margin: '7px 14px 7px 7px' }} />
<Skeleton height={18} width={176} className="skeleton" style={{ margin: '7px 50px 15px 7px' }} />
</>
}
isLoading={true}
value={
columnDetails[index].column_default !== null
? { value: columnDetails[index].column_default, label: columnDetails[index].column_default }
: defaultValue[index]
}
// foreignKeyAccessInRowForm={true}
disabled={
(columnDetails[index].data_type === 'serial' &&
columnDetails[index]?.constraints_type?.is_primary_key === true) ||
columnDetails[index].data_type === 'serial'
}
topPlaceHolder={
(columnDetails[index].data_type === 'serial' &&
columnDetails[index]?.constraints_type?.is_primary_key === true) ||
columnDetails[index].data_type === 'serial'
? 'Auto-generated'
: 'Null'
}
onChange={(value) => {
setDefaultValue((prevState) => {
const newState = [...prevState];
newState[index].value = value.value === 'Null' ? null : value.value;
newState[index].label = value.value === 'Null' ? null : value.value;
return newState;
});
const prevColumns = { ...columnDetails };
const columnConstraints = prevColumns[index]?.constraints_type ?? {};
// const data = e.target.checked === true ? true : false;
columnConstraints.is_primary_key = e.target.checked;
columnConstraints.is_not_null =
// isEditMode && e.target.checked === false
// ? true
e.target.checked === true ||
prevColumns[index].data_type === 'serial' ||
e.target.checked === false
? true
: false;
columnConstraints.is_unique =
e.target.checked === true ||
prevColumns[index].data_type === 'serial' ||
e.target.checked === false
? true
: false;
prevColumns[index].constraints_type = { ...columnConstraints };
// prevColumns[index].data_type = data === false && '';
prevColumns[index].column_default = value.value;
setColumns(prevColumns);
}}
disabled={
(primaryKeyLength === 1 && columnDetails[index]?.constraints_type?.is_primary_key === true) ||
['boolean', 'timestamp with time zone'].includes(columnDetails[index]?.data_type)
}
onAdd={true}
addBtnLabel={'Open referenced table'}
foreignKeys={foreignKeyDetails}
setReferencedColumnDetails={setReferencedColumnDetails}
scrollEventForColumnValues={true}
cellColumnName={columnDetails[index].column_name}
columnDataType={columnDetails[index].data_type}
isEditTable={isEditMode}
isCreateTable={!isEditMode}
/>
</div>
</ToolTip>
) : (
<ToolTip
message={
columnDetails[index]?.data_type === 'serial'
? 'Serial data type values cannot be modified'
: columnDetails[index]?.data_type === 'timestamp with time zone' &&
columnDetails[index]?.column_default
? convertDateToTimeZoneFormatted(
columnDetails[index].column_default,
columnDetails[index]?.configurations?.timezone || getLocalTimeZone()
)
: null
}
placement="top"
tooltipClassName="tootip-table"
style={getToolTipPlacementStyle(index, isEditMode, columnDetails)}
show={
columnDetails[index]?.data_type === 'serial' ||
(columnDetails[index]?.data_type === 'timestamp with time zone' &&
!!columnDetails[index]?.column_default)
}
>
<div className="m-0" data-cy="column-default-input-field">
{columnDetails[index].data_type === 'timestamp with time zone' && (
<div style={{ width: '125px' }}>
<DateTimePicker
timestamp={columnDetails[index].column_default}
timezone={columnDetails[index]?.configurations?.timezone || getLocalTimeZone()}
setTimestamp={(value, isTimeSelect = false) => {
const prevColumns = { ...columnDetails };
columnDetails[index].isOpenOnStart =
isTimeSelect && !!prevColumns[index]?.column_default === false && !!value === true;
prevColumns[index].column_default = value;
setColumns(prevColumns);
}}
isOpenOnStart={columnDetails[index]?.isOpenOnStart}
isClearable={true}
isPlaceholderEnabled={true}
// format="dd/MM/yyyy"
/>
</div>
)}
{columnDetails[index].data_type === 'jsonb' && (
<div
style={{ width: '125px' }}
onKeyDown={(e) => e.stopPropagation()}
className="tjdb-codehinter-wrapper-drawer-tableSchema"
>
<MeomoizedCodehinter
columnDetails={columnDetails}
index={index}
handleInputError={handleInputError}
setColumns={setColumns}
defaultValue={columnDetails[index].column_default}
/>
</div>
)}
{['jsonb', 'timestamp with time zone'].includes(columnDetails[index].data_type) || (
<input
onChange={(e) => {
e.persist();
const prevColumns = { ...columnDetails };
prevColumns[index].column_default = e.target.value;
setColumns(prevColumns);
}}
value={
columnDetails[index].data_type === 'serial'
? 'Auto-generated'
: // : checkDefaultValue(columnDetails[index].column_default)
// ? null
columnDetails[index].data_type === 'jsonb'
? columnDetails[index].constraints_type?.is_not_null
? JSON.stringify({})
: null
: columnDetails[index].column_default
}
type="text"
className="form-control defaultValue"
data-cy="default-input-field"
placeholder={
(columnDetails[index].data_type === 'serial' &&
columnDetails[index]?.constraints_type?.is_primary_key === true) ||
columnDetails[index].data_type === 'serial'
? 'Auto-generated'
: 'Enter value'
}
disabled={
(columnDetails[index].data_type === 'serial' &&
columnDetails[index]?.constraints_type?.is_primary_key === true) ||
columnDetails[index].data_type === 'serial'
}
/>
)}
</div>
</ToolTip>
)}
<ToolTip
// message="Primary key values cannot be null"
message={
columnDetails[index]?.constraints_type?.is_primary_key === true
? 'Primary key values cannot be null'
: columnDetails[index]?.data_type === 'serial' &&
columnDetails[index]?.constraints_type?.is_primary_key !== true
? 'Serial data type cannot have NULL value'
: null
}
placement="top"
tooltipClassName="tootip-table"
style={getToolTipPlacementStyle(index, isEditMode, columnDetails)}
show={
columnDetails[index]?.constraints_type?.is_primary_key === true ||
(columnDetails[index]?.data_type === 'serial' &&
columnDetails[index]?.constraints_type?.is_primary_key !== true)
}
>
<div className="d-flex not-null-toggle">
<label className={`form-switch`}>
<input
className="form-check-input"
<ToolTip
message={
columnDetails[index]?.data_type === 'boolean'
? 'Boolean type column cannot be a primary key'
: columnDetails[index]?.data_type === 'timestamp with time zone'
? ' Primary key cannot be created with this column type'
: columnDetails[index]?.data_type === 'jsonb'
? 'JSON type column cannot be a primary key'
: 'There must be atleast one Primary key'
}
placement="top"
tooltipClassName="tootip-table"
show={
(primaryKeyLength === 1 && columnDetails[index]?.constraints_type?.is_primary_key === true) ||
['boolean', 'timestamp with time zone', 'jsonb'].includes(columnDetails[index]?.data_type)
}
>
<div className="primary-check">
<IndeterminateCheckbox
checked={
columnDetails[index]?.constraints_type?.is_primary_key &&
['boolean', 'timestamp with time zone', 'jsonb'].includes(columnDetails[index]?.data_type)
? false
: columnDetails[index]?.constraints_type?.is_primary_key
? true
: false
}
onChange={(e) => {
const prevColumns = { ...columnDetails };
const columnConstraints = prevColumns[index]?.constraints_type ?? {};
// const data = e.target.checked === true ? true : false;
columnConstraints.is_primary_key = e.target.checked;
columnConstraints.is_not_null =
// isEditMode && e.target.checked === false
// ? true
e.target.checked === true ||
prevColumns[index].data_type === 'serial' ||
e.target.checked === false
? true
: false;
columnConstraints.is_unique =
e.target.checked === true ||
prevColumns[index].data_type === 'serial' ||
e.target.checked === false
? true
: false;
prevColumns[index].constraints_type = { ...columnConstraints };
// prevColumns[index].data_type = data === false && '';
setColumns(prevColumns);
}}
disabled={
(primaryKeyLength === 1 && columnDetails[index]?.constraints_type?.is_primary_key === true) ||
['boolean', 'timestamp with time zone', 'jsonb'].includes(columnDetails[index]?.data_type)
}
/>
</div>
</ToolTip>
<ToolTip
// message="Primary key values cannot be null"
message={
columnDetails[index]?.constraints_type?.is_primary_key === true
? 'Primary key values cannot be null'
: columnDetails[index]?.data_type === 'serial' &&
columnDetails[index]?.constraints_type?.is_primary_key !== true
? 'Serial data type cannot have NULL value'
: null
}
placement="top"
tooltipClassName="tootip-table"
style={getToolTipPlacementStyle(index, isEditMode, columnDetails)}
show={
columnDetails[index]?.constraints_type?.is_primary_key === true ||
(columnDetails[index]?.data_type === 'serial' &&
columnDetails[index]?.constraints_type?.is_primary_key !== true)
}
>
<div className="d-flex not-null-toggle">
<label className={`form-switch`}>
<input
className="form-check-input"
data-cy={`${String(
columnDetails[index]?.constraints_type?.is_not_null ?? false ? 'NOT NULL' : 'NULL'
)
.toLowerCase()
.replace(/\s+/g, '-')}-checkbox`}
type="checkbox"
checked={columnDetails[index]?.constraints_type?.is_not_null ?? false}
onChange={(e) => {
const prevColumns = { ...columnDetails };
const columnConstraints = prevColumns[index]?.constraints_type ?? {};
columnConstraints.is_not_null = e.target.checked;
prevColumns[index].constraints_type = { ...columnConstraints };
setColumns(prevColumns);
}}
disabled={
columnDetails[index]?.constraints_type?.is_primary_key === true ||
columnDetails[index]?.data_type === 'serial'
}
/>
</label>
<p
data-cy={`${String(
columnDetails[index]?.constraints_type?.is_not_null ?? false ? 'NOT NULL' : 'NULL'
)
.toLowerCase()
.replace(/\s+/g, '-')}-checkbox`}
type="checkbox"
checked={columnDetails[index]?.constraints_type?.is_not_null ?? false}
onChange={(e) => {
const prevColumns = { ...columnDetails };
const columnConstraints = prevColumns[index]?.constraints_type ?? {};
columnConstraints.is_not_null = e.target.checked;
prevColumns[index].constraints_type = { ...columnConstraints };
setColumns(prevColumns);
}}
disabled={
columnDetails[index]?.constraints_type?.is_primary_key === true ||
columnDetails[index]?.data_type === 'serial'
}
/>
</label>
<p
data-cy={`${String(columnDetails[index]?.constraints_type?.is_not_null ?? false ? 'NOT NULL' : 'NULL')
.toLowerCase()
.replace(/\s+/g, '-')}-text`}
className="m-0"
>
<span
className={`${
columnDetails[index]?.constraints_type?.is_primary_key === true ? 'not-null-with-disable' : ''
}`}
.replace(/\s+/g, '-')}-text`}
className="m-0"
>
NOT NULL
</span>
</p>
</div>
</ToolTip>
<div>
<UniqueConstraintPopOver
disabled={false}
onDelete={() => handleDelete(index)}
darkMode={darkMode}
columns={columnDetails}
setColumns={setColumns}
index={index}
isEditMode={isEditMode}
tzDictionary={tzDictionary}
tzOptions={tzOptions}
>
<div className="cursor-pointer">
<MenuIcon />
<span
className={`${
columnDetails[index]?.constraints_type?.is_primary_key === true ? 'not-null-with-disable' : ''
}`}
>
NOT NULL
</span>
</p>
</div>
</UniqueConstraintPopOver>
</ToolTip>
<div>
<UniqueConstraintPopOver
disabled={[].includes(columnDetails[index]?.data_type)}
onDelete={() => handleDelete(index)}
darkMode={darkMode}
columns={columnDetails}
setColumns={setColumns}
index={index}
isEditMode={isEditMode}
tzDictionary={tzDictionary}
tzOptions={tzOptions}
>
<div className="cursor-pointer">
<MenuIcon />
</div>
</UniqueConstraintPopOver>
</div>
</div>
</div>
</div>
))}
);
})}
</div>
);
}

View file

@ -778,6 +778,57 @@
max-width: 100%;
}
.tjdb-codehinter-wrapper-drawer{
.cm-editor{
max-height: 36px !important;
height: 36px !important;
&.cm-focused{
outline: none !important;
background: var(--indigo2) !important;
border: 1px solid var(--indigo9) !important;
}
.cm-content{
display: flex;
align-items: center;
font-size: 12px;
}
}
.tjdb-hinter-error{
.cm-focused{
outline: none !important;
border:1px solid red !important;
}
}
}
// added this to border when codehinter is open in the portal, if any error is there
.tjdb-hinter-error{
.cm-focused{
outline: none !important;
border:1px solid red !important;
}
}
.tjdb-codehinter-wrapper-drawer-tableSchema{
.cm-editor{
max-height: 36px !important;
min-height: 36px !important;
&.cm-focused{
outline: none !important;
border: 1px solid !important;
border-color: #90b5e2 !important;
}
.cm-content{
display: flex;
align-items: center;
font-size: 12px;
}
}
.tjdb-hinter-error{
.cm-editor{
outline: none !important;
border:1px solid red !important;
}
}
}
.width-lg{
width: 181.5px;
}

View file

@ -0,0 +1,3 @@
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 1.85363V0.869629H3.91C3.597 0.869629 3.294 0.931629 3.001 1.05463C2.70938 1.17688 2.44569 1.3572 2.226 1.58463C2.0122 1.79951 1.84445 2.05573 1.733 2.33763V2.33863C1.63379 2.60631 1.56731 2.88499 1.535 3.16863V3.17063C1.50656 3.45736 1.49854 3.74576 1.511 4.03363C1.523 4.32363 1.529 4.61363 1.529 4.90263C1.529 5.10563 1.489 5.29563 1.412 5.47463C1.26524 5.82396 0.992032 6.10603 0.647 6.26263C0.47106 6.33947 0.280986 6.37864 0.089 6.37763H0V7.36163H0.0899999C0.285 7.36163 0.47 7.40163 0.646 7.48263L0.647 7.48363C0.825 7.56163 0.976 7.66763 1.102 7.80163L1.104 7.80363C1.234 7.93363 1.337 8.08863 1.411 8.26863L1.412 8.27063C1.49 8.45063 1.529 8.63863 1.529 8.83663C1.529 9.12663 1.523 9.41663 1.511 9.70563C1.499 10.0016 1.507 10.2906 1.535 10.5756C1.568 10.8586 1.634 11.1346 1.732 11.4006C1.838 11.6736 2.003 11.9256 2.226 12.1546C2.449 12.3846 2.708 12.5616 3.001 12.6846C3.294 12.8076 3.597 12.8696 3.911 12.8696H4V11.8856H3.91C3.71 11.8856 3.523 11.8476 3.347 11.7706C3.17724 11.6913 3.02259 11.583 2.89 11.4506C2.76128 11.3136 2.65679 11.1557 2.581 10.9836C2.507 10.8036 2.471 10.6136 2.471 10.4106C2.471 10.1826 2.474 9.95763 2.482 9.73863C2.49 9.51063 2.49 9.28863 2.482 9.07363C2.47848 8.85928 2.4601 8.64544 2.427 8.43363C2.39519 8.22472 2.3388 8.0203 2.259 7.82463C2.10336 7.446 1.84881 7.11616 1.522 6.86963C1.84918 6.62323 2.10408 6.29338 2.26 5.91463C2.34 5.72263 2.395 5.52163 2.428 5.31263C2.461 5.10263 2.479 4.88963 2.483 4.67263C2.491 4.45263 2.491 4.23063 2.483 4.00663C2.475 3.78263 2.471 3.55663 2.471 3.32863C2.46926 3.04201 2.55135 2.76114 2.70717 2.52058C2.86299 2.28001 3.08573 2.09025 3.348 1.97463C3.52458 1.89366 3.71675 1.85236 3.911 1.85363H4ZM8 11.8856V12.8696H8.09C8.403 12.8696 8.706 12.8076 8.999 12.6846C9.292 12.5616 9.551 12.3846 9.774 12.1546C9.997 11.9246 10.162 11.6746 10.267 11.4016C10.367 11.1356 10.432 10.8576 10.465 10.5706V10.5686C10.493 10.2886 10.501 10.0016 10.489 9.70563C10.477 9.41563 10.471 9.12563 10.471 8.83663C10.471 8.63363 10.511 8.44363 10.588 8.26463V8.26363C10.7346 7.91419 11.0079 7.63307 11.353 7.47663C11.529 7.3999 11.719 7.36073 11.911 7.36163H12V6.37763H11.91C11.714 6.37763 11.529 6.33763 11.353 6.25663L11.352 6.25563C11.1803 6.18164 11.0255 6.07344 10.897 5.93763L10.895 5.93563C10.7629 5.80251 10.6585 5.64444 10.588 5.47063V5.46863C10.5099 5.29018 10.4697 5.09744 10.47 4.90263C10.47 4.61263 10.476 4.32263 10.488 4.03363C10.5005 3.74343 10.4925 3.4527 10.464 3.16363C10.4316 2.88206 10.3654 2.60441 10.267 2.33863V2.33763C10.1553 2.05564 9.98716 1.79942 9.773 1.58463C9.55329 1.35723 9.28961 1.1769 8.998 1.05463C8.71019 0.932397 8.40069 0.869476 8.088 0.869629H8V1.85363H8.09C8.29 1.85363 8.477 1.89163 8.652 1.96863C8.826 2.05063 8.978 2.15663 9.109 2.28863C9.236 2.42263 9.339 2.57863 9.418 2.75563C9.492 2.93563 9.528 3.12563 9.528 3.32863C9.528 3.55663 9.525 3.78063 9.517 4.00063C9.509 4.22863 9.509 4.45063 9.517 4.66563C9.521 4.88763 9.539 5.10063 9.572 5.30563C9.605 5.51963 9.661 5.72163 9.74 5.91463C9.89596 6.29335 10.1509 6.62319 10.478 6.86963C10.1509 7.11607 9.89596 7.44591 9.74 7.82463C9.66079 8.01808 9.60441 8.22011 9.572 8.42663C9.539 8.63663 9.521 8.84963 9.517 9.06663C9.50892 9.28856 9.50892 9.5107 9.517 9.73263C9.525 9.95663 9.529 10.1826 9.529 10.4106C9.53061 10.6972 9.44847 10.978 9.29266 11.2186C9.13686 11.4591 8.91419 11.6489 8.652 11.7646C8.47542 11.8456 8.28325 11.8869 8.089 11.8856H8Z" fill="#889096"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1,3 @@
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 1.85363V0.869629H3.91C3.597 0.869629 3.294 0.931629 3.001 1.05463C2.70938 1.17688 2.44569 1.3572 2.226 1.58463C2.0122 1.79951 1.84445 2.05573 1.733 2.33763V2.33863C1.63379 2.60631 1.56731 2.88499 1.535 3.16863V3.17063C1.50656 3.45736 1.49854 3.74576 1.511 4.03363C1.523 4.32363 1.529 4.61363 1.529 4.90263C1.529 5.10563 1.489 5.29563 1.412 5.47463C1.26524 5.82396 0.992032 6.10603 0.647 6.26263C0.47106 6.33947 0.280986 6.37864 0.089 6.37763H0V7.36163H0.0899999C0.285 7.36163 0.47 7.40163 0.646 7.48263L0.647 7.48363C0.825 7.56163 0.976 7.66763 1.102 7.80163L1.104 7.80363C1.234 7.93363 1.337 8.08863 1.411 8.26863L1.412 8.27063C1.49 8.45063 1.529 8.63863 1.529 8.83663C1.529 9.12663 1.523 9.41663 1.511 9.70563C1.499 10.0016 1.507 10.2906 1.535 10.5756C1.568 10.8586 1.634 11.1346 1.732 11.4006C1.838 11.6736 2.003 11.9256 2.226 12.1546C2.449 12.3846 2.708 12.5616 3.001 12.6846C3.294 12.8076 3.597 12.8696 3.911 12.8696H4V11.8856H3.91C3.71 11.8856 3.523 11.8476 3.347 11.7706C3.17724 11.6913 3.02259 11.583 2.89 11.4506C2.76128 11.3136 2.65679 11.1557 2.581 10.9836C2.507 10.8036 2.471 10.6136 2.471 10.4106C2.471 10.1826 2.474 9.95763 2.482 9.73863C2.49 9.51063 2.49 9.28863 2.482 9.07363C2.47848 8.85928 2.4601 8.64544 2.427 8.43363C2.39519 8.22472 2.3388 8.0203 2.259 7.82463C2.10336 7.446 1.84881 7.11616 1.522 6.86963C1.84918 6.62323 2.10408 6.29338 2.26 5.91463C2.34 5.72263 2.395 5.52163 2.428 5.31263C2.461 5.10263 2.479 4.88963 2.483 4.67263C2.491 4.45263 2.491 4.23063 2.483 4.00663C2.475 3.78263 2.471 3.55663 2.471 3.32863C2.46926 3.04201 2.55135 2.76114 2.70717 2.52058C2.86299 2.28001 3.08573 2.09025 3.348 1.97463C3.52458 1.89366 3.71675 1.85236 3.911 1.85363H4ZM8 11.8856V12.8696H8.09C8.403 12.8696 8.706 12.8076 8.999 12.6846C9.292 12.5616 9.551 12.3846 9.774 12.1546C9.997 11.9246 10.162 11.6746 10.267 11.4016C10.367 11.1356 10.432 10.8576 10.465 10.5706V10.5686C10.493 10.2886 10.501 10.0016 10.489 9.70563C10.477 9.41563 10.471 9.12563 10.471 8.83663C10.471 8.63363 10.511 8.44363 10.588 8.26463V8.26363C10.7346 7.91419 11.0079 7.63307 11.353 7.47663C11.529 7.3999 11.719 7.36073 11.911 7.36163H12V6.37763H11.91C11.714 6.37763 11.529 6.33763 11.353 6.25663L11.352 6.25563C11.1803 6.18164 11.0255 6.07344 10.897 5.93763L10.895 5.93563C10.7629 5.80251 10.6585 5.64444 10.588 5.47063V5.46863C10.5099 5.29018 10.4697 5.09744 10.47 4.90263C10.47 4.61263 10.476 4.32263 10.488 4.03363C10.5005 3.74343 10.4925 3.4527 10.464 3.16363C10.4316 2.88206 10.3654 2.60441 10.267 2.33863V2.33763C10.1553 2.05564 9.98716 1.79942 9.773 1.58463C9.55329 1.35723 9.28961 1.1769 8.998 1.05463C8.71019 0.932397 8.40069 0.869476 8.088 0.869629H8V1.85363H8.09C8.29 1.85363 8.477 1.89163 8.652 1.96863C8.826 2.05063 8.978 2.15663 9.109 2.28863C9.236 2.42263 9.339 2.57863 9.418 2.75563C9.492 2.93563 9.528 3.12563 9.528 3.32863C9.528 3.55663 9.525 3.78063 9.517 4.00063C9.509 4.22863 9.509 4.45063 9.517 4.66563C9.521 4.88763 9.539 5.10063 9.572 5.30563C9.605 5.51963 9.661 5.72163 9.74 5.91463C9.89596 6.29335 10.1509 6.62319 10.478 6.86963C10.1509 7.11607 9.89596 7.44591 9.74 7.82463C9.66079 8.01808 9.60441 8.22011 9.572 8.42663C9.539 8.63663 9.521 8.84963 9.517 9.06663C9.50892 9.28856 9.50892 9.5107 9.517 9.73263C9.525 9.95663 9.529 10.1826 9.529 10.4106C9.53061 10.6972 9.44847 10.978 9.29266 11.2186C9.13686 11.4591 8.91419 11.6489 8.652 11.7646C8.47542 11.8456 8.28325 11.8869 8.089 11.8856H8Z" fill="#889096"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1,265 @@
import React, { useRef, useState } from 'react';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { resolveReferences } from '@/AppBuilder/CodeEditor/utils';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import cx from 'classnames';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import { Popover } from 'react-bootstrap';
import _ from 'lodash';
const transformvalue = (value = '') => {
if (typeof value !== 'string') {
return JSON.stringify(value);
}
return value;
};
export const CellHinterWrapper = ({
isNotNull,
defaultValue,
selectedValue,
setSelectedValue,
saveFunction,
isEditCell,
columnDetails,
close,
closePopover,
show,
previousCellValue,
}) => {
const initialValueRef = useRef(selectedValue);
const defaultValueRef = useRef(defaultValue);
const [shouldUpdateToDefaultVal, setShouldUpdateDefaultVal] = useState(false);
const [shouldUpdateToNullVal, setShouldUpdateNullVal] = useState(false);
const [disabledSaveButton, setDisabledSaveButton] = useState(false);
const handleInputError = (bool = false) => {
setDisabledSaveButton(bool);
};
const handleKeyDown = (e) => {
e.stopPropagation();
if (e.key === 'Escape') {
const event = new Event('click', { bubbles: true, cancelable: true });
document.body.dispatchEvent(event);
}
};
const darkMode = localStorage.getItem('darkMode') === 'true';
const tranformedValue = (rawValue) => {
if (!rawValue) {
return JSON.stringify(rawValue);
}
const [_, __, resolvedValue] = resolveReferences(`{{${rawValue}}}`);
return resolvedValue;
};
const SaveChangesSection = () => {
const handleNullToggle = (value) => {
setShouldUpdateNullVal(false);
if (value) {
setSelectedValue(null);
shouldUpdateToDefaultVal && setShouldUpdateDefaultVal(false);
} else {
setSelectedValue(tranformedValue(previousCellValue));
}
setShouldUpdateNullVal(value);
};
const handleDefaultToggle = (value) => {
setShouldUpdateDefaultVal(false);
if (value) {
setSelectedValue(defaultValue);
shouldUpdateToNullVal && setShouldUpdateNullVal(false);
defaultValueRef.current = defaultValue;
} else {
const val = tranformedValue(previousCellValue);
defaultValueRef.current = val;
setSelectedValue(val);
}
setShouldUpdateDefaultVal(true);
};
return (
<div className="d-flex justify-content-between align-items-center">
<div className="d-flex flex-column align-items-start gap-1">
{
<div className="d-flex align-items-center gap-1">
<div className={`fw-500 tjdbCellMenuShortcutsInfo`} id="enterbutton">
<SolidIcon name="enterbutton" />
</div>
<div className={`fw-400 tjdbCellMenuShortcutsText`}>Press Enter to go on next line</div>
</div>
}
<div className="d-flex align-items-center gap-1">
<div className={`fw-500 tjdbCellMenuShortcutsInfo`} id="escbutton">
Esc
</div>
<div className={`fw-400 tjdbCellMenuShortcutsText`}>Discard Changes</div>
</div>
</div>
<div className="d-flex flex-column align-items-end gap-1">
{isNotNull === false && (
<div className="d-flex align-items-center gap-2">
<div className="d-flex flex-column">
<span style={{ width: 'auto' }} className="fw-400 fs-12">
Set to null
</span>
</div>
<div>
<label className={`form-switch`}>
<input
className="form-check-input"
type="checkbox"
checked={selectedValue === null}
onChange={(e) => handleNullToggle(e.target.checked)}
/>
</label>
</div>
</div>
)}
{defaultValue !== null && (
<div className="d-flex align-items-center gap-2">
<div className="d-flex flex-column">
<span style={{ width: 'auto' }} className="fw-400 fs-12">
Set to default
</span>
</div>
<div>
<label className={`form-switch`}>
<input
className="form-check-input"
type="checkbox"
checked={_.isEqual(selectedValue, defaultValue)}
onChange={(e) => handleDefaultToggle(e.target.checked)}
/>
</label>
</div>
</div>
)}
</div>
</div>
);
};
const handleCancel = () => {
const event = new Event('click', { bubbles: true, cancelable: true });
document.body.dispatchEvent(event);
setShouldUpdateDefaultVal(false);
setShouldUpdateNullVal(false);
};
const handleSave = (e) => {
if (e) {
e.stopPropagation();
}
saveFunction(selectedValue);
const event = new Event('click', { bubbles: true, cancelable: true });
document.body.dispatchEvent(event);
setShouldUpdateDefaultVal(false);
setShouldUpdateNullVal(false);
defaultValueRef.current = defaultValue;
};
const SaveChangesFooter = () => {
return (
<div className={cx('d-flex align-items-center', 'justify-content-end')}>
<div className="d-flex" style={{ gap: '8px' }}>
<ButtonSolid onClick={handleCancel} variant="tertiary" size="sm" className="fs-12 p-2">
Cancel
</ButtonSolid>
<ButtonSolid
onClick={(e) => handleSave(e)}
disabled={disabledSaveButton}
variant="primary"
size="sm"
className="fs-12 p-2"
>
Save
</ButtonSolid>
</div>
</div>
);
};
const popover = (
<Popover className={`${darkMode && 'dark-theme'} tjdb-table-cell-edit-popover jsonb-popover`}>
{disabledSaveButton && (
<Popover.Header className="tjdb-cell-hinter-invalid-syntax-header">
<div className="d-flex align-items-center">
<span className="d-flex mx-2">
{' '}
<SolidIcon name="warning" width="16px" fill={'var(--tomato9)'} />
</span>
<span>Invalid JSON syntax</span>
</div>
</Popover.Header>
)}
<Popover.Body className={`${darkMode && 'dark-theme'}`} onClick={(e) => e.stopPropagation()}>
<div className={`d-flex flex-column gap-3`}>
<SaveChangesSection />
<SaveChangesFooter />
</div>
</Popover.Body>
</Popover>
);
const customFooter = () => {
return (
<div
className={`tjdb-dashboard-codehinter custom-footer ${darkMode && 'dark-theme'} `}
tabIndex="0"
onClick={(e) => e.stopPropagation()}
>
{disabledSaveButton && (
<div className="tjdb-cell-hinter-invalid-syntax-header">
<div className="d-flex align-items-center">
<span className="d-flex mx-2">
{' '}
<SolidIcon name="warning" width="16px" fill={'var(--tomato9)'} />
</span>
<span>Invalid JSON syntax</span>
</div>
</div>
)}
<div className="main-body d-flex flex-column gap-3">
<SaveChangesSection />
<SaveChangesFooter />
</div>
</div>
);
};
return (
<OverlayTrigger trigger="click" placement="bottom-start" rootclose overlay={popover}>
<div className="tjdb-dashboard-codehinter-wrapper-cell" onKeyDown={handleKeyDown}>
<CodeHinter
type="tjdbHinter"
inEditor={false}
initialValue={initialValueRef.current ? transformvalue(initialValueRef.current) : ''}
lang="javascript"
onChange={(value) => {
const [_, __, resolvedValue] = resolveReferences(`{{${value}}}`);
setSelectedValue(resolvedValue);
}}
enablePreview={false}
footerComponent={customFooter}
componentName={`{} ${columnDetails.Header}`}
errorCallback={handleInputError}
defaultValue={defaultValueRef.current}
reset={shouldUpdateToDefaultVal}
shouldUpdateToNullVal={shouldUpdateToNullVal}
columnName={columnDetails?.Header}
/>
</div>
</OverlayTrigger>
);
};

View file

@ -14,6 +14,7 @@ import Skeleton from 'react-loading-skeleton';
import DateTimePicker from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker';
import { TooljetDatabaseContext } from '@/TooljetDatabase';
import { getLocalTimeZone } from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/util';
import { CellHinterWrapper } from './CellHinterWrapper';
export const CellEditMenu = ({
darkMode = false,
@ -335,9 +336,7 @@ export const CellEditMenu = ({
)}
</div>
)}
{!isBoolean && <SaveChangesSection />}
{/* Footer */}
<SaveChangesFooter />
</div>
@ -346,7 +345,14 @@ export const CellEditMenu = ({
);
return (
<OverlayTrigger show={show} trigger="click" placement="bottom-start" rootclose overlay={popover} defaultShow>
<OverlayTrigger
show={dataType === 'jsonb' ? false : show}
trigger="click"
placement="bottom-start"
rootclose
overlay={popover}
defaultShow
>
{isForeignKey ? (
<DropDownSelect
buttonClasses="border border-end-1 foreignKeyAcces-container"
@ -412,7 +418,24 @@ export const CellEditMenu = ({
isEditCell={true}
timezone={getConfigurationProperty(columnDetails?.Header, 'timezone', getLocalTimeZone())}
/>
) : dataType === 'jsonb' ? (
<div>
<CellHinterWrapper
isNotNull={columnDetails?.constraints_type.is_not_null}
defaultValue={columnDetails?.column_default}
selectedValue={selectedValue}
setSelectedValue={setSelectedValue}
saveFunction={saveFunction}
isEditCell={true}
columnDetails={columnDetails}
close={close}
closePopover={closePopover}
show={show}
previousCellValue={previousCellValue}
/>
</div>
) : (
// </div>
children
)}
</OverlayTrigger>

View file

@ -3,6 +3,9 @@
height: auto;
margin-top: 2px;
inset: 0px auto auto -9px !important;
&.jsonb-popover{
min-width: 350px;
}
.tjdb-bool-cell-menu-badge-default {
padding: 2px 12px;
@ -67,4 +70,54 @@
}
}
.portal-container:has(.tjdb-dashboard-codehinter){
.cm-editor{
border-radius: 0 !important;
}
.tjdb-dashboard-codehinter{
border: 1px solid var(--borders-disabled-on-white, #E4E7EB) ;
border-width: 0px 1px 1px 1px !important;
border-radius: 0px 0px 5px 5px;
}
}
.tjdb-dashboard-codehinter-wrapper-cell{
.cm-editor{
max-height: 32px;
border: 0;
}
}
.tjdb-dashboard-codehinter-wrapper-cell{
// display: none !important;
.footer-component{
display: none !important;
}
.tjdb-hinter-error{
.cm-theme{
border: 0 !important;
}
}
}
/* Apply a border to .tjdb-selected-cell if it contains both required nested elements */
.tjdb-selected-cell:has(.tjdb-dashboard-codehinter-wrapper-cell .tjdb-hinter-error) {
border: 1px solid red !important; /* Add your desired border style */
}
.tjdb-dashboard-codehinter.custom-footer {
background-color: var(--base);
border: 1px solid var(--slate5);
}
.tjdb-table-cell-edit-popover,.tjdb-dashboard-codehinter.custom-footer{
.tjdb-cell-hinter-invalid-syntax-header{
background-color: var(--tomato3) !important;
color: var(--tomato9) !important;
font-size: 12px !important;
padding: 5px;
}
.main-body{
padding: 10px;
}
}

View file

@ -126,6 +126,8 @@ export const UniqueConstraintPopOver = ({
? 'Boolean data type cannot be unique'
: columns[index]?.data_type === 'timestamp with time zone'
? 'Unique constraint cannot be added to this column type'
: columns[index]?.data_type === 'jsonb'
? 'JSON cannot cannot have unique constraint'
: null
}
placement="top"
@ -133,7 +135,7 @@ export const UniqueConstraintPopOver = ({
style={toolTipPlacementStyle}
show={
columns[index]?.constraints_type?.is_primary_key === true ||
['boolean', 'timestamp with time zone'].includes(columns[index]?.data_type)
['boolean', 'jsonb', 'timestamp with time zone'].includes(columns[index]?.data_type)
}
>
<div className="d-flex not-null-toggle">
@ -163,7 +165,7 @@ export const UniqueConstraintPopOver = ({
}}
disabled={
columns[index]?.constraints_type?.is_primary_key === true ||
['boolean', 'timestamp with time zone'].includes(columns[index]?.data_type)
['boolean', 'jsonb', 'timestamp with time zone'].includes(columns[index]?.data_type)
}
/>
</label>

View file

@ -149,7 +149,7 @@ const Header = ({
<div className="card border-0">
<div className="card-body tj-db-operations-header">
<div className="row align-items-center">
<div className="col-8 align-items-center p-3 gap-1">
<div className="col-8 align-items-center gap-1" style={{ padding: '0 16px' }}>
<>
{(isDirectRowExpand || Object.keys(selectedRowIds).length === 0) && (
<>

View file

@ -419,10 +419,7 @@ const Table = ({ collapseSidebar }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isEditRowDrawerOpen]);
const tableData = React.useMemo(
() => (loading ? Array(10).fill({}) : selectedTableData),
[loading, selectedTableData]
);
const [tableColumnTypes, setTableColumnTypes] = React.useState({});
const tableColumns = React.useMemo(() => {
if (loading) {
@ -433,17 +430,39 @@ const Table = ({ collapseSidebar }) => {
} else {
const primaryKeyArray = [];
const nonPrimaryKeyArray = [];
const updatedColumnTypes = {};
columns.forEach((column) => {
if (column.constraints_type.is_primary_key) {
primaryKeyArray.push({ ...column });
} else {
nonPrimaryKeyArray.push({ ...column });
}
updatedColumnTypes[column.accessor] = column.dataType;
});
setTableColumnTypes(updatedColumnTypes);
return [...primaryKeyArray, ...nonPrimaryKeyArray];
}
}, [loading, columns]);
const tableData = React.useMemo(
() =>
loading
? Array(10).fill({})
: selectedTableData.map((data) => {
return Object.entries(data).reduce((accumulator, [key, value]) => {
if (tableColumnTypes?.[key] === 'jsonb' && value !== null) {
accumulator[key] = JSON.stringify(value);
} else {
accumulator[key] = value;
}
return accumulator;
}, {});
}),
[loading, selectedTableData]
);
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable(
{
columns: tableColumns,
@ -906,7 +925,7 @@ const Table = ({ collapseSidebar }) => {
setEditPopover(false);
previousValue === null ? setNullValue(true) : setNullValue(false);
setCellVal(previousValue);
document.getElementById('edit-input-blur').blur();
document.getElementById('edit-input-blur')?.blur();
};
function shouldOpenCellEditMenu(cellColumnIndex) {
@ -1365,7 +1384,11 @@ const Table = ({ collapseSidebar }) => {
cell.value,
getConfigurationProperty(cell.column.Header, 'timezone', getLocalTimeZone())
)
: cell.value,
: cell.column.dataType === 'jsonb' &&
typeof cell?.value !== 'string' &&
cell?.value !== null
? JSON.stringify(cell?.value)
: cell?.value,
index
)}
placement="bottom"
@ -1403,10 +1426,16 @@ const Table = ({ collapseSidebar }) => {
cellClick.cellIndex === index ? (
<CellEditMenu
show={shouldOpenCellEditMenu(index) ? editPopover : false}
close={() => closeEditPopover(cell.value, index)}
close={() => {
closeEditPopover(cell.value, index);
}}
columnDetails={headerGroups[0].headers[index]}
saveFunction={(newValue) => {
handleToggleCellEdit(newValue, row.values.id, index, rIndex, false, cell.value);
// if (cell.column?.dataType === 'jsonb') {
// const event = new Event('click', { bubbles: true, cancelable: true });
// document.body.dispatchEvent(event);
// }
}}
setCellValue={setCellVal}
cellValue={cellVal}
@ -1498,6 +1527,16 @@ const Table = ({ collapseSidebar }) => {
</ToolTip> */}
</div>
) : (
// : cell.column?.dataType === 'jsonb' ? (
// <CodehinterWrapper
// cellVal={cellVal}
// shouldOpenCellEditMenu={shouldOpenCellEditMenu}
// setCellVal={setCellVal}
// headerGroups={headerGroups}
// index={index}
// setDefaultValue={setDefaultValue}
// />
// )
<div className="d-flex align-items-center justify-content-between">
<input
autoComplete="off"
@ -1539,6 +1578,8 @@ const Table = ({ collapseSidebar }) => {
<>
{cell.value === null ? (
<span className="cell-text-null">Null</span>
) : cell.column.dataType === 'jsonb' ? (
`{...}`
) : cell.column.dataType === 'boolean' ? (
// <div className="d-flex align-items-center justify-content-between">
<div className="row" style={{ width: '33px' }}>

View file

@ -168,6 +168,19 @@
width: 80%;
}
}
&:has(.tjdb-dashboard-codehinter-wrapper-cell){
padding: 0 !important;
}
.tjdb-dashboard-codehinter-wrapper-cell{
.cm-editor{
border: 0 !important;
}
.codehinter-input.focused .cm-editor{
border: 0 !important;
}
}
}
.tjdb-selected-cell {

View file

@ -8,6 +8,7 @@ import Serial from './Icons/Serial.svg';
import ArrowRight from './Icons/ArrowRight.svg';
import RightFlex from './Icons/Right-flex.svg';
import Datetime from './Icons/Datetime.svg';
import Jsonb from './Icons/Jsonb.svg';
export const dataTypes = [
{
@ -16,6 +17,12 @@ export const dataTypes = [
icon: <CharacterVar width="16" height="16" />,
value: 'character varying',
},
{
name: 'JSON data type',
label: 'jsonb',
icon: <Jsonb width="16" height="16" />,
value: 'jsonb',
},
{ name: 'Integers up to 4 bytes', label: 'int', icon: <Integer width="16" height="16" />, value: 'integer' },
{ name: 'Integers up to 8 bytes', label: 'bigint', icon: <BigInt width="16" height="16" />, value: 'bigint' },
{ name: 'Decimal numbers', label: 'float', icon: <Float width="16" height="16" />, value: 'double precision' },
@ -32,6 +39,7 @@ export const dataTypes = [
icon: <Datetime width="16" height="16" />,
value: 'timestamp with time zone',
},
// { name: 'Binary JSON data', label: 'jsonb', icon: <Jsonb width="16" height="16" />, value: 'jsonb' },
];
export const serialDataType = [
@ -312,7 +320,7 @@ export default function tjdbDropdownStyles(
...base,
width: dropdownContainerWidth,
background: darkMode ? 'rgb(31,40,55)' : 'white',
zIndex: 10,
zIndex: 10001,
}),
singleValue: (provided) => ({
...provided,
@ -348,6 +356,8 @@ export const renderDatatypeIcon = (type) => {
return <Serial width="18" height="14" className="tjdb-column-header-name" />;
case 'timestamp with time zone':
return <Datetime width="18" height="18" className="tjdb-column-header-name" />;
case 'jsonb':
return <Jsonb width="18" height="18" className="tjdb-column-header-name" />;
default:
return type;
}

View file

@ -35,7 +35,7 @@ const Portal = ({ children, ...restProps }) => {
};
return (
<Portal.Container {...restProps}>
<Portal.Container {...restProps} componentName={name?.replace(/(\S)\s+(\S)/g, '$1$2')}>
<div className={className}>
<Portal.Modal
handleClose={handleClose}

View file

@ -8,7 +8,9 @@ export function ReactPortal({ children, parent, className, componentName }) {
const target = parent && parent.appendChild ? parent : document.body;
const classList = ['portal-container', componentName];
if (className) className.split(' ').forEach((item) => classList.push(item));
classList.forEach((item) => el.classList.add(item));
target.appendChild(el);
return () => {
el.remove();

View file

@ -7644,13 +7644,29 @@ fieldset:disabled .btn {
}
.rounded {
border-radius: 4px !important
border-radius: 4px ;
}
.rounded-0 {
border-radius: 0 !important
}
.rounded-top-left{
border-top-left-radius: 4px;
}
.rounded-top-left-0{
border-top-left-radius: 0 !important;
}
.rounded-top-right-0{
border-top-right-radius: 0 !important;
}
.rounded-bottom-left-0{
border-bottom-left-radius: 0 !important;
}
.rounded-bottom-right-0{
border-bottom-right-radius: 0 !important;
}
.rounded-1 {
border-radius: 2px !important
}

View file

@ -0,0 +1,24 @@
import React from 'react';
const BigIntCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
className={className}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
opacity="0.5"
d="M9.10784 16.1376L8.29482 19.3768C8.23684 19.6213 8.11056 19.8213 7.91599 19.9765C7.72144 20.1318 7.49355 20.2094 7.23232 20.2094C6.8718 20.2094 6.57759 20.0665 6.34969 19.7806C6.12179 19.4947 6.05549 19.1799 6.15079 18.836L6.82687 16.1376H4.24534C3.86818 16.1376 3.56564 15.9915 3.33774 15.6993C3.10984 15.4071 3.04353 15.0808 3.13882 14.7202C3.1968 14.459 3.32941 14.2508 3.53664 14.0955C3.74389 13.9403 3.98013 13.8626 4.24534 13.8626H7.39264L8.32687 10.1376H5.74534C5.36818 10.1376 5.06564 9.99154 4.83774 9.69932C4.60984 9.40712 4.54353 9.08077 4.63882 8.72025C4.6968 8.45902 4.82941 8.25077 5.03664 8.09552C5.24389 7.94027 5.48013 7.86265 5.74534 7.86265H8.89264L9.70567 4.62352C9.76365 4.37896 9.88993 4.18004 10.0845 4.02677C10.279 3.87351 10.5069 3.79688 10.7682 3.79688C11.1287 3.79688 11.4229 3.93982 11.6508 4.2257C11.8787 4.51157 11.945 4.82642 11.8497 5.17025L11.1736 7.86265H14.8926L15.7057 4.62352C15.7637 4.37896 15.8899 4.18004 16.0845 4.02677C16.279 3.87351 16.5069 3.79688 16.7682 3.79688C17.1287 3.79688 17.4229 3.93982 17.6508 4.2257C17.8787 4.51157 17.945 4.82642 17.8497 5.17025L17.1736 7.86265H19.7551C20.1323 7.86265 20.4348 8.00876 20.6627 8.30098C20.8906 8.59317 20.957 8.91953 20.8617 9.28005C20.8037 9.54128 20.6711 9.74952 20.4638 9.90477C20.2566 10.06 20.0204 10.1376 19.7551 10.1376H16.6078L15.6736 13.8626H18.2551C18.6323 13.8626 18.9348 14.0088 19.1627 14.301C19.3906 14.5932 19.457 14.9195 19.3617 15.2801C19.3037 15.5413 19.1711 15.7495 18.9638 15.9048C18.7566 16.06 18.5204 16.1376 18.2551 16.1376H15.1078L14.2948 19.3768C14.2368 19.6213 14.1106 19.8213 13.916 19.9765C13.7214 20.1318 13.4936 20.2094 13.2323 20.2094C12.8718 20.2094 12.5776 20.0665 12.3497 19.7806C12.1218 19.4947 12.0555 19.1799 12.1508 18.836L12.8269 16.1376H9.10784ZM9.67362 13.8626H13.3926L14.3269 10.1376H10.6078L9.67362 13.8626Z"
fill="#889096"
/>
<path
d="M19.4654 20.4652L19.4654 15.8208L20.1223 16.4778C20.33 16.6735 20.5769 16.7744 20.863 16.7803C21.149 16.7863 21.3959 16.6855 21.6036 16.4778C21.8113 16.2701 21.9161 16.0253 21.9179 15.7433C21.9198 15.4614 21.8169 15.2166 21.6092 15.0088L19.1764 12.5761C18.9612 12.3608 18.7092 12.2532 18.4204 12.2532C18.1316 12.2532 17.8789 12.3608 17.6623 12.5761L15.2295 15.0089C15.0218 15.2166 14.918 15.4614 14.918 15.7433C14.918 16.0253 15.0218 16.2701 15.2295 16.4778C15.4372 16.6855 15.685 16.7863 15.973 16.7803C16.2609 16.7744 16.5087 16.6735 16.7164 16.4778L17.3734 15.8208L17.3734 20.4652C17.3734 20.7591 17.4742 21.0069 17.6759 21.2087C17.8776 21.4104 18.1254 21.5112 18.4194 21.5112C18.7133 21.5112 18.9611 21.4104 19.1628 21.2087C19.3645 21.0069 19.4654 20.7591 19.4654 20.4652Z"
fill={fill}
/>
</svg>
);
export default BigIntCol;

View file

@ -0,0 +1,28 @@
import React from 'react';
const BooleanCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
className={className}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
opacity="0.4"
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.99843 3.24707C4.92662 3.24707 3.24707 4.92662 3.24707 6.99843C3.24707 9.07024 4.92662 10.7498 6.99843 10.7498H17.0021C19.0739 10.7498 20.7534 9.07024 20.7534 6.99843C20.7534 4.92662 19.0739 3.24707 17.0021 3.24707H6.99843ZM6.99843 13.2507C4.92662 13.2507 3.24707 14.9302 3.24707 17.0021C3.24707 19.0739 4.92662 20.7534 6.99843 20.7534H17.0021C19.0739 20.7534 20.7534 19.0739 20.7534 17.0021C20.7534 14.9302 19.0739 13.2507 17.0021 13.2507H6.99843Z"
fill="#889096"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.3766 8.87343C17.4126 8.87343 18.2523 8.03366 18.2523 6.99775C18.2523 5.96184 17.4126 5.12207 16.3766 5.12207C15.3408 5.12207 14.501 5.96184 14.501 6.99775C14.501 8.03366 15.3408 8.87343 16.3766 8.87343ZM7.62373 18.877C8.65964 18.877 9.49941 18.0372 9.49941 17.0014C9.49941 15.9655 8.65964 15.1257 7.62373 15.1257C6.58783 15.1257 5.74805 15.9655 5.74805 17.0014C5.74805 18.0372 6.58783 18.877 7.62373 18.877Z"
fill={fill}
/>
</svg>
);
export default BooleanCol;

View file

@ -0,0 +1,30 @@
import React from 'react';
const DatetimeCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
className={className}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path opacity="0.4" d="M3 9H21V18C21 20.2091 19.2091 22 17 22H7C4.79086 22 3 20.2091 3 18V9Z" fill={fill} />
<path d="M17 3.5H7C4.79086 3.5 3 5.29086 3 7.5V9H21V7.5C21 5.29086 19.2091 3.5 17 3.5Z" fill={fill} />
<path
opacity="0.4"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 1.25C8.41421 1.25 8.75 1.58579 8.75 2V5C8.75 5.41421 8.41421 5.75 8 5.75C7.58579 5.75 7.25 5.41421 7.25 5V2C7.25 1.58579 7.58579 1.25 8 1.25ZM16 1.25C16.4142 1.25 16.75 1.58579 16.75 2V5C16.75 5.41421 16.4142 5.75 16 5.75C15.5858 5.75 15.25 5.41421 15.25 5V2C15.25 1.58579 15.5858 1.25 16 1.25Z"
fill={fill}
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18 23C20.7614 23 23 20.7614 23 18C23 15.2386 20.7614 13 18 13C15.2386 13 13 15.2386 13 18C13 20.7614 15.2386 23 18 23ZM18 14.625C17.7929 14.625 17.625 14.7929 17.625 15V17.0727C17.2585 17.221 17 17.5803 17 18C17 18.5523 17.4477 19 18 19C18.5523 19 19 18.5523 19 18C19 17.5803 18.7415 17.221 18.375 17.0727V15C18.375 14.7929 18.2071 14.625 18 14.625Z"
fill={fill}
/>
</svg>
);
export default DatetimeCol;

View file

@ -0,0 +1,29 @@
import React from 'react';
const FloatCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
className={className}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
opacity="0.4"
d="M19.0377 18.0953C17.9559 18.0953 17.0327 17.713 16.2681 16.9483C15.5034 16.1837 15.1211 15.2605 15.1211 14.1788V9.82181C15.1211 8.7401 15.5034 7.81692 16.2681 7.05227C17.0327 6.28761 17.9559 5.90527 19.0377 5.90527C20.1194 5.90527 21.0425 6.28761 21.8072 7.05227C22.5719 7.81692 22.9542 8.7401 22.9542 9.82181V14.1788C22.9542 15.2605 22.5719 16.1837 21.8072 16.9483C21.0425 17.713 20.1194 18.0953 19.0377 18.0953ZM19.0392 15.6824C19.4573 15.6824 19.8121 15.5362 20.1038 15.2439C20.3955 14.9515 20.5413 14.5965 20.5413 14.1788V9.82181C20.5413 9.40414 20.3949 9.04912 20.1023 8.75675C19.8096 8.46438 19.4542 8.31819 19.0361 8.31819C18.618 8.31819 18.2631 8.46438 17.9715 8.75675C17.6798 9.04912 17.534 9.40414 17.534 9.82181V14.1788C17.534 14.5965 17.6804 14.9515 17.973 15.2439C18.2657 15.5362 18.6211 15.6824 19.0392 15.6824Z"
fill="#889096"
/>
<path
opacity="0.4"
d="M10.3336 18.0953C9.25183 18.0953 8.32864 17.713 7.56399 16.9483C6.79933 16.1837 6.41699 15.2605 6.41699 14.1788V9.82181C6.41699 8.7401 6.79933 7.81692 7.56399 7.05227C8.32864 6.28761 9.25183 5.90527 10.3336 5.90527C11.4153 5.90527 12.3384 6.28761 13.1031 7.05227C13.8678 7.81692 14.2501 8.7401 14.2501 9.82181V14.1788C14.2501 15.2605 13.8678 16.1837 13.1031 16.9483C12.3384 17.713 11.4153 18.0953 10.3336 18.0953ZM10.3351 15.6824C10.7532 15.6824 11.108 15.5362 11.3997 15.2439C11.6914 14.9515 11.8372 14.5965 11.8372 14.1788V9.82181C11.8372 9.40414 11.6908 9.04912 11.3982 8.75675C11.1055 8.46438 10.7501 8.31819 10.332 8.31819C9.91392 8.31819 9.55904 8.46438 9.26738 8.75675C8.97573 9.04912 8.82991 9.40414 8.82991 9.82181V14.1788C8.82991 14.5965 8.97625 14.9515 9.26893 15.2439C9.56159 15.5362 9.91697 15.6824 10.3351 15.6824Z"
fill="#889096"
/>
<path
d="M3.24837 18.0947H4.34342C4.68085 18.0947 4.96563 17.9787 5.19773 17.7466C5.42984 17.5145 5.5459 17.2297 5.5459 16.8923V15.7972C5.5459 15.4598 5.42984 15.175 5.19773 14.9429C4.96563 14.7108 4.68085 14.5947 4.34342 14.5947H3.24837C2.91094 14.5947 2.62617 14.7108 2.39406 14.9429C2.16195 15.175 2.0459 15.4598 2.0459 15.7972V16.8923C2.0459 17.2297 2.16195 17.5145 2.39406 17.7466C2.62617 17.9787 2.91094 18.0947 3.24837 18.0947Z"
fill={fill}
/>
</svg>
);
export default FloatCol;

View file

@ -0,0 +1,19 @@
import React from 'react';
const IntegerCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
className={className}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.10784 16.1376L8.29482 19.3768C8.23684 19.6213 8.11056 19.8213 7.91599 19.9765C7.72144 20.1318 7.49355 20.2094 7.23232 20.2094C6.8718 20.2094 6.57759 20.0665 6.34969 19.7806C6.12179 19.4947 6.05549 19.1799 6.15079 18.836L6.82687 16.1376H4.24534C3.86818 16.1376 3.56564 15.9915 3.33774 15.6993C3.10984 15.4071 3.04353 15.0808 3.13882 14.7202C3.1968 14.459 3.32941 14.2508 3.53664 14.0955C3.74389 13.9403 3.98013 13.8626 4.24534 13.8626H7.39264L8.32687 10.1376H5.74534C5.36818 10.1376 5.06564 9.99154 4.83774 9.69932C4.60984 9.40712 4.54353 9.08077 4.63882 8.72025C4.6968 8.45902 4.82941 8.25077 5.03664 8.09552C5.24389 7.94027 5.48013 7.86265 5.74534 7.86265H8.89264L9.70567 4.62352C9.76365 4.37896 9.88993 4.18004 10.0845 4.02677C10.279 3.87351 10.5069 3.79688 10.7682 3.79688C11.1287 3.79688 11.4229 3.93982 11.6508 4.2257C11.8787 4.51157 11.945 4.82642 11.8497 5.17025L11.1736 7.86265H14.8926L15.7057 4.62352C15.7637 4.37896 15.8899 4.18004 16.0845 4.02677C16.279 3.87351 16.5069 3.79688 16.7682 3.79688C17.1287 3.79688 17.4229 3.93982 17.6508 4.2257C17.8787 4.51157 17.945 4.82642 17.8497 5.17025L17.1736 7.86265H19.7551C20.1323 7.86265 20.4348 8.00876 20.6627 8.30098C20.8906 8.59317 20.957 8.91953 20.8617 9.28005C20.8037 9.54128 20.6711 9.74952 20.4638 9.90477C20.2566 10.06 20.0204 10.1376 19.7551 10.1376H16.6078L15.6736 13.8626H18.2551C18.6323 13.8626 18.9348 14.0088 19.1627 14.301C19.3906 14.5932 19.457 14.9195 19.3617 15.2801C19.3037 15.5413 19.1711 15.7495 18.9638 15.9048C18.7566 16.06 18.5204 16.1376 18.2551 16.1376H15.1078L14.2948 19.3768C14.2368 19.6213 14.1106 19.8213 13.916 19.9765C13.7214 20.1318 13.4936 20.2094 13.2323 20.2094C12.8718 20.2094 12.5776 20.0665 12.3497 19.7806C12.1218 19.4947 12.0555 19.1799 12.1508 18.836L12.8269 16.1376H9.10784ZM9.67362 13.8626H13.3926L14.3269 10.1376H10.6078L9.67362 13.8626Z"
fill={fill}
/>
</svg>
);
export default IntegerCol;

View file

@ -0,0 +1,14 @@
import React from 'react';
const Jsonb = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={width} viewBox={viewBox} fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9 4.476V3H8.865C8.3955 3 7.941 3.093 7.5015 3.2775C7.06407 3.46087 6.66854 3.73136 6.339 4.0725C6.01829 4.39483 5.76667 4.77915 5.5995 5.202V5.2035C5.45069 5.60502 5.35097 6.02305 5.3025 6.4485V6.4515C5.25984 6.8816 5.24781 7.31419 5.2665 7.746C5.2845 8.181 5.2935 8.616 5.2935 9.0495C5.2935 9.354 5.2335 9.639 5.118 9.9075C4.89786 10.4315 4.48805 10.8546 3.9705 11.0895C3.70659 11.2048 3.42148 11.2635 3.1335 11.262H3V12.738H3.135C3.4275 12.738 3.705 12.798 3.969 12.9195L3.9705 12.921C4.2375 13.038 4.464 13.197 4.653 13.398L4.656 13.401C4.851 13.596 5.0055 13.8285 5.1165 14.0985L5.118 14.1015C5.235 14.3715 5.2935 14.6535 5.2935 14.9505C5.2935 15.3855 5.2845 15.8205 5.2665 16.254C5.2485 16.698 5.2605 17.1315 5.3025 17.559C5.352 17.9835 5.451 18.3975 5.598 18.7965C5.757 19.206 6.0045 19.584 6.339 19.9275C6.6735 20.2725 7.062 20.538 7.5015 20.7225C7.941 20.907 8.3955 21 8.8665 21H9V19.524H8.865C8.565 19.524 8.2845 19.467 8.0205 19.3515C7.76585 19.2325 7.53389 19.0701 7.335 18.8715C7.14192 18.666 6.98519 18.4291 6.8715 18.171C6.7605 17.901 6.7065 17.616 6.7065 17.3115C6.7065 16.9695 6.711 16.632 6.723 16.3035C6.735 15.9615 6.735 15.6285 6.723 15.306C6.71771 14.9845 6.69015 14.6637 6.6405 14.346C6.59278 14.0326 6.50819 13.726 6.3885 13.4325C6.15504 12.8646 5.77322 12.3698 5.283 12C5.77377 11.6304 6.15612 11.1356 6.39 10.5675C6.51 10.2795 6.5925 9.978 6.642 9.6645C6.6915 9.3495 6.7185 9.03 6.7245 8.7045C6.7365 8.3745 6.7365 8.0415 6.7245 7.7055C6.7125 7.3695 6.7065 7.0305 6.7065 6.6885C6.7039 6.25858 6.82703 5.83727 7.06076 5.47643C7.29448 5.11558 7.6286 4.83093 8.022 4.6575C8.28687 4.53605 8.57512 4.4741 8.8665 4.476H9ZM15 19.524V21H15.135C15.6045 21 16.059 20.907 16.4985 20.7225C16.938 20.538 17.3265 20.2725 17.661 19.9275C17.9955 19.5825 18.243 19.2075 18.4005 18.798C18.5505 18.399 18.648 17.982 18.6975 17.5515V17.5485C18.7395 17.1285 18.7515 16.698 18.7335 16.254C18.7155 15.819 18.7065 15.384 18.7065 14.9505C18.7065 14.646 18.7665 14.361 18.882 14.0925V14.091C19.1019 13.5668 19.5118 13.1452 20.0295 12.9105C20.2935 12.7954 20.5785 12.7367 20.8665 12.738H21V11.262H20.865C20.571 11.262 20.2935 11.202 20.0295 11.0805L20.028 11.079C19.7705 10.968 19.5382 10.8057 19.3455 10.602L19.3425 10.599C19.1443 10.3993 18.9878 10.1622 18.882 9.9015V9.8985C18.7648 9.63083 18.7045 9.34171 18.705 9.0495C18.705 8.6145 18.714 8.1795 18.732 7.746C18.7507 7.3107 18.7387 6.87461 18.696 6.441C18.6474 6.01865 18.5482 5.60217 18.4005 5.2035V5.202C18.2329 4.77902 17.9807 4.39469 17.6595 4.0725C17.3299 3.7314 16.9344 3.46091 16.497 3.2775C16.0653 3.09415 15.601 2.99977 15.132 3H15V4.476H15.135C15.435 4.476 15.7155 4.533 15.978 4.6485C16.239 4.7715 16.467 4.9305 16.6635 5.1285C16.854 5.3295 17.0085 5.5635 17.127 5.829C17.238 6.099 17.292 6.384 17.292 6.6885C17.292 7.0305 17.2875 7.3665 17.2755 7.6965C17.2635 8.0385 17.2635 8.3715 17.2755 8.694C17.2815 9.027 17.3085 9.3465 17.358 9.654C17.4075 9.975 17.4915 10.278 17.61 10.5675C17.8439 11.1356 18.2263 11.6303 18.717 12C18.2263 12.3697 17.8439 12.8644 17.61 13.4325C17.4912 13.7227 17.4066 14.0257 17.358 14.3355C17.3085 14.6505 17.2815 14.97 17.2755 15.2955C17.2634 15.6284 17.2634 15.9616 17.2755 16.2945C17.2875 16.6305 17.2935 16.9695 17.2935 17.3115C17.2959 17.7414 17.1727 18.1626 16.939 18.5234C16.7053 18.8842 16.3713 19.1689 15.978 19.3425C15.7131 19.4639 15.4249 19.5259 15.1335 19.524H15Z"
fill={fill}
/>
</svg>
);
export default Jsonb;

View file

@ -0,0 +1,28 @@
import React from 'react';
const SerialCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
className={className}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
opacity="0.4"
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.35714 0C1.05533 0 0 1.05532 0 2.35714V11.7857C0 13.0875 1.05533 14.1429 2.35714 14.1429H19.6429C20.9446 14.1429 22 13.0875 22 11.7857V2.35714C22 1.05532 20.9446 0 19.6429 0H2.35714Z"
fill="#889096"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.26819 4.20431C7.26819 3.66189 6.82848 3.22217 6.28605 3.22217C5.74362 3.22217 5.30391 3.66189 5.30391 4.20431C5.30391 4.45359 5.10184 4.65566 4.85256 4.65566H4.37472C3.83231 4.65566 3.39258 5.09537 3.39258 5.6378C3.39258 6.18023 3.83231 6.61994 4.37472 6.61994H4.85256C5.00681 6.61994 5.15769 6.60548 5.30391 6.57784V8.95614H4.37474C3.83233 8.95614 3.39259 9.39586 3.39259 9.93828C3.39259 10.4807 3.83233 10.9204 4.37474 10.9204H8.19739C8.73982 10.9204 9.17954 10.4807 9.17954 9.93828C9.17954 9.39586 8.73982 8.95614 8.19739 8.95614H7.26819V4.20431ZM11.7881 8.95614C11.2457 8.95614 10.8059 9.39586 10.8059 9.93828C10.8059 10.4807 11.2457 10.9204 11.7881 10.9204H14.5132C15.0556 10.9204 15.4953 10.4807 15.4953 9.93828C15.4953 9.39586 15.0556 8.95614 14.5132 8.95614H11.7881Z"
fill={fill}
/>
</svg>
);
export default SerialCol;

View file

@ -0,0 +1,28 @@
import React from 'react';
const VarcharCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
className={className}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
opacity="0.4"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.72616 3.35712C8.30499 3.35712 7.93786 3.64377 7.83572 4.05237L5.32515 14.0947H8.72616C9.37708 14.0947 9.90473 14.6223 9.90473 15.2732C9.90473 15.9241 9.37708 16.4518 8.72616 16.4518H4.73587L3.32221 22.1063C3.16434 22.7379 2.52446 23.1218 1.893 22.9638C1.26153 22.8061 0.877601 22.1662 1.03547 21.5346L2.66633 15.0113C2.6701 14.9946 2.67423 14.9781 2.67871 14.9617L5.54898 3.48068C5.91346 2.02276 7.22339 1 8.72616 1C10.2289 1 11.5389 2.02277 11.9034 3.48068L12.734 6.80321C12.8919 7.43468 12.5079 8.07456 11.8765 8.23243C11.245 8.39029 10.6051 8.00636 10.4473 7.3749L9.61662 4.05237C9.51446 3.64377 9.14734 3.35712 8.72616 3.35712Z"
fill="#889096"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.9107 11.5417C16.8268 11.5417 16.7521 11.595 16.725 11.6744L14.9596 16.8231H18.8618L17.0964 11.6744C17.0693 11.595 16.9946 11.5417 16.9107 11.5417ZM21.6176 17.5934L19.3261 10.9099C18.9722 9.8778 18.0017 9.18457 16.9107 9.18457C15.8196 9.18457 14.8491 9.8778 14.4952 10.9099L12.2037 17.5934C12.1974 17.6105 12.1915 17.6278 12.1859 17.6452L10.8853 21.4388C10.6742 22.0545 11.0022 22.7247 11.6179 22.9359C12.2337 23.1469 12.9039 22.819 13.115 22.2031L14.1515 19.1802H19.6698L20.7063 22.2031C20.9173 22.819 21.5877 23.1469 22.2034 22.9359C22.8191 22.7247 23.1472 22.0545 22.936 21.4388L21.6355 17.6452C21.6298 17.6278 21.624 17.6105 21.6176 17.5934Z"
fill={fill}
/>
</svg>
);
export default VarcharCol;

View file

@ -174,6 +174,14 @@ import Search01 from './Search01.jsx';
import ShiftButtonIcon from './ShiftButtonIcon.jsx';
import Unpin01 from './Unpin01.jsx';
import WarningUserNotFound from './WarningUserNotFound.jsx';
import VarcharCol from './VarcharCol.jsx';
import Jsonb from './Jsonb.jsx';
import IntegerCol from './IntegerCol.jsx';
import BigIntCol from './BigIntCol.jsx';
import FloatCol from './FloatCol.jsx';
import BooleanCol from './BooleanCol.jsx';
import SerialCol from './SerialCol.jsx';
import DatetimeCol from './DatetimeCol';
import AITag from './AITag.jsx';
import Reset from './Reset.jsx';
@ -531,6 +539,22 @@ const Icon = (props) => {
return <TriangleUpCenter {...props} />;
case 'TriangleDownCenter':
return <TriangleDownCenter {...props} />;
case 'jsonb':
return <Jsonb {...props} />;
case 'character varying':
return <VarcharCol {...props} />;
case 'integer':
return <IntegerCol {...props} />;
case 'bigint':
return <BigIntCol {...props} />;
case 'double precision':
return <FloatCol {...props} />;
case 'boolean':
return <BooleanCol {...props} />;
case 'serial':
return <SerialCol {...props} />;
case 'timestamp with time zone':
return <DatetimeCol {...props} />;
case 'AI-tag':
return <AITag {...props} />;
default:

View file

@ -21,6 +21,10 @@ class Field {
@IsString()
@IsNotEmpty({ message: '::Table names for join not selected' })
table: string;
@IsString()
@IsOptional()
jsonpath: string;
}
class Conditions {
@ -50,6 +54,10 @@ class ConditionField {
@IsString()
@IsOptional() // present only when type is column
columnName: string;
@IsString()
@IsOptional()
jsonpath: string;
}
class ConditionsList {
@ -113,6 +121,10 @@ class Order {
@IsIn(['ASC', 'DESC'], { message: '::Sort direction not selected' })
direction: string;
@IsString()
@IsOptional()
jsonpath: string;
}
export class TooljetDbJoinDto {

View file

@ -19,7 +19,7 @@ import {
IsObject,
IsIn,
} from 'class-validator';
import { sanitizeInput, formatTimestamp, validateDefaultValue } from 'src/helpers/utils.helper';
import { sanitizeInput, formatTimestamp, validateDefaultValue, formatJSONB } from 'src/helpers/utils.helper';
export function Match(property: string, validationOptions?: ValidationOptions) {
return (object: any, propertyName: string) => {
@ -48,7 +48,7 @@ export class MatchTypeConstraint implements ValidatorConstraintInterface {
}
matchType(value, relatedType) {
if (relatedType === 'character varying' || relatedType === 'timestamp with time zone') {
if (relatedType === 'character varying' || relatedType === 'timestamp with time zone' || relatedType === 'jsonb') {
return typeof value === 'string';
}
@ -197,8 +197,10 @@ export class PostgrestTableColumnDto {
@IsOptional()
@Transform(({ value, obj }) => {
const sanitizedValue = sanitizeInput(value);
const transformedJsonbData = formatJSONB(value, obj);
const sanitizedValue = sanitizeInput(transformedJsonbData);
const transformedData = formatTimestamp(sanitizedValue, obj);
return validateDefaultValue(transformedData, obj);
})
@Match('data_type', {
@ -296,7 +298,8 @@ export class EditColumnTableDto {
@IsOptional()
@Transform(({ value, obj }) => {
const sanitizedValue = sanitizeInput(value);
const transformedJsonbData = formatJSONB(value, obj);
const sanitizedValue = sanitizeInput(transformedJsonbData);
const transformedData = formatTimestamp(sanitizedValue, obj);
return validateDefaultValue(transformedData, obj);
})

View file

@ -0,0 +1,106 @@
{
"type": "object",
"required": ["id", "table_name", "schema"],
"properties": {
"id": {
"type": "string"
},
"table_name": {
"type": "string"
},
"schema": {
"type": "object",
"required": ["columns", "foreign_keys"],
"properties": {
"columns": {
"type": "array",
"items": {
"type": "object",
"required": ["column_name", "data_type", "constraints_type"],
"properties": {
"column_name": {
"type": "string"
},
"data_type": {
"type": "string"
},
"column_default": {
"type": ["string", "null", "object", "array"]
},
"character_maximum_length": {
"type": ["integer", "null"]
},
"numeric_precision": {
"type": ["integer", "null"]
},
"constraints_type": {
"type": "object",
"required": ["is_not_null", "is_primary_key", "is_unique"],
"properties": {
"is_not_null": {
"type": "boolean"
},
"is_primary_key": {
"type": "boolean"
},
"is_unique": {
"type": "boolean"
}
}
},
"keytype": {
"type": "string"
}
}
}
},
"foreign_keys": {
"type": "array",
"items": {
"type": "object",
"required": [
"referenced_table_name",
"constraint_name",
"column_names",
"referenced_column_names",
"on_update",
"on_delete",
"referenced_table_id"
],
"properties": {
"referenced_table_name": {
"type": "string"
},
"constraint_name": {
"type": "string"
},
"column_names": {
"type": "array",
"items": {
"type": "string"
}
},
"referenced_column_names": {
"type": "array",
"items": {
"type": "string"
}
},
"on_update": {
"type": "string",
"enum": ["CASCADE", "RESTRICT", "SET NULL", "NO ACTION"]
},
"on_delete": {
"type": "string",
"enum": ["CASCADE", "RESTRICT", "SET NULL", "NO ACTION"]
},
"referenced_table_id": {
"type": "string"
}
}
}
}
}
}
}
}

View file

@ -218,3 +218,19 @@ export function modifyTjdbErrorObject(error) {
if (error.detail) error['details'] = error.detail;
return error;
}
/**
* Validates the JSONB column value. We only allow valid JSON values to be added in the JSONB column.
* @param jsonbColumnList - jsonb column list
* @param inputValues - Values to be created or updated ( Object )
* @returns - Column names with invalid JSON data.
*/
export function validateTjdbJSONBColumnInputs(jsonbColumnList: Array<string>, inputValues) {
const inValidValueColumnsList = [];
Object.entries(inputValues).forEach(([key, value]) => {
if (jsonbColumnList.includes(key)) {
if (typeof value !== 'object') inValidValueColumnsList.push(key);
}
});
return inValidValueColumnsList;
}

View file

@ -68,6 +68,15 @@ export function sanitizeInput(value: string) {
});
}
export function isJSONString(value: string): boolean {
try {
JSON.parse(value);
return true;
} catch (e) {
return false;
}
}
export function formatTimestamp(value: any, params: any) {
const { data_type } = params;
if (data_type === 'timestamp with time zone' && value) {
@ -76,6 +85,45 @@ export function formatTimestamp(value: any, params: any) {
return value;
}
/**
* Since for JSONB column the default value must be in stringify format, if the input has single quotes we would need to escape the single quotes.
* @param input - Default value of JSONB column.
* @returns - Sanitized input by escaping single quotes in the input.
*/
function escapeSingleQuotesInDefaultValueForJSONB(input) {
if (typeof input === 'string') {
return input.replace(/'/g, "''");
} else if (input.length && Array.isArray(input)) {
return input.map(escapeSingleQuotesInDefaultValueForJSONB);
} else if (!Array.isArray(input) && typeof input === 'object') {
return Object.fromEntries(
Object.entries(input).map(([key, value]) => [key, escapeSingleQuotesInDefaultValueForJSONB(value)])
);
}
return input;
}
/**
* Formats default value passed to JSONB column into a stringify format.
* @param value Default value for a JSONB column.
* @returns Stringify default value.
*/
export function formatJSONB(value: any, params: any) {
const { data_type } = params;
if (data_type === 'jsonb' && value) {
const jsonString = JSON.stringify(escapeSingleQuotesInDefaultValueForJSONB(value));
return `'${jsonString}'`;
}
return value;
}
export function formatJoinsJSONBPath(jsonpath: string): string {
const addedQuotesToColumnName = jsonpath.replace(/(->>|->|'[^']*'|\w+)/g, (match) => {
return /->/.test(match) || /^'.*'$/.test(match) ? match : `'${match}'`;
});
return addedQuotesToColumnName;
}
export function lowercaseString(value: string) {
return value?.toLowerCase()?.trim();
}

View file

@ -10,6 +10,7 @@ export const TJDB = {
double_precision: 'double precision' as const,
boolean: 'boolean' as const,
timestampz: 'timestamp with time zone' as const,
jsonb: 'jsonb' as const,
};
export type TooljetDatabaseDataTypes = (typeof TJDB)[keyof typeof TJDB];
@ -100,8 +101,8 @@ const errorCodeMapping: Partial<ErrorCodeMapping> = {
default: 'Insufficient privilege',
},
[PostgresErrorCode.UndefinedFunction]: {
proxy_postgrest: '{{fxName}} - aggregate function requires serial, integer, float or big int column type',
join_tables: '{{fxName}} - aggregate function requires serial, integer, float or big int column type',
// proxy_postgrest: '{{fxName}} - aggregate function requires serial, integer, float or big int column type',
// join_tables: '{{fxName}} - aggregate function requires serial, integer, float or big int column type',
},
};
@ -238,7 +239,8 @@ export class TooljetDatabaseError extends QueryFailedError {
const regex = /function (\w+)\(([\w\s]+)\) does not exist/;
const matches = regex.exec(errorMessage);
const table = this.context.internalTables[0].tableName;
return { table, fxName: matches[1] };
if (Array.isArray(matches) && matches.length) return { table, fxName: matches[1] };
return null;
},
};
return parsers[this.code]?.() || null;

View file

@ -1,4 +1,4 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { HttpException, Injectable, NotFoundException } from '@nestjs/common';
import { isEmpty } from 'lodash';
import { EntityManager, In, QueryFailedError } from 'typeorm';
import { InternalTable } from 'src/entities/internal_table.entity';
@ -11,13 +11,16 @@ import { ActionTypes, ResourceTypes } from 'src/entities/audit_log.entity';
import { PostgrestError, TooljetDatabaseError, TooljetDbActions } from 'src/modules/tooljet_db/tooljet-db.types';
import { QueryError } from 'src/modules/data_sources/query.errors';
import got from 'got';
import { TooljetDbService } from './tooljet_db.service';
import { validateTjdbJSONBColumnInputs } from 'src/helpers/tooljet_db.helper';
@Injectable()
export class PostgrestProxyService {
constructor(
private readonly manager: EntityManager,
private readonly configService: ConfigService,
private eventEmitter: EventEmitter2
private eventEmitter: EventEmitter2,
private tooljetDbService: TooljetDbService
) {}
// NOTE: This method forwards request directly to PostgREST Using express middleware
@ -67,9 +70,21 @@ export class PostgrestProxyService {
req.headers['tableInfo'] = tableInfo;
}
if (['PATCH', 'POST'].includes(req.method)) {
await this.validateJSONBInputs(organizationId, internalTable.tableName, req.body);
}
return this.httpProxy(req, res, next);
}
/**
* Handles the TJDB request from Query Builder
* @param url
* @param method
* @param headers
* @param body
* @returns
*/
async perform(
url: string,
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
@ -102,6 +117,10 @@ export class PostgrestProxyService {
headers['tableinfo'] = tableInfo;
}
if (['PATCH', 'POST'].includes(method)) {
await this.validateJSONBInputs(headers['tj-workspace-id'], internalTable.tableName, body);
}
const reqHeaders = {
...headers,
Authorization: authToken,
@ -120,7 +139,7 @@ export class PostgrestProxyService {
return response.body;
} catch (error) {
if (!isEmpty(error.response) && (error.response.statusCode < 200 || error.response.statusCode >= 300)) {
if (!isEmpty(error.response.rawBody) && (error.response.statusCode < 200 || error.response.statusCode >= 300)) {
const postgrestResponse = JSON.parse(error.response.rawBody.toString().toString('utf8'));
const errorMessage = postgrestResponse.message;
const errorContext: {
@ -139,7 +158,7 @@ export class PostgrestProxyService {
throw new QueryError(tooljetDbError.toString(), { code: tooljetDbError.code }, {});
}
throw new QueryError('Query could not be completed', error.message, {});
throw new QueryError('Query could not be completed', error.message, { message: error.message });
}
}
@ -237,6 +256,26 @@ export class PostgrestProxyService {
throw new NotFoundException('Internal table not found: ' + tableNamesNotInOrg);
}
private async validateJSONBInputs(organizationId, tableName, body) {
const tableDetails = await this.tooljetDbService.perform(organizationId, 'view_table', {
table_name: tableName,
});
const jsonbColumns = tableDetails.columns
.filter((column) => column.data_type === 'jsonb')
.map((column) => column.column_name);
if (jsonbColumns.length) {
const inValidJsonbColumns = validateTjdbJSONBColumnInputs(jsonbColumns, body);
if (inValidJsonbColumns.length) {
throw new HttpException(
`Expected JSON values in the following columns : ${inValidJsonbColumns.join(', ')}`,
400
);
}
}
}
}
function replaceUrlForPostgrest(url: string) {

View file

@ -14,7 +14,7 @@ import { InjectEntityManager } from '@nestjs/typeorm';
import { InternalTable } from 'src/entities/internal_table.entity';
import { LicenseService } from '@licensing/service';
import { LICENSE_FIELD, LICENSE_LIMIT, LICENSE_LIMITS_LABEL } from '@licensing/helper';
import { generatePayloadForLimits } from 'src/helpers/utils.helper';
import { generatePayloadForLimits, formatJoinsJSONBPath, formatJSONB } from 'src/helpers/utils.helper';
import { isString, isEmpty, camelCase } from 'lodash';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { ActionTypes, ResourceTypes } from 'src/entities/audit_log.entity';
@ -253,9 +253,16 @@ export class TooljetDbService {
c.ORDINAL_POSITION;
`);
const transformedColumnDefaultValues = columns.map((column) => {
return {
...column,
column_default: column.data_type === 'jsonb' ? JSON.parse(column.column_default) : column.column_default,
};
});
return {
foreign_keys,
columns,
columns: transformedColumnDefaultValues,
configurations: internalTable.configurations,
};
}
@ -934,7 +941,10 @@ export class TooljetDbService {
// Building `SELECT` statement with aliased column names
if (!isEmpty(queryJson.fields) && isEmpty(queryJson.aggregates)) {
queryJson.fields.forEach((field) => {
const fieldName = `"${internalTableIdToNameMap[field.table]}"."${field.name}"`;
const fieldName = field.jsonpath
? `"${internalTableIdToNameMap[field.table]}"."${field.name}"${formatJoinsJSONBPath(field.jsonpath)}`
: `"${internalTableIdToNameMap[field.table]}"."${field.name}"`;
const fieldAlias = `${internalTableIdToNameMap[field.table]}_${field.name}`;
queryBuilder.addSelect(fieldName, fieldAlias);
});
@ -998,7 +1008,9 @@ export class TooljetDbService {
// order by
if (queryJson.order_by) {
queryJson.order_by.forEach((order) => {
const orderByColumn = `"${internalTableIdToNameMap[order.table]}"."${order.columnName}"`;
const orderByColumn = order.jsonpath
? `"${internalTableIdToNameMap[order.table]}"."${order.columnName}"${formatJoinsJSONBPath(order.jsonpath)}`
: `"${internalTableIdToNameMap[order.table]}"."${order.columnName}"`;
queryBuilder.addOrderBy(orderByColumn, order.direction as 'ASC' | 'DESC');
});
}
@ -1009,6 +1021,7 @@ export class TooljetDbService {
return queryBuilder;
}
// Param: internalTableIdToNameMap - is the aliases of tablename
private constructFilterConditions(conditions, internalTableIdToNameMap) {
let conditionString = '';
const conditionParams = {};
@ -1033,15 +1046,27 @@ export class TooljetDbService {
conditions.conditionsList.forEach((condition, index) => {
const paramName = `${condition.leftField.columnName}_${index}`;
const leftField =
condition.leftField.type == 'Column'
? `"${internalTableIdToNameMap[condition.leftField.table]}"."${condition.leftField.columnName}"`
: `${condition.leftField.columnName}`;
let leftField;
if (condition.leftField.type == 'Column') {
leftField = condition.leftField.jsonpath
? `"${internalTableIdToNameMap[condition.leftField.table]}"."${
condition.leftField.columnName
}"${formatJoinsJSONBPath(condition.leftField.jsonpath)}`
: `"${internalTableIdToNameMap[condition.leftField.table]}"."${condition.leftField.columnName}"`;
} else {
leftField = `${condition.leftField.columnName}`;
}
const rightField =
condition.rightField.type == 'Column'
? `"${internalTableIdToNameMap[condition.rightField.table]}"."${condition.rightField.columnName}"`
: maybeParameterizeValue(condition.operator, paramName, condition.rightField.value);
let rightField;
if (condition.rightField.type == 'Column') {
rightField = condition.rightField.jsonpath
? `"${internalTableIdToNameMap[condition.rightField.table]}"."${
condition.rightField.columnName
}"${formatJoinsJSONBPath(condition.rightField.jsonpath)}`
: `"${internalTableIdToNameMap[condition.rightField.table]}"."${condition.rightField.columnName}"`;
} else {
rightField = maybeParameterizeValue(condition.operator, paramName, condition.rightField.value);
}
conditionString += `${leftField} ${condition.operator} ${rightField}`;
@ -1158,6 +1183,7 @@ export class TooljetDbService {
const isSerial = () => data_type === TJDB.integer && /^nextval\(/.test(column_default);
const isCharacterVarying = () => data_type === TJDB.character_varying;
const isTimestampWithTimeZone = () => data_type === TJDB.timestampz;
const isJSONB = () => data_type === TJDB.jsonb;
if (isSerial()) return { data_type: TJDB.serial, column_default: undefined };
if (isCharacterVarying())
@ -1170,6 +1196,14 @@ export class TooljetDbService {
data_type,
column_default: this.addQuotesIfMissing(column_default),
};
if (isJSONB()) {
if (typeof column_default === 'object') {
return {
data_type,
column_default: formatJSONB(column_default, { data_type }),
};
}
}
return { data_type, column_default };
};

View file

@ -231,6 +231,8 @@ export class TooljetDbBulkUploadService {
case TJDB.double_precision:
case TJDB.bigint:
return this.convertNumber(columnValue, supportedDataType);
case TJDB.jsonb:
return JSON.parse(columnValue);
default:
return columnValue;
}

View file

@ -554,14 +554,16 @@ function buildPostgrestQuery(filters) {
Object.keys(filters).map((key) => {
if (!isEmpty(filters[key])) {
const { column, operator, value, order } = filters[key];
const { column, operator, value, order, jsonpath = '' } = filters[key];
if (!isEmpty(column) && !isEmpty(order)) {
postgrestQueryBuilder.order(column, order);
const columnName = jsonpath ? `${column}${jsonpath}` : column;
postgrestQueryBuilder.order(columnName, order);
}
if (!isEmpty(column) && !isEmpty(operator)) {
postgrestQueryBuilder[operator](column, value);
const columnName = jsonpath ? `${column}${jsonpath}` : column;
postgrestQueryBuilder[operator](columnName, value);
}
}
});