mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
Merge pull request #7512 from ToolJet/release/marketplace_1.4
Release: Marketplace 1.4
This commit is contained in:
commit
328299badd
60 changed files with 3643 additions and 395 deletions
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
2.18.0
|
||||
2.19.0
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ export const createNewColumnAndVerify = (
|
|||
.should("be.visible")
|
||||
.and("have.text", commonText.createButton)
|
||||
.click();
|
||||
cy.wait(1000);
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
createNewColumnText.columnCreatedSuccessfullyToast
|
||||
|
|
@ -402,7 +403,8 @@ export const deleteCondition = (selector, columnName = [], deleteIcon) => {
|
|||
};
|
||||
export const deleteRowAndVerify = (tableName, rowNumber = []) => {
|
||||
navigateToTable(tableName);
|
||||
cy.wait("@dbLoad");
|
||||
cy.wait(1000);
|
||||
//cy.wait("@dbLoad");
|
||||
cy.get("body")
|
||||
.find(".table>>tr")
|
||||
.its("length")
|
||||
|
|
@ -441,7 +443,8 @@ export const editRowAndVerify = (
|
|||
cy.reload();
|
||||
cy.intercept("GET", "api/tooljet_db/organizations/**").as("dbLoad");
|
||||
navigateToTable(tableName);
|
||||
cy.wait("@dbLoad");
|
||||
cy.wait(1000);
|
||||
//cy.wait("@dbLoad");
|
||||
cy.get(editRowSelectors.editRowbutton).should("be.visible").click();
|
||||
cy.get(editRowSelectors.editRowHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
|
|
@ -497,7 +500,7 @@ export const editRowWithInvalidData = (
|
|||
) => {
|
||||
cy.intercept("GET", "api/tooljet_db/organizations/**").as("dbLoad");
|
||||
navigateToTable(tableName);
|
||||
cy.wait("@dbLoad");
|
||||
//cy.wait("@dbLoad");
|
||||
|
||||
cy.get(editRowSelectors.editRowbutton).should("be.visible").click();
|
||||
cy.get(editRowSelectors.editRowHeader).verifyVisibleElement(
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.17.5
|
||||
2.19.0
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
frontend/package-lock.json
generated
91
frontend/package-lock.json
generated
|
|
@ -14248,50 +14248,6 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"../plugins/node_modules/mysql": {
|
||||
"version": "2.18.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bignumber.js": "9.0.0",
|
||||
"readable-stream": "2.3.7",
|
||||
"safe-buffer": "5.1.2",
|
||||
"sqlstring": "2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"../plugins/node_modules/mysql/node_modules/bignumber.js": {
|
||||
"version": "9.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"../plugins/node_modules/mysql/node_modules/readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"../plugins/node_modules/mysql/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../plugins/node_modules/mysql/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"../plugins/node_modules/napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
|
|
@ -17818,13 +17774,6 @@
|
|||
"version": "1.0.3",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"../plugins/node_modules/sqlstring": {
|
||||
"version": "2.3.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"../plugins/node_modules/sshpk": {
|
||||
"version": "1.17.0",
|
||||
"license": "MIT",
|
||||
|
|
@ -20141,7 +20090,6 @@
|
|||
"dependencies": {
|
||||
"@tooljet-plugins/common": "file:../common",
|
||||
"knex": "^0.95.15",
|
||||
"mysql": "^2.18.1",
|
||||
"mysql2": "^3.6.0",
|
||||
"react": "^17.0.2",
|
||||
"rimraf": "^3.0.2"
|
||||
|
|
@ -67962,7 +67910,6 @@
|
|||
"requires": {
|
||||
"@tooljet-plugins/common": "file:../common",
|
||||
"knex": "^0.95.15",
|
||||
"mysql": "^2.18.1",
|
||||
"mysql2": "^3.6.0",
|
||||
"react": "^17.0.2",
|
||||
"rimraf": "^3.0.2"
|
||||
|
|
@ -73332,41 +73279,6 @@
|
|||
"version": "0.0.8",
|
||||
"dev": true
|
||||
},
|
||||
"mysql": {
|
||||
"version": "2.18.1",
|
||||
"requires": {
|
||||
"bignumber.js": "9.0.0",
|
||||
"readable-stream": "2.3.7",
|
||||
"safe-buffer": "5.1.2",
|
||||
"sqlstring": "2.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"bignumber.js": {
|
||||
"version": "9.0.0"
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2"
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
"optional": true
|
||||
|
|
@ -75740,9 +75652,6 @@
|
|||
"sprintf-js": {
|
||||
"version": "1.0.3"
|
||||
},
|
||||
"sqlstring": {
|
||||
"version": "2.3.1"
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.17.0",
|
||||
"requires": {
|
||||
|
|
|
|||
|
|
@ -121,19 +121,18 @@ export const EventManager = ({
|
|||
});
|
||||
return componentOptions;
|
||||
}
|
||||
// currently blocking items inside subcontainer because they can't be controlled through event manager
|
||||
// use components instead of currentState?.components to get all the components in canvas
|
||||
|
||||
function getComponentOptionsOfComponentsWithActions(componentType = '') {
|
||||
let componentOptions = [];
|
||||
Object.values(currentState?.components || {}).forEach((value) => {
|
||||
Object.keys(components || {}).forEach((key) => {
|
||||
const targetComponentMeta = componentTypes.find(
|
||||
(componentType) => components[value.id]?.component?.component === componentType?.component
|
||||
(componentType) => components[key].component.component === componentType.component
|
||||
);
|
||||
if ((targetComponentMeta?.actions?.length ?? 0) > 0) {
|
||||
if (componentType === '' || components[value.id].component.component === componentType) {
|
||||
if (componentType === '' || components[key].component.component === componentType) {
|
||||
componentOptions.push({
|
||||
name: components[value.id].component.name,
|
||||
value: value.id,
|
||||
name: components[key].component.name,
|
||||
value: key,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
|
||||
function useConfirm() {
|
||||
const [show, setShow] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [heading, setHeading] = useState('Confirm action?');
|
||||
const [handleConfirm, setHandleConfirm] = useState(null);
|
||||
|
||||
const confirm = (message, heading) => {
|
||||
return new Promise((resolve) => {
|
||||
setMessage(message);
|
||||
setHeading(heading);
|
||||
setShow(true);
|
||||
|
||||
const confirmCallback = (result) => {
|
||||
setShow(false);
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
setHandleConfirm(() => confirmCallback);
|
||||
});
|
||||
};
|
||||
|
||||
const ConfirmDialog = useCallback(
|
||||
({ confirmButtonText = '', cancelButtonText = '', darkMode }) => {
|
||||
return (
|
||||
<Modal
|
||||
show={show}
|
||||
animation={false}
|
||||
onHide={() => handleConfirm(false)}
|
||||
centered
|
||||
size="sm"
|
||||
contentClassName={darkMode ? 'theme-dark dark-theme' : ''}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{heading || 'Confirm action ?'}</Modal.Title>
|
||||
<svg
|
||||
onClick={() => handleConfirm(false)}
|
||||
className="cursor-pointer"
|
||||
width="33"
|
||||
height="33"
|
||||
viewBox="0 0 33 33"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M11.5996 11.6201C11.8599 11.3597 12.282 11.3597 12.5424 11.6201L16.071 15.1487L19.5996 11.6201C19.8599 11.3597 20.282 11.3597 20.5424 11.6201C20.8027 11.8804 20.8027 12.3025 20.5424 12.5629L17.0138 16.0915L20.5424 19.6201C20.8027 19.8804 20.8027 20.3025 20.5424 20.5629C20.282 20.8232 19.8599 20.8232 19.5996 20.5629L16.071 17.0343L12.5424 20.5629C12.282 20.8232 11.8599 20.8232 11.5996 20.5629C11.3392 20.3025 11.3392 19.8804 11.5996 19.6201L15.1282 16.0915L11.5996 12.5629C11.3392 12.3025 11.3392 11.8804 11.5996 11.6201Z"
|
||||
fill="var(--slate12)"
|
||||
/>
|
||||
</svg>
|
||||
</Modal.Header>
|
||||
<Modal.Body data-cy={'tjdb-delete-confirmation-modal-message'}>{message}</Modal.Body>
|
||||
<Modal.Footer
|
||||
style={{
|
||||
borderTop: '1px solid var(--slate5)',
|
||||
padding: '0.875rem 1.5rem',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className="btn"
|
||||
onClick={() => handleConfirm(false)}
|
||||
data-cy={'tjdb-delete-confirmation-modal-cancel-btn'}
|
||||
>
|
||||
{cancelButtonText === '' ? 'Cancel' : cancelButtonText}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={() => handleConfirm(true)}
|
||||
data-cy={'tjdb-delete-confirmation-modal-confirm-btn'}
|
||||
>
|
||||
{confirmButtonText === '' ? 'Yes' : confirmButtonText}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
[show, message, heading, handleConfirm]
|
||||
);
|
||||
|
||||
return { confirm, ConfirmDialog };
|
||||
}
|
||||
|
||||
export default useConfirm;
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import SelectBox from './SelectBox';
|
||||
import cx from 'classnames';
|
||||
import useShowPopover from '@/_hooks/useShowPopover';
|
||||
import { Badge, OverlayTrigger, Popover } from 'react-bootstrap';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import CheveronDown from '@/_ui/Icon/bulkIcons/CheveronDown';
|
||||
import Remove from '@/_ui/Icon/bulkIcons/Remove';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
const DropDownSelect = ({
|
||||
darkMode,
|
||||
disabled,
|
||||
options,
|
||||
isMulti,
|
||||
addBtnLabel,
|
||||
onAdd,
|
||||
onChange,
|
||||
value,
|
||||
renderSelected,
|
||||
emptyError,
|
||||
shouldCenterAlignText = false,
|
||||
showPlaceHolder = false,
|
||||
}) => {
|
||||
const popoverId = useRef(`dd-select-${uuidv4()}`);
|
||||
const popoverBtnId = useRef(`dd-select-btn-${uuidv4()}`);
|
||||
const [showMenu, setShowMenu] = useShowPopover(false, `#${popoverId.current}`, `#${popoverBtnId.current}`);
|
||||
const [selected, setSelected] = useState(value);
|
||||
const selectRef = useRef();
|
||||
const [isOverflown, setIsOverflown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (showMenu) {
|
||||
// selectRef.current.focus();
|
||||
}
|
||||
}, [showMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Array.isArray(value) || selected?.value !== value?.value || selected?.label !== value?.label) {
|
||||
setSelected(value);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
// onChange && onChange(selected);
|
||||
const badges = document.querySelectorAll('.dd-select-value-badge');
|
||||
if (isEmpty(badges)) {
|
||||
return () => {};
|
||||
}
|
||||
let isNewOverFlown = false;
|
||||
for (let i = 0; i < badges.length; i++) {
|
||||
const el = badges[i];
|
||||
isNewOverFlown = el.clientWidth - el.scrollWidth < 0;
|
||||
if (isOverflown) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isNewOverFlown !== isOverflown) {
|
||||
setIsOverflown(isNewOverFlown);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
function checkElementPosition() {
|
||||
const selectControl = document.getElementById(popoverBtnId.current);
|
||||
if (!selectControl) {
|
||||
return 'top-start';
|
||||
}
|
||||
|
||||
const elementRect = selectControl.getBoundingClientRect();
|
||||
|
||||
// Check proximity to top
|
||||
const halfScreenHeight = window.innerHeight / 2;
|
||||
|
||||
if (elementRect.top <= halfScreenHeight) {
|
||||
return 'bottom-start';
|
||||
}
|
||||
|
||||
return 'top-start';
|
||||
}
|
||||
|
||||
function isValidInput(input) {
|
||||
if (!input) return false;
|
||||
if (Array.isArray(input)) {
|
||||
return input.length ? true : false;
|
||||
}
|
||||
if (typeof input === 'object' && !Array.isArray(input)) {
|
||||
if (!Object.keys(input).length) return false;
|
||||
if (!input.value) return false;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
show={showMenu && !disabled}
|
||||
placement={checkElementPosition()}
|
||||
// placement="auto"
|
||||
// arrowOffsetTop={90}
|
||||
// arrowOffsetLeft={90}
|
||||
overlay={
|
||||
<Popover
|
||||
key={'page.i'}
|
||||
id={popoverId.current}
|
||||
className={`${darkMode && 'popover-dark-themed dark-theme tj-dark-mode'}`}
|
||||
style={{
|
||||
width: '244px',
|
||||
maxWidth: '246px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0px 2px 4px -2px rgba(16, 24, 40, 0.06), 0px 4px 8px -2px rgba(16, 24, 40, 0.10)',
|
||||
}}
|
||||
>
|
||||
<SelectBox
|
||||
options={options}
|
||||
isMulti={isMulti}
|
||||
onSelect={(values) => {
|
||||
setIsOverflown(false);
|
||||
onChange && onChange(values);
|
||||
setSelected(values);
|
||||
}}
|
||||
selected={selected}
|
||||
closePopup={() => setShowMenu(false)}
|
||||
onAdd={onAdd}
|
||||
addBtnLabel={addBtnLabel}
|
||||
emptyError={emptyError}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<span className="col-auto" id={popoverBtnId.current}>
|
||||
<ButtonSolid
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
setShowMenu((show) => !show);
|
||||
}}
|
||||
className={cx(
|
||||
{
|
||||
'justify-content-start': !shouldCenterAlignText,
|
||||
'justify-content-centre': shouldCenterAlignText,
|
||||
},
|
||||
'tdb-dropdown-btn',
|
||||
'gap-0',
|
||||
'w-100',
|
||||
'border-0',
|
||||
'rounded-0',
|
||||
'position-relative',
|
||||
'font-weight-normal',
|
||||
'px-1'
|
||||
)}
|
||||
data-cy={`show-ds-popover-button`}
|
||||
>
|
||||
<div className={`text-truncate`}>
|
||||
{renderSelected && renderSelected(selected)}
|
||||
|
||||
{!renderSelected && isValidInput(selected) ? (
|
||||
Array.isArray(selected) ? (
|
||||
!isOverflown && (
|
||||
<MultiSelectValueBadge
|
||||
options={options}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
selected?.label
|
||||
)
|
||||
) : showPlaceHolder ? (
|
||||
<span style={{ color: '#9e9e9e' }}>Select..</span>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{!renderSelected && isOverflown && !Array.isArray(selected) && (
|
||||
<Badge className="me-1 dd-select-value-badge" bg="secondary">
|
||||
{selected?.length} selected
|
||||
<span
|
||||
role="button"
|
||||
onClick={(e) => {
|
||||
setSelected([]);
|
||||
onChange && onChange([]);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Remove fill="var(--slate12)" width="12px" />
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="dd-select-control-chevron">
|
||||
<CheveronDown width="15" height="15" />
|
||||
</div>
|
||||
</ButtonSolid>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
function MultiSelectValueBadge({ options, selected, setSelected, onChange }) {
|
||||
if (options?.length === selected?.length && selected?.length !== 0) {
|
||||
// Filter Options without 'Select All'
|
||||
const optionsWithoutSelectAll = options.filter((option) => option.value !== 'SELECT ALL');
|
||||
return (
|
||||
<Badge className={`me-1 dd-select-value-badge`} bg="secondary">
|
||||
All {optionsWithoutSelectAll?.length} selected
|
||||
<span
|
||||
role="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelected([]);
|
||||
onChange([]);
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Remove fill="var(--slate12)" />
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return selected.map((option) => (
|
||||
<Badge key={option.value} className="me-1 dd-select-value-badge" bg="secondary">
|
||||
{option.label}
|
||||
<span
|
||||
role="button"
|
||||
onClick={(e) => {
|
||||
setSelected((selected) => {
|
||||
onChange && onChange(selected.filter((opt) => opt.value !== option.value));
|
||||
return selected.filter((opt) => opt.value !== option.value);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Remove fill="var(--slate12)" />
|
||||
</span>
|
||||
</Badge>
|
||||
));
|
||||
}
|
||||
|
||||
export default DropDownSelect;
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
|
||||
import DropDownSelect from './DropDownSelect';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import AddRectangle from '@/_ui/Icon/bulkIcons/AddRectangle';
|
||||
import Trash from '@/_ui/Icon/solidIcons/Trash';
|
||||
import Remove from '@/_ui/Icon/solidIcons/Remove';
|
||||
import Information from '@/_ui/Icon/solidIcons/Information';
|
||||
import Icon from '@/_ui/Icon/solidIcons/index';
|
||||
import set from 'lodash/set';
|
||||
import { cloneDeep, isEmpty } from 'lodash';
|
||||
import { getPrivateRoute } from '@/_helpers/routes';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useConfirm from './Confirm';
|
||||
|
||||
const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
|
||||
const { selectedTableId, tables, joinOptions, findTableDetails } = useContext(TooljetDatabaseContext);
|
||||
const joinType = data?.joinType;
|
||||
const baseTableDetails = (selectedTableId && findTableDetails(selectedTableId)) || {};
|
||||
const conditionsList = isEmpty(data?.conditions?.conditionsList) ? [{}] : data?.conditions?.conditionsList;
|
||||
|
||||
const operator = data?.conditions?.operator;
|
||||
const leftFieldTable = conditionsList?.[0]?.leftField?.table || selectedTableId;
|
||||
const rightFieldTable = conditionsList?.[0]?.rightField?.table;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { confirm, ConfirmDialog } = useConfirm();
|
||||
|
||||
const tableSet = new Set();
|
||||
(joinOptions || [])
|
||||
.filter((_join, i) => i < index)
|
||||
.forEach((join) => {
|
||||
const { table, conditions } = join;
|
||||
tableSet.add(table);
|
||||
conditions?.conditionsList?.forEach((condition) => {
|
||||
const { leftField, rightField } = condition;
|
||||
if (leftField?.table) {
|
||||
tableSet.add(leftField?.table);
|
||||
}
|
||||
if (rightField?.table) {
|
||||
tableSet.add(rightField?.table);
|
||||
}
|
||||
});
|
||||
});
|
||||
tableSet.add(selectedTableId);
|
||||
|
||||
const leftTableList = [...tableSet]
|
||||
.filter((table) => table !== rightFieldTable)
|
||||
.map((t) => {
|
||||
const tableDetails = findTableDetails(t);
|
||||
return { label: tableDetails?.table_name ?? '', value: t };
|
||||
});
|
||||
|
||||
const tableList = tables
|
||||
.filter((table) => ![...tableSet, leftFieldTable].includes(table.table_id))
|
||||
.map((t) => {
|
||||
return { label: t?.table_name ?? '', value: t.table_id };
|
||||
});
|
||||
|
||||
return (
|
||||
<Container fluid className="p-0">
|
||||
<Row className={`mx-0 ${index === 0 && 'pb-2'}`}>
|
||||
<Col sm="2"></Col>
|
||||
<Col sm="4" className="text-left">
|
||||
Selected Table
|
||||
</Col>
|
||||
<Col sm="1"></Col>
|
||||
<Col sm="4" className="text-left">
|
||||
Joining Table
|
||||
</Col>
|
||||
{index !== 0 && (
|
||||
<Col sm="1" className="justify-content-end d-flex pe-0">
|
||||
<ButtonSolid
|
||||
variant="ghostBlack"
|
||||
size="sm"
|
||||
className="p-0"
|
||||
onClick={async () => {
|
||||
const result = await confirm(
|
||||
'Deleting a join will also delete its associated conditions. Are you sure you want to continue ?',
|
||||
'Delete'
|
||||
);
|
||||
if (result) onRemove();
|
||||
}}
|
||||
>
|
||||
<Remove style={{ height: '16px' }} />
|
||||
</ButtonSolid>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
<Row className="border rounded mb-2 mx-0">
|
||||
<Col sm="2" className="p-0 border-end">
|
||||
<div className="tj-small-btn px-2">Join</div>
|
||||
</Col>
|
||||
<Col sm="4" className="p-0 border-end">
|
||||
{index ? (
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
options={leftTableList}
|
||||
darkMode={darkMode}
|
||||
onChange={async (value) => {
|
||||
let result = false;
|
||||
if (leftFieldTable.length) {
|
||||
result = await confirm(
|
||||
'Changing the table will also delete its associated conditions. Are you sure you want to continue?',
|
||||
'Change table?'
|
||||
);
|
||||
} else {
|
||||
result = true;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const newData = cloneDeep({ ...data });
|
||||
const { conditionsList = [{}] } = newData?.conditions || {};
|
||||
const newConditionsList = conditionsList.map((condition) => {
|
||||
const newCondition = { ...condition };
|
||||
set(newCondition, 'leftField.table', value?.value);
|
||||
set(newCondition, 'operator', '='); //should we removed when we have more options
|
||||
return newCondition;
|
||||
});
|
||||
set(newData, 'conditions.conditionsList', newConditionsList);
|
||||
// set(newData, 'table', value?.value);
|
||||
onChange(newData);
|
||||
}
|
||||
}}
|
||||
onAdd={() => navigate(getPrivateRoute('database'))}
|
||||
addBtnLabel={'Add new table'}
|
||||
value={leftTableList.find((val) => val?.value === leftFieldTable)}
|
||||
/>
|
||||
) : (
|
||||
<div className="tj-small-btn px-2">{baseTableDetails?.table_name ?? ''}</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col sm="1" className="p-0 border-end">
|
||||
<DropDownSelect
|
||||
shouldCenterAlignText
|
||||
options={staticJoinOperationsList}
|
||||
darkMode={darkMode}
|
||||
onChange={(value) => onChange({ ...data, joinType: value?.value })}
|
||||
value={staticJoinOperationsList.find((val) => val.value === joinType)}
|
||||
renderSelected={(selected) =>
|
||||
selected ? (
|
||||
<div className="w-100">
|
||||
<Icon name={selected?.icon} height={20} width={20} viewBox="" />
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col sm="5" className="p-0">
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
options={tableList}
|
||||
darkMode={darkMode}
|
||||
onChange={async (value) => {
|
||||
let result = true;
|
||||
if (rightFieldTable?.length) {
|
||||
result = await confirm(
|
||||
'Changing the table will also delete its associated conditions. Are you sure you want to continue?',
|
||||
'Change table?'
|
||||
);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const newData = cloneDeep({ ...data });
|
||||
const { conditionsList = [] } = newData?.conditions || {};
|
||||
const newConditionsList = conditionsList.map((condition) => {
|
||||
const newCondition = { ...condition };
|
||||
set(newCondition, 'rightField.table', value?.value);
|
||||
set(newCondition, 'operator', '='); //should we removed when we have more options
|
||||
return newCondition;
|
||||
});
|
||||
set(newData, 'conditions.conditionsList', newConditionsList);
|
||||
set(newData, 'table', value?.value);
|
||||
onChange(newData);
|
||||
}
|
||||
}}
|
||||
onAdd={() => navigate(getPrivateRoute('database'))}
|
||||
addBtnLabel={'Add new table'}
|
||||
value={tableList.find((val) => val?.value === rightFieldTable)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{conditionsList.map((condition, index) => (
|
||||
<JoinOn
|
||||
condition={condition}
|
||||
leftFieldTable={leftFieldTable}
|
||||
rightFieldTable={rightFieldTable}
|
||||
darkMode={darkMode}
|
||||
key={index}
|
||||
index={index}
|
||||
groupOperator={operator}
|
||||
onOperatorChange={(value) => {
|
||||
const newData = cloneDeep(data);
|
||||
set(newData, 'conditions.operator', value);
|
||||
onChange(newData);
|
||||
}}
|
||||
onChange={(value) => {
|
||||
const newConditionsList = conditionsList.map((con, i) => {
|
||||
if (i === index) {
|
||||
return value;
|
||||
}
|
||||
return con;
|
||||
});
|
||||
const newData = cloneDeep(data);
|
||||
set(newData, 'conditions.conditionsList', newConditionsList);
|
||||
onChange(newData);
|
||||
}}
|
||||
onRemove={() => {
|
||||
const newConditionsList = conditionsList.filter((_cond, i) => i !== index);
|
||||
const newData = cloneDeep(data);
|
||||
set(newData, 'conditions.conditionsList', newConditionsList);
|
||||
onChange(newData);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<Row className="mb-2 mx-0">
|
||||
<Col className="p-0">
|
||||
<ButtonSolid
|
||||
variant="ghostBlue"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newData = { ...data };
|
||||
set(newData, 'conditions.conditionsList', [...conditionsList, { operator: '=' }]);
|
||||
onChange(newData);
|
||||
}}
|
||||
>
|
||||
<AddRectangle width="15" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />
|
||||
Add more
|
||||
</ButtonSolid>
|
||||
</Col>
|
||||
</Row>
|
||||
<ConfirmDialog confirmButtonText="Continue" darkMode={darkMode} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
const JoinOn = ({
|
||||
condition,
|
||||
leftFieldTable,
|
||||
rightFieldTable,
|
||||
darkMode,
|
||||
index,
|
||||
onChange,
|
||||
groupOperator,
|
||||
onOperatorChange,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { tableInfo, findTableDetails } = useContext(TooljetDatabaseContext);
|
||||
const { operator, leftField, rightField } = condition;
|
||||
const leftFieldColumn = leftField?.columnName;
|
||||
const rightFieldColumn = rightField?.columnName;
|
||||
|
||||
const leftFieldTableDetails = (leftFieldTable && findTableDetails(leftFieldTable)) || {};
|
||||
const rightFieldTableDetails = (rightFieldTable && findTableDetails(rightFieldTable)) || {};
|
||||
|
||||
const leftFieldOptions = leftFieldTableDetails?.table_name
|
||||
? tableInfo[leftFieldTableDetails.table_name]?.map((col) => ({ label: col.Header, value: col.Header })) ?? []
|
||||
: [];
|
||||
const selectedLeftField = leftFieldTableDetails?.table_name
|
||||
? tableInfo[leftFieldTableDetails.table_name]?.find((col) => col.Header === leftFieldColumn) ?? []
|
||||
: {};
|
||||
|
||||
const rightFieldOptions = rightFieldTableDetails?.table_name
|
||||
? tableInfo[rightFieldTableDetails.table_name]
|
||||
?.filter((col) => {
|
||||
if (selectedLeftField?.dataType) {
|
||||
return col.dataType === selectedLeftField.dataType;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((col) => ({ label: col.Header, value: col.Header })) || []
|
||||
: [];
|
||||
|
||||
const _operators = [{ label: '=', value: '=' }];
|
||||
|
||||
const groupOperators = [
|
||||
{ value: 'AND', label: 'AND' },
|
||||
{ value: 'OR', label: 'OR' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Row className="border rounded mb-2 mx-0">
|
||||
<Col
|
||||
sm="2"
|
||||
className="p-0 border-end"
|
||||
// data-tooltip-id={`tdb-join-operator-tooltip-${index}`}
|
||||
// data-tooltip-content={
|
||||
// index > 1
|
||||
// ? 'The operation is defined by the first condition'
|
||||
// : 'This operation will define all the following conditions'
|
||||
// }
|
||||
>
|
||||
{index == 1 && (
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
options={groupOperators}
|
||||
darkMode={darkMode}
|
||||
value={groupOperators.find((op) => op.value === groupOperator)}
|
||||
onChange={(value) => {
|
||||
onOperatorChange && onOperatorChange(value?.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{index == 0 && <div className="tj-small-btn px-2">On</div>}
|
||||
{index > 1 && (
|
||||
<div className="tj-small-btn px-2" style={{ color: 'var(--slate9)' }}>
|
||||
{groupOperator}
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col sm="4" className="p-0 border-end">
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
options={leftFieldOptions}
|
||||
darkMode={darkMode}
|
||||
emptyError={
|
||||
<div className="dd-select-alert-error m-2 d-flex align-items-center">
|
||||
<Information />
|
||||
No table selected
|
||||
</div>
|
||||
}
|
||||
value={leftFieldOptions.find((opt) => opt.value === leftFieldColumn)}
|
||||
onChange={(value) => {
|
||||
onChange &&
|
||||
onChange({
|
||||
...condition,
|
||||
leftField: {
|
||||
...condition.leftField,
|
||||
columnName: value?.value,
|
||||
type: 'Column',
|
||||
table: leftFieldTable,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col sm="1" className="p-0 border-end">
|
||||
{/* <DropDownSelect
|
||||
options={operators}
|
||||
darkMode={darkMode}
|
||||
value={operators.find((op) => op.value === operator)}
|
||||
onChange={(value) => {
|
||||
onChange && onChange({ ...condition, operator: value?.value });
|
||||
}}
|
||||
/> */}
|
||||
|
||||
{/* Above line is commented and value is hardcoded as below */}
|
||||
|
||||
<div className="tj-small-btn px-2 text-center">{operator}</div>
|
||||
</Col>
|
||||
<Col sm="5" className="p-0 d-flex">
|
||||
<div className="flex-grow-1">
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
options={rightFieldOptions}
|
||||
emptyError={
|
||||
<div className="dd-select-alert-error m-2 d-flex align-items-center">
|
||||
<Information />
|
||||
{rightFieldTable ? 'No columns of the same data type' : 'No table selected'}
|
||||
</div>
|
||||
}
|
||||
darkMode={darkMode}
|
||||
value={rightFieldOptions.find((opt) => opt.value === rightFieldColumn)}
|
||||
onChange={(value) => {
|
||||
onChange &&
|
||||
onChange({
|
||||
...condition,
|
||||
rightField: {
|
||||
...condition.rightField,
|
||||
columnName: value?.value,
|
||||
type: 'Column',
|
||||
table: rightFieldTable,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{index > 0 && (
|
||||
<ButtonSolid size="sm" variant="ghostBlack" className="px-1 rounded-0 border-start" onClick={onRemove}>
|
||||
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
|
||||
</ButtonSolid>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{/* {index > 0 && (
|
||||
<Tooltip
|
||||
id={`tdb-join-operator-tooltip-${index}`}
|
||||
className="tooltip"
|
||||
place="left"
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
width: '180px',
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
// Base Component for Join Drop Down ----------
|
||||
const staticJoinOperationsList = [
|
||||
{ label: 'Inner Join', value: 'INNER', icon: 'innerjoin' },
|
||||
{ label: 'Left Join', value: 'LEFT', icon: 'leftouterjoin' },
|
||||
{ label: 'Right Join', value: 'RIGHT', icon: 'rightouterjoin' },
|
||||
{ label: 'Full Outer Join', value: 'FULL OUTER', icon: 'fullouterjoin' },
|
||||
];
|
||||
|
||||
export default JoinConstraint;
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { Col, Container, Row } from 'react-bootstrap';
|
||||
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
|
||||
import DropDownSelect from './DropDownSelect';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
export default function JoinSelect({ darkMode }) {
|
||||
const { joinOptions, tableInfo, joinTableOptions, joinTableOptionsChange, findTableDetails } =
|
||||
useContext(TooljetDatabaseContext);
|
||||
|
||||
const joinSelectOptions = cloneDeep(joinTableOptions['fields']) || [];
|
||||
const setJoinSelectOptions = (fields) => {
|
||||
joinTableOptionsChange('fields', fields);
|
||||
};
|
||||
|
||||
const tableSet = new Set();
|
||||
(joinOptions || []).forEach((join) => {
|
||||
const { table, conditions } = join;
|
||||
tableSet.add(table);
|
||||
conditions?.conditionsList?.forEach((condition) => {
|
||||
const { leftField, rightField } = condition;
|
||||
if (leftField?.table) {
|
||||
tableSet.add(leftField?.table);
|
||||
}
|
||||
if (rightField?.table) {
|
||||
tableSet.add(rightField?.table);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const tables = [...tableSet].filter((table) => !!table);
|
||||
const tableOptions = {};
|
||||
for (let index = 0; index < tables.length; index++) {
|
||||
const tableId = tables[index];
|
||||
|
||||
const tableDetails = findTableDetails(tableId);
|
||||
if (tableDetails?.table_name) {
|
||||
tableOptions[tableId] = (tableInfo[tableDetails.table_name] || []).map((column) => ({
|
||||
label: column.Header,
|
||||
value: column.Header,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// When column name are same, alias has been added
|
||||
const handleChange = (columns, table) => {
|
||||
const unchangedSelectFields = [];
|
||||
const prevSelectedFields = [];
|
||||
joinSelectOptions.forEach((t) => {
|
||||
if (t.table !== table) unchangedSelectFields.push(t);
|
||||
if (t.table === table) prevSelectedFields.push(t);
|
||||
});
|
||||
|
||||
// Select All & Deselect Functionality
|
||||
const allColumnsOfTable = tableOptions[table] ?? [];
|
||||
const columnsWithoutSelectAllOption = columns.filter((column) => column.value !== 'SELECT ALL');
|
||||
const isSelectAllExists = columns.findIndex((column) => column.value === 'SELECT ALL') >= 0;
|
||||
|
||||
let newSelectFields = [...unchangedSelectFields];
|
||||
if (
|
||||
(!isSelectAllExists && prevSelectedFields.length !== columnsWithoutSelectAllOption.length) ||
|
||||
(isSelectAllExists && prevSelectedFields.length === allColumnsOfTable.length)
|
||||
)
|
||||
columnsWithoutSelectAllOption.forEach((column) => newSelectFields.push({ name: column?.value, table }));
|
||||
// Push all the Columns When Select All options is clicked
|
||||
if (isSelectAllExists && allColumnsOfTable.length && prevSelectedFields.length !== allColumnsOfTable.length)
|
||||
allColumnsOfTable.forEach((column) => newSelectFields.push({ name: column?.value, table }));
|
||||
|
||||
newSelectFields = newSelectFields.map((field) => {
|
||||
if (newSelectFields.filter(({ name }) => name === field.name).length > 1 && !('alias' in field)) {
|
||||
return {
|
||||
...field,
|
||||
// alias: field.table + '_' + field.name,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
// ...(!('alias' in field) && { alias: field.table + '_' + field.name }),
|
||||
};
|
||||
});
|
||||
setJoinSelectOptions(newSelectFields);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container fluid className="p-0">
|
||||
{tables.length ? (
|
||||
tables.map((table) => {
|
||||
const respectiveTableSelectedOptions = joinSelectOptions.filter((val) => val?.table === table);
|
||||
const respectiveTableOptions = tableOptions[table] ?? [];
|
||||
return (
|
||||
<Row key={table} className="border rounded mb-2 mx-0">
|
||||
<Col sm="3" className="p-0 border-end">
|
||||
<div className="tj-small-btn px-2">{findTableDetails(table)?.table_name ?? ''}</div>
|
||||
</Col>
|
||||
<Col sm="9" className="p-0 border-end">
|
||||
<DropDownSelect
|
||||
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>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Row className="mb-2 mx-0">
|
||||
<div
|
||||
style={{
|
||||
gap: '4px',
|
||||
height: '30px',
|
||||
border: '1px dashed var(--slate-08, #C1C8CD)',
|
||||
}}
|
||||
className="px-4 py-2 text-center rounded-1"
|
||||
>
|
||||
<SolidIcon name="information" style={{ height: 14, width: 14 }} width={14} height={14} /> Tables are not
|
||||
selected
|
||||
</div>
|
||||
</Row>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { Col, Container, Row } from 'react-bootstrap';
|
||||
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
|
||||
import DropDownSelect from './DropDownSelect';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import Trash from '@/_ui/Icon/solidIcons/Trash';
|
||||
import AddRectangle from '@/_ui/Icon/bulkIcons/AddRectangle';
|
||||
import { isEmpty } from 'lodash';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
export default function JoinSort({ darkMode }) {
|
||||
const { tableInfo, joinOrderByOptions, setJoinOrderByOptions, joinOptions, findTableDetails } =
|
||||
useContext(TooljetDatabaseContext);
|
||||
|
||||
const tableSet = new Set();
|
||||
(joinOptions || []).forEach((join) => {
|
||||
const { table, conditions } = join;
|
||||
tableSet.add(table);
|
||||
conditions?.conditionsList?.forEach((condition) => {
|
||||
const { leftField, rightField } = condition;
|
||||
if (leftField?.table) {
|
||||
tableSet.add(leftField?.table);
|
||||
}
|
||||
if (rightField?.table) {
|
||||
tableSet.add(rightField?.table);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const tables = [...tableSet];
|
||||
const tableList = [];
|
||||
|
||||
tables.forEach((tableId) => {
|
||||
const tableDetails = findTableDetails(tableId);
|
||||
if (tableDetails?.table_name && tableInfo[tableDetails.table_name]) {
|
||||
const tableDetailsForDropDown = {
|
||||
label: tableDetails.table_name,
|
||||
value: tableId,
|
||||
options:
|
||||
tableInfo[tableDetails.table_name]?.map((columns) => ({
|
||||
label: columns.Header,
|
||||
value: columns.Header + '_' + tableId,
|
||||
table: tableId,
|
||||
})) || [],
|
||||
};
|
||||
tableList.push(tableDetailsForDropDown);
|
||||
}
|
||||
});
|
||||
|
||||
const sortbyConstants = [
|
||||
{ label: 'Ascending', value: 'ASC' },
|
||||
{ label: 'Descending', value: 'DESC' },
|
||||
];
|
||||
|
||||
return (
|
||||
<Container fluid className="p-0">
|
||||
{isEmpty(joinOrderByOptions) ? (
|
||||
<Row className="mb-2 mx-0">
|
||||
<div
|
||||
style={{
|
||||
gap: '4px',
|
||||
height: '30px',
|
||||
border: '1px dashed var(--slate-08, #C1C8CD)',
|
||||
}}
|
||||
className="px-4 py-2 text-center rounded-1"
|
||||
>
|
||||
<SolidIcon name="information" style={{ height: 14, width: 14 }} width={14} height={14} /> There are no
|
||||
conditions
|
||||
</div>
|
||||
</Row>
|
||||
) : (
|
||||
joinOrderByOptions.map((options, i) => {
|
||||
const tableDetails = options?.table ? findTableDetails(options?.table) : '';
|
||||
return (
|
||||
<Row className="border rounded mb-2 mx-0" key={i}>
|
||||
<Col sm="6" className="p-0 border-end">
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
options={tableList}
|
||||
darkMode={darkMode}
|
||||
value={{
|
||||
value: options?.columnName && options.table ? options.columnName + '_' + options.table : '',
|
||||
label: tableDetails?.table_name
|
||||
? tableDetails?.table_name + '.' + options.columnName
|
||||
: options.columnName,
|
||||
table: options.table,
|
||||
}}
|
||||
onChange={(option) => {
|
||||
setJoinOrderByOptions(
|
||||
joinOrderByOptions.map((sortBy, index) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...sortBy,
|
||||
columnName: option?.label,
|
||||
table: option.table,
|
||||
};
|
||||
}
|
||||
return sortBy;
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col sm="6" className="p-0 d-flex">
|
||||
<div className="flex-grow-1 border-end">
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
options={sortbyConstants}
|
||||
darkMode={darkMode}
|
||||
value={sortbyConstants.find((opt) => opt.value === options.direction)}
|
||||
onChange={(option) => {
|
||||
setJoinOrderByOptions(
|
||||
joinOrderByOptions.map((sortBy, index) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...sortBy,
|
||||
direction: option?.value,
|
||||
};
|
||||
}
|
||||
return sortBy;
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ButtonSolid
|
||||
size="sm"
|
||||
variant="ghostBlack"
|
||||
className="px-1 rounded-0"
|
||||
onClick={() => setJoinOrderByOptions(joinOrderByOptions.filter((opt, idx) => idx !== i))}
|
||||
>
|
||||
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
|
||||
</ButtonSolid>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{/* Dynamically render below Row */}
|
||||
<Row className="mx-0">
|
||||
<Col className="p-0">
|
||||
<ButtonSolid variant="ghostBlue" size="sm" onClick={() => setJoinOrderByOptions([...joinOrderByOptions, {}])}>
|
||||
<AddRectangle width="15" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />
|
||||
Add more
|
||||
</ButtonSolid>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,483 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
|
||||
import { Col, Container, Row } from 'react-bootstrap';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import Trash from '@/_ui/Icon/solidIcons/Trash';
|
||||
import AddRectangle from '@/_ui/Icon/bulkIcons/AddRectangle';
|
||||
import { clone } from 'lodash';
|
||||
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
|
||||
import DropDownSelect from './DropDownSelect';
|
||||
import JoinConstraint from './JoinConstraint';
|
||||
import JoinSelect from './JoinSelect';
|
||||
import JoinSort from './JoinSort';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { filterOperatorOptions, nullOperatorOptions } from './util';
|
||||
|
||||
export const JoinTable = React.memo(({ darkMode }) => {
|
||||
return (
|
||||
<div>
|
||||
<SelectTableMenu darkMode={darkMode} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const SelectTableMenu = ({ darkMode }) => {
|
||||
const {
|
||||
selectedTableId,
|
||||
joinOptions,
|
||||
setJoinOptions: setJoins,
|
||||
joinTableOptions,
|
||||
joinTableOptionsChange,
|
||||
deleteJoinTableOptions,
|
||||
} = useContext(TooljetDatabaseContext);
|
||||
|
||||
const joins = clone(joinOptions);
|
||||
|
||||
const handleJoinChange = (newJoin, index) => {
|
||||
const updatedJoin = joinOptions.map((join, i) => {
|
||||
if (i === index) return newJoin;
|
||||
return join;
|
||||
});
|
||||
|
||||
const cleanedJoin = [];
|
||||
const tableSet = new Set();
|
||||
(updatedJoin || []).forEach((join, i) => {
|
||||
const { conditions } = join;
|
||||
let leftTable, rightTable;
|
||||
conditions?.conditionsList?.forEach((condition) => {
|
||||
const { leftField = {}, rightField = {} } = condition;
|
||||
if (leftField?.table) leftTable = leftField?.table;
|
||||
if (rightField?.table) rightTable = rightField?.table;
|
||||
});
|
||||
|
||||
if ((tableSet.has(leftTable) && !tableSet.has(rightTable)) || i === 0) {
|
||||
if (leftTable) tableSet.add(leftTable);
|
||||
if (rightTable) tableSet.add(rightTable);
|
||||
cleanedJoin.push({ ...join });
|
||||
}
|
||||
});
|
||||
// tableSet.add(selectedTable);
|
||||
setJoins(cleanedJoin);
|
||||
};
|
||||
|
||||
const calcUpdatedJoins = (updatedJoin) => {
|
||||
const cleanedJoin = [];
|
||||
const tableSet = new Set();
|
||||
(updatedJoin || []).forEach((join, i) => {
|
||||
const { _table, conditions } = join;
|
||||
let leftTable, rightTable;
|
||||
conditions?.conditionsList?.forEach((condition) => {
|
||||
const { leftField, rightField } = condition;
|
||||
if (leftField?.table) {
|
||||
// tableSet.add(leftField?.table);
|
||||
leftTable = leftField?.table;
|
||||
}
|
||||
if (rightField?.table) {
|
||||
// tableSet.add(rightField?.table);
|
||||
rightTable = rightField?.table;
|
||||
}
|
||||
});
|
||||
if ((tableSet.has(leftTable) && !tableSet.has(rightTable)) || i === 0) {
|
||||
tableSet.add(leftTable);
|
||||
tableSet.add(rightTable);
|
||||
cleanedJoin.push({ ...join });
|
||||
}
|
||||
});
|
||||
return cleanedJoin;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Join Section */}
|
||||
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
|
||||
<label className="form-label">From</label>
|
||||
<div className="field flex-grow-1 mt-1">
|
||||
{joins.map((join, joinIndex) => (
|
||||
<JoinConstraint
|
||||
darkMode={darkMode}
|
||||
key={join?.id}
|
||||
index={joinIndex}
|
||||
data={join}
|
||||
onChange={(value) => handleJoinChange(value, joinIndex)}
|
||||
onRemove={() => setJoins(calcUpdatedJoins(joins.filter((join, index) => index !== joinIndex)))}
|
||||
/>
|
||||
))}
|
||||
<Row className="mx-0">
|
||||
<ButtonSolid
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setJoins([
|
||||
...joins,
|
||||
{
|
||||
id: new Date().getTime(),
|
||||
conditions: {
|
||||
operator: 'AND',
|
||||
conditionsList: [
|
||||
{
|
||||
operator: '=',
|
||||
leftField: { table: selectedTableId },
|
||||
},
|
||||
],
|
||||
},
|
||||
joinType: 'INNER',
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
<AddRectangle width="15" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />
|
||||
Add another table
|
||||
</ButtonSolid>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
{/* Filter Section */}
|
||||
<div className="tdb-join-filtersection field-container d-flex" style={{ marginBottom: '1.5rem' }}>
|
||||
<label className="form-label">Filter</label>
|
||||
<div className="field flex-grow-1">
|
||||
<RenderFilterSection darkMode={darkMode} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Sort Section */}
|
||||
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
|
||||
<label className="form-label">Sort</label>
|
||||
<div className="field flex-grow-1">
|
||||
<JoinSort darkMode={darkMode} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Limit Section */}
|
||||
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
|
||||
<label className="form-label">Limit</label>
|
||||
<div className="field flex-grow-1">
|
||||
<CodeHinter
|
||||
className="codehinter-plugins"
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
height={'32px'}
|
||||
placeholder="Enter limit"
|
||||
type="code"
|
||||
initialValue={joinTableOptions?.limit ?? ''}
|
||||
onChange={(value) => {
|
||||
if (value.length) {
|
||||
joinTableOptionsChange('limit', value);
|
||||
} else {
|
||||
deleteJoinTableOptions('limit');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Offset Section */}
|
||||
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
|
||||
<label className="form-label">Offset</label>
|
||||
<div className="field flex-grow-1">
|
||||
<CodeHinter
|
||||
className="codehinter-plugins"
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
height={'32px'}
|
||||
placeholder="Enter offset"
|
||||
type="code"
|
||||
initialValue={joinTableOptions?.offset ?? ''}
|
||||
onChange={(value) => {
|
||||
if (value.length) {
|
||||
joinTableOptionsChange('offset', value);
|
||||
} else {
|
||||
deleteJoinTableOptions('offset');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Select Section */}
|
||||
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
|
||||
<label className="form-label">Select</label>
|
||||
<div className="field flex-grow-1">
|
||||
<JoinSelect darkMode={darkMode} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Component to Render Filter Section
|
||||
const RenderFilterSection = ({ darkMode }) => {
|
||||
const { tableInfo, joinTableOptions, joinTableOptionsChange, deleteJoinTableOptions, joinOptions, findTableDetails } =
|
||||
useContext(TooljetDatabaseContext);
|
||||
const { conditions = {} } = joinTableOptions;
|
||||
const { conditionsList = [] } = conditions;
|
||||
|
||||
function handleWhereFilterChange(conditionsEdited) {
|
||||
joinTableOptionsChange('conditions', conditionsEdited);
|
||||
}
|
||||
|
||||
function addNewFilterConditionEntry() {
|
||||
let editedFilterCondition = {};
|
||||
|
||||
const emptyConditionTemplate = { operator: '=', leftField: {}, rightField: {} };
|
||||
|
||||
// First time populate operator & conditionList details
|
||||
if (!Object.keys(conditions).length) {
|
||||
editedFilterCondition = {
|
||||
operator: 'AND',
|
||||
conditionsList: [{ ...emptyConditionTemplate }],
|
||||
};
|
||||
} else {
|
||||
editedFilterCondition = {
|
||||
...conditions,
|
||||
conditionsList: [...conditionsList, { ...emptyConditionTemplate }],
|
||||
};
|
||||
}
|
||||
|
||||
handleWhereFilterChange(editedFilterCondition);
|
||||
}
|
||||
|
||||
function removeFilterConditionEntry(index) {
|
||||
if (!Object.keys(conditions).length || !conditionsList.length) return;
|
||||
|
||||
// If there is one condition left, then make the 'conditions' state to default.
|
||||
let editedFilterConditions = {};
|
||||
if (conditionsList.length > 1) {
|
||||
editedFilterConditions = {
|
||||
...conditions,
|
||||
conditionsList: conditionsList.filter((condition, i) => i !== index),
|
||||
};
|
||||
}
|
||||
|
||||
if (Object.keys(editedFilterConditions).length === 0) {
|
||||
deleteJoinTableOptions('conditions');
|
||||
} else {
|
||||
handleWhereFilterChange(editedFilterConditions);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilterConditionEntry(type, indexToUpdate, valueToUpdate) {
|
||||
if (!Object.keys(conditions).length || !conditionsList.length) return;
|
||||
// type: Column | Value | Operator
|
||||
|
||||
// @desc : Input Need for Each Type
|
||||
// Column -> table, columnName, isLeftSideCondition
|
||||
// Value -> value, isLeftSideCondition
|
||||
// Operator -> operator
|
||||
|
||||
const editedConditionList = conditionsList.map((conditionDetail, index) => {
|
||||
if (indexToUpdate === index) {
|
||||
switch (type) {
|
||||
case 'Column':
|
||||
return valueToUpdate.isLeftSideCondition
|
||||
? {
|
||||
...conditionDetail,
|
||||
leftField: {
|
||||
columnName: valueToUpdate.columnName,
|
||||
table: valueToUpdate.table,
|
||||
type: 'Column',
|
||||
},
|
||||
}
|
||||
: {
|
||||
...conditionDetail,
|
||||
rightField: {
|
||||
columnName: valueToUpdate.columnName,
|
||||
table: valueToUpdate.table,
|
||||
type: 'Column',
|
||||
},
|
||||
};
|
||||
case 'Value':
|
||||
return valueToUpdate.isLeftSideCondition
|
||||
? {
|
||||
...conditionDetail,
|
||||
leftField: {
|
||||
value: valueToUpdate.value,
|
||||
type: 'Value',
|
||||
},
|
||||
}
|
||||
: {
|
||||
...conditionDetail,
|
||||
rightField: {
|
||||
value: valueToUpdate.value,
|
||||
type: 'Value',
|
||||
},
|
||||
};
|
||||
case 'Operator':
|
||||
return {
|
||||
...conditionDetail,
|
||||
...((conditionDetail.operator === 'IS' || valueToUpdate.operator === 'IS') && {
|
||||
rightField: {
|
||||
value: '',
|
||||
type: 'Value',
|
||||
},
|
||||
}),
|
||||
operator: valueToUpdate.operator,
|
||||
};
|
||||
default:
|
||||
return conditionDetail;
|
||||
}
|
||||
}
|
||||
return conditionDetail;
|
||||
});
|
||||
handleWhereFilterChange({ ...conditions, conditionsList: [...editedConditionList] });
|
||||
}
|
||||
|
||||
function updateOperatorForConditions(changedOperator) {
|
||||
let editedFilterConditions = { ...conditions, operator: changedOperator };
|
||||
handleWhereFilterChange(editedFilterConditions);
|
||||
}
|
||||
|
||||
const tableSet = new Set();
|
||||
(joinOptions || []).forEach((join) => {
|
||||
const { table, conditions } = join;
|
||||
tableSet.add(table);
|
||||
conditions?.conditionsList?.forEach((condition) => {
|
||||
const { leftField, rightField } = condition;
|
||||
if (leftField?.table) {
|
||||
tableSet.add(leftField?.table);
|
||||
}
|
||||
if (rightField?.table) {
|
||||
tableSet.add(rightField?.table);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const tables = [...tableSet];
|
||||
const tableList = [];
|
||||
|
||||
tables.forEach((tableId) => {
|
||||
const tableDetails = findTableDetails(tableId);
|
||||
if (tableDetails?.table_name && tableInfo[tableDetails.table_name]) {
|
||||
const tableDetailsForDropDown = {
|
||||
label: tableDetails.table_name,
|
||||
value: tableId,
|
||||
options:
|
||||
tableInfo[tableDetails.table_name]?.map((columns) => ({
|
||||
label: columns.Header,
|
||||
value: columns.Header + '-' + tableId,
|
||||
table: tableId,
|
||||
})) || [],
|
||||
};
|
||||
tableList.push(tableDetailsForDropDown);
|
||||
}
|
||||
});
|
||||
|
||||
const groupOperators = [
|
||||
{ value: 'AND', label: 'AND' },
|
||||
{ value: 'OR', label: 'OR' },
|
||||
];
|
||||
|
||||
const filterComponents = conditionsList.map((conditionDetail, index) => {
|
||||
const { operator = '', leftField = {}, rightField = {} } = conditionDetail;
|
||||
const LeftSideTableDetails = leftField?.table ? findTableDetails(leftField?.table) : '';
|
||||
return (
|
||||
<Row className="border rounded mb-2 mx-0" key={index}>
|
||||
<Col sm="2" className="p-0 border-end">
|
||||
{index === 1 && (
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
onChange={(change) => updateOperatorForConditions(change?.value)}
|
||||
options={groupOperators}
|
||||
darkMode={darkMode}
|
||||
value={groupOperators.find((op) => op.value === conditions.operator)}
|
||||
/>
|
||||
)}
|
||||
{index === 0 && <div className="tj-small-btn px-2">Where</div>}
|
||||
{index > 1 && <div className="tj-small-btn px-2">{conditions?.operator}</div>}
|
||||
</Col>
|
||||
<Col sm="3" className="p-0 border-end">
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
onChange={(newValue) =>
|
||||
updateFilterConditionEntry('Column', index, {
|
||||
table: newValue.table,
|
||||
columnName: newValue.label,
|
||||
isLeftSideCondition: true,
|
||||
})
|
||||
}
|
||||
value={{
|
||||
label: LeftSideTableDetails?.table_name
|
||||
? LeftSideTableDetails?.table_name + '.' + leftField?.columnName
|
||||
: leftField?.columnName,
|
||||
value: leftField?.columnName && leftField?.table ? leftField?.columnName + '-' + leftField?.table : '',
|
||||
table: leftField?.table,
|
||||
}}
|
||||
options={tableList}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
</Col>
|
||||
<Col sm="3" className="p-0 border-end">
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
onChange={(change) => updateFilterConditionEntry('Operator', index, { operator: change?.value })}
|
||||
value={filterOperatorOptions.find((op) => op.value === operator)}
|
||||
options={filterOperatorOptions}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
</Col>
|
||||
<Col sm="4" className="p-0 d-flex">
|
||||
<div className="flex-grow-1">
|
||||
{operator === 'IS' ? (
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
onChange={(change) =>
|
||||
updateFilterConditionEntry('Value', index, { value: change?.value, isLeftSideCondition: false })
|
||||
}
|
||||
options={nullOperatorOptions}
|
||||
darkMode={darkMode}
|
||||
value={nullOperatorOptions.find((op) => op.value === rightField.value)}
|
||||
/>
|
||||
) : (
|
||||
<CodeHinter
|
||||
initialValue={
|
||||
rightField?.value
|
||||
? typeof rightField?.value === 'string'
|
||||
? rightField?.value
|
||||
: JSON.stringify(rightField?.value)
|
||||
: rightField?.value
|
||||
}
|
||||
className="codehinter-plugins"
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
height={'28px'}
|
||||
placeholder="Value"
|
||||
onChange={(newValue) =>
|
||||
updateFilterConditionEntry('Value', index, { value: newValue, isLeftSideCondition: false })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ButtonSolid
|
||||
size="sm"
|
||||
variant="ghostBlack"
|
||||
className="px-1 rounded-0 border-start"
|
||||
onClick={() => removeFilterConditionEntry(index)}
|
||||
>
|
||||
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
|
||||
</ButtonSolid>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Container fluid className="p-0">
|
||||
{conditionsList.length === 0 && (
|
||||
<Row className="mb-2 mx-0">
|
||||
<div
|
||||
style={{
|
||||
gap: '4px',
|
||||
height: '30px',
|
||||
border: '1px dashed var(--slate-08, #C1C8CD)',
|
||||
}}
|
||||
className="px-4 py-2 text-center rounded-1"
|
||||
>
|
||||
<SolidIcon name="information" style={{ height: 14, width: 14 }} width={14} height={14} /> There are no
|
||||
conditions
|
||||
</div>
|
||||
</Row>
|
||||
)}
|
||||
{filterComponents}
|
||||
<Row className="mx-0">
|
||||
<Col className="p-0">
|
||||
<ButtonSolid variant="ghostBlue" size="sm" onClick={() => addNewFilterConditionEntry()}>
|
||||
<AddRectangle width="15" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />
|
||||
Add more
|
||||
</ButtonSolid>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
import React, { isValidElement, useCallback, useState } from 'react';
|
||||
import Select, { components } from 'react-select';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { authenticationService } from '@/_services';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import Search from '@/_ui/Icon/solidIcons/Search';
|
||||
import { Form } from 'react-bootstrap';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
function DataSourceSelect({
|
||||
darkMode,
|
||||
isDisabled,
|
||||
selectRef,
|
||||
closePopup,
|
||||
options,
|
||||
isMulti,
|
||||
onSelect,
|
||||
onAdd,
|
||||
addBtnLabel,
|
||||
selected,
|
||||
emptyError,
|
||||
}) {
|
||||
const handleChangeDataSource = (source) => {
|
||||
onSelect && onSelect(source);
|
||||
closePopup && !isMulti && closePopup();
|
||||
};
|
||||
|
||||
let optionsCount = options.length;
|
||||
|
||||
options.forEach((item) => {
|
||||
if (item.options && item.options.length > 0) {
|
||||
optionsCount += item.options.length;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
onChange={(option) => handleChangeDataSource(option)}
|
||||
classNames={{
|
||||
menu: () => 'tj-scrollbar',
|
||||
}}
|
||||
ref={selectRef}
|
||||
controlShouldRenderValue={false}
|
||||
menuPlacement="auto"
|
||||
menuIsOpen
|
||||
hideSelectedOptions={false}
|
||||
components={{
|
||||
// ...(isMulti && {
|
||||
Option: ({ children, ...props }) => {
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
className="dd-select-option"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
// width: '20px',
|
||||
}}
|
||||
>
|
||||
{isMulti && (
|
||||
<Form.Check // prettier-ignore
|
||||
type={'checkbox'}
|
||||
id={props.value}
|
||||
className="me-1"
|
||||
checked={props.isSelected}
|
||||
// label={`default ${type}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{props?.data?.icon &&
|
||||
(isValidElement(props.data.icon) ? (
|
||||
props.data.icon
|
||||
) : (
|
||||
<SolidIcon
|
||||
name={props.data.icon}
|
||||
style={{ height: 16, width: 16 }}
|
||||
width={20}
|
||||
height={17}
|
||||
viewBox=""
|
||||
/>
|
||||
))}
|
||||
<span className={`${props?.data?.icon ? 'ms-1 ' : ''}flex-grow-1`}>{children}</span>
|
||||
{props.isSelected && (
|
||||
<SolidIcon
|
||||
fill="var(--indigo9)"
|
||||
name="tick"
|
||||
style={{ height: 16, width: 16 }}
|
||||
viewBox="0 0 20 20"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
},
|
||||
// }),
|
||||
MenuList: useCallback(
|
||||
(props) => <MenuList {...props} onAdd={onAdd} addBtnLabel={addBtnLabel} emptyError={emptyError} />,
|
||||
[onAdd, addBtnLabel, emptyError]
|
||||
),
|
||||
IndicatorSeparator: () => null,
|
||||
DropdownIndicator,
|
||||
GroupHeading: CustomGroupHeading,
|
||||
...(optionsCount < 5 && { Control: () => '' }),
|
||||
}}
|
||||
styles={{
|
||||
control: (style) => ({
|
||||
...style,
|
||||
// width: '240px',
|
||||
background: 'var(--base)',
|
||||
color: 'var(--slate9)',
|
||||
borderWidth: '0',
|
||||
// borderBottom: '1px solid var(--slate7)',
|
||||
// marginBottom: '1px',
|
||||
boxShadow: 'none',
|
||||
borderRadius: '4px 4px 0 0',
|
||||
borderBottom: '1px solid var(--slate-05, #E6E8EB)',
|
||||
':hover': {
|
||||
borderColor: 'var(--slate7)',
|
||||
},
|
||||
flexDirection: 'row-reverse',
|
||||
}),
|
||||
menu: (style) => ({
|
||||
...style,
|
||||
position: 'static',
|
||||
backgroundColor: 'var(--base)',
|
||||
color: 'var(--slate12)',
|
||||
boxShadow: 'none',
|
||||
border: '0',
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
width: '240px',
|
||||
borderTopRightRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
}),
|
||||
// indicatorSeparator: () => ({ display: 'none' }),
|
||||
input: (style) => ({
|
||||
...style,
|
||||
color: 'var(--slate12)',
|
||||
'caret-color': 'var(--slate9)',
|
||||
border: 0,
|
||||
':placeholder': { color: 'var(--slate9)' },
|
||||
}),
|
||||
groupHeading: (style) => ({
|
||||
...style,
|
||||
fontSize: '100%',
|
||||
color: 'var(--slate-11, #687076)',
|
||||
// font-size: 12px;
|
||||
// font-style: normal;
|
||||
fontWeight: 500,
|
||||
lineHeight: '20px',
|
||||
textTransform: 'uppercase',
|
||||
}),
|
||||
option: (style, { data: { isNested }, isFocused, isDisabled, isSelected }) => ({
|
||||
...style,
|
||||
cursor: 'pointer',
|
||||
color: 'inherit',
|
||||
backgroundColor: isSelected
|
||||
? 'var(--indigo3, #F0F4FF)'
|
||||
: isFocused && !isNested
|
||||
? 'var(--slate4)'
|
||||
: 'transparent',
|
||||
...(isNested
|
||||
? { padding: '0 8px', marginLeft: '19px', borderLeft: '1px solid var(--slate5)', width: 'auto' }
|
||||
: {}),
|
||||
...(!isNested && { borderRadius: '4px' }),
|
||||
':hover': {
|
||||
backgroundColor: isNested ? 'transparent' : 'var(--slate4)',
|
||||
'.option-nested-datasource-selector': { backgroundColor: 'var(--slate4)' },
|
||||
},
|
||||
...(isFocused &&
|
||||
isNested && {
|
||||
'.option-nested-datasource-selector': { backgroundColor: 'var(--slate4)' },
|
||||
}),
|
||||
}),
|
||||
group: (style) => ({
|
||||
...style,
|
||||
':not(:first-child)': {
|
||||
borderTop: '1px solid var(--slate-05, #E6E8EB)',
|
||||
marginTop: '8px',
|
||||
},
|
||||
paddingBottom: 0,
|
||||
'.dd-select-option': { marginLeft: '19px' },
|
||||
}),
|
||||
container: (styles) => ({
|
||||
...styles,
|
||||
borderRadius: '6px',
|
||||
// border: '1px solid var(--slate3)',
|
||||
// boxShadow: '0px 2px 4px -2px rgba(16, 24, 40, 0.06), 0px 4px 8px -2px rgba(16, 24, 40, 0.10)',
|
||||
}),
|
||||
valueContainer: (styles) => ({
|
||||
...styles,
|
||||
paddingLeft: 0,
|
||||
}),
|
||||
}}
|
||||
placeholder="Search"
|
||||
options={options}
|
||||
isDisabled={isDisabled}
|
||||
isClearable={false}
|
||||
// menuIsOpen
|
||||
isMulti={isMulti}
|
||||
maxMenuHeight={400}
|
||||
minMenuHeight={300}
|
||||
value={selected}
|
||||
// onKeyDown={handleKeyDown}
|
||||
onInputChange={() => {
|
||||
const _queryDsSelectMenu = document.getElementById('query-ds-select-menu');
|
||||
// if (queryDsSelectMenu && !queryDsSelectMenu?.style?.height) {
|
||||
// queryDsSelectMenu.style.height = queryDsSelectMenu.offsetHeight + 'px';
|
||||
// }
|
||||
}}
|
||||
// filterOption={(data, search) => {
|
||||
// if (data?.data?.source) {
|
||||
// //Disabled below eslint check since already checking in above line)
|
||||
// // eslint-disable-next-line no-unsafe-optional-chaining
|
||||
// const { name, kind } = data?.data?.source;
|
||||
// const searchTerm = search.toLowerCase();
|
||||
// return name.toLowerCase().includes(searchTerm) || kind.toLowerCase().includes(searchTerm);
|
||||
// }
|
||||
// return true;
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MenuList = ({ children, getStyles, innerRef, onAdd, addBtnLabel, emptyError, options, ...props }) => {
|
||||
const menuListStyles = getStyles('menuList', props);
|
||||
const { admin } = authenticationService.currentSessionValue;
|
||||
if (admin) {
|
||||
//offseting for height of button since react-select calculates only the size of options list
|
||||
menuListStyles.maxHeight = 225 - 48;
|
||||
}
|
||||
menuListStyles.padding = '4px';
|
||||
|
||||
return (
|
||||
<>
|
||||
{isEmpty(options) && emptyError ? (
|
||||
emptyError
|
||||
) : (
|
||||
<div ref={innerRef} style={menuListStyles} id="query-ds-select-menu">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
{onAdd && (
|
||||
<div className="p-2 mt-2 border-slate3-top">
|
||||
<ButtonSolid variant="secondary" size="md" className="w-100" onClick={onAdd}>
|
||||
+ {addBtnLabel || 'Add new'}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownIndicator = (props) => {
|
||||
return (
|
||||
components.DropdownIndicator && (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<Search style={{ width: '16px' }} />
|
||||
</components.DropdownIndicator>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const CustomGroupHeading = (props) => {
|
||||
const [isGroupListCollapsed, setIsGroupListCollapsed] = useState(false);
|
||||
|
||||
const handleHeaderClick = (id) => {
|
||||
const node = document.querySelector(`#${id}`)?.parentElement?.nextElementSibling;
|
||||
const classes = node?.classList;
|
||||
const hidden = classes?.contains('d-none');
|
||||
|
||||
if (hidden) {
|
||||
setIsGroupListCollapsed(false);
|
||||
node.classList.remove('d-none');
|
||||
} else {
|
||||
setIsGroupListCollapsed(true);
|
||||
node.classList.add('d-none');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group-heading-wrapper d-flex justify-content-between"
|
||||
onClick={() => handleHeaderClick(props.id)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<components.GroupHeading {...props} />{' '}
|
||||
<SolidIcon name={isGroupListCollapsed ? 'cheverondown' : 'cheveronup'} height={20} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataSourceSelect;
|
||||
|
|
@ -7,23 +7,28 @@ import { CreateRow } from './CreateRow';
|
|||
import { UpdateRows } from './UpdateRows';
|
||||
import { DeleteRows } from './DeleteRows';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import Select from '@/_ui/Select';
|
||||
import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles';
|
||||
import { useMounted } from '@/_hooks/use-mount';
|
||||
import { useCurrentState } from '@/_stores/currentStateStore';
|
||||
import { JoinTable } from './JoinTable';
|
||||
import { cloneDeep, difference } from 'lodash';
|
||||
import DropDownSelect from './DropDownSelect';
|
||||
import { getPrivateRoute } from '@/_helpers/routes';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLayout }) => {
|
||||
const computeSelectStyles = (darkMode, width) => {
|
||||
return queryManagerSelectComponentStyle(darkMode, width);
|
||||
};
|
||||
const currentState = useCurrentState();
|
||||
const navigate = useNavigate();
|
||||
const { current_organization_id: organizationId } = authenticationService.currentSessionValue;
|
||||
const mounted = useMounted();
|
||||
const [operation, setOperation] = useState(options['operation'] || '');
|
||||
const [columns, setColumns] = useState([]);
|
||||
const [tables, setTables] = useState([]);
|
||||
const [tableInfo, setTableInfo] = useState({});
|
||||
const [selectedTableId, setSelectedTableId] = useState(options['table_id']);
|
||||
const [selectedTableName, setSelectedTableName] = useState(null);
|
||||
const [listRowsOptions, setListRowsOptions] = useState(() => options['list_rows'] || {});
|
||||
const [updateRowsOptions, setUpdateRowsOptions] = useState(
|
||||
options['update_rows'] || { columns: {}, where_filters: {} }
|
||||
|
|
@ -33,6 +38,100 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
limit: 1,
|
||||
}
|
||||
);
|
||||
const [joinTableOptions, setJoinTableOptions] = useState(options['join_table'] || {});
|
||||
|
||||
const joinOptions = options['join_table']?.['joins'] || [
|
||||
{ conditions: { conditionsList: [{ leftField: { table: selectedTableId } }] } },
|
||||
];
|
||||
|
||||
const setJoinOptions = (values) => {
|
||||
const tableSet = new Set();
|
||||
(values || []).forEach((join) => {
|
||||
const { table, conditions } = join;
|
||||
tableSet.add(table);
|
||||
conditions?.conditionsList?.forEach((condition) => {
|
||||
const { leftField, rightField } = condition;
|
||||
if (leftField?.table) {
|
||||
tableSet.add(leftField?.table);
|
||||
}
|
||||
if (rightField?.table) {
|
||||
tableSet.add(rightField?.table);
|
||||
}
|
||||
});
|
||||
});
|
||||
tableSet.add(selectedTableId);
|
||||
|
||||
setJoinTableOptions((prevJoinOptions) => {
|
||||
const { conditions, order_by = [], joins: currJoins, fields: currFields = [] } = prevJoinOptions;
|
||||
const conditionsList = cloneDeep(conditions?.conditionsList || []);
|
||||
const newConditionsList = conditionsList.filter((condition) => {
|
||||
const { leftField } = condition || {};
|
||||
if (tableSet.has(leftField?.table)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const newOrderBy = order_by.filter((order) => tableSet.has(order.table));
|
||||
|
||||
//getting old states
|
||||
const currTableSet = new Set();
|
||||
(currJoins || []).forEach((join) => {
|
||||
const { table, conditions } = join;
|
||||
currTableSet.add(table);
|
||||
conditions?.conditionsList?.forEach((condition) => {
|
||||
const { leftField, rightField } = condition;
|
||||
if (leftField?.table) {
|
||||
currTableSet.add(leftField?.table);
|
||||
}
|
||||
if (rightField?.table) {
|
||||
currTableSet.add(rightField?.table);
|
||||
}
|
||||
});
|
||||
});
|
||||
currTableSet.add(selectedTableId);
|
||||
const newTables = difference([...tableSet], [...currTableSet]);
|
||||
const newFields = newTables.reduce(
|
||||
(acc, newTable) => [
|
||||
...acc,
|
||||
...(tableInfo[newTable]
|
||||
? tableInfo[newTable].map((col) => ({
|
||||
name: col.Header,
|
||||
table: newTable,
|
||||
}))
|
||||
: []),
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const updatedFields = [...currFields.filter((field) => tableSet.has(field.table)), ...newFields];
|
||||
newTables.forEach((tableId) => tableId && loadTableInformation(tableId, true));
|
||||
|
||||
return {
|
||||
...prevJoinOptions,
|
||||
joins: values,
|
||||
conditions: {
|
||||
...(conditions?.operator && { operator: conditions.operator }),
|
||||
conditionsList: newConditionsList,
|
||||
},
|
||||
order_by: newOrderBy,
|
||||
fields: updatedFields,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const joinOrderByOptions = options?.['join_table']?.['order_by'] || [];
|
||||
const setJoinOrderByOptions = (values) => {
|
||||
if (values.length) {
|
||||
setJoinTableOptions((prevJoinOptions) => {
|
||||
return {
|
||||
...prevJoinOptions,
|
||||
order_by: values,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
deleteJoinTableOptions('order_by');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTables();
|
||||
|
|
@ -40,17 +139,30 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (tables.length > 0) {
|
||||
const tableInfo = tables.find((table) => table.table_id == selectedTableId);
|
||||
tableInfo && setSelectedTableName(tableInfo.table_name);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tables]);
|
||||
const tableSet = new Set();
|
||||
const joinOptions = options['join_table']?.['joins'];
|
||||
(joinOptions || []).forEach((join) => {
|
||||
const { table, conditions } = join;
|
||||
tableSet.add(table);
|
||||
conditions?.conditionsList?.forEach((condition) => {
|
||||
const { leftField, rightField } = condition;
|
||||
if (leftField?.table) {
|
||||
tableSet.add(leftField?.table);
|
||||
}
|
||||
if (rightField?.table) {
|
||||
tableSet.add(rightField?.table);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const tables = [...tableSet];
|
||||
tables.forEach((tableId) => tableId && loadTableInformation(tableId));
|
||||
}, [options['join_table']?.['joins'], tables]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedTableName && fetchTableInformation(selectedTableName);
|
||||
selectedTableId && fetchTableInformation(selectedTableId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedTableName]);
|
||||
}, [selectedTableId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted) {
|
||||
|
|
@ -77,6 +189,11 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updateRowsOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
mounted && optionchanged('join_table', joinTableOptions);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [joinTableOptions]);
|
||||
|
||||
const handleOptionsChange = (optionsChanged, value) => {
|
||||
setListRowsOptions((prev) => ({ ...prev, [optionsChanged]: value }));
|
||||
};
|
||||
|
|
@ -97,6 +214,66 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
setDeleteRowsOptions((prev) => ({ ...prev, limit: limit }));
|
||||
};
|
||||
|
||||
const loadTableInformation = async (tableId, isNewTableAdded) => {
|
||||
const tableDetails = findTableDetails(tableId);
|
||||
if (tableDetails?.table_name && !tableInfo[tableDetails?.table_name]) {
|
||||
const { table_name } = tableDetails;
|
||||
const { data } = await tooljetDatabaseService.viewTable(organizationId, table_name);
|
||||
|
||||
setTableInfo((info) => ({
|
||||
...info,
|
||||
[table_name]: data?.result.map(({ column_name, data_type, keytype, ...rest }) => ({
|
||||
Header: column_name,
|
||||
accessor: column_name,
|
||||
dataType: data_type,
|
||||
isPrimaryKey: keytype?.toLowerCase() === 'primary key',
|
||||
...rest,
|
||||
})),
|
||||
}));
|
||||
|
||||
if (isNewTableAdded) {
|
||||
setJoinTableOptions((joinOptions) => {
|
||||
const { fields } = joinOptions;
|
||||
const newFields = cloneDeep(fields).filter((field) => field.table !== tableId);
|
||||
newFields.push(
|
||||
...(data?.result
|
||||
? data.result.map((col) => ({
|
||||
name: col.column_name,
|
||||
table: tableId,
|
||||
// alias: `${tableId}_${col.column_name}`,
|
||||
}))
|
||||
: [])
|
||||
);
|
||||
|
||||
return {
|
||||
...joinOptions,
|
||||
fields: newFields,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const joinTableOptionsChange = (optionsChanged, value) => {
|
||||
setJoinTableOptions((prev) => ({ ...prev, [optionsChanged]: value }));
|
||||
};
|
||||
|
||||
const deleteJoinTableOptions = (optionToDelete) => {
|
||||
setJoinTableOptions((prev) => {
|
||||
const prevOptions = { ...prev };
|
||||
if (prevOptions[optionToDelete]) delete prevOptions[optionToDelete];
|
||||
return prevOptions;
|
||||
});
|
||||
};
|
||||
|
||||
const findTableDetails = (tableId) => {
|
||||
return tables.find((table) => table.table_id == tableId);
|
||||
};
|
||||
|
||||
const findTableDetailsByName = (tableName) => {
|
||||
return tables.find((table) => table.table_name == tableName);
|
||||
};
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
organizationId,
|
||||
|
|
@ -106,8 +283,6 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
setColumns,
|
||||
selectedTableId,
|
||||
setSelectedTableId,
|
||||
selectedTableName,
|
||||
setSelectedTableName,
|
||||
listRowsOptions,
|
||||
setListRowsOptions,
|
||||
limitOptionChanged,
|
||||
|
|
@ -117,16 +292,31 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
deleteOperationLimitOptionChanged,
|
||||
updateRowsOptions,
|
||||
handleUpdateRowsOptionsChange,
|
||||
joinTableOptions,
|
||||
joinTableOptionsChange,
|
||||
tableInfo,
|
||||
loadTableInformation,
|
||||
joinOptions,
|
||||
setJoinOptions,
|
||||
joinOrderByOptions,
|
||||
setJoinOrderByOptions,
|
||||
deleteJoinTableOptions,
|
||||
findTableDetails,
|
||||
findTableDetailsByName,
|
||||
}),
|
||||
[
|
||||
organizationId,
|
||||
tables,
|
||||
columns,
|
||||
selectedTableName,
|
||||
selectedTableId,
|
||||
listRowsOptions,
|
||||
deleteRowsOptions,
|
||||
updateRowsOptions,
|
||||
joinTableOptions,
|
||||
tableInfo,
|
||||
loadTableInformation,
|
||||
joinOptions,
|
||||
joinOrderByOptions,
|
||||
selectedTableId,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -139,42 +329,72 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
}
|
||||
|
||||
if (Array.isArray(data?.result)) {
|
||||
const selectedTableInfo = data.result.find((table) => table.id === options['table_id']);
|
||||
|
||||
selectedTableInfo && setSelectedTableId(selectedTableInfo.id);
|
||||
setTables(
|
||||
data.result.map((table) => {
|
||||
return { table_name: table.table_name, table_id: table.id };
|
||||
}) || []
|
||||
);
|
||||
const selectedTableInfo = data.result.find((table) => table.id === options['table_id']);
|
||||
if (selectedTableInfo) {
|
||||
setSelectedTableId(selectedTableInfo.id);
|
||||
fetchTableInformation(selectedTableInfo.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTableInformation = async (table) => {
|
||||
const { error, data } = await tooljetDatabaseService.viewTable(organizationId, table);
|
||||
/**
|
||||
* TODO: This function to be removed and replaced with loadTableInformation function everywhere
|
||||
*/
|
||||
const fetchTableInformation = async (tableId, isNewTableAdded) => {
|
||||
const tableDetails = findTableDetails(tableId);
|
||||
if (tableDetails?.table_name) {
|
||||
const { table_name } = tableDetails;
|
||||
const { error, data } = await tooljetDatabaseService.viewTable(organizationId, table_name);
|
||||
|
||||
if (error) {
|
||||
toast.error(error?.message ?? 'Failed to fetch table information');
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
toast.error(error?.message ?? 'Failed to fetch table information');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.result?.length > 0) {
|
||||
setColumns(
|
||||
data?.result.map(({ column_name, data_type, keytype, ...rest }) => ({
|
||||
if (data?.result?.length > 0) {
|
||||
const columnList = data?.result.map(({ column_name, data_type, keytype, ...rest }) => ({
|
||||
Header: column_name,
|
||||
accessor: column_name,
|
||||
dataType: data_type,
|
||||
isPrimaryKey: keytype?.toLowerCase() === 'primary key',
|
||||
...rest,
|
||||
}))
|
||||
);
|
||||
}));
|
||||
setColumns(columnList);
|
||||
setTableInfo((prevTableInfo) => ({ ...prevTableInfo, [table_name]: columnList }));
|
||||
|
||||
if (isNewTableAdded) {
|
||||
setJoinTableOptions((joinOptions) => {
|
||||
const { fields } = joinOptions;
|
||||
const newFields = cloneDeep(fields).filter((field) => field.table !== tableId);
|
||||
newFields.push(
|
||||
...(data?.result
|
||||
? data.result.map((col) => ({
|
||||
name: col.column_name,
|
||||
table: tableId,
|
||||
// alias: `${tableId}_${col.column_name}`,
|
||||
}))
|
||||
: [])
|
||||
);
|
||||
|
||||
return {
|
||||
...joinOptions,
|
||||
fields: newFields,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const generateListForDropdown = (tableList) => {
|
||||
return tableList.map((tableMap) =>
|
||||
Object.fromEntries([
|
||||
['name', tableMap.table_name],
|
||||
['label', tableMap.table_name],
|
||||
['value', tableMap.table_id],
|
||||
])
|
||||
);
|
||||
|
|
@ -182,11 +402,34 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
|
||||
const handleTableNameSelect = (tableId) => {
|
||||
setSelectedTableId(tableId);
|
||||
const { table_name: tableName } = tables.find((t) => t.table_id === tableId);
|
||||
tableName && setSelectedTableName(tableName);
|
||||
|
||||
fetchTableInformation(tableId, true);
|
||||
optionchanged('organization_id', organizationId);
|
||||
optionchanged('table_id', tableId);
|
||||
|
||||
setJoinTableOptions(() => {
|
||||
return {
|
||||
joins: [
|
||||
{
|
||||
id: new Date().getTime(),
|
||||
conditions: {
|
||||
operator: 'AND',
|
||||
conditionsList: [
|
||||
{
|
||||
operator: '=',
|
||||
leftField: { table: tableId },
|
||||
},
|
||||
],
|
||||
},
|
||||
joinType: 'INNER',
|
||||
},
|
||||
],
|
||||
from: {
|
||||
name: tableId,
|
||||
type: 'Table',
|
||||
},
|
||||
fields: [],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const getComponent = () => {
|
||||
|
|
@ -199,9 +442,19 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
return UpdateRows;
|
||||
case 'delete_rows':
|
||||
return DeleteRows;
|
||||
case 'join_tables':
|
||||
return JoinTable;
|
||||
}
|
||||
};
|
||||
|
||||
const tooljetDbOperationList = [
|
||||
{ label: 'List rows', value: 'list_rows' },
|
||||
{ label: 'Create row', value: 'create_row' },
|
||||
{ label: 'Update rows', value: 'update_rows' },
|
||||
{ label: 'Delete rows', value: 'delete_rows' },
|
||||
{ label: 'Join tables', value: 'join_tables' },
|
||||
];
|
||||
|
||||
const ComponentToRender = getComponent(operation);
|
||||
|
||||
return (
|
||||
|
|
@ -210,15 +463,17 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
<div className={cx({ row: !isHorizontalLayout })}>
|
||||
<div className={cx({ 'col-4': !isHorizontalLayout, 'd-flex': isHorizontalLayout })}>
|
||||
<label className={cx('form-label')}>Table name</label>
|
||||
<div className={cx({ 'flex-grow-1': isHorizontalLayout })}>
|
||||
<Select
|
||||
<div className={cx({ 'flex-grow-1': isHorizontalLayout }, 'border', 'rounded')}>
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
options={generateListForDropdown(tables)}
|
||||
value={selectedTableId}
|
||||
onChange={(value) => handleTableNameSelect(value)}
|
||||
width="100%"
|
||||
// useMenuPortal={false}
|
||||
useCustomStyles={true}
|
||||
styles={computeSelectStyles(darkMode, '100%')}
|
||||
darkMode={darkMode}
|
||||
onChange={(value) => {
|
||||
value?.value && handleTableNameSelect(value?.value);
|
||||
}}
|
||||
onAdd={() => navigate(getPrivateRoute('database'))}
|
||||
addBtnLabel={'Add new table'}
|
||||
value={generateListForDropdown(tables).find((val) => val?.value === selectedTableId)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -231,20 +486,15 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
className={cx({ 'col-4': !isHorizontalLayout, 'd-flex': isHorizontalLayout })}
|
||||
>
|
||||
<label className={cx('form-label')}>Operations</label>
|
||||
<div className={cx({ 'flex-grow-1': isHorizontalLayout })}>
|
||||
<Select
|
||||
options={[
|
||||
{ name: 'List rows', value: 'list_rows' },
|
||||
{ name: 'Create row', value: 'create_row' },
|
||||
{ name: 'Update rows', value: 'update_rows' },
|
||||
{ name: 'Delete rows', value: 'delete_rows' },
|
||||
]}
|
||||
value={operation}
|
||||
onChange={(value) => setOperation(value)}
|
||||
width="100%"
|
||||
// useMenuPortal={false}
|
||||
useCustomStyles={true}
|
||||
styles={computeSelectStyles(darkMode, '100%')}
|
||||
<div className={cx({ 'flex-grow-1': isHorizontalLayout }, 'border', 'rounded')}>
|
||||
<DropDownSelect
|
||||
showPlaceHolder
|
||||
options={tooljetDbOperationList}
|
||||
darkMode={darkMode}
|
||||
onChange={(value) => {
|
||||
value?.value && setOperation(value?.value);
|
||||
}}
|
||||
value={tooljetDbOperationList.find((val) => val?.value === operation)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { tooljetDatabaseService } from '@/_services';
|
||||
import { tooljetDatabaseService, authenticationService } from '@/_services';
|
||||
import { isEmpty } from 'lodash';
|
||||
import PostgrestQueryBuilder from '@/_helpers/postgrestQueryBuilder';
|
||||
import { resolveReferences } from '@/_helpers/utils';
|
||||
|
|
@ -18,6 +18,8 @@ async function perform(dataQuery, currentState) {
|
|||
return updateRows(dataQuery, currentState);
|
||||
case 'delete_rows':
|
||||
return deleteRows(dataQuery, currentState);
|
||||
case 'join_tables':
|
||||
return joinTables(dataQuery, currentState);
|
||||
|
||||
default:
|
||||
return {
|
||||
|
|
@ -173,3 +175,79 @@ async function deleteRows(dataQuery, currentState) {
|
|||
const headers = { 'data-query-id': dataQuery.id };
|
||||
return await tooljetDatabaseService.deleteRows(headers, tableId, query.join('&'));
|
||||
}
|
||||
|
||||
// Function:- To valid Empty fields in JSON ( Works for Nested JSON too )
|
||||
// function validateInputJsonHasEmptyFields(input) {
|
||||
// let isValid = true;
|
||||
|
||||
// if (isEmpty(input)) return false;
|
||||
// if (Array.isArray(input)) {
|
||||
// let isIncludesInvalidJson = input
|
||||
// .map((eachValue) => {
|
||||
// let isValidJson = validateInputJsonHasEmptyFields(eachValue);
|
||||
// return isValidJson;
|
||||
// })
|
||||
// .includes(false);
|
||||
// if (isIncludesInvalidJson) isValid = false;
|
||||
// }
|
||||
|
||||
// if (typeof input === 'object') {
|
||||
// let isIncludesInvalidJson = Object.entries(input)
|
||||
// .map(([key, value]) => {
|
||||
// let isValidJson = validateInputJsonHasEmptyFields(value);
|
||||
// return isValidJson;
|
||||
// })
|
||||
// .includes(false);
|
||||
// if (isIncludesInvalidJson) isValid = false;
|
||||
// }
|
||||
|
||||
// return isValid;
|
||||
// }
|
||||
|
||||
async function joinTables(dataQuery, currentState) {
|
||||
const organizationId = authenticationService.currentSessionValue.current_organization_id;
|
||||
const queryOptions = dataQuery.options;
|
||||
const resolvedOptions = resolveReferences(queryOptions, currentState);
|
||||
const { join_table = {} } = resolvedOptions;
|
||||
|
||||
// Empty Input is restricted
|
||||
if (Object.keys(join_table).length === 0) {
|
||||
return {
|
||||
status: 'failed',
|
||||
statusText: 'failed',
|
||||
message: `Input can't be empty`,
|
||||
description: 'Empty inputs are not allowed',
|
||||
data: {},
|
||||
};
|
||||
}
|
||||
|
||||
const sanitizedJoinTableJson = { ...join_table };
|
||||
// If mandatory fields ( Select, JOin & From section ), are empty throw error
|
||||
let mandatoryFieldsButEmpty = [];
|
||||
if (!sanitizedJoinTableJson?.fields.length) mandatoryFieldsButEmpty.push('Select');
|
||||
if (sanitizedJoinTableJson?.from && !Object.keys(sanitizedJoinTableJson?.from).length)
|
||||
mandatoryFieldsButEmpty.push('From');
|
||||
// if (join_table?.joins && !validateInputJsonHasEmptyFields(join_table?.joins)) mandatoryFieldsButEmpty.push('Joins');
|
||||
if (mandatoryFieldsButEmpty.length) {
|
||||
return {
|
||||
status: 'failed',
|
||||
statusText: 'failed',
|
||||
message: `Empty values are found in the following section - ${mandatoryFieldsButEmpty.join(', ')}.`,
|
||||
description: 'Mandatory fields are not empty',
|
||||
data: {},
|
||||
};
|
||||
}
|
||||
|
||||
// If non-mandatory fields ( Filter & Sort ) are empty - remove the particular field
|
||||
if (
|
||||
sanitizedJoinTableJson?.conditions &&
|
||||
(!Object.keys(sanitizedJoinTableJson?.conditions)?.length ||
|
||||
!sanitizedJoinTableJson?.conditions?.conditionsList?.length)
|
||||
) {
|
||||
delete sanitizedJoinTableJson.conditions;
|
||||
}
|
||||
if (sanitizedJoinTableJson?.order_by && !sanitizedJoinTableJson?.order_by.length)
|
||||
delete sanitizedJoinTableJson.order_by;
|
||||
|
||||
return await tooljetDatabaseService.joinTables(organizationId, sanitizedJoinTableJson);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,3 +40,26 @@ export const isOperatorOptions = [
|
|||
{ value: 'null', label: 'null' },
|
||||
{ value: 'notNull', label: 'not null' },
|
||||
];
|
||||
|
||||
export const filterOperatorOptions = [
|
||||
{ label: 'equals', value: '=' },
|
||||
{ label: 'greater than', value: '>' },
|
||||
{ label: 'greater than or equal', value: '>=' },
|
||||
{ label: 'less than', value: '<' },
|
||||
{ label: 'less than or equal', value: '<=' },
|
||||
{ label: 'not equal', value: '!=' },
|
||||
{ label: 'like', value: 'LIKE' },
|
||||
{ label: 'not like', value: 'NOT LIKE' },
|
||||
{ label: 'ilike', value: 'ILIKE' },
|
||||
{ label: 'not ilike', value: 'NOT ILIKE' },
|
||||
{ label: 'match', value: '~' },
|
||||
{ label: 'imatch', value: '~*' },
|
||||
{ label: 'in', value: 'IN' },
|
||||
{ label: 'not in', value: 'NOT IN' },
|
||||
{ label: 'is', value: 'IS' },
|
||||
];
|
||||
|
||||
export const nullOperatorOptions = [
|
||||
{ label: 'null', value: 'NULL' },
|
||||
{ label: 'not null', value: 'NOT NULL' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -109,19 +109,33 @@ const InstalledPluginCard = ({ plugin, marketplacePlugin, fetchPlugins, isDevMod
|
|||
toast.success(`${capitalizeFirstLetter(name)} reloaded`);
|
||||
};
|
||||
|
||||
const pluginDeleteMessage = (
|
||||
<>
|
||||
Deleting <strong>{capitalizeFirstLetter(name)}</strong> plugin will result in the permanent removal of all
|
||||
associated datasources and its dataqueries. This action cannot be undone. Are you sure you wish to proceed with
|
||||
the deletion?
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmDialog
|
||||
title={'Delete plugin'}
|
||||
show={isDeleteModalVisible}
|
||||
message={'Are you sure you want to delete ' + capitalizeFirstLetter(name) + '?'}
|
||||
message={pluginDeleteMessage}
|
||||
confirmButtonText={'Delete'}
|
||||
confirmButtonLoading={isDeletingPlugin}
|
||||
onConfirm={executePluginDeletion}
|
||||
onCancel={cancelDeletePlugin}
|
||||
darkMode={darkMode}
|
||||
footerStyle={{
|
||||
borderTop: '1px solid var(--slate5)',
|
||||
padding: '0.875rem 1.5rem',
|
||||
}}
|
||||
/>
|
||||
<div key={plugin.id} className="col-sm-6 col-lg-4">
|
||||
<div className="plugins-card">
|
||||
<div className="card-body">
|
||||
<div className="card-body card-body-alignment">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="text-white avatar">
|
||||
|
|
@ -163,7 +177,7 @@ const InstalledPluginCard = ({ plugin, marketplacePlugin, fetchPlugins, isDevMod
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<sub>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export const MarketplaceCard = ({ id, name, repo, description, version, isInstal
|
|||
return (
|
||||
<div className="col-sm-6 col-lg-4">
|
||||
<div className="plugins-card card-borderless">
|
||||
<div className="card-body">
|
||||
<div className="card-body card-body-alignment">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="text-white app-icon-main">
|
||||
|
|
@ -57,7 +57,7 @@ export const MarketplaceCard = ({ id, name, repo, description, version, isInstal
|
|||
<div>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<sub>v{version}</sub>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import BulkIcon from '@/_ui/Icon/BulkIcons';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
export function FileDropzone({ handleClick, hiddenFileInput, errors, handleFileChange, onButtonClick, onDrop }) {
|
||||
const [fileData, setFileData] = useState();
|
||||
const { getRootProps, getInputProps, isDragActive, acceptedFiles } = useDropzone({
|
||||
accept: { parsedFileType: ['text/csv'] },
|
||||
onDrop,
|
||||
noClick: true,
|
||||
onDropRejected: (files) => {
|
||||
if (Math.round(files[0].size / 1024) > 2 * 1024) {
|
||||
handleFileChange(files[0]);
|
||||
} else {
|
||||
toast.error('Please upload a CSV file');
|
||||
}
|
||||
},
|
||||
maxFiles: 1,
|
||||
onFileDialogCancel: () => {
|
||||
toast.error('Please upload a CSV file');
|
||||
},
|
||||
noKeyboard: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
{...getRootProps({ className: 'dropzone' })}
|
||||
onSubmit={onButtonClick}
|
||||
noValidate
|
||||
className="upload-user-form"
|
||||
id="onButtonClick"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="form-group">
|
||||
<div>
|
||||
<div className="csv-upload-icon-wrap" data-cy="icon-bulk-upload">
|
||||
<BulkIcon name="fileupload" width="27" fill="#3E63DD" />
|
||||
</div>
|
||||
<p className="tj-text tj-text-md font-weight-500 select-csv-text" data-cy="helper-text-select-file">
|
||||
Select a CSV file to upload
|
||||
</p>
|
||||
<span className="tj-text tj-text-sm drag-and-drop-text" data-cy="helper-text-drop-file">
|
||||
{!isDragActive ? 'Or drag and drop it here' : ''}
|
||||
</span>
|
||||
<input
|
||||
{...getInputProps()}
|
||||
style={{ display: 'none' }}
|
||||
ref={hiddenFileInput}
|
||||
onChange={(e) => {
|
||||
const file = e.target.files[0];
|
||||
setFileData(file);
|
||||
handleFileChange(file);
|
||||
}}
|
||||
accept=".csv"
|
||||
type="file"
|
||||
className="form-control"
|
||||
data-cy="input-field-bulk-upload"
|
||||
/>
|
||||
<ul>{acceptedFiles}</ul>
|
||||
{fileData?.name && <ul data-cy="uploaded-file-data">{` ${fileData?.name} - ${fileData?.size} bytes`}</ul>}
|
||||
</div>
|
||||
{errors.client.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<SolidIcon name="reloaderror" width="16" height="17" />
|
||||
<span className="file-upload-error">Kindly check the file and try again!</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="file-upload-error">{errors.client}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{errors.server.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
<SolidIcon name="reloaderror" width="16" height="17" />
|
||||
<span className="file-upload-error">Kindly check the file and try again!</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="file-upload-error">{errors.server}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
144
frontend/src/TooljetDatabase/Drawers/BulkUploadDrawer/index.jsx
Normal file
144
frontend/src/TooljetDatabase/Drawers/BulkUploadDrawer/index.jsx
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import React, { useState, useContext, useCallback, useRef } from 'react';
|
||||
import Drawer from '@/_ui/Drawer';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { TooljetDatabaseContext } from '../../index';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import { FileDropzone } from './FileDropzone';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
function BulkUploadDrawer({
|
||||
isBulkUploadDrawerOpen,
|
||||
setIsBulkUploadDrawerOpen,
|
||||
bulkUploadFile,
|
||||
handleBulkUploadFileChange,
|
||||
handleBulkUpload,
|
||||
isBulkUploading,
|
||||
errors,
|
||||
}) {
|
||||
const [isDownloadingTemplate, setIsDownloadingTemplate] = useState(false);
|
||||
const { columns, selectedTable } = useContext(TooljetDatabaseContext);
|
||||
const hiddenFileInput = useRef(null);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles) => {
|
||||
const file = acceptedFiles[0];
|
||||
if (Math.round(file.size / 1024) > 2 * 1024) {
|
||||
toast.error('File size cannot exceed more than 2MB');
|
||||
} else {
|
||||
handleBulkUploadFileChange(file);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleTemplateDownload = () => {
|
||||
setIsDownloadingTemplate(true);
|
||||
|
||||
return setTimeout(() => {
|
||||
// Create a CSV content string with the column names as the header row
|
||||
const headerRow = columns.map((col) => col.Header).join(',');
|
||||
const csvContent = [headerRow].join('\n');
|
||||
// Create a Blob with the CSV content
|
||||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||||
// Create a temporary URL for the Blob
|
||||
const href = URL.createObjectURL(blob);
|
||||
// Create a link element to trigger the download
|
||||
const link = document.createElement('a');
|
||||
link.href = href;
|
||||
link.download = `${selectedTable.table_name}.csv`;
|
||||
// Trigger the download
|
||||
link.click();
|
||||
|
||||
setIsDownloadingTemplate(false);
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(href);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
hiddenFileInput.current.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsBulkUploadDrawerOpen(!isBulkUploadDrawerOpen)}
|
||||
className={`ghost-black-operation ${isBulkUploadDrawerOpen ? 'open' : ''}`}
|
||||
>
|
||||
<SolidIcon name="fileupload" width="14" fill={isBulkUploadDrawerOpen ? '#3E63DD' : '#889096'} />
|
||||
<span className=" tj-text-xsm font-weight-500" style={{ marginLeft: '6px' }}>
|
||||
Bulk upload data
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<Drawer
|
||||
isOpen={isBulkUploadDrawerOpen}
|
||||
onClose={() => setIsBulkUploadDrawerOpen(false)}
|
||||
position="right"
|
||||
drawerStyle={{ 'overflow-y': 'hidden' }}
|
||||
>
|
||||
<div className="drawer-card-wrapper">
|
||||
<div className="drawer-card-title ">
|
||||
<h3 className="" data-cy="create-new-column-header">
|
||||
Bulk upload data
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="manage-users-drawer-content-bulk">
|
||||
<div className="manage-users-drawer-content-bulk-download-prompt">
|
||||
<div className="user-csv-template-wrap">
|
||||
<div>
|
||||
<SolidIcon name="information" fill="#F76808" width="26" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="tj-text tj-text-sm" data-cy="helper-text-bulk-upload">
|
||||
Download the template to add your data or format your file in the same as the template. ToolJet
|
||||
won’t be able to recognise files in any other format.
|
||||
</p>
|
||||
<ButtonSolid
|
||||
variant="tertiary"
|
||||
className="download-template-btn"
|
||||
leftIcon="file01"
|
||||
iconWidth="13"
|
||||
data-cy="button-download-template"
|
||||
isLoading={isDownloadingTemplate}
|
||||
onClick={handleTemplateDownload}
|
||||
>
|
||||
Download Template
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FileDropzone
|
||||
handleClick={handleClick}
|
||||
hiddenFileInput={hiddenFileInput}
|
||||
errors={errors}
|
||||
handleFileChange={handleBulkUploadFileChange}
|
||||
onButtonClick={handleBulkUpload}
|
||||
onDrop={onDrop}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="position-sticky bottom-0 right-0 w-100 mt-auto">
|
||||
<div className="d-flex justify-content-end drawer-footer-btn-wrap">
|
||||
<ButtonSolid variant="tertiary" data-cy={`cancel-button`} onClick={() => setIsBulkUploadDrawerOpen(false)}>
|
||||
Cancel
|
||||
</ButtonSolid>
|
||||
<ButtonSolid
|
||||
disabled={!bulkUploadFile || errors.client.length > 0 || errors.server.length > 0}
|
||||
data-cy={`save-changes-button`}
|
||||
onClick={handleBulkUpload}
|
||||
fill="#fff"
|
||||
leftIcon="floppydisk"
|
||||
loading={isBulkUploading}
|
||||
>
|
||||
Upload data
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default BulkUploadDrawer;
|
||||
|
|
@ -13,7 +13,7 @@ const CreateColumnDrawer = ({ setIsCreateColumnDrawerOpen, isCreateColumnDrawerO
|
|||
<>
|
||||
<button
|
||||
onClick={() => setIsCreateColumnDrawerOpen(!isCreateColumnDrawerOpen)}
|
||||
className={`add-new-column-btn ghost-black-operation ${isCreateColumnDrawerOpen && 'open'}`}
|
||||
className={`ghost-black-operation ${isCreateColumnDrawerOpen ? 'open' : ''}`}
|
||||
data-cy="add-new-column-button"
|
||||
>
|
||||
<SolidIcon name="column" width="14" fill={isCreateColumnDrawerOpen ? '#3E63DD' : '#889096'} />
|
||||
|
|
@ -43,16 +43,18 @@ const CreateColumnDrawer = ({ setIsCreateColumnDrawerOpen, isCreateColumnDrawerO
|
|||
);
|
||||
}
|
||||
});
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable.id).then(({ data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to fetch table "${selectedTable.table_name}"`);
|
||||
return;
|
||||
}
|
||||
tooljetDatabaseService
|
||||
.findOne(organizationId, selectedTable.id, 'order=id.desc')
|
||||
.then(({ data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to fetch table "${selectedTable.table_name}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(data) && data?.length > 0) {
|
||||
setSelectedTableData(data);
|
||||
}
|
||||
});
|
||||
if (Array.isArray(data) && data?.length > 0) {
|
||||
setSelectedTableData(data);
|
||||
}
|
||||
});
|
||||
setIsCreateColumnDrawerOpen(false);
|
||||
}}
|
||||
onClose={() => setIsCreateColumnDrawerOpen(false)}
|
||||
|
|
|
|||
|
|
@ -15,26 +15,30 @@ const CreateRowDrawer = ({ isCreateRowDrawerOpen, setIsCreateRowDrawerOpen }) =>
|
|||
onClick={() => {
|
||||
setIsCreateRowDrawerOpen(!isCreateRowDrawerOpen);
|
||||
}}
|
||||
className="tj-db-header-add-new-row-btn tj-text-xsm font-weight-500"
|
||||
className={`ghost-black-operation ${isCreateRowDrawerOpen ? 'open' : ''}`}
|
||||
>
|
||||
<SolidIcon name="row" width="14" />
|
||||
<span data-cy="add-new-row-button-text">Add new row</span>
|
||||
<SolidIcon name="row" width="14" fill={isCreateRowDrawerOpen ? '#3E63DD' : '#889096'} />
|
||||
<span data-cy="add-new-row-button-text" className="tj-text-xsm font-weight-500" style={{ marginLeft: '6px' }}>
|
||||
Add new row
|
||||
</span>
|
||||
</button>
|
||||
<Drawer isOpen={isCreateRowDrawerOpen} onClose={() => setIsCreateRowDrawerOpen(false)} position="right">
|
||||
<CreateRowForm
|
||||
onCreate={() => {
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable.id).then(({ headers, data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to fetch table "${selectedTable.table_name}"`);
|
||||
return;
|
||||
}
|
||||
tooljetDatabaseService
|
||||
.findOne(organizationId, selectedTable.id, 'order=id.desc')
|
||||
.then(({ headers, data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to fetch table "${selectedTable.table_name}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(data) && data?.length > 0) {
|
||||
const totalContentRangeRecords = headers['content-range'].split('/')[1] || 0;
|
||||
setTotalRecords(totalContentRangeRecords);
|
||||
setSelectedTableData(data);
|
||||
}
|
||||
});
|
||||
if (Array.isArray(data) && data?.length > 0) {
|
||||
const totalContentRangeRecords = headers['content-range'].split('/')[1] || 0;
|
||||
setTotalRecords(totalContentRangeRecords);
|
||||
setSelectedTableData(data);
|
||||
}
|
||||
});
|
||||
setIsCreateRowDrawerOpen(false);
|
||||
}}
|
||||
onClose={() => setIsCreateRowDrawerOpen(false)}
|
||||
|
|
|
|||
|
|
@ -12,14 +12,21 @@ const EditRowDrawer = ({ isCreateRowDrawerOpen, setIsCreateRowDrawerOpen }) => {
|
|||
<>
|
||||
<button
|
||||
onClick={() => setIsCreateRowDrawerOpen(!isCreateRowDrawerOpen)}
|
||||
className={`edit-row-btn border-0 ghost-black-operation ${isCreateRowDrawerOpen && 'open'}`}
|
||||
className={`ghost-black-operation ${isCreateRowDrawerOpen ? 'open' : ''}`}
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* <SolidIcon name="editrectangle" width="14" fill={isCreateRowDrawerOpen ? '#3E63DD' : '#889096'} /> */}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 12 13" fill="none">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.390524 1.21767C0.640573 0.967625 0.979711 0.827148 1.33333 0.827148H10.6667C11.0203 0.827148 11.3594 0.967624 11.6095 1.21767C11.8595 1.46772 12 1.80686 12 2.16048V4.82715C12 5.18077 11.8595 5.51991 11.6095 5.76996C11.3594 6.02001 11.0203 6.16048 10.6667 6.16048H1.33333C0.979711 6.16048 0.640573 6.02001 0.390524 5.76996C0.140476 5.51991 0 5.18077 0 4.82715V2.16048C0 1.80686 0.140476 1.46772 0.390524 1.21767ZM10.6667 2.16048H1.33333L1.33333 4.82715H10.6667V2.16048ZM6 7.49381C6.36819 7.49381 6.66667 7.79229 6.66667 8.16048V8.82715H7.33333C7.70152 8.82715 8 9.12562 8 9.49381C8 9.862 7.70152 10.1605 7.33333 10.1605H6.66667V10.8271C6.66667 11.1953 6.36819 11.4938 6 11.4938C5.63181 11.4938 5.33333 11.1953 5.33333 10.8271V10.1605H4.66667C4.29848 10.1605 4 9.862 4 9.49381C4 9.12562 4.29848 8.82715 4.66667 8.82715H5.33333V8.16048C5.33333 7.79229 5.63181 7.49381 6 7.49381Z"
|
||||
fill="#3E63DD"
|
||||
d="M8.216 1.30645C8.80114 0.721316 9.74983 0.721316 10.335 1.30645L11.3944 2.36593C11.9796 2.95106 11.9796 3.89975 11.3944 4.48489L10.4551 5.42426C10.3813 5.38774 10.3037 5.34826 10.2233 5.30591C9.68182 5.02084 9.03927 4.62074 8.55972 4.1412C8.08017 3.66165 7.68008 3.01909 7.39501 2.47762C7.35265 2.39716 7.31316 2.31956 7.27664 2.24581L8.216 1.30645Z"
|
||||
fill={isCreateRowDrawerOpen ? '#3E63DD' : '#889096'}
|
||||
/>
|
||||
<path
|
||||
d="M7.87225 4.82866C8.43972 5.39613 9.1614 5.84275 9.73294 6.14639L6.03887 9.84046C5.80963 10.0697 5.51223 10.2184 5.19129 10.2642L2.96638 10.5821C2.47196 10.6527 2.04817 10.2289 2.1188 9.73451L2.43664 7.5096C2.48249 7.18867 2.6312 6.89126 2.86044 6.66202L6.55451 2.96794C6.85815 3.53949 7.30478 4.26119 7.87225 4.82866Z"
|
||||
fill={isCreateRowDrawerOpen ? '#3E63DD' : '#889096'}
|
||||
/>
|
||||
<path
|
||||
d="M0.652737 11.562C0.384265 11.562 0.166626 11.7797 0.166626 12.0482C0.166626 12.3166 0.384265 12.5343 0.652737 12.5343H11.3472C11.6157 12.5343 11.8333 12.3166 11.8333 12.0482C11.8333 11.7797 11.6157 11.562 11.3472 11.562H0.652737Z"
|
||||
fill={isCreateRowDrawerOpen ? '#3E63DD' : '#889096'}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
|
@ -30,18 +37,20 @@ const EditRowDrawer = ({ isCreateRowDrawerOpen, setIsCreateRowDrawerOpen }) => {
|
|||
<Drawer isOpen={isCreateRowDrawerOpen} onClose={() => setIsCreateRowDrawerOpen(false)} position="right">
|
||||
<EditRowForm
|
||||
onEdit={() => {
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable.id).then(({ headers, data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to fetch table "${selectedTable.table_name}"`);
|
||||
return;
|
||||
}
|
||||
tooljetDatabaseService
|
||||
.findOne(organizationId, selectedTable.id, 'order=id.desc')
|
||||
.then(({ headers, data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to fetch table "${selectedTable.table_name}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(data) && data?.length > 0) {
|
||||
const totalContentRangeRecords = headers['content-range'].split('/')[1] || 0;
|
||||
setTotalRecords(totalContentRangeRecords);
|
||||
setSelectedTableData(data);
|
||||
}
|
||||
});
|
||||
if (Array.isArray(data) && data?.length > 0) {
|
||||
const totalContentRangeRecords = headers['content-range'].split('/')[1] || 0;
|
||||
setTotalRecords(totalContentRangeRecords);
|
||||
setSelectedTableData(data);
|
||||
}
|
||||
});
|
||||
setIsCreateRowDrawerOpen(false);
|
||||
}}
|
||||
onClose={() => setIsCreateRowDrawerOpen(false)}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import React from 'react';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
function ExportSchema({ onClick }) {
|
||||
return (
|
||||
<button className={`export-table-button tj-text-xsm font-weight-500 ghost-black-operation`} onClick={onClick}>
|
||||
<SolidIcon name="arrowsortrectangle" width="14" fill={'#889096'} />
|
||||
Export table
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExportSchema;
|
||||
|
|
@ -4,7 +4,7 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
|||
import Popover from 'react-bootstrap/Popover';
|
||||
import { FilterForm } from '../Forms/FilterForm';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { pluralize } from '@/_helpers/utils';
|
||||
// import { pluralize } from '@/_helpers/utils';
|
||||
import { useMounted } from '@/_hooks/use-mount';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
|
|
@ -94,9 +94,9 @@ const Filter = ({ filters, setFilters, handleBuildFilterQuery, resetFilterQuery
|
|||
>
|
||||
<SolidIcon name="filter" width="14" fill={areFiltersApplied ? '#46A758' : show ? '#3E63DD' : '#889096'} />
|
||||
Filter
|
||||
{areFiltersApplied && (
|
||||
{/* {areFiltersApplied && (
|
||||
<span>ed by {pluralize(Object.values(filters).filter(checkIsFilterObjectEmpty).length, 'column')}</span>
|
||||
)}
|
||||
)} */}
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ const RenderElement = ({ columnName, dataType, isPrimaryKey, defaultValue, value
|
|||
switch (dataType) {
|
||||
case 'character varying':
|
||||
case 'integer':
|
||||
case 'bigint':
|
||||
case 'serial':
|
||||
case 'double precision':
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ const RowForm = ({ onCreate, onClose }) => {
|
|||
switch (dataType) {
|
||||
case 'character varying':
|
||||
case 'integer':
|
||||
case 'bigint':
|
||||
case 'serial':
|
||||
case 'double precision':
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import cx from 'classnames';
|
|||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import Popover from 'react-bootstrap/Popover';
|
||||
import { SortForm } from '../Forms/SortForm';
|
||||
import { pluralize } from '@/_helpers/utils';
|
||||
// import { pluralize } from '@/_helpers/utils';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useMounted } from '@/_hooks/use-mount';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
|
@ -102,9 +102,9 @@ const Sort = ({ filters, setFilters, handleBuildSortQuery, resetSortQuery }) =>
|
|||
fill={areFiltersApplied ? '#46A758' : show ? '#3E63DD' : '#889096'}
|
||||
/>
|
||||
Sort
|
||||
{areFiltersApplied && (
|
||||
{/* {areFiltersApplied && (
|
||||
<span>ed by {pluralize(Object.values(filters).filter(checkIsFilterObjectEmpty).length, 'column')}</span>
|
||||
)}
|
||||
)} */}
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ const Table = ({ openCreateRowDrawer, openCreateColumnDrawer }) => {
|
|||
};
|
||||
|
||||
const fetchTableData = (queryParams = '', pagesize = 50, pagecount = 1) => {
|
||||
const defaultQueryParams = `limit=${pagesize}&offset=${(pagecount - 1) * pagesize}`;
|
||||
const defaultQueryParams = `limit=${pagesize}&offset=${(pagecount - 1) * pagesize}&order=id.desc`;
|
||||
let params = queryParams ? queryParams : defaultQueryParams;
|
||||
setLoading(true);
|
||||
|
||||
|
|
@ -109,6 +109,8 @@ const Table = ({ openCreateRowDrawer, openCreateColumnDrawer }) => {
|
|||
switch (type) {
|
||||
case 'integer':
|
||||
return 'int';
|
||||
case 'bigint':
|
||||
return 'bigint';
|
||||
case 'character varying':
|
||||
return 'varchar';
|
||||
case 'boolean':
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import EditIcon from './Icons/Edit.svg';
|
|||
import DeleteIcon from './Icons/Delete.svg';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
export const ListItemPopover = ({ onEdit, onDelete, darkMode }) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
export const ListItemPopover = ({ onEdit, onDelete, darkMode, handleExportTable, onMenuToggle }) => {
|
||||
const closeMenu = () => {
|
||||
document.body.click();
|
||||
};
|
||||
|
||||
const popover = (
|
||||
<Popover id="popover-contained" className={`table-list-items ${darkMode && 'dark-theme'}`}>
|
||||
|
|
@ -20,14 +22,30 @@ export const ListItemPopover = ({ onEdit, onDelete, darkMode }) => {
|
|||
<div
|
||||
className="col text-truncate"
|
||||
data-cy="edit-option"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
closeMenu();
|
||||
onEdit();
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</div>
|
||||
</div>
|
||||
<div className="row mt-3 cursor-pointer">
|
||||
<div className="col-auto" data-cy="export-option-icon">
|
||||
<SolidIcon name="filedownload" width="13" viewBox="0 0 25 25" />
|
||||
</div>
|
||||
<div
|
||||
className="col text-truncate"
|
||||
data-cy="export-table-option"
|
||||
onClick={() => {
|
||||
closeMenu();
|
||||
handleExportTable();
|
||||
}}
|
||||
>
|
||||
Export table
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="row mt-3">
|
||||
<div className="col-auto">
|
||||
<CloneIcon />
|
||||
|
|
@ -38,7 +56,14 @@ export const ListItemPopover = ({ onEdit, onDelete, darkMode }) => {
|
|||
<div className="col-auto" data-cy="delete-option-icon">
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
<div className="col text-truncate" data-cy="delete-option" onClick={onDelete}>
|
||||
<div
|
||||
className="col text-truncate"
|
||||
data-cy="delete-option"
|
||||
onClick={() => {
|
||||
closeMenu();
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -47,27 +72,12 @@ export const ListItemPopover = ({ onEdit, onDelete, darkMode }) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(`float-right cursor-pointer table-list-item-popover`, {
|
||||
'd-grid': open,
|
||||
})}
|
||||
data-cy="table-kebab-icon"
|
||||
>
|
||||
<OverlayTrigger
|
||||
onToggle={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
}}
|
||||
show={open}
|
||||
rootClose
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
overlay={popover}
|
||||
transition={false}
|
||||
>
|
||||
<OverlayTrigger trigger="click" placement="bottom" rootClose onToggle={onMenuToggle} overlay={popover}>
|
||||
<div className={cx(`float-right cursor-pointer table-list-item-popover`)} data-cy="table-kebab-icon">
|
||||
<span>
|
||||
<SolidIcon name="morevertical" width="14" fill={darkMode ? '#FDFDFE' : '#11181C'} />
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import React, { useState, useContext } from 'react';
|
||||
import React, { useState, useContext, useCallback, useEffect } from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { tooljetDatabaseService } from '@/_services';
|
||||
import { tooljetDatabaseService, appService } from '@/_services';
|
||||
import { ListItemPopover } from './ActionsPopover';
|
||||
import { TooljetDatabaseContext } from '../index';
|
||||
import { ToolTip } from '@/_components';
|
||||
|
|
@ -14,11 +13,41 @@ export const ListItem = ({ active, onClick, text = '', onDeleteCallback }) => {
|
|||
const { organizationId, columns, selectedTable, setTables, setSelectedTable } = useContext(TooljetDatabaseContext);
|
||||
const [isEditTableDrawerOpen, setIsEditTableDrawerOpen] = useState(false);
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [showDropDownMenu, setShowDropDownMenu] = useState(false);
|
||||
const [focused, setFocused] = useState(false);
|
||||
|
||||
function updateSelectedTable(tableObj) {
|
||||
setSelectedTable(tableObj);
|
||||
}
|
||||
|
||||
const handleExportTable = () => {
|
||||
appService
|
||||
.exportResource({
|
||||
tooljet_database: [{ table_id: selectedTable.id }],
|
||||
organization_id: organizationId,
|
||||
})
|
||||
.then((data) => {
|
||||
const tableName = selectedTable.table_name.replace(/\s+/g, '-').toLowerCase();
|
||||
const fileName = `${tableName}-export-${new Date().getTime()}`;
|
||||
// simulate link click download
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const href = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = href;
|
||||
link.download = fileName + '.json';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Could not export table.', {
|
||||
position: 'top-center',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteTable = async () => {
|
||||
const shouldDelete = confirm(`Are you sure you want to delete the table "${text}"?`);
|
||||
if (shouldDelete) {
|
||||
|
|
@ -39,8 +68,23 @@ export const ListItem = ({ active, onClick, text = '', onDeleteCallback }) => {
|
|||
return acc;
|
||||
}, {});
|
||||
|
||||
const onMenuToggle = useCallback(
|
||||
(status) => {
|
||||
setShowDropDownMenu(!!status);
|
||||
!status && !isHovered && setFocused(false);
|
||||
},
|
||||
[isHovered]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
!showDropDownMenu && setFocused(!!isHovered);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isHovered]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className={cx(
|
||||
'table-list-item mb-1 rounded-3 d-inline-flex align-items-center justify-content-between h-4 list-group-item cursor-pointer list-group-item-action border-0 py-1',
|
||||
{
|
||||
|
|
@ -59,7 +103,21 @@ export const ListItem = ({ active, onClick, text = '', onDeleteCallback }) => {
|
|||
{text}
|
||||
</span>
|
||||
</ToolTip>
|
||||
<ListItemPopover onEdit={() => setIsEditTableDrawerOpen(true)} onDelete={handleDeleteTable} darkMode={darkMode} />
|
||||
{focused && (
|
||||
<div>
|
||||
<ListItemPopover
|
||||
onEdit={() => {
|
||||
setShowDropDownMenu(false);
|
||||
setIsEditTableDrawerOpen(true);
|
||||
}}
|
||||
onDelete={handleDeleteTable}
|
||||
darkMode={darkMode}
|
||||
handleExportTable={handleExportTable}
|
||||
onMenuToggle={onMenuToggle}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Drawer
|
||||
disableFocus={true}
|
||||
isOpen={isEditTableDrawerOpen}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import React, { useState, useContext } from 'react';
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import cx from 'classnames';
|
||||
import Table from '../Table';
|
||||
import CreateColumnDrawer from '../Drawers/CreateColumnDrawer';
|
||||
import CreateRowDrawer from '../Drawers/CreateRowDrawer';
|
||||
import EditRowDrawer from '../Drawers/EditRowDrawer';
|
||||
import BulkUploadDrawer from '../Drawers/BulkUploadDrawer';
|
||||
import Filter from '../Filter';
|
||||
import Sort from '../Sort';
|
||||
import Sidebar from '../Sidebar';
|
||||
import { TooljetDatabaseContext } from '../index';
|
||||
import EmptyFoldersIllustration from '@assets/images/icons/no-queries-added.svg';
|
||||
import ExportSchema from '../ExportSchema/ExportSchema';
|
||||
import { appService } from '@/_services/app.service';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { tooljetDatabaseService } from '@/_services';
|
||||
import { pluralize } from '@/_helpers/utils';
|
||||
|
||||
const TooljetDatabasePage = ({ totalTables }) => {
|
||||
const {
|
||||
|
|
@ -27,11 +28,65 @@ const TooljetDatabasePage = ({ totalTables }) => {
|
|||
sortFilters,
|
||||
setSortFilters,
|
||||
organizationId,
|
||||
setTotalRecords,
|
||||
setSelectedTableData,
|
||||
} = useContext(TooljetDatabaseContext);
|
||||
|
||||
const [isCreateRowDrawerOpen, setIsCreateRowDrawerOpen] = useState(false);
|
||||
const [isBulkUploadDrawerOpen, setIsBulkUploadDrawerOpen] = useState(false);
|
||||
const [isEditRowDrawerOpen, setIsEditRowDrawerOpen] = useState(false);
|
||||
const [isCreateColumnDrawerOpen, setIsCreateColumnDrawerOpen] = useState(false);
|
||||
const [bulkUploadFile, setBulkUploadFile] = useState(null);
|
||||
const [isBulkUploading, setIsBulkUploading] = useState(false);
|
||||
const [errors, setErrors] = useState({ client: [], server: [] });
|
||||
const [uploadResult, setUploadResult] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setErrors({ client: [], server: [] });
|
||||
handleFileValidation();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bulkUploadFile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBulkUploadDrawerOpen) {
|
||||
setErrors({ client: [], server: [] });
|
||||
setBulkUploadFile(null);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isBulkUploadDrawerOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmpty(selectedTable)) return;
|
||||
|
||||
const reloadTableData = async () => {
|
||||
const { headers, data, error } = await tooljetDatabaseService.findOne(
|
||||
organizationId,
|
||||
selectedTable.id,
|
||||
'order=id.desc'
|
||||
);
|
||||
|
||||
if (error) {
|
||||
toast.error(error?.message ?? 'Something went wrong');
|
||||
return;
|
||||
}
|
||||
const totalRecords = headers['content-range'].split('/')[1] || 0;
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
setTotalRecords(totalRecords);
|
||||
setSelectedTableData(data);
|
||||
}
|
||||
};
|
||||
|
||||
setIsBulkUploading(false);
|
||||
setBulkUploadFile(null);
|
||||
setIsBulkUploadDrawerOpen(false);
|
||||
setQueryFilters({});
|
||||
resetFilterQuery();
|
||||
setSortFilters({});
|
||||
resetSortQuery();
|
||||
reloadTableData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [uploadResult]);
|
||||
|
||||
const EmptyState = () => {
|
||||
return (
|
||||
|
|
@ -56,31 +111,54 @@ const TooljetDatabasePage = ({ totalTables }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const exportTable = () => {
|
||||
appService
|
||||
.exportResource({
|
||||
tooljet_database: [{ table_id: selectedTable.id }],
|
||||
organization_id: organizationId,
|
||||
})
|
||||
.then((data) => {
|
||||
const tableName = selectedTable.table_name.replace(/\s+/g, '-').toLowerCase();
|
||||
const fileName = `${tableName}-export-${new Date().getTime()}`;
|
||||
// simulate link click download
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const href = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = href;
|
||||
link.download = fileName + '.json';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Could not export table.', {
|
||||
position: 'top-center',
|
||||
});
|
||||
const handleFileValidation = () => {
|
||||
const fileValidationErrors = [];
|
||||
|
||||
if (bulkUploadFile && bulkUploadFile.size / 1024 > 2 * 1024) {
|
||||
fileValidationErrors.push('File size cannot exceed 2mb');
|
||||
}
|
||||
|
||||
setErrors({ server: [], client: fileValidationErrors });
|
||||
};
|
||||
|
||||
const handleBulkUpload = async (event) => {
|
||||
event.preventDefault();
|
||||
setErrors({ client: [], server: [] });
|
||||
setIsBulkUploading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', bulkUploadFile);
|
||||
try {
|
||||
const { error, data } = await tooljetDatabaseService.bulkUpload(
|
||||
organizationId,
|
||||
selectedTable.table_name,
|
||||
formData
|
||||
);
|
||||
|
||||
if (error) {
|
||||
setErrors({ ...errors, ...{ server: error.message } });
|
||||
setIsBulkUploading(false);
|
||||
toast.error('Upload failed!', { position: 'top-center' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { processed_rows: processedRows, rows_inserted: rowsInserted, rows_updated: rowsUpdated } = data.result;
|
||||
const toastMessage =
|
||||
`${pluralize(rowsInserted, 'new row')} added, ` + `${pluralize(rowsUpdated, 'row')} updated.`;
|
||||
|
||||
toast.success(toastMessage, {
|
||||
position: 'top-center',
|
||||
});
|
||||
|
||||
setUploadResult({ processedRows, rowsInserted, rowsUpdated });
|
||||
} catch (error) {
|
||||
toast.error(error.errors, { position: 'top-center' });
|
||||
setIsBulkUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkUploadFileChange = (file) => {
|
||||
setBulkUploadFile(file);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -92,28 +170,15 @@ const TooljetDatabasePage = ({ totalTables }) => {
|
|||
<>
|
||||
<div className="database-table-header-wrapper">
|
||||
<div className="card border-0">
|
||||
<div className="card-body tj-db-operaions-header">
|
||||
<div className="card-body tj-db-operations-header">
|
||||
<div className="row align-items-center">
|
||||
<div className="col d-flex">
|
||||
<div className="col-8 align-items-center p-3">
|
||||
<CreateColumnDrawer
|
||||
isCreateColumnDrawerOpen={isCreateColumnDrawerOpen}
|
||||
setIsCreateColumnDrawerOpen={setIsCreateColumnDrawerOpen}
|
||||
/>
|
||||
{columns?.length > 0 && (
|
||||
<>
|
||||
<Filter
|
||||
filters={queryFilters}
|
||||
setFilters={setQueryFilters}
|
||||
handleBuildFilterQuery={handleBuildFilterQuery}
|
||||
resetFilterQuery={resetFilterQuery}
|
||||
/>
|
||||
<Sort
|
||||
filters={sortFilters}
|
||||
setFilters={setSortFilters}
|
||||
handleBuildSortQuery={handleBuildSortQuery}
|
||||
resetSortQuery={resetSortQuery}
|
||||
/>
|
||||
<ExportSchema onClick={exportTable} />
|
||||
<CreateRowDrawer
|
||||
isCreateRowDrawerOpen={isCreateRowDrawerOpen}
|
||||
setIsCreateRowDrawerOpen={setIsCreateRowDrawerOpen}
|
||||
|
|
@ -122,9 +187,38 @@ const TooljetDatabasePage = ({ totalTables }) => {
|
|||
isCreateRowDrawerOpen={isEditRowDrawerOpen}
|
||||
setIsCreateRowDrawerOpen={setIsEditRowDrawerOpen}
|
||||
/>
|
||||
<BulkUploadDrawer
|
||||
isBulkUploadDrawerOpen={isBulkUploadDrawerOpen}
|
||||
setIsBulkUploadDrawerOpen={setIsBulkUploadDrawerOpen}
|
||||
bulkUploadFile={bulkUploadFile}
|
||||
handleBulkUploadFileChange={handleBulkUploadFileChange}
|
||||
handleBulkUpload={handleBulkUpload}
|
||||
isBulkUploading={isBulkUploading}
|
||||
errors={errors}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-4 align-items-end">
|
||||
<div className="row d-flex align-items-center justify-content-end">
|
||||
<div className="col-3 p-1">
|
||||
<Filter
|
||||
filters={queryFilters}
|
||||
setFilters={setQueryFilters}
|
||||
handleBuildFilterQuery={handleBuildFilterQuery}
|
||||
resetFilterQuery={resetFilterQuery}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-3 p-1">
|
||||
<Sort
|
||||
filters={sortFilters}
|
||||
setFilters={setSortFilters}
|
||||
handleBuildSortQuery={handleBuildSortQuery}
|
||||
resetSortQuery={resetSortQuery}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export const dataTypes = [
|
||||
{ value: 'character varying', label: 'varchar' },
|
||||
{ value: 'integer', label: 'int' },
|
||||
{ value: 'bigint', label: 'bigint' },
|
||||
{ value: 'double precision', label: 'float' },
|
||||
{ value: 'boolean', label: 'boolean' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -25,10 +25,14 @@ export const usePostgrestQueryBuilder = ({ organizationId, selectedTable, setSel
|
|||
};
|
||||
|
||||
const updateSelectedTableData = async () => {
|
||||
const sortQuery = isEmpty(postgrestQueryBuilder.current.sortQuery.url.toString())
|
||||
? 'order=id.desc'
|
||||
: postgrestQueryBuilder.current.sortQuery.url.toString();
|
||||
|
||||
const query =
|
||||
postgrestQueryBuilder.current.filterQuery.url.toString() +
|
||||
'&' +
|
||||
postgrestQueryBuilder.current.sortQuery.url.toString() +
|
||||
sortQuery +
|
||||
'&' +
|
||||
postgrestQueryBuilder.current.paginationQuery.url.toString();
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export function ConfirmDialog({
|
|||
cancelButtonText = 'Cancel',
|
||||
backdropClassName,
|
||||
onCloseIconClick,
|
||||
footerStyle,
|
||||
}) {
|
||||
darkMode = darkMode ?? (localStorage.getItem('darkMode') === 'true' || false);
|
||||
const [showModal, setShow] = useState(show);
|
||||
|
|
@ -73,7 +74,7 @@ export function ConfirmDialog({
|
|||
<Modal.Body className="confirm-dialogue-body" data-cy="modal-message">
|
||||
{message}
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="mt-3">
|
||||
<Modal.Footer className="mt-3" style={footerStyle}>
|
||||
<ButtonSolid variant={cancelButtonType} onClick={handleClose} data-cy="cancel-button">
|
||||
{cancelButtonText ?? t('globals.cancel', 'Cancel')}
|
||||
</ButtonSolid>
|
||||
|
|
|
|||
|
|
@ -889,6 +889,7 @@ export function previewQuery(_ref, query, calledFromQuery = false, parameters =
|
|||
setPreviewLoading(false);
|
||||
setPreviewData(finalData);
|
||||
}
|
||||
let queryStatusCode = data?.status ?? null;
|
||||
const queryStatus =
|
||||
query.kind === 'tooljetdb'
|
||||
? data.statusText
|
||||
|
|
@ -896,23 +897,29 @@ export function previewQuery(_ref, query, calledFromQuery = false, parameters =
|
|||
? data?.data?.status ?? 'ok'
|
||||
: data.status;
|
||||
|
||||
switch (queryStatus) {
|
||||
case 'Bad Request':
|
||||
case 'failed': {
|
||||
switch (true) {
|
||||
// Note: Need to move away from statusText -> statusCode
|
||||
case queryStatus === 'Bad Request' ||
|
||||
queryStatus === 'Not Found' ||
|
||||
queryStatus === 'Unprocessable Entity' ||
|
||||
queryStatus === 'failed' ||
|
||||
queryStatusCode === 400 ||
|
||||
queryStatusCode === 404 ||
|
||||
queryStatusCode === 422: {
|
||||
const err = query.kind == 'tooljetdb' ? data?.error || data : _.isEmpty(data.data) ? data : data.data;
|
||||
toast.error(`${err.message}`);
|
||||
break;
|
||||
}
|
||||
case 'needs_oauth': {
|
||||
case queryStatus === 'needs_oauth': {
|
||||
const url = data.data.auth_url; // Backend generates and return sthe auth url
|
||||
fetchOAuthToken(url, query.data_source_id);
|
||||
break;
|
||||
}
|
||||
case 'ok':
|
||||
case 'OK':
|
||||
case 'Created':
|
||||
case 'Accepted':
|
||||
case 'No Content': {
|
||||
case queryStatus === 'ok' ||
|
||||
queryStatus === 'OK' ||
|
||||
queryStatus === 'Created' ||
|
||||
queryStatus === 'Accepted' ||
|
||||
queryStatus === 'No Content': {
|
||||
toast(`Query ${'(' + query.name + ') ' || ''}completed.`, {
|
||||
icon: '🚀',
|
||||
});
|
||||
|
|
@ -997,15 +1004,45 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
|
|||
fetchOAuthToken(url, dataQuery['data_source_id'] || dataQuery['dataSourceId']);
|
||||
}
|
||||
|
||||
let queryStatusCode = data?.status ?? null;
|
||||
const promiseStatus =
|
||||
query.kind === 'tooljetdb'
|
||||
? data.statusText
|
||||
: query.kind === 'runpy'
|
||||
? data?.data?.status ?? 'ok'
|
||||
: data.status;
|
||||
|
||||
if (promiseStatus === 'failed' || promiseStatus === 'Bad Request') {
|
||||
const errorData = query.kind === 'runpy' ? data.data : data;
|
||||
// Note: Need to move away from statusText -> statusCode
|
||||
if (
|
||||
promiseStatus === 'failed' ||
|
||||
promiseStatus === 'Bad Request' ||
|
||||
promiseStatus === 'Not Found' ||
|
||||
promiseStatus === 'Unprocessable Entity' ||
|
||||
queryStatusCode === 400 ||
|
||||
queryStatusCode === 404 ||
|
||||
queryStatusCode === 422
|
||||
) {
|
||||
let errorData = {};
|
||||
switch (query.kind) {
|
||||
case 'runpy':
|
||||
errorData = data.data;
|
||||
break;
|
||||
case 'tooljetdb':
|
||||
if (data?.error) {
|
||||
errorData = {
|
||||
message: data?.error?.message || 'Something went wrong',
|
||||
description: data?.error?.message || 'Something went wrong',
|
||||
status: data?.statusText || 'Failed',
|
||||
data: data?.error || {},
|
||||
};
|
||||
} else {
|
||||
errorData = data;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
errorData = data;
|
||||
break;
|
||||
}
|
||||
// errorData = query.kind === 'runpy' ? data.data : data;
|
||||
useCurrentStateStore.getState().actions.setErrors({
|
||||
[queryName]: {
|
||||
type: 'query',
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class HttpClient {
|
|||
const endpoint = urlJoin(this.host, this.namespace, url);
|
||||
const options = {
|
||||
method,
|
||||
headers: this.headers,
|
||||
headers: { ...this.headers },
|
||||
credentials: 'include',
|
||||
};
|
||||
let session = authenticationService.currentSessionValue;
|
||||
|
|
@ -48,9 +48,13 @@ class HttpClient {
|
|||
}
|
||||
|
||||
options.headers['tj-workspace-id'] = session?.current_organization_id;
|
||||
|
||||
if (data) {
|
||||
options.body = JSON.stringify(data);
|
||||
// fetch library generates content type with boundary for form data
|
||||
data instanceof FormData && delete options.headers['content-type'];
|
||||
options.body = data instanceof FormData ? data : JSON.stringify(data);
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, options);
|
||||
const payload = {
|
||||
status: response.status,
|
||||
|
|
|
|||
|
|
@ -4,30 +4,34 @@ const tooljetAdapter = new HttpClient();
|
|||
|
||||
function findOne(headers, tableId, query = '') {
|
||||
tooljetAdapter.headers = { ...tooljetAdapter.headers, ...headers };
|
||||
return tooljetAdapter.get(`/tooljet_db/proxy/${tableId}?${query}`, headers);
|
||||
return tooljetAdapter.get(`/tooljet-db/proxy/${tableId}?${query}`, headers);
|
||||
}
|
||||
|
||||
function findAll(organizationId) {
|
||||
return tooljetAdapter.get(`/tooljet_db/organizations/${organizationId}/tables`);
|
||||
return tooljetAdapter.get(`/tooljet-db/organizations/${organizationId}/tables`);
|
||||
}
|
||||
|
||||
function createTable(organizationId, tableName, columns) {
|
||||
return tooljetAdapter.post(`/tooljet_db/organizations/${organizationId}/table`, {
|
||||
return tooljetAdapter.post(`/tooljet-db/organizations/${organizationId}/table`, {
|
||||
table_name: tableName,
|
||||
columns,
|
||||
});
|
||||
}
|
||||
|
||||
function viewTable(organizationId, tableName) {
|
||||
return tooljetAdapter.get(`/tooljet_db/organizations/${organizationId}/table/${tableName}`);
|
||||
return tooljetAdapter.get(`/tooljet-db/organizations/${organizationId}/table/${tableName}`);
|
||||
}
|
||||
|
||||
function bulkUpload(organizationId, tableName, file) {
|
||||
return tooljetAdapter.post(`/tooljet-db/organizations/${organizationId}/table/${tableName}/bulk-upload`, file);
|
||||
}
|
||||
|
||||
function createRow(headers, tableId, data) {
|
||||
return tooljetAdapter.post(`/tooljet_db/proxy/${tableId}`, data, headers);
|
||||
return tooljetAdapter.post(`/tooljet-db/proxy/${tableId}`, data, headers);
|
||||
}
|
||||
|
||||
function createColumn(organizationId, tableId, columnName, dataType, defaultValue) {
|
||||
return tooljetAdapter.post(`/tooljet_db/organizations/${organizationId}/table/${tableId}/column`, {
|
||||
return tooljetAdapter.post(`/tooljet-db/organizations/${organizationId}/table/${tableId}/column`, {
|
||||
column: {
|
||||
column_name: columnName,
|
||||
data_type: dataType,
|
||||
|
|
@ -37,7 +41,7 @@ function createColumn(organizationId, tableId, columnName, dataType, defaultValu
|
|||
}
|
||||
|
||||
function updateTable(organizationId, tableName, columns) {
|
||||
return tooljetAdapter.patch(`/tooljet_db/${organizationId}/perform`, {
|
||||
return tooljetAdapter.patch(`/tooljet-db/${organizationId}/perform`, {
|
||||
action: 'update_table',
|
||||
table_name: tableName,
|
||||
columns,
|
||||
|
|
@ -45,7 +49,7 @@ function updateTable(organizationId, tableName, columns) {
|
|||
}
|
||||
|
||||
function renameTable(organizationId, tableName, newTableName) {
|
||||
return tooljetAdapter.patch(`/tooljet_db/organizations/${organizationId}/table/${tableName}`, {
|
||||
return tooljetAdapter.patch(`/tooljet-db/organizations/${organizationId}/table/${tableName}`, {
|
||||
action: 'rename_table',
|
||||
table_name: tableName,
|
||||
new_table_name: newTableName,
|
||||
|
|
@ -53,19 +57,23 @@ function renameTable(organizationId, tableName, newTableName) {
|
|||
}
|
||||
|
||||
function updateRows(headers, tableId, data, query = '') {
|
||||
return tooljetAdapter.patch(`/tooljet_db/proxy/${tableId}?${query}`, data, headers);
|
||||
return tooljetAdapter.patch(`/tooljet-db/proxy/${tableId}?${query}`, data, headers);
|
||||
}
|
||||
|
||||
function deleteRows(headers, tableId, query = '') {
|
||||
return tooljetAdapter.delete(`/tooljet_db/proxy/${tableId}?${query}`, headers);
|
||||
return tooljetAdapter.delete(`/tooljet-db/proxy/${tableId}?${query}`, headers);
|
||||
}
|
||||
|
||||
function deleteColumn(organizationId, tableName, columnName) {
|
||||
return tooljetAdapter.delete(`/tooljet_db/organizations/${organizationId}/table/${tableName}/column/${columnName}`);
|
||||
return tooljetAdapter.delete(`/tooljet-db/organizations/${organizationId}/table/${tableName}/column/${columnName}`);
|
||||
}
|
||||
|
||||
function deleteTable(organizationId, tableName) {
|
||||
return tooljetAdapter.delete(`/tooljet_db/organizations/${organizationId}/table/${tableName}`);
|
||||
return tooljetAdapter.delete(`/tooljet-db/organizations/${organizationId}/table/${tableName}`);
|
||||
}
|
||||
|
||||
function joinTables(organizationId, data) {
|
||||
return tooljetAdapter.post(`tooljet-db/organizations/${organizationId}/join`, data);
|
||||
}
|
||||
|
||||
export const tooljetDatabaseService = {
|
||||
|
|
@ -81,4 +89,6 @@ export const tooljetDatabaseService = {
|
|||
deleteColumn,
|
||||
deleteTable,
|
||||
renameTable,
|
||||
bulkUpload,
|
||||
joinTables,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -496,6 +496,11 @@ $border-radius: 4px;
|
|||
width: 34px;
|
||||
}
|
||||
|
||||
.delete-field-option-dark {
|
||||
background-color: #272822 !important;
|
||||
border-color: #272822 !important;
|
||||
}
|
||||
|
||||
.code-hinter.codehinter-default-input {
|
||||
border: 1px solid transparent !important;
|
||||
}
|
||||
|
|
@ -1525,6 +1530,51 @@ $border-radius: 4px;
|
|||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.dd-select-control-chevron {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.dd-select-value-badge {
|
||||
border-radius: 6px;
|
||||
background: var(--slate3) !important;
|
||||
text-transform: none;
|
||||
color: var(--slate12) !important;
|
||||
}
|
||||
|
||||
.dd-select-alert-error {
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--tomato-05, #FDD8D3);
|
||||
background: var(--tomato-02, #FFF8F7);
|
||||
padding: 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
color: var(--tomato-09, #E54D2E);
|
||||
svg {
|
||||
height: 17px;
|
||||
width: 17px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
svg>path{
|
||||
fill:var(--tomato-09, #E54D2E);
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.tdb-join-filtersection {
|
||||
.codehinter-plugins {
|
||||
border: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tdb-dropdown-btn {
|
||||
&:active {
|
||||
border: 1px solid var(--indigo-09, #3E63DD) !important;
|
||||
background: var(--indigo2, #F8FAFF);
|
||||
box-shadow: 0px 0px 0px 1px #C6D4F9
|
||||
}
|
||||
}
|
||||
|
||||
.copilot-section-header {
|
||||
background-color: var(--slate2);
|
||||
border: 1px solid var(--slate5);
|
||||
|
|
|
|||
|
|
@ -4609,16 +4609,16 @@ input[type="text"] {
|
|||
color: $white !important;
|
||||
|
||||
.modal-title {
|
||||
color: $white !important;
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.tj-version-wrap-sub-footer {
|
||||
background-color: $bg-dark-light !important;
|
||||
background-color: $bg-dark-light !important;
|
||||
border-top: 1px solid #3A3F42 !important;
|
||||
|
||||
|
||||
p {
|
||||
color: $white !important;
|
||||
color: $white !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4629,7 +4629,9 @@ input[type="text"] {
|
|||
}
|
||||
|
||||
.modal-header {
|
||||
background-color: $bg-dark-light !important;
|
||||
background-color: $bg-dark-light !important;
|
||||
color: $white !important;
|
||||
border-bottom: 2px solid #3A3F42 !important;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
|
|
@ -6583,9 +6585,9 @@ input.hide-input-arrows {
|
|||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--slate3) !important;
|
||||
left: 109px !important;
|
||||
top: 8px !important;
|
||||
position: absolute !important;
|
||||
// left: 109px !important;
|
||||
// top: 8px !important;
|
||||
// position: absolute !important;
|
||||
|
||||
|
||||
.card-body,
|
||||
|
|
@ -6603,9 +6605,9 @@ input.hide-input-arrows {
|
|||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--slate3) !important;
|
||||
left: 193px !important;
|
||||
top: 10px !important;
|
||||
position: absolute !important;
|
||||
// left: 193px !important;
|
||||
// top: 10px !important;
|
||||
// position: absolute !important;
|
||||
|
||||
|
||||
.card-body,
|
||||
|
|
@ -6909,14 +6911,6 @@ tbody {
|
|||
.delete-row-btn {
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
.table-list-item-popover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-list-item:hover .table-list-item-popover {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.apploader {
|
||||
|
|
@ -8725,7 +8719,7 @@ tbody {
|
|||
}
|
||||
}
|
||||
|
||||
.tj-db-operaions-header {
|
||||
.tj-db-operations-header {
|
||||
height: 48px;
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
|
|
@ -8734,19 +8728,19 @@ tbody {
|
|||
|
||||
.row {
|
||||
margin-left: 0px;
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
.col {
|
||||
.col-8 {
|
||||
padding-left: 0px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.add-new-column-btn {
|
||||
margin-left: 16px;
|
||||
width: 144px !important;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
padding: 0 !important;
|
||||
|
|
@ -8759,7 +8753,7 @@ tbody {
|
|||
}
|
||||
|
||||
.tj-db-filter-btn {
|
||||
width: 81px;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
|
|
@ -8777,7 +8771,7 @@ tbody {
|
|||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
padding: 4px 16px !important;
|
||||
width: 171px !important;
|
||||
width: 100% !important;
|
||||
height: 28px !important;
|
||||
background: var(--grass2) !important;
|
||||
border-radius: 6px !important;
|
||||
|
|
@ -8785,19 +8779,22 @@ tbody {
|
|||
|
||||
.tj-db-filter-btn-active,
|
||||
.tj-db-sort-btn-active {
|
||||
width: 81px !important;
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
padding: 4px 16px !important;
|
||||
width: 100% !important;
|
||||
height: 28px !important;
|
||||
background: var(--indigo4) !important;
|
||||
border: 1px solid var(--indigo9) !important;
|
||||
border-radius: 6px !important;
|
||||
justify-content: center;
|
||||
background: var(--indigo4) !important;
|
||||
//border: 1px solid var(--indigo9) !important;
|
||||
color: var(--indigo9) !important;
|
||||
}
|
||||
|
||||
.tj-db-header-add-new-row-btn {
|
||||
width: 125px;
|
||||
height: 28px;
|
||||
background: var(--indigo3);
|
||||
background: transparent;
|
||||
border-radius: 6px !important;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
@ -8806,13 +8803,13 @@ tbody {
|
|||
gap: 6px;
|
||||
border: none;
|
||||
|
||||
span {
|
||||
color: var(--indigo9);
|
||||
padding: span {
|
||||
//color: var(--indigo9);
|
||||
}
|
||||
}
|
||||
|
||||
.tj-db-sort-btn {
|
||||
width: 75px;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
background: transparent;
|
||||
color: var(--slate12);
|
||||
|
|
@ -8820,6 +8817,7 @@ tbody {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
.edit-row-btn {
|
||||
|
|
@ -8828,6 +8826,7 @@ tbody {
|
|||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.workspace-variable-header {
|
||||
|
|
@ -9687,7 +9686,6 @@ tbody {
|
|||
padding: 60px 0px;
|
||||
gap: 36px;
|
||||
width: 486px;
|
||||
height: 244px;
|
||||
border: 2px dashed var(--indigo9);
|
||||
border-radius: 6px;
|
||||
align-items: center;
|
||||
|
|
@ -10425,6 +10423,7 @@ tbody {
|
|||
|
||||
.upload-user-form span.file-upload-error {
|
||||
color: var(--tomato10) !important;
|
||||
margin-top: 12px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.tj-onboarding-phone-input {
|
||||
|
|
@ -10613,7 +10612,7 @@ tbody {
|
|||
}
|
||||
|
||||
.export-table-button {
|
||||
width: 135px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -10621,7 +10620,7 @@ tbody {
|
|||
|
||||
|
||||
#global-settings-popover.theme-dark {
|
||||
background-color: $bg-dark-light !important;
|
||||
background-color: $bg-dark-light !important;
|
||||
border: 1px solid #2B2F31;
|
||||
|
||||
.global-popover-text {
|
||||
|
|
@ -10629,7 +10628,7 @@ tbody {
|
|||
}
|
||||
|
||||
.maximum-canvas-width-input-select {
|
||||
background-color: $bg-dark-light !important;
|
||||
background-color: $bg-dark-light !important;
|
||||
border: 1px solid #324156;
|
||||
color: $white;
|
||||
}
|
||||
|
|
@ -10924,6 +10923,13 @@ tbody {
|
|||
border: 1px solid var(--slate3);
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
border-radius: 6px;
|
||||
|
||||
.card-body-alignment {
|
||||
min-height: 145px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.template-source-name {
|
||||
|
|
@ -11581,6 +11587,14 @@ tbody {
|
|||
.custom-gap-6{
|
||||
gap:6px
|
||||
}
|
||||
|
||||
// ToolJet Database buttons
|
||||
|
||||
.ghost-black-operation {
|
||||
border: 1px solid transparent !important;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.custom-gap-2{
|
||||
gap:2px
|
||||
}
|
||||
|
|
@ -11599,4 +11613,4 @@ tbody {
|
|||
border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,13 +64,14 @@
|
|||
.tj-primary-btn {
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.00);
|
||||
background: var(--indigo9) ;
|
||||
background: var(--indigo9);
|
||||
box-shadow: 0px 2px 0px 0px rgba(0, 0, 0, 0.04);
|
||||
color: #FDFDFE;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid rgba(255, 255, 255, 0.00);
|
||||
background: var(--indigo10) ;
|
||||
color: #FDFDFE;
|
||||
background: var(--indigo10);
|
||||
color: #FDFDFE;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
|
|
@ -93,6 +94,7 @@
|
|||
color: var(--indigo9) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.00) !important;
|
||||
background: var(--indigo3, #F0F4FF) !important;
|
||||
|
||||
&:hover {
|
||||
color: var(--indigo10) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.00) !important;
|
||||
|
|
@ -115,7 +117,8 @@
|
|||
|
||||
/* Focus rings/Indigo/light */
|
||||
box-shadow: 0px 0px 0px 4px var(--indigo6);
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
.tj-tertiary-btn {
|
||||
color: var(--slate12);
|
||||
|
|
@ -128,22 +131,27 @@
|
|||
background: var(--slate4, #ECEEF0) !important;
|
||||
}
|
||||
|
||||
&:active,&:focus,&.always-active-btn {
|
||||
&:active,
|
||||
&:focus,
|
||||
&.always-active-btn {
|
||||
border: 1px solid var(--slate7, #D7DBDF) !important;
|
||||
background: var(--slate5) !important;
|
||||
color: var(--slate12);
|
||||
border: 1px solid var(--slate7);
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
background: transparent;
|
||||
background: var(--slate1);
|
||||
box-shadow: none;
|
||||
color: var(--slate12) !important;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
color: var(--slate11) !important;
|
||||
box-shadow: 0px 0px 0px 4px var(--slate6) !important;
|
||||
background: var(--slate1);
|
||||
color: var(--slate11);
|
||||
outline: 1px solid var(--slate8);
|
||||
box-shadow: 0px 0px 0px 4px var(--slate6);
|
||||
outline: none;
|
||||
border: 1px solid var(--slate8, #C1C8CD) !important;
|
||||
background: var(--slate1, #FBFCFD) !important;
|
||||
|
|
@ -188,7 +196,8 @@
|
|||
color: var(--slate11);
|
||||
border: 1px solid rgba(255, 255, 255, 0.00) !important;
|
||||
}
|
||||
&:focus-visible{
|
||||
|
||||
&:focus-visible {
|
||||
color: var(--slate11) !important;
|
||||
// background: var(--base);
|
||||
// border: none;
|
||||
|
|
@ -197,13 +206,17 @@
|
|||
border: 1px solid rgba(255, 255, 255, 0.00) !important;
|
||||
background: var(--slate1, #FBFCFD) !important;
|
||||
}
|
||||
&:active,&:focus,&.always-active-btn {
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&.always-active-btn {
|
||||
color: var(--slate12) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.00) !important;
|
||||
background: var(--slate5, #E6E8EB) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
&:disabled{
|
||||
|
||||
&:disabled {
|
||||
background: rgba(255, 255, 255, 0.00) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.00) !important;
|
||||
color: var(--slate8) !important;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const Drawer = ({
|
|||
onClose,
|
||||
position = 'left',
|
||||
removeWhenClosed = true,
|
||||
drawerStyle,
|
||||
}) => {
|
||||
const bodyRef = useRef(document.querySelector('body'));
|
||||
const portalRootRef = useRef(document.getElementById('tooljet-drawer-root') || createPortalRoot());
|
||||
|
|
@ -79,7 +80,7 @@ const Drawer = ({
|
|||
|
||||
return createPortal(
|
||||
<ErrorBoundary showFallback={true}>
|
||||
<FocusTrap active={isOpen && !disableFocus}>
|
||||
<FocusTrap focusTrapOptions={{ initialFocus: false }} active={isOpen && !disableFocus}>
|
||||
<div
|
||||
aria-hidden={`${!isOpen}`}
|
||||
className={cx('drawer-container', {
|
||||
|
|
@ -90,7 +91,7 @@ const Drawer = ({
|
|||
})}
|
||||
>
|
||||
<Toast toastOptions={toastOptions} />
|
||||
<div className={cx('drawer', position)} role="dialog">
|
||||
<div className={cx('drawer', position)} role="dialog" style={drawerStyle}>
|
||||
{children}
|
||||
</div>
|
||||
<div className="backdrop" onClick={onClose} />
|
||||
|
|
|
|||
19
frontend/src/_ui/Icon/solidIcons/FullOuterJoin.jsx
Normal file
19
frontend/src/_ui/Icon/solidIcons/FullOuterJoin.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
const FullOuterJoin = ({ fill = '#889096', width = '25', className = '', viewBox = '0 0 25 25' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
className={className}
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M13.3335 16.1278C12.9053 16.1278 12.4997 16.0835 12.1169 15.9951C11.7341 15.9066 11.361 15.7809 10.9974 15.6178C11.8598 14.8841 12.5432 14.034 13.0477 13.0675C13.5523 12.101 13.8045 11.0785 13.8045 10C13.8045 8.92153 13.5523 7.89904 13.0477 6.93254C12.5432 5.96606 11.8598 5.11597 10.9974 4.38227C11.3577 4.22109 11.7309 4.09581 12.1169 4.00642C12.5028 3.91702 12.9084 3.87231 13.3335 3.87231C15.0353 3.87231 16.4821 4.46831 17.6737 5.66029C18.8654 6.85229 19.4612 8.29946 19.4612 10.0018C19.4612 11.7041 18.8654 13.1507 17.6737 14.3415C16.4821 15.5324 15.0353 16.1278 13.3335 16.1278ZM10.0001 15.1332C9.16379 14.5897 8.48897 13.868 7.97569 12.9679C7.4624 12.0678 7.20575 11.0785 7.20575 10C7.20575 8.92154 7.4624 7.93226 7.97569 7.03219C8.48897 6.13212 9.16379 5.41035 10.0001 4.86688C10.8504 5.41035 11.5287 6.13212 12.035 7.03219C12.5414 7.93226 12.7945 8.92154 12.7945 10C12.7945 11.0785 12.5414 12.0678 12.035 12.9679C11.5287 13.868 10.8504 14.5897 10.0001 15.1332ZM6.66681 16.1278C4.96497 16.1278 3.51822 15.5318 2.32656 14.3398C1.1349 13.1478 0.539062 11.7006 0.539062 9.99829C0.539062 8.29596 1.1349 6.84938 2.32656 5.65854C3.51822 4.46772 4.96497 3.87231 6.66681 3.87231C7.09504 3.87231 7.50402 3.91654 7.89377 4.005C8.28352 4.09347 8.65322 4.21923 9.00286 4.38227C8.14054 5.11597 7.4571 5.96583 6.95256 6.93186C6.44804 7.8979 6.19577 8.9199 6.19577 9.99786C6.19577 11.1473 6.44653 12.2087 6.94804 13.1821C7.44956 14.1556 8.12363 14.9744 8.97025 15.6386C8.62107 15.8 8.25279 15.9219 7.86542 16.0043C7.47804 16.0866 7.07851 16.1278 6.66681 16.1278Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default FullOuterJoin;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
const Information = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
|
||||
const Information = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25', style }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
|
|
@ -9,6 +9,7 @@ const Information = ({ fill = '#C1C8CD', width = '25', className = '', viewBox =
|
|||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
data-cy="information-icon"
|
||||
style={style}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
|
|
|
|||
19
frontend/src/_ui/Icon/solidIcons/InnerJoinIcon.jsx
Normal file
19
frontend/src/_ui/Icon/solidIcons/InnerJoinIcon.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
const InnerJoinIcon = ({ fill = '#889096', width = '25', className = '', viewBox = '0 0 25 25' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M10.0001 15.1333C9.88111 15.1333 9.76516 15.1095 9.65223 15.0619C9.53932 15.0144 9.43727 14.9578 9.34609 14.8923C8.64016 14.2559 8.13441 13.5558 7.82886 12.7922C7.5233 12.0286 7.37052 11.0999 7.37052 10.006C7.37052 8.91207 7.52677 7.98333 7.83927 7.21974C8.15177 6.45616 8.66099 5.76005 9.36692 5.13143C9.45467 5.05412 9.55164 4.99268 9.65784 4.9471C9.76403 4.9015 9.87825 4.8787 10.0005 4.8787C10.1306 4.8787 10.2501 4.9015 10.359 4.9471C10.468 4.99268 10.5663 5.05412 10.654 5.13143C11.36 5.75401 11.8657 6.44709 12.1713 7.21068C12.4768 7.97428 12.6296 8.90801 12.6296 10.0119C12.6296 11.1157 12.4734 12.0495 12.1609 12.8131C11.8484 13.5767 11.3382 14.2702 10.6303 14.8937C10.5411 14.9701 10.4459 15.0291 10.3448 15.0708C10.2436 15.1124 10.1287 15.1333 10.0001 15.1333ZM13.5218 16.1278C13.1148 16.1278 12.7217 16.0886 12.3425 16.0101C11.9632 15.9316 11.597 15.8208 11.2437 15.6777C11.4603 15.4918 11.6688 15.2704 11.8695 15.0133C12.0701 14.7563 12.2516 14.517 12.414 14.2954C12.5873 14.3491 12.7665 14.3929 12.9516 14.4267C13.1367 14.4605 13.3268 14.4774 13.5218 14.4774C14.7562 14.4774 15.811 14.0398 16.6863 13.1646C17.5615 12.2893 17.9992 11.2345 17.9992 10.0001C17.9992 8.75372 17.5615 7.6959 16.6863 6.82664C15.811 5.95737 14.7562 5.52274 13.5218 5.52274C13.3268 5.52274 13.1387 5.53618 12.9575 5.56305C12.7763 5.58993 12.6021 5.63416 12.4349 5.69574C12.2913 5.45166 12.1096 5.20135 11.89 4.94482C11.6703 4.68831 11.4549 4.47787 11.2437 4.31349C11.5868 4.1621 11.9508 4.04878 12.3359 3.97351C12.721 3.89823 13.1163 3.8606 13.5218 3.8606C15.2214 3.8606 16.6676 4.45612 17.8604 5.64718C19.0532 6.83825 19.6495 8.28732 19.6495 9.99439C19.6495 11.6957 19.0532 13.1432 17.8604 14.3371C16.6676 15.5309 15.2214 16.1278 13.5218 16.1278ZM6.47831 16.1278C4.77872 16.1278 3.33253 15.5314 2.13975 14.3387C0.946975 13.1459 0.350586 11.6997 0.350586 10.0001C0.350586 8.29143 0.946975 6.84101 2.13975 5.64885C3.33253 4.45668 4.77872 3.8606 6.47831 3.8606C6.88304 3.8606 7.27754 3.89833 7.66184 3.9738C8.04613 4.04929 8.41491 4.16252 8.76817 4.31349C8.54836 4.47955 8.32877 4.69055 8.10938 4.94651C7.89 5.20246 7.70863 5.4522 7.56525 5.69574C7.39799 5.63416 7.22378 5.58993 7.04263 5.56305C6.86146 5.53618 6.67336 5.52274 6.47831 5.52274C5.24391 5.52274 4.18909 5.95737 3.31384 6.82664C2.43859 7.6959 2.00096 8.75179 2.00096 9.9943C2.00096 11.2307 2.43859 12.2869 3.31384 13.1631C4.18909 14.0393 5.24391 14.4774 6.47831 14.4774C6.67336 14.4774 6.86342 14.4605 7.0485 14.4267C7.23359 14.3929 7.41278 14.3491 7.58609 14.2954C7.74853 14.517 7.93006 14.7563 8.13067 15.0133C8.33128 15.2704 8.53986 15.4918 8.7564 15.6777C8.40313 15.8208 8.03689 15.9316 7.65767 16.0101C7.27843 16.0886 6.88531 16.1278 6.47831 16.1278Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default InnerJoinIcon;
|
||||
19
frontend/src/_ui/Icon/solidIcons/LeftOuterJoinIcon.jsx
Normal file
19
frontend/src/_ui/Icon/solidIcons/LeftOuterJoinIcon.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
const LeftOuterJoinIcon = ({ fill = '#889096', width = '25', className = '', viewBox = '0 0 25 25' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M10.0001 15.1333C9.88113 15.1333 9.76518 15.1095 9.65225 15.0619C9.53934 15.0144 9.43729 14.9578 9.34611 14.8923C8.64018 14.2559 8.13443 13.5558 7.82888 12.7922C7.52332 12.0286 7.37054 11.0999 7.37054 10.006C7.37054 8.91207 7.52679 7.98333 7.83929 7.21974C8.15179 6.45616 8.66101 5.76005 9.36694 5.13143C9.45469 5.05412 9.55166 4.99268 9.65786 4.9471C9.76405 4.9015 9.87827 4.8787 10.0005 4.8787C10.1306 4.8787 10.2501 4.9015 10.359 4.9471C10.468 4.99268 10.5663 5.05412 10.6541 5.13143C11.36 5.75401 11.8657 6.44709 12.1713 7.21068C12.4768 7.97428 12.6296 8.90801 12.6296 10.0119C12.6296 11.1157 12.4734 12.0495 12.1609 12.8131C11.8484 13.5767 11.3382 14.2702 10.6304 14.8937C10.5411 14.9701 10.4459 15.0291 10.3448 15.0708C10.2437 15.1124 10.1288 15.1333 10.0001 15.1333ZM13.5218 16.1278C13.1148 16.1278 12.7217 16.0886 12.3425 16.0101C11.9633 15.9316 11.597 15.8208 11.2438 15.6777C11.4603 15.4918 11.6689 15.2704 11.8695 15.0133C12.0701 14.7563 12.2516 14.517 12.4141 14.2954C12.5874 14.3491 12.7666 14.3929 12.9516 14.4267C13.1367 14.4605 13.3268 14.4774 13.5218 14.4774C14.7562 14.4774 15.8111 14.0398 16.6863 13.1646C17.5616 12.2893 17.9992 11.2345 17.9992 10.0001C17.9992 8.75372 17.5616 7.6959 16.6863 6.82664C15.8111 5.95737 14.7562 5.52274 13.5218 5.52274C13.3268 5.52274 13.1387 5.53618 12.9575 5.56305C12.7764 5.58993 12.6022 5.63416 12.4349 5.69574C12.2913 5.45166 12.1097 5.20135 11.89 4.94482C11.6704 4.68831 11.4549 4.47787 11.2438 4.31349C11.5868 4.1621 11.9509 4.04878 12.3359 3.97351C12.721 3.89823 13.1163 3.8606 13.5218 3.8606C15.2214 3.8606 16.6676 4.45612 17.8604 5.64718C19.0532 6.83825 19.6496 8.28732 19.6496 9.99439C19.6496 11.6957 19.0532 13.1432 17.8604 14.3371C16.6676 15.5309 15.2214 16.1278 13.5218 16.1278ZM6.47834 16.1278C4.77484 16.1218 3.32767 15.5224 2.13684 14.3297C0.946003 13.137 0.350586 11.6967 0.350586 10.009C0.350586 8.31346 0.945537 6.87026 2.13544 5.67943C3.32531 4.48861 4.77295 3.88626 6.47834 3.87237C6.87192 3.87237 7.26028 3.91358 7.64342 3.99601C8.02657 4.07843 8.39063 4.1912 8.73559 4.33433C7.88474 4.98589 7.27619 5.81726 6.90994 6.82843C6.5437 7.83961 6.36059 8.89683 6.36059 10.0001C6.36059 11.1034 6.5437 12.1606 6.90994 13.1718C7.27619 14.1829 7.88474 15.0182 8.73559 15.6777C8.39017 15.8208 8.02596 15.9316 7.64296 16.0101C7.25996 16.0886 6.87175 16.1278 6.47834 16.1278Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default LeftOuterJoinIcon;
|
||||
24
frontend/src/_ui/Icon/solidIcons/ReloadError.jsx
Normal file
24
frontend/src/_ui/Icon/solidIcons/ReloadError.jsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
|
||||
const ReloadError = ({ fill = '#E54D2E', width = '16', height = '17', className = '', viewBox = '0 0 16 17' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g id="Reload">
|
||||
<path
|
||||
id="Vector (Stroke)"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.83795 3.98293C8.77431 3.48537 10.7617 4.25802 11.8479 5.83528L6.83795 3.98293ZM11.8479 5.83528H9.99988C9.63169 5.83528 9.33321 6.13376 9.33321 6.50195C9.33321 6.87014 9.63169 7.16862 9.99988 7.16862H12.9401C12.9503 7.16885 12.9605 7.16886 12.9708 7.16862H13.3332C13.7014 7.16862 13.9999 6.87014 13.9999 6.50195V3.16862C13.9999 2.80043 13.7014 2.50195 13.3332 2.50195C12.965 2.50195 12.6665 2.80043 12.6665 3.16862V4.70479C11.2216 2.92479 8.82835 2.09477 6.50594 2.6916L6.5058 2.69164C5.45093 2.96297 4.48981 3.51635 3.72553 4.2924C2.96125 5.06846 2.42264 6.03792 2.16747 7.09683C1.91229 8.15573 1.95018 9.26412 2.27706 10.3031C2.60394 11.3421 3.20748 12.2726 4.02297 12.9946C4.83845 13.7167 5.8351 14.2031 6.90604 14.4018C7.97698 14.6005 9.08181 14.5039 10.102 14.1224C11.1222 13.7409 12.0194 13.0889 12.6972 12.2363C13.375 11.3836 13.8079 10.3626 13.9496 9.28264C13.9974 8.91757 13.7403 8.58282 13.3752 8.53494C13.0102 8.48707 12.6754 8.7442 12.6275 9.10927C12.5174 9.94924 12.1807 10.7434 11.6535 11.4065C11.1263 12.0697 10.4285 12.5768 9.63502 12.8735C8.84151 13.1703 7.98221 13.2454 7.14925 13.0909C6.3163 12.9363 5.54112 12.558 4.90686 11.9964C4.2726 11.4348 3.80317 10.7111 3.54893 9.90298C3.29469 9.09486 3.26522 8.23278 3.46369 7.40919C3.66216 6.5856 4.08108 5.83157 4.67552 5.22797C5.26993 4.62441 6.01741 4.19402 6.83781 3.98297"
|
||||
fill={fill}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default ReloadError;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
const Remove = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
|
||||
const Remove = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25', style }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
|
|
@ -8,6 +8,7 @@ const Remove = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0
|
|||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
|
|
|
|||
19
frontend/src/_ui/Icon/solidIcons/RightOuterJoin.jsx
Normal file
19
frontend/src/_ui/Icon/solidIcons/RightOuterJoin.jsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
const RightOuterJoin = ({ fill = '#889096', width = '25', className = '', viewBox = '0 0 25 25' }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
className={className}
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M9.99417 15.1333C9.87129 15.1333 9.75883 15.1124 9.65677 15.0708C9.55472 15.0291 9.45906 14.9701 9.36979 14.8937C8.66195 14.2702 8.15177 13.5767 7.83927 12.8131C7.52677 12.0495 7.37052 11.1157 7.37052 10.0119C7.37052 8.90801 7.5233 7.97428 7.82886 7.21068C8.13441 6.44709 8.64016 5.75401 9.34609 5.13143C9.43384 5.05412 9.53218 4.99268 9.64111 4.9471C9.75004 4.9015 9.86954 4.8787 9.99963 4.8787C10.1219 4.8787 10.2361 4.9015 10.3423 4.9471C10.4485 4.99268 10.5455 5.05412 10.6332 5.13143C11.3391 5.76005 11.8484 6.45616 12.1609 7.21974C12.4734 7.98333 12.6296 8.91207 12.6296 10.006C12.6296 11.0999 12.4768 12.0286 12.1713 12.7922C11.8657 13.5558 11.3591 14.2563 10.6515 14.8937C10.562 14.9622 10.4608 15.0193 10.3479 15.0649C10.235 15.1105 10.1171 15.1333 9.99417 15.1333ZM6.47831 16.1278C4.77872 16.1278 3.33253 15.5314 2.13975 14.3387C0.946975 13.1459 0.350586 11.6997 0.350586 10.0001C0.350586 8.29143 0.946975 6.84101 2.13975 5.64885C3.33253 4.45668 4.77872 3.8606 6.47831 3.8606C6.88304 3.8606 7.27754 3.89833 7.66184 3.9738C8.04613 4.04929 8.41491 4.16252 8.76817 4.31349C8.54836 4.47955 8.32877 4.69055 8.10938 4.94651C7.89 5.20246 7.70863 5.4522 7.56525 5.69574C7.39799 5.63416 7.22378 5.58993 7.04263 5.56305C6.86146 5.53618 6.67336 5.52274 6.47831 5.52274C5.24391 5.52274 4.18909 5.95737 3.31384 6.82664C2.43859 7.6959 2.00096 8.75179 2.00096 9.9943C2.00096 11.2307 2.43859 12.2869 3.31384 13.1631C4.18909 14.0393 5.24391 14.4774 6.47831 14.4774C6.67336 14.4774 6.86342 14.4605 7.0485 14.4267C7.23359 14.3929 7.41278 14.3491 7.58609 14.2954C7.74853 14.517 7.93006 14.7563 8.13067 15.0133C8.33128 15.2704 8.53986 15.4918 8.7564 15.6777C8.40313 15.8208 8.03689 15.9316 7.65767 16.0101C7.27843 16.0886 6.88531 16.1278 6.47831 16.1278ZM13.5218 16.1278C13.1284 16.1278 12.7402 16.0886 12.3572 16.0101C11.9742 15.9316 11.61 15.8208 11.2646 15.6777C12.1154 15.0182 12.724 14.1829 13.0902 13.1718C13.4564 12.1606 13.6396 11.1034 13.6396 10.0001C13.6396 8.89683 13.4564 7.83961 13.0902 6.82843C12.724 5.81726 12.1154 4.98589 11.2646 4.33433C11.6095 4.1912 11.9736 4.07843 12.3567 3.99601C12.7399 3.91358 13.1282 3.87237 13.5218 3.87237C15.2272 3.88626 16.6748 4.48956 17.8647 5.68228C19.0546 6.87501 19.6495 8.31915 19.6495 10.0147C19.6495 11.7024 19.0541 13.1417 17.8633 14.3325C16.6725 15.5234 15.2253 16.1218 13.5218 16.1278Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default RightOuterJoin;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
const Tick = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
|
||||
const Tick = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25', style }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
|
|
@ -8,6 +8,7 @@ const Tick = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 2
|
|||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
|
|
|
|||
|
|
@ -42,15 +42,18 @@ import FloppyDisk from './FloppyDisk.jsx';
|
|||
import Folder from './Folder.jsx';
|
||||
import FolderDownload from './FolderDownload.jsx';
|
||||
import FolderUpload from './FolderUpload.jsx';
|
||||
import FullOuterJoin from './FullOuterJoin.jsx';
|
||||
import Globe from './Globe.jsx';
|
||||
import Grid from './Grid.jsx';
|
||||
import HelpPolygon from './HelpPolygon.jsx';
|
||||
import Home from './Home.jsx';
|
||||
import Information from './Information.jsx';
|
||||
import InnerJoinIcon from './InnerJoinIcon.jsx';
|
||||
import InRectangle from './InRectangle.jsx';
|
||||
import Interactive from './Interactive.jsx';
|
||||
import Layers from './Layers.jsx';
|
||||
import LeftArrow from './LeftArrow.jsx';
|
||||
import LeftOuterJoinIcon from './LeftOuterJoinIcon.jsx';
|
||||
import LightMode from './LightMode.jsx';
|
||||
import ListView from './ListView.jsx';
|
||||
import Logout from './Logout.jsx';
|
||||
|
|
@ -75,10 +78,12 @@ import Play from './Play.jsx';
|
|||
import Plus from './Plus.jsx';
|
||||
import Plus01 from './Plus01.jsx';
|
||||
import Reload from './Reload.jsx';
|
||||
import ReloadError from './ReloadError.jsx';
|
||||
import Remove from './Remove.jsx';
|
||||
import Remove01 from './Remove01.jsx';
|
||||
import RemoveRectangle from './RemoveRectangle.jsx';
|
||||
import RightArrow from './RightArrow.jsx';
|
||||
import RightOuterJoin from './RightOuterJoin.jsx';
|
||||
import Row from './Row.jsx';
|
||||
import SadRectangle from './SadRectangle.jsx';
|
||||
import Search from './Search.jsx';
|
||||
|
|
@ -231,6 +236,8 @@ const Icon = (props) => {
|
|||
return <FolderDownload {...props} />;
|
||||
case 'folderupload':
|
||||
return <FolderUpload {...props} />;
|
||||
case 'fullouterjoin':
|
||||
return <FullOuterJoin {...props} />;
|
||||
case 'globe':
|
||||
return <Globe {...props} />;
|
||||
case 'grid':
|
||||
|
|
@ -241,6 +248,8 @@ const Icon = (props) => {
|
|||
return <Home {...props} />;
|
||||
case 'information':
|
||||
return <Information {...props} />;
|
||||
case 'innerjoin':
|
||||
return <InnerJoinIcon {...props} />;
|
||||
case 'inrectangle':
|
||||
return <InRectangle {...props} />;
|
||||
case 'interactive':
|
||||
|
|
@ -249,6 +258,8 @@ const Icon = (props) => {
|
|||
return <Layers {...props} />;
|
||||
case 'leftarrow':
|
||||
return <LeftArrow {...props} />;
|
||||
case 'leftouterjoin':
|
||||
return <LeftOuterJoinIcon {...props} />;
|
||||
case 'lightmode':
|
||||
return <LightMode {...props} />;
|
||||
case 'listview':
|
||||
|
|
@ -301,6 +312,8 @@ const Icon = (props) => {
|
|||
return <PlusRectangle {...props} />;
|
||||
case 'reload':
|
||||
return <Reload {...props} />;
|
||||
case 'reloaderror':
|
||||
return <ReloadError {...props} />;
|
||||
case 'remove':
|
||||
return <Remove {...props} />;
|
||||
case 'remove01':
|
||||
|
|
@ -309,6 +322,8 @@ const Icon = (props) => {
|
|||
return <RemoveRectangle {...props} />;
|
||||
case 'rightarrrow':
|
||||
return <RightArrow {...props} />;
|
||||
case 'rightouterjoin':
|
||||
return <RightOuterJoin {...props} />;
|
||||
case 'row':
|
||||
return <Row {...props} />;
|
||||
case 'sadrectangle':
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.18.0
|
||||
2.19.0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,21 @@
|
|||
import { All, Controller, Req, Res, Next, UseGuards, Get, Post, Body, Param, Delete, Patch } from '@nestjs/common';
|
||||
import {
|
||||
All,
|
||||
Controller,
|
||||
Req,
|
||||
Res,
|
||||
Next,
|
||||
UseGuards,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Delete,
|
||||
Patch,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { Express } from 'express';
|
||||
import { JwtAuthGuard } from 'src/modules/auth/jwt-auth.guard';
|
||||
import { ActiveWorkspaceGuard } from 'src/modules/auth/active-workspace.guard';
|
||||
import { TooljetDbService } from '@services/tooljet_db.service';
|
||||
|
|
@ -10,12 +27,17 @@ import { Action, TooljetDbAbility } from 'src/modules/casl/abilities/tooljet-db-
|
|||
import { TooljetDbGuard } from 'src/modules/casl/tooljet-db.guard';
|
||||
import { CreatePostgrestTableDto, RenamePostgrestTableDto, PostgrestTableColumnDto } from '@dto/tooljet-db.dto';
|
||||
import { OrganizationAuthGuard } from 'src/modules/auth/organization-auth.guard';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { TooljetDbBulkUploadService } from '@services/tooljet_db_bulk_upload.service';
|
||||
|
||||
@Controller('tooljet_db')
|
||||
const MAX_CSV_FILE_SIZE = 1024 * 1024 * 2; // 2MB
|
||||
|
||||
@Controller('tooljet-db')
|
||||
export class TooljetDbController {
|
||||
constructor(
|
||||
private readonly tooljetDbService: TooljetDbService,
|
||||
private readonly postgrestProxyService: PostgrestProxyService
|
||||
private readonly postgrestProxyService: PostgrestProxyService,
|
||||
private readonly tooljetDbBulkUploadService: TooljetDbBulkUploadService
|
||||
) {}
|
||||
|
||||
@All('/proxy/*')
|
||||
|
|
@ -97,4 +119,31 @@ export class TooljetDbController {
|
|||
const result = await this.tooljetDbService.perform(organizationId, 'drop_column', params);
|
||||
return decamelizeKeys({ result });
|
||||
}
|
||||
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@Post('/organizations/:organizationId/table/:tableName/bulk-upload')
|
||||
async bulkUpload(
|
||||
@Param('organizationId') organizationId,
|
||||
@Param('tableName') tableName,
|
||||
@UploadedFile() file: Express.Multer.File
|
||||
) {
|
||||
if (file.size > MAX_CSV_FILE_SIZE) {
|
||||
throw new BadRequestException('File size cannot be greater than 2MB');
|
||||
}
|
||||
const result = await this.tooljetDbBulkUploadService.perform(organizationId, tableName, file.buffer);
|
||||
|
||||
return decamelizeKeys({ result });
|
||||
}
|
||||
|
||||
@Post('/organizations/:organizationId/join')
|
||||
@UseGuards(TooljetDbGuard)
|
||||
@CheckPolicies((ability: TooljetDbAbility) => ability.can(Action.JoinTables, 'all'))
|
||||
async joinTables(@Body() joinQueryJsonDto: any, @Param('organizationId') organizationId) {
|
||||
const params = {
|
||||
joinQueryJson: { ...joinQueryJsonDto },
|
||||
};
|
||||
|
||||
const result = await this.tooljetDbService.perform(organizationId, 'join_tables', params);
|
||||
return decamelizeKeys({ result });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export class MatchTypeConstraint implements ValidatorConstraintInterface {
|
|||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
if (relatedType === 'integer' || relatedType === 'double precision') {
|
||||
if (relatedType === 'integer' || relatedType === 'bigint' || relatedType === 'double precision') {
|
||||
const isInt = Number.isInteger(value);
|
||||
const isFloat = !Number.isInteger(value) && !isNaN(value);
|
||||
return isInt || isFloat;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ export enum Action {
|
|||
DropTable = 'dropTable',
|
||||
AddColumn = 'addColumn',
|
||||
DropColumn = 'dropColumn',
|
||||
BulkUpload = 'bulkUpload',
|
||||
JoinTables = 'joinTables',
|
||||
}
|
||||
|
||||
type Subjects = 'all';
|
||||
|
|
@ -36,6 +38,7 @@ export class TooljetDbAbilityFactory {
|
|||
can(Action.AddColumn, 'all');
|
||||
can(Action.DropColumn, 'all');
|
||||
can(Action.RenameTable, 'all');
|
||||
can(Action.BulkUpload, 'all');
|
||||
}
|
||||
|
||||
if (isPublicAppRequest || isUserLoggedin) {
|
||||
|
|
@ -44,6 +47,7 @@ export class TooljetDbAbilityFactory {
|
|||
|
||||
can(Action.ViewTables, 'all');
|
||||
can(Action.ViewTable, 'all');
|
||||
can(Action.JoinTables, 'all');
|
||||
|
||||
return build({
|
||||
detectSubjectType: (item) => item as ExtractSubjectType<Subjects>,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { AppsService } from '@services/apps.service';
|
|||
import { App } from 'src/entities/app.entity';
|
||||
import { AppVersion } from 'src/entities/app_version.entity';
|
||||
import { AppUser } from 'src/entities/app_user.entity';
|
||||
import { PostgrestProxyService } from '@services/postgrest_proxy.service';
|
||||
|
||||
const imports = [
|
||||
PluginsModule,
|
||||
|
|
@ -45,6 +46,7 @@ if (process.env.ENABLE_TOOLJET_DB === 'true') {
|
|||
PluginsHelper,
|
||||
AppsService,
|
||||
CredentialsService,
|
||||
PostgrestProxyService,
|
||||
],
|
||||
})
|
||||
export class ImportExportResourcesModule {}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,17 @@ import { TooljetDbService } from '@services/tooljet_db.service';
|
|||
import { CredentialsService } from '@services/credentials.service';
|
||||
import { EncryptionService } from '@services/encryption.service';
|
||||
import { PostgrestProxyService } from '@services/postgrest_proxy.service';
|
||||
import { TooljetDbBulkUploadService } from '@services/tooljet_db_bulk_upload.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Credential]), CaslModule],
|
||||
controllers: [TooljetDbController],
|
||||
providers: [TooljetDbService, PostgrestProxyService, EncryptionService, CredentialsService],
|
||||
providers: [
|
||||
TooljetDbService,
|
||||
TooljetDbBulkUploadService,
|
||||
PostgrestProxyService,
|
||||
EncryptionService,
|
||||
CredentialsService,
|
||||
],
|
||||
})
|
||||
export class TooljetDbModule {}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class PostgrestProxyService {
|
|||
|
||||
private httpProxy = proxy(this.configService.get<string>('PGRST_HOST'), {
|
||||
proxyReqPathResolver: function (req) {
|
||||
const path = '/api/tooljet_db';
|
||||
const path = '/api/tooljet-db';
|
||||
const pathRegex = new RegExp(`${maybeSetSubPath(path)}/proxy`);
|
||||
const parts = req.url.split('?');
|
||||
const queryString = parts[1];
|
||||
|
|
@ -85,7 +85,7 @@ export class PostgrestProxyService {
|
|||
return urlBeingReplaced;
|
||||
}
|
||||
|
||||
private async findOrFailAllInternalTableFromTableNames(requestedTableNames: Array<string>, organizationId: string) {
|
||||
async findOrFailAllInternalTableFromTableNames(requestedTableNames: Array<string>, organizationId: string) {
|
||||
const internalTables = await this.manager.find(InternalTable, {
|
||||
where: {
|
||||
organizationId,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,21 @@
|
|||
import { BadRequestException, Injectable, NotFoundException, Optional } from '@nestjs/common';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { BadRequestException, HttpException, Injectable, NotFoundException, Optional } from '@nestjs/common';
|
||||
import { EntityManager, In, QueryFailedError } from 'typeorm';
|
||||
import { InjectEntityManager } from '@nestjs/typeorm';
|
||||
import { InternalTable } from 'src/entities/internal_table.entity';
|
||||
import { isString } from 'lodash';
|
||||
import { isString, isEmpty } from 'lodash';
|
||||
|
||||
export type TableColumnSchema = {
|
||||
column_name: string;
|
||||
data_type: SupportedDataTypes;
|
||||
column_default: string | null;
|
||||
character_maximum_length: number | null;
|
||||
numeric_precision: number | null;
|
||||
is_nullable: 'YES' | 'NO';
|
||||
constraint_type: string | null;
|
||||
keytype: string | null;
|
||||
};
|
||||
|
||||
export type SupportedDataTypes = 'character varying' | 'integer' | 'bigint' | 'serial' | 'double precision' | 'boolean';
|
||||
|
||||
@Injectable()
|
||||
export class TooljetDbService {
|
||||
|
|
@ -10,7 +23,7 @@ export class TooljetDbService {
|
|||
private readonly manager: EntityManager,
|
||||
@Optional()
|
||||
@InjectEntityManager('tooljetDb')
|
||||
private tooljetDbManager: EntityManager
|
||||
private readonly tooljetDbManager: EntityManager
|
||||
) {}
|
||||
|
||||
async perform(organizationId: string, action: string, params = {}) {
|
||||
|
|
@ -29,12 +42,14 @@ export class TooljetDbService {
|
|||
return await this.dropColumn(organizationId, params);
|
||||
case 'rename_table':
|
||||
return await this.renameTable(organizationId, params);
|
||||
case 'join_tables':
|
||||
return await this.joinTable(organizationId, params);
|
||||
default:
|
||||
throw new BadRequestException('Action not defined');
|
||||
}
|
||||
}
|
||||
|
||||
private async viewTable(organizationId: string, params) {
|
||||
private async viewTable(organizationId: string, params): Promise<TableColumnSchema[]> {
|
||||
const { table_name: tableName, id: id } = params;
|
||||
|
||||
const internalTable = await this.manager.findOne(InternalTable, {
|
||||
|
|
@ -232,4 +247,210 @@ export class TooljetDbService {
|
|||
await this.tooljetDbManager.query("NOTIFY pgrst, 'reload schema'");
|
||||
return result;
|
||||
}
|
||||
|
||||
private async joinTable(organizationId: string, params) {
|
||||
const { joinQueryJson } = params;
|
||||
if (!Object.keys(joinQueryJson).length) throw new BadRequestException("Input can't be empty");
|
||||
|
||||
// Gathering tables used, from Join coditions
|
||||
const tableSet = new Set();
|
||||
const joinOptions = joinQueryJson?.['joins'];
|
||||
(joinOptions || []).forEach((join) => {
|
||||
const { table, conditions } = join;
|
||||
tableSet.add(table);
|
||||
conditions?.conditionsList?.forEach((condition) => {
|
||||
const { leftField, rightField } = condition;
|
||||
if (leftField?.table) {
|
||||
tableSet.add(leftField?.table);
|
||||
}
|
||||
if (rightField?.table) {
|
||||
tableSet.add(rightField?.table);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const tables = [...tableSet].map((tableId: string) => ({
|
||||
name: tableId,
|
||||
type: 'Table',
|
||||
}));
|
||||
|
||||
if (!tables?.length) throw new BadRequestException('Tables are not chosen');
|
||||
|
||||
const tableIdList: Array<string> = tables
|
||||
.filter((table) => table.type === 'Table')
|
||||
.map((filteredTable) => filteredTable.name);
|
||||
|
||||
const internalTables = await this.findOrFailInternalTableFromTableId(tableIdList, organizationId);
|
||||
const internalTableIdToNameMap = tableIdList.reduce((acc, tableId) => {
|
||||
return {
|
||||
...acc,
|
||||
[tableId]: internalTables.find((table) => table.id === tableId).tableName,
|
||||
};
|
||||
}, {});
|
||||
|
||||
const finalQuery = await this.buildJoinQuery(organizationId, joinQueryJson, internalTableIdToNameMap);
|
||||
|
||||
try {
|
||||
return await this.tooljetDbManager.query(finalQuery);
|
||||
} catch (error) {
|
||||
// custom error handling - for Query error
|
||||
if (error instanceof QueryFailedError) {
|
||||
let customErrorMessage: string = (error as QueryFailedError).message;
|
||||
Object.entries(internalTableIdToNameMap).forEach(([key, value]) => {
|
||||
customErrorMessage = customErrorMessage.replace(key, value as string);
|
||||
});
|
||||
throw new HttpException(customErrorMessage, 422);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async buildJoinQuery(_organizationId: string, queryJson, internalTableIdToNameMap) {
|
||||
// Pending: For Subquery, Alias is its table name. Need to handle it on Internal Table details mapping
|
||||
// Pending: SELECT Statement - Nested params --> SUM( price * quantity )
|
||||
|
||||
// @description: Only SELECT & FROM statement is Mandatory, else is Optional
|
||||
let finalQuery = ``;
|
||||
finalQuery += `SELECT ${await this.constructSelectStatement(queryJson.fields, internalTableIdToNameMap)}`;
|
||||
finalQuery += `\nFROM ${await this.constructFromStatement(queryJson, internalTableIdToNameMap)}`;
|
||||
if (queryJson?.joins?.length)
|
||||
finalQuery += `\n${await this.constructJoinStatements(queryJson.joins, internalTableIdToNameMap)}`;
|
||||
if (
|
||||
queryJson?.conditions &&
|
||||
Object.keys(queryJson?.conditions).length &&
|
||||
queryJson?.conditions?.conditionsList.length
|
||||
)
|
||||
finalQuery += `\nWHERE ${await this.constructWhereStatement(queryJson.conditions, internalTableIdToNameMap)}`;
|
||||
if (queryJson?.group_by?.length)
|
||||
finalQuery += `\nGROUP BY ${await this.constructGroupByStatement(queryJson.group_by, internalTableIdToNameMap)}`;
|
||||
if (queryJson?.having && Object.keys(queryJson?.having).length)
|
||||
finalQuery += `\nHAVING ${await this.constructWhereStatement(queryJson.having, internalTableIdToNameMap)}`;
|
||||
if (queryJson?.order_by?.length)
|
||||
finalQuery += `\nORDER BY ${await this.constructOrderByStatement(queryJson.order_by, internalTableIdToNameMap)}`;
|
||||
if (queryJson?.limit && queryJson?.limit.length) finalQuery += `\nLIMIT ${queryJson.limit}`;
|
||||
if (queryJson?.offset && queryJson?.offset.length) finalQuery += `\nOFFSET ${queryJson.offset}`;
|
||||
|
||||
return finalQuery;
|
||||
}
|
||||
|
||||
// Assuming tableId is being passed, tableName to tableId mapping is removed
|
||||
private constructSelectStatement(selectStatementInputList, internalTableIdToNameMap) {
|
||||
if (selectStatementInputList.length) {
|
||||
const selectQueryFields = selectStatementInputList
|
||||
.map((field) => {
|
||||
let fieldExpression = ``;
|
||||
if (field.function) fieldExpression += `${field.function}(`;
|
||||
fieldExpression += `${field.table ? '"' + field.table + '"' + '.' : ''}${field.name}`;
|
||||
if (field.function) fieldExpression += `)`;
|
||||
if (field.alias) {
|
||||
fieldExpression += ` AS ${field.alias}`;
|
||||
} else {
|
||||
// By Default Alias has been added here for tooljetdb join flow
|
||||
fieldExpression += ` AS ${internalTableIdToNameMap[field.table]}_${field.name}`;
|
||||
}
|
||||
return fieldExpression;
|
||||
})
|
||||
.join(', ');
|
||||
return selectQueryFields;
|
||||
}
|
||||
|
||||
throw new BadRequestException('Select statement is empty');
|
||||
}
|
||||
|
||||
private constructFromStatement(queryJson, _internalTableIdToNameMap) {
|
||||
const { from } = queryJson;
|
||||
if (from.name) {
|
||||
return `${'"' + from.name + '"'} ${from.alias ? from.alias : ''}`;
|
||||
}
|
||||
|
||||
throw new BadRequestException('From table is not selected');
|
||||
}
|
||||
|
||||
private constructJoinStatements(joinsInputList, internalTableIdToNameMap) {
|
||||
const joinStatementOutput = joinsInputList
|
||||
.map((joinCondition) => {
|
||||
const { table, joinType, conditions } = joinCondition;
|
||||
return `${joinType} JOIN ${'"' + table + '"'} ${
|
||||
joinCondition.alias ? joinCondition.alias : ''
|
||||
} ON ${this.constructWhereStatement(conditions, internalTableIdToNameMap)}`;
|
||||
})
|
||||
.join('\n');
|
||||
return joinStatementOutput;
|
||||
}
|
||||
|
||||
private constructWhereStatement(whereStatementConditions, internalTableIdToNameMap) {
|
||||
const { operator = 'AND', conditionsList = [] } = whereStatementConditions;
|
||||
const whereConditionOutput = conditionsList
|
||||
.map((condition) => {
|
||||
// @description: Recursive call to build - Sub-condition
|
||||
if (condition.conditions)
|
||||
return `(${this.constructWhereStatement(condition.conditions, internalTableIdToNameMap)})`;
|
||||
// @description: Building a Condition for 'WHERE & HAVING statements' - LHS, operator and RHS
|
||||
// @description: In LHS & RHS it is not mandatory to provide table name, but column name is mandatory
|
||||
// @description: In LHS & RHS - We get function only in HAVING statement
|
||||
const { operator, leftField, rightField } = condition;
|
||||
// @desc: When 'IS' operator is choosed, 'NULL' & 'NOT NULL' keywords will be provided as value and it should not be converted to string
|
||||
const keywords = ['NULL', 'NOT NULL'];
|
||||
|
||||
let leftSideInput = ``;
|
||||
if (leftField.type === 'Value') {
|
||||
const dontAddQuotes =
|
||||
(keywords.includes(leftField.value) && operator === 'IS') || operator === 'IN' || operator === 'NOT IN';
|
||||
|
||||
leftSideInput += dontAddQuotes ? leftField.value : this.addQuotesIfString(leftField.value);
|
||||
} else {
|
||||
if (leftField.function) leftSideInput += `${leftField.function}(`;
|
||||
leftSideInput += `${leftField.table ? '"' + leftField.table + '"' + '.' : ''}${leftField.columnName}`;
|
||||
if (leftField.function) leftSideInput += `)`;
|
||||
}
|
||||
|
||||
let rightSideInput = ``;
|
||||
if (rightField.type === 'Value') {
|
||||
const dontAddQuotes =
|
||||
(keywords.includes(rightField.value) && operator === 'IS') || operator === 'IN' || operator === 'NOT IN';
|
||||
|
||||
rightSideInput += dontAddQuotes ? rightField.value : this.addQuotesIfString(rightField.value);
|
||||
} else {
|
||||
if (rightField.function) rightSideInput += `${rightField.function}(`;
|
||||
rightSideInput += `${rightField.table ? '"' + rightField.table + '"' + '.' : ''}${rightField.columnName}`;
|
||||
if (rightField.function) rightSideInput += `)`;
|
||||
}
|
||||
|
||||
return `${leftSideInput} ${operator} ${rightSideInput}`;
|
||||
})
|
||||
.join(` ${operator} `);
|
||||
return whereConditionOutput;
|
||||
}
|
||||
|
||||
private constructGroupByStatement(groupByInputList, _internalTableIdToNameMap) {
|
||||
return groupByInputList
|
||||
.map((groupByInput) => `${'"' + groupByInput.table + '"'}.${groupByInput.columnName}`)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
private constructOrderByStatement(orderByInputList, internalTableIdToNameMap) {
|
||||
// @description: For "ORDER BY" statement table field is optional. But column_name & order_by direction is mandatory
|
||||
return orderByInputList
|
||||
.map((orderByInput) => {
|
||||
const { columnName, direction } = orderByInput;
|
||||
return `${orderByInput.table ? '"' + orderByInput.table + '"' + '.' : ''}${columnName} ${direction}`;
|
||||
})
|
||||
.join(`, `);
|
||||
}
|
||||
|
||||
private async findOrFailInternalTableFromTableId(requestedTableIdList: Array<string>, organizationId: string) {
|
||||
const internalTables = await this.manager.find(InternalTable, {
|
||||
where: {
|
||||
organizationId,
|
||||
id: In(requestedTableIdList),
|
||||
},
|
||||
});
|
||||
|
||||
const obtainedTableNames = internalTables.map((t) => t.id);
|
||||
const tableNamesNotInOrg = requestedTableIdList.filter((tableId) => !obtainedTableNames.includes(tableId));
|
||||
|
||||
if (isEmpty(tableNamesNotInOrg)) return internalTables;
|
||||
|
||||
throw new NotFoundException('Some tables are not found');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
200
server/src/services/tooljet_db_bulk_upload.service.ts
Normal file
200
server/src/services/tooljet_db_bulk_upload.service.ts
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { InternalTable } from 'src/entities/internal_table.entity';
|
||||
import * as csv from 'fast-csv';
|
||||
import { SupportedDataTypes, TableColumnSchema, TooljetDbService } from './tooljet_db.service';
|
||||
import { InjectEntityManager } from '@nestjs/typeorm';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { PassThrough } from 'stream';
|
||||
|
||||
const MAX_ROW_COUNT = 1000;
|
||||
|
||||
@Injectable()
|
||||
export class TooljetDbBulkUploadService {
|
||||
constructor(
|
||||
private readonly manager: EntityManager,
|
||||
@InjectEntityManager('tooljetDb')
|
||||
private readonly tooljetDbManager: EntityManager,
|
||||
private readonly tooljetDbService: TooljetDbService
|
||||
) {}
|
||||
|
||||
async perform(organizationId: string, tableName: string, fileBuffer: Buffer) {
|
||||
const internalTable = await this.manager.findOne(InternalTable, {
|
||||
select: ['id'],
|
||||
where: { organizationId, tableName },
|
||||
});
|
||||
|
||||
if (!internalTable) {
|
||||
throw new NotFoundException(`Table ${tableName} not found`);
|
||||
}
|
||||
|
||||
const internalTableColumnSchema = await this.tooljetDbService.perform(organizationId, 'view_table', {
|
||||
table_name: tableName,
|
||||
});
|
||||
|
||||
return await this.bulkUploadCsv(internalTable.id, internalTableColumnSchema, fileBuffer);
|
||||
}
|
||||
|
||||
async bulkUploadCsv(
|
||||
internalTableId: string,
|
||||
internalTableColumnSchema: TableColumnSchema[],
|
||||
fileBuffer: Buffer
|
||||
): Promise<{ processedRows: number; rowsInserted: number; rowsUpdated: number }> {
|
||||
const csvStream = csv.parseString(fileBuffer.toString(), {
|
||||
headers: true,
|
||||
ignoreEmpty: true,
|
||||
strictColumnHandling: true,
|
||||
discardUnmappedColumns: true,
|
||||
});
|
||||
const rowsToInsert = [];
|
||||
const rowsToUpdate = [];
|
||||
const idstoUpdate = new Set();
|
||||
let rowsProcessed = 0;
|
||||
|
||||
const passThrough = new PassThrough();
|
||||
|
||||
csvStream
|
||||
.on('headers', (headers) => this.validateHeadersAsColumnSubset(internalTableColumnSchema, headers, csvStream))
|
||||
.transform((row) => this.validateAndParseColumnDataType(internalTableColumnSchema, row, rowsProcessed, csvStream))
|
||||
.on('data', (row) => {
|
||||
rowsProcessed++;
|
||||
if (row.id) {
|
||||
if (idstoUpdate.has(row.id)) {
|
||||
throw new BadRequestException(`Duplicate 'id' value found on row[${rowsProcessed + 1}]: ${row.id}`);
|
||||
}
|
||||
|
||||
idstoUpdate.add(row.id);
|
||||
rowsToUpdate.push(row);
|
||||
} else {
|
||||
rowsToInsert.push(row);
|
||||
}
|
||||
})
|
||||
.on('error', (error) => {
|
||||
csvStream.destroy();
|
||||
passThrough.emit('error', new BadRequestException(error));
|
||||
})
|
||||
.on('end', () => {
|
||||
passThrough.emit('end');
|
||||
});
|
||||
|
||||
await pipeline(passThrough, csvStream);
|
||||
|
||||
await this.tooljetDbManager.transaction(async (tooljetDbManager) => {
|
||||
await this.bulkInsertRows(tooljetDbManager, rowsToInsert, internalTableId);
|
||||
await this.bulkUpdateRows(tooljetDbManager, rowsToUpdate, internalTableId);
|
||||
});
|
||||
|
||||
return { processedRows: rowsProcessed, rowsInserted: rowsToInsert.length, rowsUpdated: rowsToUpdate.length };
|
||||
}
|
||||
|
||||
async bulkUpdateRows(tooljetDbManager: EntityManager, rowsToUpdate: unknown[], internalTableId: string) {
|
||||
if (isEmpty(rowsToUpdate)) return;
|
||||
|
||||
const updateQueries = rowsToUpdate.map((row) => {
|
||||
const columnNames = Object.keys(rowsToUpdate[0]);
|
||||
const setClauses = columnNames
|
||||
.map((column) => {
|
||||
return `${column} = $${columnNames.indexOf(column) + 1}`;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
return {
|
||||
text: `UPDATE "${internalTableId}" SET ${setClauses} WHERE id = $${columnNames.indexOf('id') + 1}`,
|
||||
values: columnNames.map((column) => row[column]),
|
||||
};
|
||||
});
|
||||
|
||||
for (const updateQuery of updateQueries) {
|
||||
await tooljetDbManager.query(updateQuery.text, updateQuery.values);
|
||||
}
|
||||
}
|
||||
|
||||
async bulkInsertRows(tooljetDbManager: EntityManager, rowsToInsert: unknown[], internalTableId: string) {
|
||||
if (isEmpty(rowsToInsert)) return;
|
||||
|
||||
const insertQueries = rowsToInsert.map((row, index) => {
|
||||
return {
|
||||
text: `INSERT INTO "${internalTableId}" (${Object.keys(row).join(', ')}) VALUES (${Object.values(row).map(
|
||||
(_, index) => `$${index + 1}`
|
||||
)})`,
|
||||
values: Object.values(row),
|
||||
};
|
||||
});
|
||||
|
||||
for (const insertQuery of insertQueries) {
|
||||
await tooljetDbManager.query(insertQuery.text, insertQuery.values);
|
||||
}
|
||||
}
|
||||
|
||||
async validateHeadersAsColumnSubset(
|
||||
internalTableColumnSchema: TableColumnSchema[],
|
||||
headers: string[],
|
||||
csvStream: csv.CsvParserStream<csv.ParserRow<any>, csv.ParserRow<any>>
|
||||
) {
|
||||
const internalTableColumns = new Set<string>(internalTableColumnSchema.map((c) => c.column_name));
|
||||
const columnsInCsv = new Set<string>(headers);
|
||||
const isSubset = (subset: Set<string>, superset: Set<string>) => [...subset].every((item) => superset.has(item));
|
||||
|
||||
if (!isSubset(columnsInCsv, internalTableColumns)) {
|
||||
const columnsNotIntable = [...columnsInCsv].filter((element) => !internalTableColumns.has(element));
|
||||
|
||||
csvStream.emit('error', `Columns ${columnsNotIntable.join(',')} not found in table`);
|
||||
}
|
||||
}
|
||||
|
||||
validateAndParseColumnDataType(
|
||||
internalTableColumnSchema: TableColumnSchema[],
|
||||
row: unknown,
|
||||
rowsProcessed: number,
|
||||
csvStream: csv.CsvParserStream<csv.ParserRow<any>, csv.ParserRow<any>>
|
||||
) {
|
||||
if (rowsProcessed >= MAX_ROW_COUNT) csvStream.emit('error', `Row count cannot be greater than ${MAX_ROW_COUNT}`);
|
||||
|
||||
try {
|
||||
const columnsInCsv = Object.keys(row);
|
||||
const transformedRow = columnsInCsv.reduce((result, columnInCsv) => {
|
||||
const columnDetails = internalTableColumnSchema.find((colDetails) => colDetails.column_name === columnInCsv);
|
||||
const convertedValue = this.validateDataType(row[columnInCsv], columnDetails.data_type);
|
||||
|
||||
if (convertedValue) result[columnInCsv] = this.validateDataType(row[columnInCsv], columnDetails.data_type);
|
||||
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return transformedRow;
|
||||
} catch (error) {
|
||||
csvStream.emit('error', `Data type error at row[${rowsProcessed + 1}]: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
validateDataType(columnValue: string, supportedDataType: SupportedDataTypes) {
|
||||
if (!columnValue) return null;
|
||||
|
||||
switch (supportedDataType) {
|
||||
case 'boolean':
|
||||
return this.validateBoolean(columnValue);
|
||||
case 'integer':
|
||||
case 'double precision':
|
||||
case 'bigint':
|
||||
return this.validateNumber(columnValue, supportedDataType);
|
||||
default:
|
||||
return columnValue;
|
||||
}
|
||||
}
|
||||
|
||||
validateBoolean(str: string) {
|
||||
const parsedString = str.toLowerCase().trim();
|
||||
if (parsedString === 'true' || parsedString === 'false') return str;
|
||||
|
||||
throw `${str} is not a valid boolean string`;
|
||||
}
|
||||
|
||||
validateNumber(str: string, dataType: 'integer' | 'bigint' | 'double precision') {
|
||||
if (dataType === 'integer' && !isNaN(parseInt(str, 10))) return str;
|
||||
if (dataType === 'double precision' && !isNaN(parseFloat(str))) return str;
|
||||
if (dataType === 'bigint' && typeof BigInt(str) === 'bigint') return str;
|
||||
|
||||
throw `${str} is not a valid ${dataType}`;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue