e.stopPropagation()}>
+ {inputValues[index]?.value === null ? (
+
+
+ Null
+
+
+ ) : activeTab[index] === 'Default' ? (
+
+ {transformJSONValue(column_default)}
+
+ ) : (
+
e.stopPropagation()}>
+ {
+ 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}
+ />
+
+ )}
+
+ {(inputValues[index]?.disabled || shouldInputBeDisabled) && (
+
{
+ 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',
+ }}
+ />
+ )}
+
+ );
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}
/>
)}
diff --git a/frontend/src/TooljetDatabase/Forms/RowForm.jsx b/frontend/src/TooljetDatabase/Forms/RowForm.jsx
index 799c99fd54..3b59c5bce5 100644
--- a/frontend/src/TooljetDatabase/Forms/RowForm.jsx
+++ b/frontend/src/TooljetDatabase/Forms/RowForm.jsx
@@ -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 && (
handleDisabledInputClick(index, columnName)}
+ onClick={() => handleDisabledInputClick(index, columnName, defaultValue, isNullable, dataType)}
style={{
position: 'absolute',
top: 0,
@@ -479,7 +530,7 @@ const RowForm = ({
/>
{inputValues[index]?.disabled && (
handleDisabledInputClick(index, columnName)}
+ onClick={() => handleDisabledInputClick(index, columnName, defaultValue, isNullable, dataType)}
style={{
position: 'absolute',
top: 0,
@@ -495,6 +546,100 @@ const RowForm = ({
);
+ case 'jsonb': {
+ return (
+
+ {inputValues[index]?.value === null ? (
+
+
+ Null
+
+
+ ) : activeTab[index] === 'Default' ? (
+
+ {transformJSONValue(defaultValue)}
+
+ ) : (
+
e.stopPropagation()}>
+ {
+ 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}
+ />
+
+ )}
+
+ {inputValues[index]?.disabled && (
+
{
+ handleDisabledInputClick(index, columnName, defaultValue, isNullable, dataType);
+ }}
+ style={{
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100%',
+ zIndex: 1,
+ cursor: 'pointer',
+ backgroundColor: 'transparent',
+ }}
+ />
+ )}
+
+ );
+ }
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}
/>
diff --git a/frontend/src/TooljetDatabase/Forms/TableForm.jsx b/frontend/src/TooljetDatabase/Forms/TableForm.jsx
index 178d456bc4..987c282e14 100644
--- a/frontend/src/TooljetDatabase/Forms/TableForm.jsx
+++ b/frontend/src/TooljetDatabase/Forms/TableForm.jsx
@@ -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}
/>
{
+ return (
+ {
+ 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 (
- {Object.keys(columnDetails).map((index) => (
-
-
- {/*
+ {Object.keys(columnDetails).map((index) => {
+ return (
+
+
+ {/*
*/}
-
-
{
- 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;
- }),
- };
+
+ {
+ 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}
- />
-
-
-
item.column_names[0] === columnDetails[index]?.column_name) ? (
-
-
Foreign key relation
-
-
- {
- foreignKeyDetails.find((item) => item.column_names[0] === columnDetails[index]?.column_name)
- ?.column_names[0]
- }
-
-
-
{`${
- 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]
- }`}
-
-
- ) : 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"
- >
- item.column_names[0] === columnDetails[index]?.column_name
- ),
- 'foreign-key-relation': foreignKeyDetails?.some(
- (item) => item.column_names[0] !== columnDetails[index]?.column_name
- ),
- })}
- >
-
-
-
-
-
-
-
-
- {checkMatchingColumnNamesInForeignKey(foreignKeyDetails, columnDetails[index].column_name) ? (
-
-
- No data found
-
- }
- loader={
- <>
-
-
-
- >
- }
- 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}
- />
- ) : (
+
item.column_names[0] === columnDetails[index]?.column_name) ? (
+
+
Foreign key relation
+
+
+ {
+ foreignKeyDetails.find((item) => item.column_names[0] === columnDetails[index]?.column_name)
+ ?.column_names[0]
+ }
+
+
+
{`${
+ 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]
+ }`}
+
+
+ ) : 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"
+ >
+ item.column_names[0] === columnDetails[index]?.column_name
+ ),
+ 'foreign-key-relation': foreignKeyDetails?.some(
+ (item) => item.column_names[0] !== columnDetails[index]?.column_name
+ ),
+ })}
+ >
+
+
+
+
+
-
- {columnDetails[index].data_type === 'timestamp with time zone' ? (
-
- {
- 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"
- />
-
- ) : (
-
{
- 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'
- }
- />
- )}
+
+
- )}
-
-
-
-
+
+ No data found
+
}
- onChange={(e) => {
+ loader={
+ <>
+
+
+
+ >
+ }
+ 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}
/>
-
-
+ ) : (
+
+
+ {columnDetails[index].data_type === 'timestamp with time zone' && (
+
+ {
+ 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"
+ />
+
+ )}
+ {columnDetails[index].data_type === 'jsonb' && (
+
e.stopPropagation()}
+ className="tjdb-codehinter-wrapper-drawer-tableSchema"
+ >
+
+
+ )}
+ {['jsonb', 'timestamp with time zone'].includes(columnDetails[index].data_type) || (
+
{
+ 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'
+ }
+ />
+ )}
+
+
+ )}
-
-
-
-
- ))}
+ );
+ })}
);
}
diff --git a/frontend/src/TooljetDatabase/Forms/styles.scss b/frontend/src/TooljetDatabase/Forms/styles.scss
index 0339eb6132..bbea1465af 100644
--- a/frontend/src/TooljetDatabase/Forms/styles.scss
+++ b/frontend/src/TooljetDatabase/Forms/styles.scss
@@ -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;
}
diff --git a/frontend/src/TooljetDatabase/Icons/JSONB.svg b/frontend/src/TooljetDatabase/Icons/JSONB.svg
new file mode 100644
index 0000000000..04e9da4079
--- /dev/null
+++ b/frontend/src/TooljetDatabase/Icons/JSONB.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/TooljetDatabase/Icons/Jsonb.svg b/frontend/src/TooljetDatabase/Icons/Jsonb.svg
new file mode 100644
index 0000000000..04e9da4079
--- /dev/null
+++ b/frontend/src/TooljetDatabase/Icons/Jsonb.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/src/TooljetDatabase/Menu/CellEditMenu/CellHinterWrapper.jsx b/frontend/src/TooljetDatabase/Menu/CellEditMenu/CellHinterWrapper.jsx
new file mode 100644
index 0000000000..c11782a668
--- /dev/null
+++ b/frontend/src/TooljetDatabase/Menu/CellEditMenu/CellHinterWrapper.jsx
@@ -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 (
+
+
+ {
+
+
+
+
+
Press Enter to go on next line
+
+ }
+
+
+ Esc
+
+
Discard Changes
+
+
+
+ {isNotNull === false && (
+
+
+
+ Set to null
+
+
+
+
+ handleNullToggle(e.target.checked)}
+ />
+
+
+
+ )}
+
+ {defaultValue !== null && (
+
+
+
+ Set to default
+
+
+
+
+ handleDefaultToggle(e.target.checked)}
+ />
+
+
+
+ )}
+
+
+ );
+ };
+
+ 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 (
+
+
+
+ Cancel
+
+ handleSave(e)}
+ disabled={disabledSaveButton}
+ variant="primary"
+ size="sm"
+ className="fs-12 p-2"
+ >
+ Save
+
+
+
+ );
+ };
+ const popover = (
+
+ {disabledSaveButton && (
+
+
+
+ {' '}
+
+
+ Invalid JSON syntax
+
+
+ )}
+ e.stopPropagation()}>
+
+
+
+
+
+
+ );
+
+ const customFooter = () => {
+ return (
+
e.stopPropagation()}
+ >
+ {disabledSaveButton && (
+
+
+
+ {' '}
+
+
+ Invalid JSON syntax
+
+
+ )}
+
+
+
+
+
+ );
+ };
+
+ return (
+
+
+ {
+ 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}
+ />
+
+
+ );
+};
diff --git a/frontend/src/TooljetDatabase/Menu/CellEditMenu/index.jsx b/frontend/src/TooljetDatabase/Menu/CellEditMenu/index.jsx
index 68aa6da3dd..4afd83db28 100644
--- a/frontend/src/TooljetDatabase/Menu/CellEditMenu/index.jsx
+++ b/frontend/src/TooljetDatabase/Menu/CellEditMenu/index.jsx
@@ -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 = ({
)}
)}
-
{!isBoolean &&
}
-
{/* Footer */}
@@ -346,7 +345,14 @@ export const CellEditMenu = ({
);
return (
-
+
{isForeignKey ? (
+ ) : dataType === 'jsonb' ? (
+
+
+
) : (
+ //
children
)}
diff --git a/frontend/src/TooljetDatabase/Menu/CellEditMenu/styles.scss b/frontend/src/TooljetDatabase/Menu/CellEditMenu/styles.scss
index 33902d3d45..d2e8f53689 100644
--- a/frontend/src/TooljetDatabase/Menu/CellEditMenu/styles.scss
+++ b/frontend/src/TooljetDatabase/Menu/CellEditMenu/styles.scss
@@ -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;
+ }
+
}
\ No newline at end of file
diff --git a/frontend/src/TooljetDatabase/Table/ActionsPopover/UniqueConstraintPopOver.jsx b/frontend/src/TooljetDatabase/Table/ActionsPopover/UniqueConstraintPopOver.jsx
index 6c518a9889..cada3e658c 100644
--- a/frontend/src/TooljetDatabase/Table/ActionsPopover/UniqueConstraintPopOver.jsx
+++ b/frontend/src/TooljetDatabase/Table/ActionsPopover/UniqueConstraintPopOver.jsx
@@ -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)
}
>
@@ -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)
}
/>
diff --git a/frontend/src/TooljetDatabase/Table/Header.jsx b/frontend/src/TooljetDatabase/Table/Header.jsx
index 33f21ee2c1..ddb293e334 100644
--- a/frontend/src/TooljetDatabase/Table/Header.jsx
+++ b/frontend/src/TooljetDatabase/Table/Header.jsx
@@ -149,7 +149,7 @@ const Header = ({
-
+
<>
{(isDirectRowExpand || Object.keys(selectedRowIds).length === 0) && (
<>
diff --git a/frontend/src/TooljetDatabase/Table/index.jsx b/frontend/src/TooljetDatabase/Table/index.jsx
index a9221f2e6a..5d98ce70fe 100644
--- a/frontend/src/TooljetDatabase/Table/index.jsx
+++ b/frontend/src/TooljetDatabase/Table/index.jsx
@@ -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 ? (
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 }) => {
*/}
) : (
+ // : cell.column?.dataType === 'jsonb' ? (
+ //
+ // )
{
<>
{cell.value === null ? (
Null
+ ) : cell.column.dataType === 'jsonb' ? (
+ `{...}`
) : cell.column.dataType === 'boolean' ? (
//
diff --git a/frontend/src/TooljetDatabase/Table/styles.scss b/frontend/src/TooljetDatabase/Table/styles.scss
index a1a29276d0..b63d7615b2 100644
--- a/frontend/src/TooljetDatabase/Table/styles.scss
+++ b/frontend/src/TooljetDatabase/Table/styles.scss
@@ -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 {
diff --git a/frontend/src/TooljetDatabase/constants.js b/frontend/src/TooljetDatabase/constants.js
index 10e8ffaaab..2eb3c073ba 100644
--- a/frontend/src/TooljetDatabase/constants.js
+++ b/frontend/src/TooljetDatabase/constants.js
@@ -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:
,
value: 'character varying',
},
+ {
+ name: 'JSON data type',
+ label: 'jsonb',
+ icon:
,
+ value: 'jsonb',
+ },
{ name: 'Integers up to 4 bytes', label: 'int', icon:
, value: 'integer' },
{ name: 'Integers up to 8 bytes', label: 'bigint', icon:
, value: 'bigint' },
{ name: 'Decimal numbers', label: 'float', icon:
, value: 'double precision' },
@@ -32,6 +39,7 @@ export const dataTypes = [
icon:
,
value: 'timestamp with time zone',
},
+ // { name: 'Binary JSON data', label: 'jsonb', icon:
, 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
;
case 'timestamp with time zone':
return
;
+ case 'jsonb':
+ return
;
default:
return type;
}
diff --git a/frontend/src/_components/Portal/Portal.jsx b/frontend/src/_components/Portal/Portal.jsx
index 61e0ddc1db..0e332e2e9c 100644
--- a/frontend/src/_components/Portal/Portal.jsx
+++ b/frontend/src/_components/Portal/Portal.jsx
@@ -35,7 +35,7 @@ const Portal = ({ children, ...restProps }) => {
};
return (
-
+
classList.push(item));
+
classList.forEach((item) => el.classList.add(item));
+
target.appendChild(el);
return () => {
el.remove();
diff --git a/frontend/src/_styles/tabler.scss b/frontend/src/_styles/tabler.scss
index 9629d17941..6507a349b2 100644
--- a/frontend/src/_styles/tabler.scss
+++ b/frontend/src/_styles/tabler.scss
@@ -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
}
diff --git a/frontend/src/_ui/Icon/solidIcons/BigIntCol.jsx b/frontend/src/_ui/Icon/solidIcons/BigIntCol.jsx
new file mode 100644
index 0000000000..f95735ce6f
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/BigIntCol.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+const BigIntCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
+
+);
+
+export default BigIntCol;
diff --git a/frontend/src/_ui/Icon/solidIcons/BooleanCol.jsx b/frontend/src/_ui/Icon/solidIcons/BooleanCol.jsx
new file mode 100644
index 0000000000..8de12d2963
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/BooleanCol.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+
+const BooleanCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
+
+);
+
+export default BooleanCol;
diff --git a/frontend/src/_ui/Icon/solidIcons/DatetimeCol.jsx b/frontend/src/_ui/Icon/solidIcons/DatetimeCol.jsx
new file mode 100644
index 0000000000..020d7b5f95
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/DatetimeCol.jsx
@@ -0,0 +1,30 @@
+import React from 'react';
+
+const DatetimeCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
+
+);
+
+export default DatetimeCol;
diff --git a/frontend/src/_ui/Icon/solidIcons/FloatCol.jsx b/frontend/src/_ui/Icon/solidIcons/FloatCol.jsx
new file mode 100644
index 0000000000..86e9845df1
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/FloatCol.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+
+const FloatCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
+
+);
+
+export default FloatCol;
diff --git a/frontend/src/_ui/Icon/solidIcons/IntegerCol.jsx b/frontend/src/_ui/Icon/solidIcons/IntegerCol.jsx
new file mode 100644
index 0000000000..8312c92aea
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/IntegerCol.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+
+const IntegerCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
+
+);
+
+export default IntegerCol;
diff --git a/frontend/src/_ui/Icon/solidIcons/Jsonb.jsx b/frontend/src/_ui/Icon/solidIcons/Jsonb.jsx
new file mode 100644
index 0000000000..6124cf8ad8
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/Jsonb.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+const Jsonb = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
+
+);
+
+export default Jsonb;
diff --git a/frontend/src/_ui/Icon/solidIcons/SerialCol.jsx b/frontend/src/_ui/Icon/solidIcons/SerialCol.jsx
new file mode 100644
index 0000000000..cee27441dc
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/SerialCol.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+
+const SerialCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
+
+);
+
+export default SerialCol;
diff --git a/frontend/src/_ui/Icon/solidIcons/VarcharCol.jsx b/frontend/src/_ui/Icon/solidIcons/VarcharCol.jsx
new file mode 100644
index 0000000000..da1a7d9618
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/VarcharCol.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+
+const VarcharCol = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
+
+);
+
+export default VarcharCol;
diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js
index d4106ae049..9c84429d09 100644
--- a/frontend/src/_ui/Icon/solidIcons/index.js
+++ b/frontend/src/_ui/Icon/solidIcons/index.js
@@ -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 ;
case 'TriangleDownCenter':
return ;
+ case 'jsonb':
+ return ;
+ case 'character varying':
+ return ;
+ case 'integer':
+ return ;
+ case 'bigint':
+ return ;
+ case 'double precision':
+ return ;
+ case 'boolean':
+ return ;
+ case 'serial':
+ return ;
+ case 'timestamp with time zone':
+ return ;
case 'AI-tag':
return ;
default:
diff --git a/server/src/dto/tooljet-db-join.dto.ts b/server/src/dto/tooljet-db-join.dto.ts
index 82b82c874b..b79750d50c 100644
--- a/server/src/dto/tooljet-db-join.dto.ts
+++ b/server/src/dto/tooljet-db-join.dto.ts
@@ -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 {
diff --git a/server/src/dto/tooljet-db.dto.ts b/server/src/dto/tooljet-db.dto.ts
index 2534b1efee..09dca956b2 100644
--- a/server/src/dto/tooljet-db.dto.ts
+++ b/server/src/dto/tooljet-db.dto.ts
@@ -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);
})
diff --git a/server/src/dto/validators/schemas/3.0.3/tooljet_database.json b/server/src/dto/validators/schemas/3.0.3/tooljet_database.json
new file mode 100644
index 0000000000..714935cba9
--- /dev/null
+++ b/server/src/dto/validators/schemas/3.0.3/tooljet_database.json
@@ -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"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/server/src/helpers/tooljet_db.helper.ts b/server/src/helpers/tooljet_db.helper.ts
index eff4fd9d6c..dc327858c8 100644
--- a/server/src/helpers/tooljet_db.helper.ts
+++ b/server/src/helpers/tooljet_db.helper.ts
@@ -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, inputValues) {
+ const inValidValueColumnsList = [];
+ Object.entries(inputValues).forEach(([key, value]) => {
+ if (jsonbColumnList.includes(key)) {
+ if (typeof value !== 'object') inValidValueColumnsList.push(key);
+ }
+ });
+ return inValidValueColumnsList;
+}
diff --git a/server/src/helpers/utils.helper.ts b/server/src/helpers/utils.helper.ts
index 41dde11284..cb9cadc6f4 100644
--- a/server/src/helpers/utils.helper.ts
+++ b/server/src/helpers/utils.helper.ts
@@ -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();
}
diff --git a/server/src/modules/tooljet_db/tooljet-db.types.ts b/server/src/modules/tooljet_db/tooljet-db.types.ts
index ef2f5e92a2..f72abaeb00 100644
--- a/server/src/modules/tooljet_db/tooljet-db.types.ts
+++ b/server/src/modules/tooljet_db/tooljet-db.types.ts
@@ -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 = {
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;
diff --git a/server/src/services/postgrest_proxy.service.ts b/server/src/services/postgrest_proxy.service.ts
index 12530b04c1..3b20ed5ba2 100644
--- a/server/src/services/postgrest_proxy.service.ts
+++ b/server/src/services/postgrest_proxy.service.ts
@@ -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) {
diff --git a/server/src/services/tooljet_db.service.ts b/server/src/services/tooljet_db.service.ts
index 37ff4b56f4..2d08c80803 100644
--- a/server/src/services/tooljet_db.service.ts
+++ b/server/src/services/tooljet_db.service.ts
@@ -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 };
};
diff --git a/server/src/services/tooljet_db_bulk_upload.service.ts b/server/src/services/tooljet_db_bulk_upload.service.ts
index a99a539a3d..bd01d13d06 100644
--- a/server/src/services/tooljet_db_bulk_upload.service.ts
+++ b/server/src/services/tooljet_db_bulk_upload.service.ts
@@ -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;
}
diff --git a/server/src/services/tooljet_db_operations.service.ts b/server/src/services/tooljet_db_operations.service.ts
index 0aa062a29b..5eab39bcaa 100644
--- a/server/src/services/tooljet_db_operations.service.ts
+++ b/server/src/services/tooljet_db_operations.service.ts
@@ -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);
}
}
});