Query manager revamp (#6680)

* global store init

* Moved query data to new component

* Removed unwanted code

* Removed data queries prop drilling

* Moved query state out of editor

* Added unsafe to componentWillReceiveProps

* Selected first query when the version is changed

* Fixed bug on renaming query

* Fixed issue on dark theme

* Fixed running query on page load in viewer

* Query manager refactor init

* Added global data source in store

* Disabled devtools on production

* Fixed bug on selecting query after deletion

* Reset store when editor is loaded

* Moved query manager to functional component

* Fixed conflict issues

* Fixed infinite loop on tooljetDB

* Set the store name and updated devtools logic

* Fixed issue on displaying draft query from data sources

* Updated comments on the store

* Fixed bug on changing data source and creating query from data source

* Fixed bug on showing unsaved changes popup

* Fixed issue on showing confirmation modal everytime without any changes

* feat: autosave data query functionality

* feat: show publish button only when the status in draft state

* Fixed issues on query renaming

* feat: removed discard popup for data query create/edit widget

* stye: reduced autosave api call timeout and added draft tag

* feat: added minor style changes

* feat: fixed issues with restapi plugin, removed unused api calls

* fix: fixed issue that breaks restapi creation

* fix: reload selected query details after update query

* perf: reduced debounce time for data query update apis

* feat: removed full reloading of query list on query renaming

* feat: duplicate data query feature added

* Fixed issue on creating restAPI query

* fix: fixed issue in transforming response from update queyr api

* fix: refresh selected query details when the selected query is updated

* fix: rename query on click enter

* fix: full refresh of query list on update

* fix: style changes

* fix: subscribing to state to autsave

* feat: updated the query manager styles to new design

* feat: revamped the querypane header buttons

* fix: fixed the padding for query panel maximize button

* feat: updated search box style

* refactor: moved function to render data source icon to its own component

* fix: fixed querymanager widget breaking issue

* merged with feat/query-manager-autosave

* refactor: removed unused consoles

* refactor: removed unused consoles

* refactor: removed unused consoles

* fix: removed commented code

* fix: removed unused code

* refactor: removed unused comments

* fix: show change datasource select only if valid ds available

* Update frontend/src/Editor/Inspector/EventManager.jsx

Co-authored-by: Kavin Venkatachalam <50441969+kavinvenkatachalam@users.noreply.github.com>

* Update frontend/src/Editor/QueryManager/Components/DataSourceLister.jsx

Co-authored-by: Kavin Venkatachalam <50441969+kavinvenkatachalam@users.noreply.github.com>

* Update frontend/src/Editor/QueryManager/Components/DataSourceLister.jsx

Co-authored-by: Kavin Venkatachalam <50441969+kavinvenkatachalam@users.noreply.github.com>

* Update frontend/src/Editor/QueryManager/Components/QueryManagerBody.jsx

Co-authored-by: Kavin Venkatachalam <50441969+kavinvenkatachalam@users.noreply.github.com>

* feat: modify behaviour of search icon in query panel

* fix: fixed theme color mismatch in query manager

* refactor: remove dead code

* refactor: updated theme for data source listner

* fix: theming in filter and sort popup

* refactor: remove unused variables

* fix: removed draftQuery logic from query manager

* refactor: removed unused varibales

* Update frontend/src/Editor/QueryManager/QueryEditors/Restapi/TabParams.jsx

Co-authored-by: Kavin Venkatachalam <50441969+kavinvenkatachalam@users.noreply.github.com>

* Update frontend/src/Editor/QueryPanel/QueryCard.jsx

Co-authored-by: Kavin Venkatachalam <50441969+kavinvenkatachalam@users.noreply.github.com>

* feat: diable preview for draft queries

* fix: added tooltip for query panel button

* fix: fixed issues in saving query manager events

* fix: moved query save subscriber to QuerPanel component

* feat: converted query run api to save and run

* fix: made varibale an optional param in updateDataQuery dto

* refactor: cleanup update dataquery status api response

* refactor: moved query status to constants file

* feat: prompt for queryname when creating new query

* fix: store new queryname in state on create query pageload

* fix: fixed alignment of Tooljet db component form

* fix: correct translation and format file

* refactor: removed consoles

* merge: merge appbuilder-1.2

* style: updated rename input/button UX

* style: revamped dataquery create widget styles

* style: revamped data source selector styles

* fix: removed code added for debugging

* style: updated data query filter design

* style: Add prop to control visibility of clear button in search box

* style: implement new style for query filter

* merge appbuilder-1.2 to feat/query-manager-sort-filter

* refactor: remove unintended file change

* fix: set default value for method in respapi

* style: updated copilot info popup style

* style: updated quer panel header icons

* style: updated button styles

* style: fixed query manager button styles

* style: smoothened query preview modal view

* fix: correct import for some funs

* fix: fixed minor UX bugs

* style: fixed styling of REST api GDS

* style: fixed styleing of sort and filter popup

* style: improved data queries sort filter UI/UX

* fix: remove click listner when overlay is closed

* fix: moved component declaration out of parent component

* fix: set selected datasource for default sources

* fix: filter DS based on saerch in create dropdown

* fix: restrict draft query running to preview mode

* fix: query renamed on input change in create screen

* fix: set name to state as soon as user renames query

* fix: make query notification message consistent

* style: correct s3 bucket plugin layout config

* fix: fixed issues with cloning of Static DS queries

* fix: made change so that newly created query is reflected immediatly

* style: updated spacing for query manager components

* fix: hide rename input when no query selected

* fix: check bothe selected query and DS before rendering query manager

* fix: set isSaving to true only for api calls in querymanager

* fix: added success message form in qm

* fix: filter out draft queries from viewer on running

* fix: fixed inconsistent gutter for runpy and runjs editors

* fix: reload dataqueris on LDS deletion

* fix: redesigned filter/sort popup

* fix: fixed issue that resets filter on search

* fix: fixed query manager breaking on plugin select

* fix: diable json preview for text output

* fix: reset to filter and sort main menu on close filter popup

* refactor: rename varibales

* stye: redesigned query create panel

* feat: revert data query status column from backend

* style: redesign query picker section

* refactor: removed dead code

* style: querypanel expand/collapse btn style

* style: add query select and query filter popup style redesign

* style: updated filter popup style

* feat: removed draft query checks everywhere

* style: empty dataqueries style changed

* style: updated query selector popup and rest options styles

* style: removed 100% height to query option remove btn

* feat: added the query runnable status check

* style: updated query manager footer style

* feat: changed DS filter from kind to DS ID

* style: minor ui tweaks in filter popup

* style: disable DS filter if no DQs created

* style: minor ui change

* fix: rerender filter popup post DS api call. fixed rest api copy feature

* fix: add local DS to filter popup

* refactor: removed dead code/comments

* add new row is crashing when no data is fed to table (#7102)

* fix: fixed condition that blocked GDS run on load

* fix: revert name back to og name if update fails in rename query

* feat: added tooltip for show query btn

* fix: added click interaction for pill btn as well

* fix: minor UI tweaks to make UX better

* style: fixed the styling of filter popup

* style: minor UI tweaks in query filter popup

* fix: fixed minor css issue in ds picker

* style: wrap overflowing text in queryname

* fix: update updated_at after query update api call success

* fix: update remove the caller query from event query dropdown

* style: minor ui spacing tweaks

* fix: fix issue that cuased app crash when tjdb opened

* fix: fixed update row styles

* fix: fixed info popup dark theme bg

* fix: fixed headers styling according to general QM styles

* style: fixed stripe QM UI

* fix: added tooltip for quernames

* feat: add tooltip for select ds options

* added consoles to debug debugger issue

* fix: fixed :active style of ds select dropdown in QM

* fix: fixed DS kind name in data source selector in QM

* fix: fixed border color mismatch for ds select dd

* fix: change tooltip msg for maximize/minize QM

* Fix automation for query manager revamp. (#7223)

* Add data-cy to support modified specs

* Fix event handler

* Fix RunPy and RunJS specs

* Fix event handler label

* Fix basic components spec

* Fix basic components failure

* Fix tabel spec failure.

* Fix runjs and runpy actions

* Fix table column options

* Add data-cy

* version: version updated to 2.13.0

* Version bump

---------

Co-authored-by: Kavin Venkatachalam <kavin.saratha@gmail.com>
Co-authored-by: Kavin Venkatachalam <50441969+kavinvenkatachalam@users.noreply.github.com>
Co-authored-by: Manish Kushare <37823141+manishkushare@users.noreply.github.com>
Co-authored-by: Midhun Kumar E <midhun752@gmail.com>
This commit is contained in:
Johnson Cherian 2023-08-09 18:01:48 +05:30 committed by GitHub
parent f3f23f199c
commit 55cdc7a0b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
101 changed files with 3972 additions and 2537 deletions

View file

@ -1 +1 @@
2.12.0
2.13.0

View file

@ -170,7 +170,7 @@ export const commonText = {
// iframeLinkLabel: "Get embeddable link for this application",
// ifameLinkCopyButton: "copy",
},
groupInputFieldLabel: "Select Group"
groupInputFieldLabel: "Select Group",
};
export const commonWidgetText = {
@ -199,7 +199,7 @@ export const commonWidgetText = {
codeMirrorInputTrue: codeMirrorInputLabel(true),
codeMirrorInputFalse: codeMirrorInputLabel("false"),
addEventHandlerLink: "+ Add event handler",
addEventHandlerLink: "Add handler",
inspectorComponentLabel: "components",
componentValueLabel: "Value",
labelDefaultValue: "Default Value",

View file

@ -15,7 +15,7 @@ export const multipageText = {
optionEventHandler: "Event Handlers",
eventModalTitle: "Page Events",
labelEvents: "Events",
addEventHandlerLink: "+ Add event handler",
addEventHandlerLink: "Add handler",
noEventHandlerInfo: "This page doesn't have any event handlers",
optionDeletePage: "Delete Page",

View file

@ -193,7 +193,7 @@ describe("Multipage", () => {
multipageText.labelEvents
);
cy.get(multipageSelector.addEventHandlerLink).verifyVisibleElement(
"have.text",
"contain.text",
multipageText.addEventHandlerLink
);
cy.get(multipageSelector.noEventHandlerMessage).verifyVisibleElement(

View file

@ -33,7 +33,7 @@ import {
} from "Support/utils/events";
import {
selectQuery,
selectQueryFromLandingPage,
deleteQuery,
query,
changeQueryToggles,
@ -66,17 +66,15 @@ describe("RunJS", () => {
cy.createApp();
cy.viewport(1800, 1800);
cy.dragAndDropWidget("Button");
resizeQueryPanel("50");
resizeQueryPanel("80");
});
it("should verify basic runjs", () => {
const data = {};
data.customText = randomString(12);
selectQuery("Run JavaScript code");
selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField("runjs", "return true");
query("create");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
query("preview");
verifypreview("raw", "true");
query("run");
@ -92,7 +90,7 @@ describe("RunJS", () => {
const data = {};
data.customText = randomString(12);
selectQuery("Run JavaScript code");
selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField(
"runjs",
`setTimeout(() => {
@ -101,7 +99,6 @@ describe("RunJS", () => {
}, [0]) `
);
query("run");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(".tooltip-inner").invoke("hide");
verifyNodeData("variables", "Object", "1 entry ");
@ -134,7 +131,6 @@ describe("RunJS", () => {
"actions.showAlert('success', 'alert from runjs');"
);
query("run");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Saved");
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs");
cy.get(multipageSelector.sidebarPageButton).click();
@ -154,8 +150,6 @@ describe("RunJS", () => {
addInputOnQueryField("runjs", "actions.closeModal('modal1');");
query("run");
cy.intercept("GET", "api/data_queries?**").as("addQuery");
cy.wait("@addQuery");
cy.wait(200);
cy.notVisible('[data-cy="modal-title"]');
@ -164,7 +158,6 @@ describe("RunJS", () => {
"actions.copyToClipboard('data from runjs');"
);
query("run");
cy.wait("@addQuery");
cy.window().then((win) => {
win.navigator.clipboard.readText().then((text) => {
@ -176,7 +169,6 @@ describe("RunJS", () => {
"actions.setLocalStorage('localStorage','data from runjs');"
);
query("run");
cy.wait("@addQuery");
cy.getAllLocalStorage().then((result) => {
expect(result[Cypress.config().baseUrl].localStorage).to.deep.equal(
@ -189,8 +181,6 @@ describe("RunJS", () => {
"actions.generateFile('runjscsv', 'csv', [{ name: 'John', email: 'john@tooljet.com' }])"
);
query("run");
cy.wait("@addQuery");
cy.wait(3000);
cy.readFile("cypress/downloads/runjscsv.csv", "utf-8")
.should("contain", "name,email")
@ -201,11 +191,9 @@ describe("RunJS", () => {
// "actions.goToApp('111234')"
// );
// query("run");
// cy.wait("@addQuery");
addInputOnQueryField("runjs", "actions.logout()");
query("run");
cy.wait("@addQuery");
cy.wait(3000);
cy.get('[data-cy="sign-in-header"]').should("be.visible");
});
@ -213,10 +201,8 @@ describe("RunJS", () => {
const data = {};
data.customText = randomString(12);
selectQuery("Run JavaScript code");
selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField("runjs", "return [page.handle,page.name]");
query("create");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
query("preview");
verifypreview("raw", `["home","Home"]`);
@ -267,22 +253,20 @@ describe("RunJS", () => {
const data = {};
data.customText = randomString(12);
selectQuery("Run JavaScript code");
selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField(
"runjs",
"actions.showAlert('success', 'alert from runjs');"
);
query("create");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
query("run");
openEditorSidebar("button1");
selectEvent("On Click", "Run query", 1);
cy.get('[data-cy="query-selection-field"]').type("runjs1{enter}");
cy.get(commonWidgetSelector.draggableWidget("button1")).click();
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs");
renameQueryFromEditor("newrunjs");
cy.wait(3000);
cy.waitForAutoSave();
cy.get('[data-cy="event-handler"]').click();
cy.get('[data-cy="query-selection-field"]').should("have.text", "newrunjs");
@ -291,26 +275,25 @@ describe("RunJS", () => {
});
it("should verify runjs toggle options", () => {
cy.intercept("PATCH", "api/data_queries/**").as("editQuery");
const data = {};
data.customText = randomString(12);
selectQuery("Run JavaScript code");
selectQueryFromLandingPage("runjs", "JavaScript");
addInputOnQueryField(
"runjs",
"actions.showAlert('success', 'alert from runjs');"
);
query("create");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
changeQueryToggles("run-on-app-load");
query("save");
cy.wait(`@editQuery`);
cy.waitForAutoSave();
cy.reload();
cy.wait(3000);
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs");
changeQueryToggles("confirmation-before-run");
query("save");
cy.wait(`@editQuery`);
cy.waitForAutoSave();
cy.reload();
cy.wait(3000);
cy.get('[data-cy="modal-message"]').verifyVisibleElement(
"have.text",
"Do you want to run this query - runjs1?"
@ -318,13 +301,15 @@ describe("RunJS", () => {
cy.get('[data-cy="modal-confirm-button"]').realClick();
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs");
resizeQueryPanel("80");
changeQueryToggles("notification-on-success");
cy.get('[data-cy="success-message-input-field"]').clearAndTypeOnCodeMirror(
"Success alert"
);
query("save");
cy.get('[data-cy="runjs-input-field"]').realClick();
cy.wait(1000);
cy.waitForAutoSave();
cy.reload();
cy.wait(3000);
cy.get('[data-cy="modal-confirm-button"]').realClick();
cy.verifyToastMessage(commonSelectors.toastMessage, "Success alert");
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runjs");

View file

@ -33,12 +33,12 @@ import {
} from "Support/utils/events";
import {
selectQuery,
deleteQuery,
selectQueryFromLandingPage,
query,
changeQueryToggles,
renameQueryFromEditor,
addInputOnQueryField,
waitForQueryAction,
} from "Support/utils/queries";
import {
@ -66,17 +66,17 @@ describe("runpy", () => {
cy.createApp();
cy.viewport(1800, 1800);
cy.dragAndDropWidget("Button");
resizeQueryPanel("50");
resizeQueryPanel("80");
cy.intercept("PATCH", "api/data_queries/**").as("editQuery");
});
it("should verify basic runpy", () => {
const data = {};
data.customText = randomString(12);
selectQuery("Run Python code");
selectQueryFromLandingPage("runpy", "Python");
addInputOnQueryField("runpy", "True");
query("create");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
cy.waitForAutoSave();
query("preview");
verifypreview("raw", "true");
query("run");
@ -92,14 +92,14 @@ describe("runpy", () => {
const data = {};
data.customText = randomString(12);
selectQuery("Run Python code");
selectQueryFromLandingPage("runpy", "Python");
addInputOnQueryField(
"runpy",
`actions.setVariable('var', 'test')
actions.setPageVariable('pageVar', 'pageTest')`
);
cy.waitForAutoSave();
query("run");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
cy.get(commonWidgetSelector.sidebarinspector).click();
cy.get(".tooltip-inner").invoke("hide");
verifyNodeData("variables", "Object", "1 entry ");
@ -130,9 +130,12 @@ actions.unsetPageVariable('pageVar')`
"actions.showAlert('success', 'alert from runpy')"
);
query("run");
// cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy");
cy.verifyToastMessage(
commonSelectors.toastMessage,
"alert from runpy",
false
);
cy.get(multipageSelector.sidebarPageButton).click();
addNewPage("test_page");
cy.url().should("contain", "/test-page");
@ -146,22 +149,21 @@ actions.unsetPageVariable('pageVar')`
cy.waitForAutoSave();
addInputOnQueryField("runpy", "actions.showModal('modal1')");
query("run");
cy.closeToastMessage();
cy.get('[data-cy="modal-title"]').should("be.visible");
cy.get('[data-cy="runpy-input-field"]').click({ force: true });
addInputOnQueryField("runpy", "actions.closeModal('modal1')");
cy.wait(2000);
cy.wait(`@editQuery`);
cy.waitForAutoSave();
query("run");
cy.intercept("GET", "api/data_queries?**").as("addQuery");
cy.wait("@addQuery");
cy.wait(10000);
waitForQueryAction("run");
cy.notVisible('[data-cy="modal-title"]');
addInputOnQueryField("runpy", "actions.copyToClipboard('data from runpy')");
cy.wait(`@editQuery`);
cy.waitForAutoSave();
query("run");
cy.wait("@addQuery");
cy.wait(10000);
waitForQueryAction("run");
cy.window().then((win) => {
win.navigator.clipboard.readText().then((text) => {
expect(text).to.eq("data from runpy");
@ -171,9 +173,10 @@ actions.unsetPageVariable('pageVar')`
"runpy",
"actions.setLocalStorage('localStorage','data from runpy')"
);
cy.wait(`@editQuery`);
cy.waitForAutoSave();
query("run");
cy.wait("@addQuery");
cy.wait(10000);
waitForQueryAction("run");
cy.getAllLocalStorage().then((result) => {
expect(result[Cypress.config().baseUrl].localStorage).to.deep.equal(
@ -186,7 +189,7 @@ actions.unsetPageVariable('pageVar')`
// "actions.generateFile('runpycsv', 'csv', [{ 'name': 'John', 'email': 'john@tooljet.com' }])"
// );
// query("run");
// cy.wait("@addQuery");
// cy.verifyToastMessage(
// commonSelectors.toastMessage,
// "Query (runpy1) completed."
@ -203,11 +206,13 @@ actions.unsetPageVariable('pageVar')`
// "actions.goToApp('111234')"
// );
// query("run");
// cy.wait("@addQuery");
addInputOnQueryField("runpy", "actions.logout()");
cy.wait(`@editQuery`);
cy.wait(200);
cy.waitForAutoSave();
query("run");
cy.wait("@addQuery");
cy.wait(3000);
waitForQueryAction("run");
cy.get('[data-cy="sign-in-header"]').should("be.visible");
});
@ -215,10 +220,9 @@ actions.unsetPageVariable('pageVar')`
const data = {};
data.customText = randomString(12);
selectQuery("Run Python code");
selectQueryFromLandingPage("runpy", "Python");
addInputOnQueryField("runpy", "tj_globals.theme");
query("create");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
cy.waitForAutoSave();
query("preview");
verifypreview("raw", `{"name":"light"}`);
@ -236,8 +240,11 @@ actions.unsetPageVariable('pageVar')`
verifypreview("raw", `Developer`);
addInputOnQueryField("runpy", "tj_globals.currentUser.groups");
query("preview");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query completed.");
cy.wait(10000);
cy.verifyToastMessage(
commonSelectors.toastMessage,
"Query (runpy1) completed."
);
waitForQueryAction("preview");
verifypreview("raw", `["all_users","admin"]`);
if (Cypress.env("environment") != "Community") {
addInputOnQueryField("runpy", "tj_globals.mode.value");
@ -261,21 +268,20 @@ actions.unsetPageVariable('pageVar')`
const data = {};
data.customText = randomString(12);
selectQuery("Run Python code");
selectQueryFromLandingPage("runpy", "Python");
addInputOnQueryField(
"runpy",
"actions.showAlert('success', 'alert from runpy');"
);
query("create");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
cy.waitForAutoSave();
query("run");
openEditorSidebar("button1");
selectEvent("On Click", "Run query", 1);
cy.get('[data-cy="query-selection-field"]').type("runpy1{enter}");
cy.get(commonWidgetSelector.draggableWidget("button1")).click();
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy");
renameQueryFromEditor("newrunpy");
cy.wait(3000);
cy.get('[data-cy="event-handler"]').click();
cy.get('[data-cy="query-selection-field"]').should("have.text", "newrunpy");
@ -287,23 +293,26 @@ actions.unsetPageVariable('pageVar')`
const data = {};
data.customText = randomString(12);
selectQuery("Run Python code");
selectQueryFromLandingPage("runpy", "Python");
cy.waitForAutoSave();
addInputOnQueryField(
"runpy",
"actions.showAlert('success', 'alert from runpy');"
);
query("create");
cy.verifyToastMessage(commonSelectors.toastMessage, "Query Added");
cy.wait("@editQuery");
cy.wait(200);
cy.waitForAutoSave();
changeQueryToggles("run-on-app-load");
query("save");
cy.wait("@editQuery");
cy.waitForAutoSave();
cy.reload();
cy.wait(3000);
cy.verifyToastMessage(commonSelectors.toastMessage, "alert from runpy");
changeQueryToggles("confirmation-before-run");
query("save");
cy.wait("@editQuery");
cy.wait(200);
cy.waitForAutoSave();
cy.reload();
cy.wait(3000);
cy.get('[data-cy="modal-message"]').verifyVisibleElement(
"have.text",
"Do you want to run this query - runpy1?"
@ -315,10 +324,12 @@ actions.unsetPageVariable('pageVar')`
cy.get('[data-cy="success-message-input-field"]').clearAndTypeOnCodeMirror(
"Success alert"
);
query("save");
cy.forceClickOnCanvas();
cy.wait("@editQuery");
cy.wait(200);
cy.waitForAutoSave();
cy.reload();
cy.wait(4000);
cy.get('[data-cy="modal-confirm-button"]').realClick();
cy.get('[data-cy="modal-confirm-button"]', { timeout: 10000 }).realClick();
cy.verifyToastMessage(commonSelectors.toastMessage, "Success alert", false);
cy.verifyToastMessage(
commonSelectors.toastMessage,

View file

@ -29,6 +29,7 @@ import {
commonWidgetText,
codeMirrorInputLabel,
} from "Texts/common";
import { resizeQueryPanel } from "Support/utils/dataSource";
describe("Basic components", () => {
const data = {};
@ -37,7 +38,6 @@ describe("Basic components", () => {
cy.appUILogin();
cy.createApp();
cy.modifyCanvasSize(900, 900);
cy.get('[data-tooltip-id="tooltip-for-hide-query-editor"]').click();
cy.renameApp(data.appName);
cy.intercept("GET", "/api/comments/*").as("loadComments");
});
@ -617,7 +617,9 @@ describe("Basic components", () => {
});
it("Should verify Tabs", () => {
cy.dragAndDropWidget("Tabs", 50, 50);
cy.viewport(1200, 1300);
resizeQueryPanel("0");
cy.dragAndDropWidget("Tabs", 100, 100);
verifyComponent("tabs1");
deleteComponentAndVerify("image1");

View file

@ -368,7 +368,7 @@ describe("Table", () => {
verifyAndEnterColumnOptionInput("Text color", "red");
verifyAndEnterColumnOptionInput(
"Cell Background Color",
"{backspace}{backspace}{backspace}{backspace}{backspace}yellow"
"{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}yellow"
);
cy.get(
'[data-cy="input-and-label-cell-background-color"] > .form-label'

View file

@ -67,7 +67,7 @@ export const verifyAndModifyToggleFx = (
export const addDefaultEventHandler = (message) => {
cy.get(commonWidgetSelector.addEventHandlerLink)
.should("have.text", commonWidgetText.addEventHandlerLink)
.should("contain.text", commonWidgetText.addEventHandlerLink)
.click();
cy.get(commonWidgetSelector.eventHandlerCard).click();
cy.get(commonWidgetSelector.alertMessageInputField)

View file

@ -1,12 +1,12 @@
import { postgreSqlSelector } from "Selectors/postgreSql";
export const selectQuery = (dbName) => {
cy.get(postgreSqlSelector.buttonAddNewQueries).click();
export const selectQueryFromLandingPage = (dbName, label) => {
cy.get(
`[data-cy="${dbName.toLowerCase().replace(/\s+/g, "-")}-add-query-card"]`
)
.should("contain", dbName)
.should("contain", label)
.click();
cy.waitForAutoSave();
};
export const deleteQuery = (queryName) => {
@ -34,4 +34,12 @@ export const addInputOnQueryField = (field, data) => {
.click()
.clearAndTypeOnCodeMirror(`{backSpace}`);
cy.get(`[data-cy="${field}-input-field"]`).clearAndTypeOnCodeMirror(data);
cy.forceClickOnCanvas();
};
export const waitForQueryAction = (action) => {
cy.get(`[data-cy="query-${action}-button"]`, { timeout: 20000 }).should(
"not.have.class",
"button-loading"
);
};

View file

@ -67,8 +67,10 @@ export const verifyAndEnterColumnOptionInput = (label, value) => {
cy.get(`[data-cy="input-and-label-${cyParamName(label)}"]`)
.realClick()
.realPress(["Meta", "A"])
.realType(`{backspace}{backspace}{backspace}{backspace}`)
.realPress(["Meta", "A"])
.realType(
`{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}${value}`
`{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}{backspace}{rightarrow}${value}`
);
};

View file

@ -180,7 +180,7 @@
"table": "Tabelle",
"pageIndex": "Seitenindex",
"component": "Komponente",
"addHandler": "+ Handler hinzufügen",
"addHandler": "Handler hinzufügen",
"addEventHandler": "+ Ereignishandler hinzufügen",
"emptyMessage": "Dieser {{componentName}} hat keine Ereignishandler"
}
@ -914,4 +914,4 @@
"tip": "Zurück zur Startseite"
}
}
}
}

View file

@ -181,7 +181,7 @@
"events": "Events",
"transformation": {
"transformationToolTip": "Transformations can be enabled on queries to transform the query results. ToolJet allows you to transform the query results using two programming languages: JavaScript and Python",
"transformations": "Enable Transformations"
"transformations": "Transformations"
}
},
"inspector": {
@ -204,7 +204,7 @@
"table": "Table",
"pageIndex": "Page index",
"component": "Component",
"addHandler": "+ Add handler",
"addHandler": "Add handler",
"addEventHandler": "+ Add event handler",
"emptyMessage": "This {{componentName}} doesn't have any event handlers",
"page": "Page"
@ -259,7 +259,7 @@
"enterLastName": "Enter Last Name",
"enterEmail": "Enter email id",
"enterFulltName": "Enter full name",
"inviteNewUsers":"Invite new users"
"inviteNewUsers": "Invite new users"
},
"manageGroups": {
"permissions": {

View file

@ -180,7 +180,7 @@
"table": "Tabla",
"pageIndex": "Índice de página",
"component": "Componente",
"addHandler": "+ Añadir manejador",
"addHandler": "Añadir manejador",
"addEventHandler": "+ Añadir manejador de eventos",
"emptyMessage": "Este {{componentName}} no tiene manejadores de eventos."
}

View file

@ -180,7 +180,7 @@
"table": "Table",
"pageIndex": "Index de page",
"component": "Composante",
"addHandler": "+ Ajouter le gestionnaire",
"addHandler": "Ajouter le gestionnaire",
"addEventHandler": "+ Ajouter un gestionnaire d'événements",
"emptyMessage": "Ce {{componentName}} n'a pas de gestionnaires d'événements"
}
@ -909,10 +909,9 @@
"maxHeightOfCanvas": "Hauteur maximale de la toile",
"backgroundColorOfCanvas": "Couleur d'arrière-plan de la toile"
},
"Back":
{
"Back": {
"text": "Retour",
"tip": "De retour à la maison"
}
}
}
}

View file

@ -180,7 +180,7 @@
"table": "Tabella",
"pageIndex": "Indice delle Pagine",
"component": "Componente",
"addHandler": "+ Aggiungi handler",
"addHandler": "Aggiungi handler",
"addEventHandler": "+ Aggiungi handler eventi",
"emptyMessage": "Questo {{componentName}} non ha handler eventi"
}

View file

@ -305,7 +305,7 @@ export function CodeHinter({
className={`row${height === '150px' || height === '300px' ? ' tablr-gutter-x-0' : ''} custom-row`}
style={{ width: width, display: codeShow ? 'flex' : 'none' }}
>
<div className={`col code-hinter-col`} style={{ marginBottom: '0.5rem' }}>
<div className={`col code-hinter-col`}>
<div
className="code-hinter-wrapper position-relative"
style={{ width: '100%', backgroundColor: darkMode && '#272822' }}

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import cx from 'classnames';
var tinycolor = require('tinycolor2');
const tinycolor = require('tinycolor2');
export const Button = function Button(props) {
const { height, properties, styles, fireEvent, registerAction, id, dataCy, setExposedVariable } = props;

View file

@ -3,6 +3,7 @@ import { isEqual } from 'lodash';
import iframeContent from './iframe.html';
import { useDataQueries } from '@/_stores/dataQueriesStore';
import { isQueryRunnable } from '@/_helpers/utils';
export const CustomComponent = (props) => {
const dataQueries = useDataQueries();
@ -44,7 +45,9 @@ export const CustomComponent = (props) => {
if (e.data.message === 'UPDATE_DATA') {
setCustomProps({ ...customPropRef.current, ...e.data.updatedObj });
} else if (e.data.message === 'RUN_QUERY') {
const filteredQuery = dataQueryRef.current.filter((query) => query.name === e.data.queryName);
const filteredQuery = dataQueryRef.current.filter(
(query) => query.name === e.data.queryName && isQueryRunnable(query)
);
const parameters = e.data.parameters ? JSON.parse(e.data.parameters) : {};
filteredQuery.length === 1 &&
fireEvent('onTrigger', {

View file

@ -99,10 +99,10 @@ export default function generateColumnsData({
const rowChangeSet = updatedChangeSet ? updatedChangeSet[cell.row.index] : null;
let cellValue = rowChangeSet ? rowChangeSet[column.key || column.name] ?? cell.value : cell.value;
const rowData = tableData[cell.row.index];
const rowData = tableData?.[cell?.row?.index];
if (
cell.row.index === 0 &&
variablesExposedForPreview &&
!_.isEmpty(variablesExposedForPreview) &&
!_.isEqual(variablesExposedForPreview[id]?.rowData, rowData)
) {
const customResolvables = {};

View file

@ -3,6 +3,7 @@ import { appService, authenticationService, appVersionService, orgEnvironmentVar
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { defaults, cloneDeep, isEqual, isEmpty, debounce, omit } from 'lodash';
import { shallow } from 'zustand/shallow';
import { Container } from './Container';
import { EditorKeyHooks } from './EditorKeyHooks';
import { CustomDragLayer } from './CustomDragLayer';
@ -21,6 +22,7 @@ import {
debuggerActions,
cloneComponents,
removeSelectedComponent,
computeQueryState,
} from '@/_helpers/appUtils';
import { Confirm } from './Viewer/Confirm';
import { Tooltip as ReactTooltip } from 'react-tooltip';
@ -48,10 +50,10 @@ import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { useEditorStore } from '@/_stores/editorStore';
import { useQueryPanelStore } from '@/_stores/queryPanelStore';
import { useAppDataStore } from '@/_stores/appDataStore';
import { useCurrentStateStore, useCurrentState } from '@/_stores/currentStateStore';
import { resetAllStores } from '@/_stores/utils';
import { setCookie } from '@/_helpers/cookie';
import { shallow } from 'zustand/shallow';
setAutoFreeze(false);
enablePatches();
@ -61,6 +63,8 @@ class EditorComponent extends React.Component {
super(props);
resetAllStores();
const appId = this.props.params.id;
useAppDataStore.getState().actions.setAppId(appId);
useEditorStore.getState().actions.setIsEditorActive(true);
const { socket } = createWebsocketConnection(appId);
@ -112,13 +116,10 @@ class EditorComponent extends React.Component {
apps: [],
queryConfirmationList: [],
isSourceSelected: false,
isSaving: false,
isUnsavedQueriesAvailable: false,
selectionInProgress: false,
scrollOptions: {},
currentPageId: defaultPageId,
pages: {},
draftQuery: null,
selectedDataSource: null,
};
@ -190,6 +191,7 @@ class EditorComponent extends React.Component {
threshold: 0,
},
});
const globals = {
...this.props.currentState.globals,
theme: { name: this.props.darkMode ? 'dark' : 'light' },
@ -201,6 +203,16 @@ class EditorComponent extends React.Component {
variables: {},
};
useCurrentStateStore.getState().actions.setCurrentState({ globals, page });
this.appDataStoreListner = useAppDataStore.subscribe(({ isSaving } = {}) => {
if (isSaving !== this.state.isSaving) {
this.setState({ isSaving });
}
});
this.dataQueriesStoreListner = useDataQueriesStore.subscribe(({ dataQueries }) => {
computeQueryState(dataQueries, this);
}, shallow);
}
/**
@ -252,9 +264,10 @@ class EditorComponent extends React.Component {
initEventListeners() {
this.socket?.addEventListener('message', (event) => {
const data = event.data.replace(/^"(.+(?="$))"$/, '$1');
if (data === 'versionReleased') this.fetchApp();
else if (data === 'dataQueriesChanged') {
this.fetchDataQueries(this.props.editingVersion?.id);
if (data === 'versionReleased') {
this.fetchApp();
// } else if (data === 'dataQueriesChanged') { //Commented since this need additional BE changes to work.
// this.fetchDataQueries(this.state.editingVersion?.id); //Also needs revamping to exclude notifying the client of their own changes.
} else if (data === 'dataSourcesChanged') {
this.fetchDataSources(this.props.editingVersion?.id);
}
@ -266,6 +279,8 @@ class EditorComponent extends React.Component {
this.socket && this.socket?.close();
this.subscription && this.subscription.unsubscribe();
if (config.ENABLE_MULTIPLAYER_EDITING) this.props?.provider?.disconnect();
this.appDataStoreListner && this.appDataStoreListner();
this.dataQueriesStoreListner && this.dataQueriesStoreListner();
useEditorStore.getState().actions.setIsEditorActive(false);
}
@ -355,11 +370,7 @@ class EditorComponent extends React.Component {
async () => {
computeComponentState(this, this.state.appDefinition.pages[homePageId]?.components ?? {}).then(async () => {
this.setWindowTitle(data.name);
useEditorStore.getState().actions.setShowComments(!!queryString.parse(this.props.location.search).threadId);
for (const event of dataDefinition.pages[homePageId]?.events ?? []) {
await this.handleEvent(event.eventId, event);
}
});
}
);
@ -401,19 +412,13 @@ class EditorComponent extends React.Component {
if (version?.id === this.state.app?.current_version_id) {
(this.canUndo = false), (this.canRedo = false);
}
useAppDataStore.getState().actions.setIsSaving(false);
useAppVersionStore.getState().actions.updateEditingVersion(version);
this.setState(
{
isSaving: false,
},
() => {
shouldWeEditVersion && this.saveEditingVersion(true);
this.fetchDataSources(this.props.editingVersion?.id);
this.fetchDataQueries(this.props.editingVersion?.id, true);
this.initComponentVersioning();
}
);
shouldWeEditVersion && this.saveEditingVersion(true);
this.fetchDataSources(this.props.editingVersion?.id);
this.fetchDataQueries(this.props.editingVersion?.id, true);
this.initComponentVersioning();
}
};
@ -437,10 +442,10 @@ class EditorComponent extends React.Component {
this.fetchGlobalDataSources();
};
/**
* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
*/
dataQueriesChanged = () => {
dataQueriesChanged = (options) => {
/**
* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
*/
if (this.socket instanceof WebSocket && this.socket?.readyState === WebSocket.OPEN) {
this.socket?.send(
JSON.stringify({
@ -448,9 +453,8 @@ class EditorComponent extends React.Component {
data: { message: 'dataQueriesChanged', appId: this.state.appId },
})
);
} else {
this.fetchDataQueries(this.props.editingVersion?.id);
}
options?.isReloadSelf && this.fetchDataQueries(this.props.editingVersion?.id, true);
};
switchSidebarTab = (tabIndex) => {
@ -509,10 +513,10 @@ class EditorComponent extends React.Component {
this.currentVersion[this.state.currentPageId] = currentVersion - 1;
if (!appDefinition) return;
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
appDefinition,
isSaving: true,
},
() => {
this.props.ymap?.set('appDef', {
@ -540,10 +544,10 @@ class EditorComponent extends React.Component {
this.currentVersion[this.state.currentPageId] = currentVersion + 1;
if (!appDefinition) return;
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
appDefinition,
isSaving: true,
},
() => {
this.props.ymap?.set('appDef', {
@ -569,10 +573,9 @@ class EditorComponent extends React.Component {
if (opts?.versionChanged) {
currentPageId = newDefinition.homePageId;
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
isSaving: true,
currentPageId: currentPageId,
appDefinition: newDefinition,
appDefinitionLocalVersion: uuid(),
@ -592,9 +595,16 @@ class EditorComponent extends React.Component {
},
this.handleAddPatch
);
this.setState({ isSaving: true, appDefinition: newDefinition, appDefinitionLocalVersion: uuid() }, () => {
if (!opts.skipAutoSave) this.autoSave();
});
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
appDefinition: newDefinition,
appDefinitionLocalVersion: uuid(),
},
() => {
if (!opts.skipAutoSave) this.autoSave();
}
);
};
handleInspectorView = () => {
@ -696,7 +706,8 @@ class EditorComponent extends React.Component {
);
setStateAsync(_self, newDefinition).then(() => {
computeComponentState(_self, _self.state.appDefinition.pages[currentPageId].components);
this.setState({ isSaving: true, appDefinitionLocalVersion: uuid() });
useAppDataStore.getState().actions.setIsSaving(true);
this.setState({ appDefinitionLocalVersion: uuid() });
this.autoSave();
this.props.ymap?.set('appDef', {
newDefinition: newDefinition.appDefinition,
@ -775,9 +786,9 @@ class EditorComponent extends React.Component {
const hexCode = `${value?.[0]}${this.decimalToHex(value?.[1]?.a)}`;
appDefinition.globalSettings[key] = hexCode;
}
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
isSaving: true,
appDefinition,
},
() => {
@ -865,7 +876,7 @@ class EditorComponent extends React.Component {
saveEditingVersion = (isUserSwitchedVersion = false) => {
if (this.props.isVersionReleased && !isUserSwitchedVersion) {
this.setState({ isSaving: false });
useAppDataStore.getState().actions.setIsSaving(false);
} else if (!isEmpty(this.props?.editingVersion)) {
appVersionService
.save(
@ -885,14 +896,13 @@ class EditorComponent extends React.Component {
saveError: false,
},
() => {
this.setState({
isSaving: false,
});
useAppDataStore.getState().actions.setIsSaving(false);
}
);
})
.catch(() => {
this.setState({ saveError: true, isSaving: false }, () => {
useAppDataStore.getState().actions.setIsSaving(false);
this.setState({ saveError: true }, () => {
toast.error('App could not save.');
});
});
@ -945,10 +955,6 @@ class EditorComponent extends React.Component {
runQuery = (queryId, queryName) => runQuery(this, queryId, queryName);
dataSourceModalHandler = () => {
this.dataSourceModalRef.current.dataSourceModalToggleStateHandler();
};
onAreaSelectionStart = (e) => {
const isMultiSelect = e.inputEvent.shiftKey || this.state.selectedComponents.length > 0;
this.setState((prevState) => {
@ -1030,9 +1036,9 @@ class EditorComponent extends React.Component {
},
};
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
@ -1089,10 +1095,10 @@ class EditorComponent extends React.Component {
? Object.keys(this.state.appDefinition.pages)[0]
: this.state.appDefinition.homePageId;
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
currentPageId: newCurrentPageId,
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
isDeletingPage: false,
@ -1107,9 +1113,9 @@ class EditorComponent extends React.Component {
};
updateHomePage = (pageId) => {
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
isSaving: true,
appDefinition: {
...this.state.appDefinition,
homePageId: pageId,
@ -1178,9 +1184,9 @@ class EditorComponent extends React.Component {
},
};
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
@ -1203,9 +1209,9 @@ class EditorComponent extends React.Component {
return;
}
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
isSaving: true,
appDefinition: {
...this.state.appDefinition,
pages: {
@ -1227,9 +1233,9 @@ class EditorComponent extends React.Component {
};
updateOnPageLoadEvents = (pageId, events) => {
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
isSaving: true,
appDefinition: {
...this.state.appDefinition,
pages: {
@ -1254,9 +1260,9 @@ class EditorComponent extends React.Component {
showViewerNavigation: !this.state.appDefinition.showViewerNavigation,
};
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
@ -1284,9 +1290,9 @@ class EditorComponent extends React.Component {
},
};
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
@ -1308,9 +1314,9 @@ class EditorComponent extends React.Component {
},
};
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
@ -1332,9 +1338,9 @@ class EditorComponent extends React.Component {
},
};
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
@ -1410,9 +1416,9 @@ class EditorComponent extends React.Component {
pages: pagesObj,
};
useAppDataStore.getState().actions.setIsSaving(true);
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
@ -1695,7 +1701,6 @@ class EditorComponent extends React.Component {
allComponents={appDefinition.pages[this.state.currentPageId]?.components ?? {}}
appId={appId}
appDefinition={appDefinition}
dataSourceModalHandler={this.dataSourceModalHandler}
editorRef={this}
/>
<ReactTooltip id="tooltip-for-add-query" className="tooltip" />

View file

@ -3,7 +3,7 @@ import { ToolTip } from '@/_components';
import { appService } from '@/_services';
import { handleHttpErrorMessages, validateName } from '../../_helpers/utils';
function EditAppName({ appId, appName, onNameChanged }) {
function EditAppName({ appId, appName = '', onNameChanged }) {
const darkMode = localStorage.getItem('darkMode') === 'true';
const [name, setName] = React.useState(appName);

View file

@ -19,7 +19,9 @@ function RunjsParameters({ event, darkMode, index, handlerChanged }) {
return (
<div className="row mt-3">
<label className="form-label mt-2">Parameters</label>
<label className="form-label mt-2" data-cy="label-run-js-parameters">
Parameters
</label>
{dataQuery?.options?.parameters.map((param) => (
<React.Fragment key={param.name}>
<div className="col-3 p-2">{param.name}</div>

View file

@ -13,8 +13,13 @@ import Select from '@/_ui/Select';
import defaultStyles from '@/_ui/Select/styles';
import { useTranslation } from 'react-i18next';
import { useDataQueries } from '@/_stores/dataQueriesStore';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import AddRectangle from '@/_ui/Icon/bulkIcons/AddRectangle';
import { Tooltip } from 'react-tooltip';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import RunjsParameters from './ActionConfigurationPanels/RunjsParamters';
import { isQueryRunnable } from '@/_helpers/utils';
import { shallow } from 'zustand/shallow';
export const EventManager = ({
component,
@ -27,8 +32,15 @@ export const EventManager = ({
popoverPlacement,
pages,
hideEmptyEventsAlert,
callerQueryId,
}) => {
const dataQueries = useDataQueries();
const dataQueries = useDataQueriesStore(({ dataQueries = [] }) => {
if (callerQueryId) {
//filter the same query getting attached to itself
return dataQueries.filter((query) => query.id != callerQueryId);
}
return dataQueries;
}, shallow);
const [events, setEvents] = useState(() => component.component.definition.events || []);
const [focusedEventIndex, setFocusedEventIndex] = useState(null);
const { t } = useTranslation();
@ -294,7 +306,7 @@ export const EventManager = ({
</div>
</div>
{actionLookup[event.actionId].options?.length > 0 && (
{actionLookup[event.actionId]?.options?.length > 0 && (
<div className="hr-text" data-cy="action-option">
{t('editor.inspector.eventManager.actionOptions', 'Action options')}
</div>
@ -419,15 +431,18 @@ export const EventManager = ({
<div className="col-9" data-cy="query-selection-field">
<Select
className={`${darkMode ? 'select-search-dark' : 'select-search'} w-100`}
options={dataQueries.map((query) => {
return { name: query.name, value: query.id };
})}
options={dataQueries
.filter((qry) => isQueryRunnable(qry))
.map((qry) => ({ name: qry.name, value: qry.id }))}
value={event.queryId}
search={true}
onChange={(value) => {
const query = dataQueries.find((dataquery) => dataquery.id === value);
const parameters = (query?.options?.parameters ?? []).reduce(
(paramObj, param) => ({ ...paramObj, [param.name]: param.defaultValue }),
(paramObj, param) => ({
...paramObj,
[param.name]: param.defaultValue,
}),
{}
);
handlerChanged(index, 'queryId', query.id);
@ -825,7 +840,7 @@ export const EventManager = ({
{...provided.dragHandleProps}
className="mb-1"
>
<div className="card column-sort-row">
<div className="card column-sort-row border-0 bg-slate2">
<div className={rowClassName} data-cy="event-handler-card">
<div className="row p-2" role="button">
<div className="col-auto" style={{ cursor: 'grab' }}>
@ -871,7 +886,7 @@ export const EventManager = ({
<div className="col text-truncate" data-cy="event-handler">
{componentMeta.events[event.eventId]['displayName']}
</div>
<div className="col text-truncate" data-cy="event-name">
<div className="col text-truncate color-slate11" data-cy="event-name">
<small className="event-action font-weight-light text-truncate">
{actionMeta.name}
</small>
@ -884,6 +899,8 @@ export const EventManager = ({
removeHandler(index);
}}
data-cy="delete-button"
data-tooltip-id="event-delete-btn-icon"
data-tooltip-content="Delete"
>
<svg
width="10"
@ -894,10 +911,11 @@ export const EventManager = ({
>
<path
d="M0 13.8333C0 14.75 0.75 15.5 1.66667 15.5H8.33333C9.25 15.5 10 14.75 10 13.8333V3.83333H0V13.8333ZM1.66667 5.5H8.33333V13.8333H1.66667V5.5ZM7.91667 1.33333L7.08333 0.5H2.91667L2.08333 1.33333H0V3H10V1.33333H7.91667Z"
fill="#8092AC"
fill="var(--slate8)"
/>
</svg>
</span>
<Tooltip id="event-delete-btn-icon" className="tooltip" />
</div>
</div>
</div>
@ -917,20 +935,29 @@ export const EventManager = ({
);
};
const renderAddHandlerBtn = () => {
return (
<div className={`mb-3 ${events.length === 0 ? '' : 'mt-2'}`}>
<ButtonSolid
variant="ghostBlue"
size="sm"
onClick={addHandler}
data-cy={events.length === 0 ? 'add-event-handler' : 'add-more-event-handler'}
>
<AddRectangle width="15" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />
&nbsp;&nbsp;
{t('editor.inspector.eventManager.addHandler', 'Add handler')}
</ButtonSolid>
</div>
);
};
const componentName = componentMeta.name ? componentMeta.name : 'query';
if (events.length === 0) {
return (
<>
<div className="text-left mb-3">
<button
className="btn btn-sm border-0 font-weight-normal padding-2 col-auto color-primary inspector-add-button"
onClick={addHandler}
data-cy="add-event-handler"
>
{t('editor.inspector.eventManager.addEventHandler', '+ Add event handler')}
</button>
</div>
{renderAddHandlerBtn()}
{!hideEmptyEventsAlert ? (
<div className="text-left">
<small className="color-disabled" data-cy="no-event-handler-message">
@ -950,16 +977,8 @@ export const EventManager = ({
return (
<>
<div className="text-right mb-3">
<button
className="btn btn-sm border-0 font-weight-normal padding-2 col-auto color-primary inspector-add-button"
onClick={addHandler}
data-cy="add-more-event-handler"
>
{t('editor.inspector.eventManager.addHandler', '+ Add handler')}
</button>
</div>
{renderHandlers(events)}
{renderAddHandlerBtn()}
</>
);
};

View file

@ -65,7 +65,7 @@ export const LeftSidebarDataSources = ({
setSelectedDataSource(null);
dataSourcesChanged();
globalDataSourcesChanged();
dataQueriesChanged();
dataQueriesChanged({ isReloadSelf: true });
})
.catch(({ error }) => {
setDeletingDatasource(false);

View file

@ -6,6 +6,7 @@ import cx from 'classnames';
function Logs({ logProps, idx, darkMode }) {
const [open, setOpen] = React.useState(false);
console.log('Debug debugger: open:', open);
const title = ` [${capitalize(logProps?.type)} ${logProps?.key}]`;
const message = logProps?.isQuerySuccessLog
@ -26,7 +27,10 @@ function Logs({ logProps, idx, darkMode }) {
<div className="tab-content debugger-content mb-1" key={`${logProps?.key}-${idx}`}>
<p
className="m-0 d-flex"
onClick={() => setOpen((prev) => !prev)}
onClick={(e) => {
console.log('Debug debugger: setOpen:', e);
setOpen((prev) => !prev);
}}
style={{ pointerEvents: logProps?.isQuerySuccessLog ? 'none' : 'default' }}
>
<span className={cx('mx-1 position-absolute')} style={defaultStyles}>

View file

@ -28,6 +28,7 @@ export const LeftSidebarInspector = ({
pinned,
}) => {
const dataSources = useGlobalDataSources();
const dataQueries = useDataQueries();
const { isVersionReleased } = useAppVersionStore(
(state) => ({
@ -44,17 +45,17 @@ export const LeftSidebarInspector = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appDefinition['selectedComponent']]);
const currentState = useCurrentState();
const queries = {};
if (!_.isEmpty(dataQueries)) {
dataQueries.forEach((query) => {
queries[query.name] = { id: query.id };
});
}
const memoizedJSONData = React.useMemo(() => {
const data = _.merge(currentState, { queries });
const jsontreeData = { ...data };
const updatedQueries = {};
const { queries: currentQueries } = currentState;
if (!_.isEmpty(dataQueries)) {
dataQueries.forEach((query) => {
updatedQueries[query.name] = _.merge(currentQueries[query.name], { id: query.id });
});
}
// const data = _.merge(currentState, { queries: updatedQueries });
const jsontreeData = { ...currentState, queries: updatedQueries };
delete jsontreeData.errors;
delete jsontreeData.client;
delete jsontreeData.server;
@ -87,7 +88,7 @@ export const LeftSidebarInspector = ({
return jsontreeData;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState]);
}, [currentState, JSON.stringify(dataQueries)]);
const queryIcons = Object.entries(currentState['queries']).map(([key, value]) => {
const allDs = [...staticDataSources, ...dataSources];

View file

@ -25,7 +25,7 @@ export const SettingsModal = ({
onHide={handleClose}
size="sm"
centered
className={`${darkMode && 'theme-dark'} page-handle-edit-modal`}
className={`${darkMode && 'theme-dark dark-theme'} page-handle-edit-modal`}
backdrop="static"
enforceFocus={false}
>

View file

@ -153,7 +153,6 @@ export const LeftSidebar = forwardRef((props, ref) => {
updateOnSortingPages={updateOnSortingPages}
updateOnPageLoadEvents={updateOnPageLoadEvents}
apps={apps}
popoverContentHeight={popoverContentHeight}
setPinned={handlePin}
pinned={pinned}
/>

View file

@ -1,14 +1,12 @@
import React from 'react';
import Select from '@/_ui/Select';
export const ChangeDataSource = ({ dataSources, onChange, value, selectedQuery }) => {
export const ChangeDataSource = ({ dataSources, onChange, value }) => {
return (
<Select
className="px-4"
options={dataSources
.filter((ds) => ds.kind === selectedQuery?.kind)
.map((ds) => ({ label: ds.name, value: ds.id }))}
value={value.id}
className="w-100"
options={dataSources.map((ds) => ({ label: ds.name, value: ds.id }))}
value={value?.id}
onChange={(value) => {
const dataSource = dataSources.find((ds) => ds.id === value);
onChange(dataSource);

View file

@ -0,0 +1,23 @@
import React from 'react';
import { getSvgIcon } from '@/_helpers/appUtils';
import RunjsIcon from '@/Editor/Icons/runjs.svg';
import RunTooljetDbIcon from '@/Editor/Icons/tooljetdb.svg';
import RunpyIcon from '@/Editor/Icons/runpy.svg';
const DataSourceIcon = ({ source, height = 25, styles }) => {
const iconFile = source?.plugin?.iconFile?.data ?? source?.plugin?.icon_file?.data;
const Icon = () => getSvgIcon(source.kind, height, height, iconFile, styles);
switch (source.kind) {
case 'runjs':
return <RunjsIcon style={{ height: height, width: height, marginTop: '-3px' }} />;
case 'runpy':
return <RunpyIcon style={{ height: height, width: height, marginTop: '-3px' }} />;
case 'tooljetdb':
return <RunTooljetDbIcon style={{ height: height, width: height, marginTop: '-3px' }} />;
default:
return <Icon />;
}
};
export default DataSourceIcon;

View file

@ -1,83 +0,0 @@
import React, { useState, useEffect } from 'react';
import RunjsIcon from '@/Editor/Icons/runjs.svg';
import RunTooljetDbIcon from '@/Editor/Icons/tooljetdb.svg';
import RunpyIcon from '@/Editor/Icons/runpy.svg';
import AddIcon from '@assets/images/icons/add-source.svg';
import { useTranslation } from 'react-i18next';
import { getSvgIcon } from '@/_helpers/appUtils';
function DataSourceLister({
dataSources,
staticDataSources,
changeDataSource,
handleBackButton,
darkMode,
dataSourceModalHandler,
showAddDatasourceBtn = true,
dataSourceBtnComponent = null,
}) {
const [allSources, setAllSources] = useState([...dataSources, ...staticDataSources]);
const { t } = useTranslation();
const computedStyles = {
background: darkMode ? '#2f3c4c' : 'white',
color: darkMode ? 'white' : '#1f2936',
border: darkMode && '1px solid #2f3c4c',
};
const handleChangeDataSource = (source) => {
changeDataSource(source);
handleBackButton();
};
useEffect(() => {
setAllSources([...dataSources, ...staticDataSources]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataSources]);
const fetchIconForSource = (source) => {
const iconFile = source?.plugin?.iconFile?.data ?? undefined;
const Icon = () => getSvgIcon(source.kind, 20, 20, iconFile);
switch (source.kind) {
case 'runjs':
return <RunjsIcon style={{ height: 25, width: 25, marginTop: '-3px' }} />;
case 'runpy':
return <RunpyIcon style={{ height: 25, width: 25, marginTop: '-3px' }} />;
case 'tooljetdb':
return <RunTooljetDbIcon />;
default:
return <Icon />;
}
};
return (
<div className="query-datasource-card-container">
{showAddDatasourceBtn && dataSourceBtnComponent && dataSourceBtnComponent}
{allSources.map((source) => {
return (
<div
className="query-datasource-card"
style={computedStyles}
key={`${source.id}-${source.kind}`}
onClick={() => {
handleChangeDataSource(source);
}}
>
{fetchIconForSource(source)}
<p data-cy={`${String(source.name).toLocaleLowerCase().replace(/\s+/g, '-')}-add-query-card`}>
{' '}
{source.name}
</p>
</div>
);
})}
{showAddDatasourceBtn && !dataSourceBtnComponent && (
<div className="query-datasource-card" style={computedStyles} onClick={dataSourceModalHandler}>
<AddIcon style={{ height: 25, width: 25, marginTop: '-3px' }} />
<p>{t('editor.queryManager.addDatasource', 'Add datasource')}</p>
</div>
)}
</div>
);
}
export default DataSourceLister;

View file

@ -0,0 +1,175 @@
import React, { useState, useEffect } from 'react';
import Plus from '@/_ui/Icon/solidIcons/Plus';
import Information from '@/_ui/Icon/solidIcons/Information';
import Search from '@/_ui/Icon/solidIcons/Search';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { getWorkspaceId } from '@/_helpers/utils';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { SearchBox as SearchBox2 } from '@/_components/SearchBox';
import DataSourceIcon from './DataSourceIcon';
import { isEmpty } from 'lodash';
import { Col, Container, Row } from 'react-bootstrap';
import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import { Tooltip } from 'react-tooltip';
function DataSourcePicker({ dataSources, staticDataSources, darkMode, globalDataSources }) {
const allUserDefinedSources = [...dataSources, ...globalDataSources];
const [searchTerm, setSearchTerm] = useState();
const [filteredUserDefinedDataSources, setFilteredUserDefinedDataSources] = useState(allUserDefinedSources);
const navigate = useNavigate();
const { createDataQuery } = useDataQueriesActions();
const { setPreviewData } = useQueryPanelActions();
const handleChangeDataSource = (source) => {
createDataQuery(source);
setPreviewData(null);
};
useEffect(() => {
if (searchTerm) {
const formattedSearchTerm = searchTerm.toLowerCase();
const filteredResults = allUserDefinedSources.filter(
({ name, kind }) =>
name.toLowerCase().includes(formattedSearchTerm) || kind.toLowerCase().includes(formattedSearchTerm)
);
setFilteredUserDefinedDataSources(filteredResults);
} else {
setFilteredUserDefinedDataSources(allUserDefinedSources);
}
}, [searchTerm, globalDataSources, dataSources]);
const handleAddClick = () => {
const workspaceId = getWorkspaceId();
navigate(`/${workspaceId}/global-datasources`);
};
return (
<>
<h4 className="w-100 text-center" data-cy={'label-select-datasource'} style={{ fontWeight: 500 }}>
Connect to a datasource
</h4>
<p className="mb-3" style={{ textAlign: 'center' }}>
Select a datasource to start creating a new query. To know more about queries in ToolJet, you can read our
&nbsp;
<a target="_blank" href="https://docs.tooljet.com/docs/app-builder/query-panel" rel="noreferrer">
documentation
</a>
</p>
<div>
<label className="form-label" data-cy={`landing-page-label-default`}>
Default
</label>
<div className="query-datasource-card-container d-flex justify-content-between mb-3 mt-2">
{staticDataSources.map((source) => {
return (
<ButtonSolid
key={`${source.id}-${source.kind}`}
variant="tertiary"
size="sm"
onClick={() => {
handleChangeDataSource(source);
}}
className="text-truncate"
data-cy={`${source.kind.toLowerCase().replace(/\s+/g, '-')}-add-query-card`}
>
<DataSourceIcon source={source} height={14} /> {source.shortName}
</ButtonSolid>
);
})}
</div>
<div className="d-flex d-flex justify-content-between">
<label className="form-label py-1" style={{ width: 'auto' }} data-cy={`label-avilable-ds`}>
{`Available Datasources ${!isEmpty(allUserDefinedSources) ? '(' + allUserDefinedSources.length + ')' : 0}`}
</label>
<ButtonSolid
size="sm"
variant="ghostBlue"
onClick={handleAddClick}
data-cy={`landing-page-add-new-ds-button`}
>
<Plus style={{ height: '16px' }} fill="var(--indigo9)" />
Add new
</ButtonSolid>
</div>
{isEmpty(allUserDefinedSources) ? (
<EmptyDataSourceBanner />
) : (
<Container className="p-0">
{allUserDefinedSources.length > 4 && (
<SearchBox
onSearch={setSearchTerm}
darkMode={darkMode}
searchTerm={searchTerm}
dataCy={`gds-querymanager`}
/>
)}
<Row className="mt-2">
{filteredUserDefinedDataSources.map((source) => (
<Col sm="6" key={source.id} className="ps-1">
<ButtonSolid
key={`${source.id}-${source.kind}`}
variant="ghostBlack"
size="sm"
className="font-weight-400 py-3 mb-1 w-100 justify-content-start"
onClick={() => {
handleChangeDataSource(source);
}}
data-tooltip-id="tooltip-for-query-panel-ds-picker-btn"
data-tooltip-content={source.name}
data-cy={`${String(source.name).toLowerCase().replace(/\s+/g, '-')}-add-query-card`}
>
<DataSourceIcon source={source} height={14} styles={{ minWidth: 14 }} />
<span className="text-truncate">{source.name}</span>
<Tooltip id="tooltip-for-query-panel-ds-picker-btn" className="tooltip" />
</ButtonSolid>
</Col>
))}
</Row>
</Container>
)}
</div>
</>
);
}
const EmptyDataSourceBanner = () => (
<div className="bg-slate3 p-3 d-flex align-items-center lh-lg mt-2" style={{ borderRadius: '6px' }}>
<div className="me-2">
<Information fill="var(--slate9)" />
</div>
<div>
No global datasources have been added yet. <br />
Add new datasources to connect to your app! 🚀
</div>
</div>
);
const SearchBox = ({ onSearch, darkMode, searchTerm, dataCy }) => {
const { t } = useTranslation();
return (
<Row>
<Col className="mt-2 mb-2">
<SearchBox2
width="100%"
type="text"
className={`form-control ${darkMode && 'dark-theme-placeholder'}`}
placeholder={t('globals.search', 'Search') + '...'}
value={searchTerm}
callBack={(e) => onSearch(e.target.value)}
onClearCallback={() => onSearch('')}
dataCy={dataCy}
/>
{/* <span
className="position-absolute"
style={{ top: '50%', transform: 'translate(0%, -50%)', paddingLeft: '10px' }}
>
<Search style={{ width: '16px' }} />
</span> */}
</Col>
</Row>
);
};
export default DataSourcePicker;

View file

@ -0,0 +1,266 @@
import React, { useState, useEffect } from 'react';
import Select, { components } from 'react-select';
import { groupBy, isEmpty } from 'lodash';
import { useNavigate } from 'react-router-dom';
import DataSourceIcon from './DataSourceIcon';
import { authenticationService } from '@/_services';
import { getWorkspaceId } from '@/_helpers/utils';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { useDataSources, useGlobalDataSources } from '@/_stores/dataSourcesStore';
import { useDataQueriesActions } from '@/_stores/dataQueriesStore';
import { staticDataSources } from '../constants';
import { useQueryPanelActions } from '@/_stores/queryPanelStore';
import Search from '@/_ui/Icon/solidIcons/Search';
import { Tooltip } from 'react-tooltip';
import { DataBaseSources, ApiSources, CloudStorageSources } from '@/Editor/DataSourceManager/SourceComponents';
function DataSourceSelect({ darkMode, isDisabled, selectRef, closePopup }) {
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const [userDefinedSources, setUserDefinedSources] = useState([...dataSources, ...globalDataSources]);
const [dataSourcesKinds, setDataSourcesKinds] = useState([]);
const [userDefinedSourcesOpts, setUserDefinedSourcesOpts] = useState([]);
const { createDataQuery } = useDataQueriesActions();
const { setPreviewData } = useQueryPanelActions();
const handleChangeDataSource = (source) => {
createDataQuery(source);
setPreviewData(null);
closePopup();
};
console.log(dataSourcesKinds);
useEffect(() => {
const allDataSources = [...dataSources, ...globalDataSources];
setUserDefinedSources(allDataSources);
const dataSourceKindsList = [...DataBaseSources, ...ApiSources, ...CloudStorageSources];
allDataSources.forEach(({ plugin }) => {
//plugin names are fetched from list data source api call only
if (isEmpty(plugin)) {
return;
}
dataSourceKindsList.push({ name: plugin.name, kind: plugin.pluginId });
});
setDataSourcesKinds(dataSourceKindsList);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataSources]);
useEffect(() => {
setUserDefinedSourcesOpts(
Object.entries(groupBy(userDefinedSources, 'kind')).map(([kind, sources], index) => ({
label: (
<div>
{index === 0 && (
<div className="color-slate9 mb-2 pb-1" style={{ fontWeight: 500, marginTop: '-8px' }}>
Global datasources
</div>
)}
<DataSourceIcon source={sources?.[0]} height={16} />
<span className="ms-1 small">{dataSourcesKinds.find((dsk) => dsk.kind === kind)?.name || kind}</span>
</div>
),
options: sources.map((source) => ({
label: (
<div
className="py-2 px-2 rounded option-nested-datasource-selector small text-truncate"
data-tooltip-id="tooltip-for-add-query-dd-option"
data-tooltip-content={source.name}
>
{source.name}
<Tooltip id="tooltip-for-add-query-dd-option" className="tooltip query-manager-ds-select-tooltip" />
</div>
),
value: source.id,
isNested: true,
source,
})),
}))
);
}, [userDefinedSources]);
const DataSourceOptions = [
{
label: (
<span className="color-slate9" style={{ fontWeight: 500 }}>
Defaults
</span>
),
isDisabled: true,
options: [
...staticDataSources.map((source) => ({
label: (
<div>
<DataSourceIcon source={source} height={16} /> <span className="ms-1 small">{source.name}</span>
</div>
),
value: source.id,
source,
})),
],
},
...userDefinedSourcesOpts,
];
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
closePopup();
}
};
return (
<div>
<Select
onChange={({ source } = {}) => handleChangeDataSource(source)}
classNames={{
menu: () => 'tj-scrollbar',
}}
ref={selectRef}
controlShouldRenderValue={false}
menuPlacement="auto"
components={{
MenuList: MenuList,
IndicatorSeparator: () => null,
DropdownIndicator,
}}
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',
':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,
}),
input: (style) => ({
...style,
color: 'var(--slate12)',
'caret-color': 'var(--slate9)',
':placeholder': { color: 'var(--slate9)' },
}),
groupHeading: (style) => ({
...style,
fontSize: '100%',
textTransform: '',
color: 'inherit',
fontWeight: '400',
}),
option: (style, { data: { isNested }, isFocused, isDisabled }) => ({
...style,
cursor: 'pointer',
backgroundColor: 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)' },
}),
}),
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={DataSourceOptions}
isDisabled={isDisabled}
menuIsOpen
maxMenuHeight={400}
minMenuHeight={300}
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, ...props }) => {
const navigate = useNavigate();
const menuListStyles = getStyles('menuList', props);
const { admin } = authenticationService.currentSessionValue;
const workspaceId = getWorkspaceId();
if (admin) {
//offseting for height of button since react-select calculates only the size of options list
menuListStyles.maxHeight = 400 - 48;
}
menuListStyles.padding = '4px';
const handleAddClick = () => navigate(`/${workspaceId}/global-datasources`);
return (
<>
<div ref={innerRef} style={menuListStyles} id="query-ds-select-menu">
{children}
</div>
{admin && (
<div className="p-2 mt-2 border-slate3-top">
<ButtonSolid variant="secondary" size="md" className="w-100" onClick={handleAddClick}>
+ Add new datasource
</ButtonSolid>
</div>
)}
</>
);
};
const DropdownIndicator = (props) => {
return (
components.DropdownIndicator && (
<components.DropdownIndicator {...props}>
<Search style={{ width: '16px' }} />
</components.DropdownIndicator>
)
);
};
export default DataSourceSelect;

View file

@ -1,19 +1,34 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { JSONTree } from 'react-json-tree';
import { Tab, ListGroup, Row } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Tab, ListGroup, Row, Col } from 'react-bootstrap';
import { usePreviewLoading, usePreviewData, useQueryPanelActions } from '@/_stores/queryPanelStore';
import { getTheme, tabs } from '../constants';
import RemoveRectangle from '@/_ui/Icon/solidIcons/RemoveRectangle';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
const Preview = ({ previewPanelRef, previewLoading, queryPreviewData, darkMode }) => {
const { t } = useTranslation();
const Preview = ({ darkMode }) => {
const [key, setKey] = useState('raw');
const [isJson, setIsJson] = useState(false);
const [theme, setTheme] = useState(() => getTheme(darkMode));
const queryPreviewData = usePreviewData();
const previewLoading = usePreviewLoading();
const { setPreviewData } = useQueryPanelActions();
const previewPanelRef = useRef();
useEffect(() => {
setTheme(() => getTheme(darkMode));
}, [darkMode]);
useLayoutEffect(() => {
if (queryPreviewData || previewLoading) {
previewPanelRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest',
});
}
}, [queryPreviewData, previewLoading]);
useEffect(() => {
if (queryPreviewData !== null && typeof queryPreviewData === 'object') {
setKey('json');
@ -31,43 +46,65 @@ const Preview = ({ previewPanelRef, previewLoading, queryPreviewData, darkMode }
};
return (
<div>
<div className="preview-header preview-section d-flex align-items-baseline font-weight-500" ref={previewPanelRef}>
<div className={`py-2 font-weight-400 ${darkMode ? 'color-dark-slate12' : 'color-light-slate-12'}`}>
{t('editor.preview', 'Preview')}
</div>
<div className="preview-header preview-section d-flex align-items-baseline font-weight-500" ref={previewPanelRef}>
<div className="w-100 border rounded-top">
<Tab.Container activeKey={key} onSelect={(k) => setKey(k)} defaultActiveKey="raw">
<Row style={{ width: '100%', paddingLeft: '20px' }}>
<div className="keys">
<ListGroup className={`query-preview-list-group ${darkMode ? 'dark' : ''}`} variant="flush">
{tabs.map((tab) => (
<ListGroup.Item key={tab} eventKey={tab.toLowerCase()} disabled={!queryPreviewData}>
<span data-cy={`preview-tab-${String(tab).toLowerCase()}`}>{tab}</span>
</ListGroup.Item>
))}
</ListGroup>
</div>
<div className="position-relative">
{previewLoading && (
<center>
<center className="position-absolute w-100">
<div className="spinner-border text-azure mt-5" role="status"></div>
</center>
)}
<Tab.Content style={{ overflowWrap: 'anywhere' }}>
{!queryPreviewData && <div className="col preview-default-container"></div>}
<Tab.Pane eventKey="json" transition={false}>
{previewLoading === false && isJson && (
<div className="w-100 " data-cy="preview-json-data-container">
<Row className="py-2 border-bottom preview-section-header m-0">
<Col className="d-flex align-items-center color-slate9">Preview</Col>
<Col className="keys text-center d-flex align-items-center">
<ListGroup
className={`query-preview-list-group rounded ${darkMode ? 'dark' : ''}`}
variant="flush"
style={{ backgroundColor: '#ECEEF0', padding: '2px' }}
>
{tabs.map((tab) => (
<ListGroup.Item
key={tab}
eventKey={tab.toLowerCase()}
disabled={!queryPreviewData || (tab == 'JSON' && !isJson)}
style={{ minWidth: '74px', textAlign: 'center' }}
className="rounded"
>
<span
data-cy={`preview-tab-${String(tab).toLowerCase()}`}
style={{ width: '100%' }}
className="rounded"
>
{tab}
</span>
</ListGroup.Item>
))}
</ListGroup>
</Col>
<Col className="text-right d-flex align-items-center justify-content-end">
{queryPreviewData && (
<ButtonSolid variant="ghostBlack" size="sm" onClick={() => setPreviewData()}>
<RemoveRectangle width={17} viewBox="0 0 28 28" fill="var(--slate8)" /> Clear
</ButtonSolid>
)}
</Col>
</Row>
<Row className="m-0">
<Tab.Content style={{ overflowWrap: 'anywhere', padding: 0 }}>
<Tab.Pane eventKey="json" transition={false}>
<div className="w-100 preview-data-container" data-cy="preview-json-data-container">
<JSONTree theme={theme} data={queryPreviewData} invertTheme={!darkMode} collectionLimit={100} />
</div>
)}
</Tab.Pane>
<Tab.Pane eventKey="raw" transition={false}>
<div className={`p-3 raw-container `} data-cy="preview-raw-data-container">
{renderRawData()}
</div>
</Tab.Pane>
</Tab.Content>
</Row>
</Tab.Pane>
<Tab.Pane eventKey="raw" transition={false}>
<div className={`p-3 raw-container preview-data-container`} data-cy="preview-raw-data-container">
{renderRawData()}
</div>
</Tab.Pane>
</Tab.Content>
</Row>
</div>
</Tab.Container>
</div>
</div>

View file

@ -1,434 +1,310 @@
import React, { useEffect, useState, useRef, forwardRef } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import cx from 'classnames';
import { capitalize, isEqual } from 'lodash';
import { cloneDeep, isEmpty } from 'lodash';
// eslint-disable-next-line import/no-unresolved
import { diff } from 'deep-object-diff';
import { allSources, source } from '../QueryEditors';
import DataSourceLister from './DataSourceLister';
import DataSourcePicker from './DataSourcePicker';
import { Transformation } from './Transformation';
import Preview from './Preview';
import { ChangeDataSource } from './ChangeDataSource';
import { CustomToggleSwitch } from './CustomToggleSwitch';
import AddGlobalDataSourceButton from './AddGlobalDataSourceButton';
import EmptyGlobalDataSources from './EmptyGlobalDataSources';
import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
import { EventManager } from '@/Editor/Inspector/EventManager';
import { allOperations } from '@tooljet/plugins/client';
import { staticDataSources, customToggles, mockDataQueryAsComponent, schemaUnavailableOptions } from '../constants';
import { staticDataSources, customToggles, mockDataQueryAsComponent } from '../constants';
import { DataSourceTypes } from '../../DataSourceManager/SourceComponents';
import { useDataSources, useGlobalDataSources } from '@/_stores/dataSourcesStore';
import { useDataQueries, useDataQueriesActions } from '@/_stores/dataQueriesStore';
import {
useUnsavedChanges,
useSelectedQuery,
useSelectedDataSource,
useQueryPanelActions,
} from '@/_stores/queryPanelStore';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useDataQueriesActions, useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useSelectedQuery, useSelectedDataSource } from '@/_stores/queryPanelStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import SuccessNotificationInputs from './SuccessNotificationInputs';
export const QueryManagerBody = forwardRef(
(
{
darkMode,
mode,
dataSourceModalHandler,
options,
previewLoading,
queryPreviewData,
allComponents,
apps,
appDefinition,
createDraftQuery,
setOptions,
},
ref
) => {
const { t } = useTranslation();
const dataQueries = useDataQueries();
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const selectedQuery = useSelectedQuery();
const isUnsavedQueriesAvailable = useUnsavedChanges();
const selectedDataSource = useSelectedDataSource();
const { setSelectedDataSource, setUnSavedChanges, setPreviewData } = useQueryPanelActions();
const { changeDataQuery } = useDataQueriesActions();
export const QueryManagerBody = ({
darkMode,
options,
currentState,
allComponents,
apps,
appDefinition,
setOptions,
appId,
}) => {
const { t } = useTranslation();
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const selectedQuery = useSelectedQuery();
const selectedDataSource = useSelectedDataSource();
const { changeDataQuery, updateDataQuery } = useDataQueriesActions();
const [dataSourceMeta, setDataSourceMeta] = useState(null);
const currentState = useCurrentState();
/* - Added the below line to cause re-rendering when the query is switched
const [dataSourceMeta, setDataSourceMeta] = useState(null);
/* - Added the below line to cause re-rendering when the query is switched
- QueryEditors are not updating when the query is switched
- TODO: Remove the below line and make query editors update when the query is switched
- Ref PR #6763
*/
const [selectedQueryId, setSelectedQueryId] = useState(selectedQuery?.id);
const [selectedQueryId, setSelectedQueryId] = useState(selectedQuery?.id);
const queryName = selectedQuery?.name ?? '';
const sourcecomponentName = selectedDataSource?.kind.charAt(0).toUpperCase() + selectedDataSource?.kind.slice(1);
const ElementToRender = selectedDataSource?.pluginId ? source : allSources[sourcecomponentName];
const queryName = selectedQuery?.name ?? '';
const sourcecomponentName = selectedDataSource?.kind?.charAt(0).toUpperCase() + selectedDataSource?.kind?.slice(1);
const defaultOptions = useRef({});
const { isVersionReleased } = useAppVersionStore(
(state) => ({
isVersionReleased: state.isVersionReleased,
}),
shallow
const ElementToRender = selectedDataSource?.pluginId ? source : allSources[sourcecomponentName];
const defaultOptions = useRef({});
const { isVersionReleased } = useAppVersionStore(
(state) => ({
isVersionReleased: state.isVersionReleased,
}),
shallow
);
useEffect(() => {
setDataSourceMeta(
selectedQuery?.pluginId
? selectedQuery?.manifestFile?.data?.source
: DataSourceTypes.find((source) => source.kind === selectedQuery?.kind)
);
setSelectedQueryId(selectedQuery?.id);
defaultOptions.current = selectedQuery?.options && JSON.parse(JSON.stringify(selectedQuery?.options));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedQuery]);
useEffect(() => {
setDataSourceMeta(
selectedQuery?.pluginId
? selectedQuery?.manifestFile?.data?.source
: DataSourceTypes.find((source) => source.kind === selectedQuery?.kind)
);
setSelectedQueryId(selectedQuery?.id);
defaultOptions.current = selectedQuery?.options && JSON.parse(JSON.stringify(selectedQuery?.options));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedQuery]);
const computeQueryName = (kind) => {
const currentQueriesForKind = dataQueries.filter((query) => query.kind === kind);
let currentNumber = currentQueriesForKind.length + 1;
// eslint-disable-next-line no-constant-condition
while (true) {
const newName = `${kind}${currentNumber}`;
if (dataQueries.find((query) => query.name === newName) === undefined) {
return newName;
}
currentNumber += 1;
// Clear the focus field value from options
const cleanFocusedFields = (newOptions) => {
const diffFields = diff(newOptions, defaultOptions.current);
const updatedOptions = { ...newOptions };
Object.keys(diffFields).forEach((key) => {
if (newOptions[key] === '' && defaultOptions.current[key] === undefined) {
delete updatedOptions[key];
}
};
});
return updatedOptions;
};
const changeDataSource = (source) => {
const isSchemaUnavailable = Object.keys(schemaUnavailableOptions).includes(source.kind);
let newOptions = {};
const validateNewOptions = (newOptions) => {
const updatedOptions = cleanFocusedFields(newOptions);
setOptions((options) => ({ ...options, ...updatedOptions }));
if (isSchemaUnavailable) {
newOptions = {
...{ ...schemaUnavailableOptions[source.kind] },
...(source?.kind != 'runjs' && { transformationLanguage: 'javascript', enableTransformation: false }),
};
} else {
const selectedSourceDefault =
source?.plugin?.operationsFile?.data?.defaults ?? allOperations[capitalize(source.kind)]?.defaults;
if (selectedSourceDefault) {
newOptions = {
...{ ...selectedSourceDefault },
...(source?.kind != 'runjs' && { transformationLanguage: 'javascript', enableTransformation: false }),
};
} else {
newOptions = {
...(source?.kind != 'runjs' && { transformationLanguage: 'javascript', enableTransformation: false }),
};
}
}
updateDataQuery(cloneDeep({ ...options, ...updatedOptions }));
};
const newQueryName = computeQueryName(source.kind);
defaultOptions.current = { ...newOptions };
const optionchanged = (option, value) => {
const newOptions = { ...options, [option]: value };
validateNewOptions(newOptions);
};
setSelectedDataSource(source);
setOptions({ ...newOptions });
const optionsChanged = (newOptions) => {
validateNewOptions(newOptions);
};
createDraftQuery(
{ ...source, data_source_id: source.id, name: newQueryName, id: 'draftQuery', options: { ...newOptions } },
source
);
};
const eventsChanged = (events) => {
optionchanged('events', events);
//added this here since the subscriber added in QueryManager component does not detect this change
useDataQueriesStore
.getState()
.actions.saveData({ ...selectedQuery, options: { ...selectedQuery.options, events: events } });
};
// Clear the focus field value from options
const cleanFocusedFields = (newOptions) => {
const diffFields = diff(newOptions, defaultOptions.current);
const updatedOptions = { ...newOptions };
Object.keys(diffFields || {}).forEach((key) => {
if (newOptions[key] === '' && defaultOptions.current[key] === undefined) {
delete updatedOptions[key];
}
});
return updatedOptions;
};
const toggleOption = (option) => {
const currentValue = selectedQuery?.options?.[option] ?? false;
optionchanged(option, !currentValue);
};
const removeRestKey = (options) => {
delete options.arrayValuesChanged;
return options;
};
const renderDataSourcesList = () => {
return (
<div
className={cx(`datasource-picker p-0`, {
'disabled ': isVersionReleased,
})}
>
<DataSourcePicker
dataSources={dataSources}
staticDataSources={staticDataSources}
globalDataSources={globalDataSources}
darkMode={darkMode}
/>
</div>
);
};
const validateNewOptions = (newOptions) => {
const headersChanged = newOptions.arrayValuesChanged ?? false;
const updatedOptions = cleanFocusedFields(newOptions);
let isFieldsChanged = false;
if (selectedQuery) {
const isQueryChanged = !isEqual(removeRestKey(updatedOptions), removeRestKey(defaultOptions.current));
if (isQueryChanged) {
isFieldsChanged = true;
} else if (selectedQuery?.kind === 'restapi') {
if (headersChanged) {
isFieldsChanged = true;
}
}
}
setOptions((options) => ({ ...options, ...updatedOptions }));
if (isFieldsChanged !== isUnsavedQueriesAvailable) setUnSavedChanges(isFieldsChanged);
};
const renderTransformation = () => {
if (
dataSourceMeta?.disableTransformations ||
selectedDataSource?.kind === 'runjs' ||
selectedDataSource?.kind === 'runpy'
)
return;
return (
<Transformation
changeOption={optionchanged}
options={options ?? {}}
currentState={currentState}
darkMode={darkMode}
queryId={selectedQuery?.id}
/>
);
};
const optionchanged = (option, value) => {
const newOptions = { ...options, [option]: value };
validateNewOptions(newOptions);
};
const handleBlur = () => {
updateDataQuery(options);
};
const optionsChanged = (newOptions) => {
validateNewOptions(newOptions);
};
const renderQueryElement = () => {
return (
<div style={{ padding: '0 32px' }}>
<div>
<div
className={cx({
'disabled ': isVersionReleased,
})}
>
<ElementToRender
key={selectedQuery?.id}
pluginSchema={selectedDataSource?.plugin?.operationsFile?.data}
selectedDataSource={selectedDataSource}
options={selectedQuery?.options}
optionsChanged={optionsChanged}
optionchanged={optionchanged}
currentState={currentState}
darkMode={darkMode}
isEditMode={true} // Made TRUE always to avoid setting default options again
queryName={queryName}
onBlur={handleBlur} // Applies only to textarea, text box, etc. where `optionchanged` is triggered for every character change.
/>
{renderTransformation()}
</div>
</div>
</div>
);
};
const handleBackButton = () => {
setPreviewData(null);
};
const renderEventManager = () => {
const queryComponent = mockDataQueryAsComponent(options?.events || []);
return (
<div className="d-flex">
<div className={`form-label`}>{t('editor.queryManager.eventsHandler', 'Events')}</div>
<div className="query-manager-events pb-4 flex-grow-1">
<EventManager
eventsChanged={eventsChanged}
component={queryComponent.component}
componentMeta={queryComponent.componentMeta}
currentState={currentState}
components={allComponents}
callerQueryId={selectedQueryId}
apps={apps}
popoverPlacement="top"
pages={
appDefinition?.pages
? Object.entries(appDefinition?.pages).map(([id, page]) => ({
...page,
id,
}))
: []
}
/>
</div>
</div>
);
};
const eventsChanged = (events) => {
optionchanged('events', events);
};
const toggleOption = (option) => {
const currentValue = options[option] ? options[option] : false;
optionchanged(option, !currentValue);
};
const renderDataSources = (labelText, dataSourcesList, staticList = [], isGlobalDataSource = false) => {
return (
const renderQueryOptions = () => {
return (
<div style={{ padding: '0 32px' }}>
<div
className={cx(`datasource-picker`, {
className={cx(`d-flex pb-1`, {
'disabled ': isVersionReleased,
})}
>
<label className="form-label col-md-3" data-cy={'label-select-datasource'}>
{labelText}
</label>
{isGlobalDataSource && dataSourcesList?.length < 1 ? (
<EmptyGlobalDataSources darkMode={darkMode} />
) : (
<DataSourceLister
dataSources={dataSourcesList}
staticDataSources={staticList}
changeDataSource={changeDataSource}
handleBackButton={handleBackButton}
darkMode={darkMode}
dataSourceModalHandler={dataSourceModalHandler}
showAddDatasourceBtn={isGlobalDataSource}
dataSourceBtnComponent={isGlobalDataSource ? <AddGlobalDataSourceButton /> : null}
/>
)}
</div>
);
};
const renderDataSourcesList = () => (
<>
{renderDataSources(
t('editor.queryManager.selectDatasource', 'Select Datasource'),
dataSources,
staticDataSources
)}
{renderDataSources(
t('editor.queryManager.selectGlobalDatasource', 'Select Global Datasource'),
globalDataSources,
[],
true
)}
</>
);
const renderTransformation = () => {
if (
dataSourceMeta?.disableTransformations ||
selectedDataSource?.kind === 'runjs' ||
selectedDataSource?.kind === 'runpy'
)
return;
return (
<Transformation
changeOption={optionchanged}
options={options ?? {}}
darkMode={darkMode}
queryId={selectedQuery?.id}
/>
);
};
const renderQueryElement = () => {
return (
<div style={{ padding: '0 32px' }}>
<div>
<div
className={cx({
'disabled ': isVersionReleased,
})}
>
<ElementToRender
pluginSchema={selectedDataSource?.plugin?.operationsFile?.data}
selectedDataSource={selectedDataSource}
options={options}
optionsChanged={optionsChanged}
optionchanged={optionchanged}
currentState={currentState}
<div className="form-label">{t('editor.queryManager.settings', 'Settings')}</div>
<div className="flex-grow-1">
{Object.keys(customToggles).map((toggle, index) => (
<CustomToggleFlag
{...customToggles[toggle]}
toggleOption={toggleOption}
value={selectedQuery?.options?.[customToggles[toggle]?.action]}
index={index}
key={toggle}
darkMode={darkMode}
isEditMode={true} // Made TRUE always to avoid setting default options again
queryName={queryName}
mode={mode}
/>
{renderTransformation()}
</div>
<Preview
previewPanelRef={ref}
previewLoading={previewLoading}
queryPreviewData={queryPreviewData}
darkMode={darkMode}
/>
))}
</div>
</div>
);
};
const renderEventManager = () => {
const queryComponent = mockDataQueryAsComponent(options?.events || []);
return (
<>
<div
className={`border-top query-manager-border-color hr-text-left px-4 ${
darkMode ? 'color-white' : 'color-light-slate-12'
}`}
style={{ paddingTop: '28px' }}
>
{t('editor.queryManager.eventsHandler', 'Events Handler')}
</div>
<div className="query-manager-events px-4 mt-2 pb-4">
<EventManager
eventsChanged={eventsChanged}
component={queryComponent.component}
componentMeta={queryComponent.componentMeta}
dataQueries={dataQueries}
components={allComponents}
apps={apps}
popoverPlacement="top"
pages={
appDefinition?.pages ? Object.entries(appDefinition?.pages).map(([id, page]) => ({ ...page, id })) : []
}
/>
</div>
</>
);
};
const renderSuccessNotification = () => (
<div className="mx-4" style={{ paddingLeft: '100px', paddingTop: '12px' }}>
<div className="row mt-1">
<div className="col-auto" style={{ width: '200px' }}>
<label className="form-label p-2 font-size-12" data-cy={'label-success-message-input'}>
{t('editor.queryManager.successMessage', 'Success Message')}
</label>
</div>
<div className="col">
<CodeHinter
initialValue={options.successMessage}
height="36px"
theme={darkMode ? 'monokai' : 'default'}
onChange={(value) => optionchanged('successMessage', value)}
placeholder={t('editor.queryManager.queryRanSuccessfully', 'Query ran successfully')}
cyLabel={'success-message'}
/>
</div>
</div>
<div className="row mt-3">
<div className="col-auto" style={{ width: '200px' }}>
<label className="form-label p-2 font-size-12" data-cy={'label-notification-duration-input'}>
{t('editor.queryManager.notificationDuration', 'Notification duration (s)')}
</label>
</div>
<div className="col query-manager-input-elem">
<input
type="number"
disabled={!options.showSuccessNotification}
onChange={(e) => optionchanged('notificationDuration', e.target.value)}
placeholder={5}
className="form-control"
value={options.notificationDuration}
data-cy={'notification-duration-input-field'}
/>
</div>
</div>
</div>
);
const renderCustomToggle = ({ dataCy, action, translatedLabel, label }, index) => (
<div className={cx('mx-4', { 'pb-3 pt-3': index === 1 })}>
<CustomToggleSwitch
dataCy={dataCy}
isChecked={options && options[action]}
toggleSwitchFunction={toggleOption}
action={action}
<SuccessNotificationInputs
currentState={currentState}
options={options}
darkMode={darkMode}
label={t(translatedLabel, label)}
optionchanged={optionchanged}
/>
{renderEventManager()}
<Preview darkMode={darkMode} />
</div>
);
};
const renderQueryOptions = () => {
return (
<div
className={cx(`advanced-options-container font-weight-400 border-top query-manager-border-color`, {
'disabled ': isVersionReleased,
})}
>
<div className="advance-options-input-form-container">
{Object.keys(customToggles).map((toggle, index) => renderCustomToggle(customToggles[toggle], index))}
{options?.showSuccessNotification && renderSuccessNotification()}
</div>
{renderEventManager()}
const renderChangeDataSource = () => {
const selectableDataSources = [...globalDataSources, ...dataSources].filter(
(ds) => ds.kind === selectedQuery?.kind
);
if (isEmpty(selectableDataSources)) {
return '';
}
return (
<div className={cx('mt-2 d-flex px-4 mb-3', { 'disabled ': isVersionReleased })}>
<div className={`d-flex query-manager-border-color hr-text-left py-2 form-label font-weight-500`}>
Datasource
</div>
);
};
const renderChangeDataSource = () => {
return (
<div
className={cx(`mt-2 pb-4`, {
'disabled ': isVersionReleased,
})}
>
<div
className={`border-top query-manager-border-color px-4 hr-text-left py-2 ${
darkMode ? 'color-white' : 'color-light-slate-12'
}`}
>
Change Datasource
</div>
<div className="d-flex flex-grow-1">
<ChangeDataSource
dataSources={[...globalDataSources, ...dataSources]}
dataSources={selectableDataSources}
value={selectedDataSource}
selectedQuery={selectedQuery}
onChange={(newDataSource) => {
changeDataQuery(newDataSource);
}}
/>
</div>
);
};
if (selectedQueryId !== selectedQuery?.id) return;
return (
<div
className={`row row-deck px-2 mt-0 query-details ${
selectedDataSource?.kind === 'tooljetdb' ? 'tooljetdb-query-details' : ''
}`}
>
{selectedDataSource === null ? renderDataSourcesList() : renderQueryElement()}
{selectedDataSource !== null ? renderQueryOptions() : null}
{selectedQuery?.data_source_id && mode === 'edit' && selectedDataSource !== null
? renderChangeDataSource()
: null}
</div>
);
}
);
};
if (selectedQueryId !== selectedQuery?.id) return;
return (
<div
className={`row row-deck px-2 mt-0 query-details ${
selectedDataSource?.kind === 'tooljetdb' ? 'tooljetdb-query-details' : ''
}`}
>
{selectedQuery?.data_source_id && selectedDataSource !== null ? renderChangeDataSource() : null}
{selectedDataSource === null || !selectedQuery ? renderDataSourcesList() : renderQueryElement()}
{selectedDataSource !== null ? renderQueryOptions() : null}
</div>
);
};
const CustomToggleFlag = ({ dataCy, action, translatedLabel, label, value, toggleOption, darkMode, index }) => {
const [flag, setFlag] = useState(false);
const { t } = useTranslation();
useEffect(() => {
setFlag(value);
}, [value]);
return (
<div className={cx({ 'pb-3 pt-3': index === 1 })}>
<CustomToggleSwitch
dataCy={dataCy}
isChecked={flag}
toggleSwitchFunction={(flag) => {
setFlag((state) => !state);
toggleOption(flag);
}}
action={action}
darkMode={darkMode}
label={t(translatedLabel, label)}
/>
</div>
);
};

View file

@ -1,244 +1,245 @@
import React, { useState, forwardRef } from 'react';
import RunIcon from '../Icons/RunIcon';
import BreadcrumbsIcon from '../Icons/BreadcrumbsIcon';
import React, { useState, forwardRef, useRef, useEffect } from 'react';
import RenameIcon from '../Icons/RenameIcon';
import PreviewIcon from '../Icons/PreviewIcon';
import CreateIcon from '../Icons/CreateIcon';
import FloppyDisk from '@/_ui/Icon/solidIcons/FloppyDisk';
import Eye1 from '@/_ui/Icon/solidIcons/Eye1';
import Play from '@/_ui/Icon/solidIcons/Play';
import cx from 'classnames';
import { toast } from 'react-hot-toast';
import { Tooltip } from 'react-tooltip';
import { useTranslation } from 'react-i18next';
import { previewQuery, checkExistingQueryName, runQuery } from '@/_helpers/appUtils';
import { useDataQueriesActions, useQueryCreationLoading, useQueryUpdationLoading } from '@/_stores/dataQueriesStore';
import { useSelectedQuery, useSelectedDataSource, useUnsavedChanges } from '@/_stores/queryPanelStore';
import ToggleQueryEditorIcon from '../Icons/ToggleQueryEditorIcon';
import {
useSelectedQuery,
useSelectedDataSource,
usePreviewLoading,
useShowCreateQuery,
useNameInputFocussed,
} from '@/_stores/queryPanelStore';
import { useCurrentState } from '@/_stores/currentStateStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import { Tooltip } from 'react-tooltip';
import { Button } from 'react-bootstrap';
export const QueryManagerHeader = forwardRef(
(
{
darkMode,
mode,
addNewQueryAndDeselectSelectedQuery,
updateDraftQueryName,
toggleQueryEditor,
previewLoading = false,
options,
appId,
editorRef,
},
ref
) => {
const { renameQuery, updateDataQuery, createDataQuery } = useDataQueriesActions();
const selectedQuery = useSelectedQuery();
const isCreationInProcess = useQueryCreationLoading();
const isUpdationInProcess = useQueryUpdationLoading();
const isUnsavedQueriesAvailable = useUnsavedChanges();
const selectedDataSource = useSelectedDataSource();
const { t } = useTranslation();
const queryName = selectedQuery?.name ?? '';
const [renamingQuery, setRenamingQuery] = useState(false);
const { queries } = useCurrentState((state) => ({ queries: state.queries }), shallow);
const { isVersionReleased, editingVersionId } = useAppVersionStore(
(state) => ({
isVersionReleased: state.isVersionReleased,
editingVersionId: state.editingVersion?.id,
}),
shallow
export const QueryManagerHeader = forwardRef(({ darkMode, options, editorRef }, ref) => {
const { renameQuery } = useDataQueriesActions();
const selectedQuery = useSelectedQuery();
const selectedDataSource = useSelectedDataSource();
const [showCreateQuery, setShowCreateQuery] = useShowCreateQuery();
const queryName = selectedQuery?.name ?? '';
const { queries } = useCurrentState((state) => ({ queries: state.queries }), shallow);
const { isVersionReleased } = useAppVersionStore(
(state) => ({
isVersionReleased: state.isVersionReleased,
editingVersionId: state.editingVersion?.id,
}),
shallow
);
useEffect(() => {
if (selectedQuery?.name) {
setShowCreateQuery(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedQuery?.name]);
const isInDraft = selectedQuery?.status === 'draft';
const executeQueryNameUpdation = (newName) => {
const { name } = selectedQuery;
if (name === newName || !newName) {
return false;
}
const isNewQueryNameAlreadyExists = checkExistingQueryName(newName);
if (isNewQueryNameAlreadyExists) {
toast.error('Query name already exists');
return false;
}
if (newName) {
renameQuery(selectedQuery?.id, newName, editorRef);
return true;
}
};
const buttonLoadingState = (loading, disabled = false) => {
return cx(
`${loading ? (darkMode ? 'btn-loading' : 'button-loading') : ''}`,
{ 'theme-dark ': darkMode },
{ disabled: disabled || !selectedDataSource }
);
};
const buttonText = mode === 'edit' ? 'Save' : 'Create';
const buttonDisabled = isUpdationInProcess || isCreationInProcess;
const executeQueryNameUpdation = (newName) => {
const { id, name } = selectedQuery;
if (name === newName) {
return setRenamingQuery(false);
}
const isNewQueryNameAlreadyExists = checkExistingQueryName(newName);
if (newName && !isNewQueryNameAlreadyExists) {
if (id === 'draftQuery') {
toast.success('Query Name Updated');
updateDraftQueryName(newName);
} else {
renameQuery(selectedQuery?.id, newName, editorRef);
}
setRenamingQuery(false);
} else {
if (isNewQueryNameAlreadyExists) {
toast.error('Query name already exists');
}
setRenamingQuery(false);
}
const previewButtonOnClick = () => {
const _options = { ...options };
const query = {
data_source_id: selectedDataSource.id === 'null' ? null : selectedDataSource.id,
pluginId: selectedDataSource.pluginId,
options: _options,
kind: selectedDataSource.kind,
name: queryName,
};
const hasParamSupport = selectedQuery?.options?.hasParamSupport;
previewQuery(editorRef, query, false, undefined, hasParamSupport)
.then(() => {
ref.current.scrollIntoView();
})
.catch(({ error, data }) => {
console.log(error, data);
});
};
const createOrUpdateDataQuery = (shouldRunQuery = false) => {
if (selectedQuery?.id === 'draftQuery') return createDataQuery(appId, editingVersionId, options, shouldRunQuery);
if (isUnsavedQueriesAvailable) return updateDataQuery(options, shouldRunQuery);
shouldRunQuery && runQuery(editorRef, selectedQuery?.id, selectedQuery?.name);
};
const renderRenameInput = () => (
<input
data-cy={`query-rename-input`}
type="text"
className={cx('border-indigo-09 bg-transparent', { 'text-white': darkMode })}
autoFocus
defaultValue={queryName}
onKeyUp={(event) => {
event.persist();
if (event.keyCode === 13) {
executeQueryNameUpdation(event.target.value);
}
}}
onBlur={({ target }) => executeQueryNameUpdation(target.value)}
/>
);
const renderBreadcrumb = () => {
if (selectedQuery === null) return;
return (
<>
<span
className={`${darkMode ? 'color-light-gray-c3c3c3' : 'color-light-slate-11'}
cursor-pointer font-weight-400`}
onClick={addNewQueryAndDeselectSelectedQuery}
data-cy={`query-type-header`}
>
{mode === 'create' ? 'New Query' : 'Queries'}
</span>
<span className="breadcrum">
<BreadcrumbsIcon />
</span>
<div className="query-name-breadcrum d-flex align-items-center">
<span
className={cx('query-manager-header-query-name font-weight-400', { ellipsis: !renamingQuery })}
data-cy={`query-name-label`}
>
{renamingQuery ? renderRenameInput() : queryName}
</span>
{!isVersionReleased && (
<span
className={cx('breadcrum-rename-query-icon', { 'd-none': renamingQuery && isVersionReleased })}
onClick={() => setRenamingQuery(true)}
>
<RenameIcon />
</span>
)}
</div>
</>
);
};
const buttonLoadingState = (loading, disabled = false) => {
return cx(
`${loading ? (darkMode ? 'btn-loading' : 'button-loading') : ''}`,
{ 'theme-dark ': darkMode },
{ disabled: disabled || !selectedDataSource }
);
};
const previewButtonOnClick = () => {
const _options = { ...options };
const query = {
data_source_id: selectedDataSource.id === 'null' ? null : selectedDataSource.id,
pluginId: selectedDataSource.pluginId,
options: _options,
kind: selectedDataSource.kind,
};
const hasParamSupport = mode === 'create' || selectedQuery?.options?.hasParamSupport;
previewQuery(editorRef, query, false, undefined, hasParamSupport)
.then(() => {
ref.current.scrollIntoView();
})
.catch(({ error, data }) => {
console.log(error, data);
});
};
const renderPreviewButton = () => {
return (
const renderRunButton = () => {
const { isLoading } = queries[selectedQuery?.name] ?? false;
return (
<span
{...(isInDraft && {
'data-tooltip-id': 'query-header-btn-run',
'data-tooltip-content': 'Connect a data source to run',
})}
>
<button
onClick={previewButtonOnClick}
className={`default-tertiary-button float-right1 ${buttonLoadingState(previewLoading)}`}
data-cy={'query-preview-button'}
>
<span className="query-preview-svg d-flex align-items-center query-icon-wrapper">
<PreviewIcon />
</span>
<span>{t('editor.queryManager.preview', 'Preview')}</span>
</button>
);
};
const renderSaveButton = () => {
return (
<button
className={`default-tertiary-button ${buttonLoadingState(
isCreationInProcess || isUpdationInProcess,
onClick={() => runQuery(editorRef, selectedQuery?.id, selectedQuery?.name)}
className={`border-0 default-secondary-button float-right1 ${buttonLoadingState(
isLoading,
isVersionReleased
)}`}
onClick={() => createOrUpdateDataQuery(false)}
disabled={buttonDisabled}
data-cy={`query-${buttonText.toLowerCase()}-button`}
>
<span className="d-flex query-create-run-svg query-icon-wrapper">
<CreateIcon />
</span>
<span>{buttonText}</span>
</button>
);
};
const renderRunButton = () => {
const { isLoading } = queries[selectedQuery?.name] ?? false;
return (
<button
onClick={() => createOrUpdateDataQuery(true)}
className={`border-0 default-secondary-button float-right1 ${buttonLoadingState(isLoading)}`}
data-cy="query-run-button"
disabled={isInDraft}
{...(isInDraft && {
'data-tooltip-id': 'query-header-btn-run',
'data-tooltip-content': 'Publish the query to run',
})}
>
<span
className={cx('query-manager-btn-svg-wrapper d-flex align-item-center query-icon-wrapper query-run-svg', {
className={cx({
invisible: isLoading,
})}
>
<RunIcon />
<Play width={14} fill="var(--indigo9)" viewBox="0 0 14 14" />
</span>
<span className="query-manager-btn-name">{isLoading ? ' ' : 'Run'}</span>
</button>
);
};
const renderButtons = () => {
if (selectedQuery === null) return;
return (
<>
{renderPreviewButton()}
{renderSaveButton()}
{renderRunButton()}
</>
);
};
return (
<div className="row header">
<div className="col font-weight-500">{renderBreadcrumb()}</div>
<div className="query-header-buttons">
{renderButtons()}
<span
onClick={toggleQueryEditor}
className={`toggle-query-editor-svg m-3`}
data-tooltip-id="tooltip-for-hide-query-editor"
data-tooltip-content="Hide query editor"
>
<ToggleQueryEditorIcon />
</span>
<Tooltip id="tooltip-for-hide-query-editor" className="tooltip" />
</div>
</div>
{isInDraft && <Tooltip id="query-header-btn-run" className="tooltip" />}
</span>
);
}
);
};
const renderButtons = () => {
if (selectedQuery === null || showCreateQuery) return;
return (
<>
<PreviewButton onClick={previewButtonOnClick} buttonLoadingState={buttonLoadingState} />
{renderRunButton()}
</>
);
};
return (
<div className="row header">
<div className="col font-weight-500">
{selectedQuery && <NameInput onInput={executeQueryNameUpdation} value={queryName} darkMode={darkMode} />}
</div>
<div className="query-header-buttons me-3">{renderButtons()}</div>
</div>
);
});
const PreviewButton = ({ buttonLoadingState, onClick }) => {
const previewLoading = usePreviewLoading();
const { t } = useTranslation();
return (
<button
onClick={onClick}
className={`default-tertiary-button float-right1 ${buttonLoadingState(previewLoading)}`}
data-cy={'query-preview-button'}
>
<span className="query-preview-svg d-flex align-items-center query-icon-wrapper">
<Eye1 width={14} fill="var(--slate9)" />
</span>
<span>{t('editor.queryManager.preview', 'Preview')}</span>
</button>
);
};
const NameInput = ({ onInput, value, darkMode }) => {
const [isFocussed, setIsFocussed] = useNameInputFocussed(false);
const [name, setName] = useState(value);
const isVersionReleased = useAppVersionStore((state) => state.isVersionReleased);
const inputRef = useRef();
useEffect(() => {
setName(value);
}, [value]);
useEffect(() => {
if (isFocussed) {
inputRef.current?.focus();
inputRef.current?.select();
}
}, [isFocussed]);
const handleChange = (event) => {
const sanitizedValue = event.target.value.replace(/[ \t&]/g, '');
setName(sanitizedValue);
};
const handleInput = (newName) => {
const result = onInput(newName);
if (!result) {
setName(value);
}
};
return (
<div className="query-name-breadcrum d-flex align-items-center ms-1">
<span
className="query-manager-header-query-name font-weight-400"
data-cy={`query-name-label`}
style={{ width: '150px' }}
>
{isFocussed ? (
<input
data-cy={`query-rename-input`}
type="text"
className={cx('border-indigo-09 bg-transparent query-rename-input py-1 px-2 rounded', {
'text-white': darkMode,
})}
autoFocus
ref={inputRef}
onChange={handleChange}
value={name}
onKeyDown={(event) => {
event.persist();
if (event.keyCode === 13) {
setIsFocussed(false);
handleInput(event.target.value);
}
}}
onBlur={({ target }) => {
setIsFocussed(false);
handleInput(target.value);
}}
/>
) : (
<Button
size="sm"
onClick={() => setIsFocussed(true)}
className={'bg-transparent justify-content-between color-slate12 w-100 px-2 py-1 rounded font-weight-500'}
>
<span className="text-truncate">{name} </span>
<span
className={cx('breadcrum-rename-query-icon', { 'd-none': isFocussed && isVersionReleased })}
style={{ minWidth: 14 }}
>
<RenameIcon />
</span>
</Button>
)}
</span>
</div>
);
};

View file

@ -0,0 +1,47 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CodeHinter } from '../../CodeBuilder/CodeHinter';
export default function SuccessNotificationInputs({ currentState, options, darkMode, optionchanged }) {
const { t } = useTranslation();
if (!options?.showSuccessNotification) {
return <div className="mb-3"></div>;
}
return (
<div className="me-4 mb-3 mt-2 pt-1" style={{ paddingLeft: '112px' }}>
<div className="d-flex">
<label className="form-label" data-cy={'label-success-message-input'} style={{ width: 150 }}>
{t('editor.queryManager.successMessage', 'Message')}
</label>
<div className="flex-grow-1">
<CodeHinter
currentState={currentState}
initialValue={options.successMessage}
height="36px"
theme={darkMode ? 'monokai' : 'default'}
onChange={(value) => optionchanged('successMessage', value)}
placeholder={t('editor.queryManager.queryRanSuccessfully', 'Query ran successfully')}
cyLabel={'success-message'}
/>
</div>
</div>
<div className="d-flex">
<label className="form-label" data-cy={'label-notification-duration-input'} style={{ width: 150 }}>
{t('editor.queryManager.notificationDuration', 'duration (s)')}
</label>
{/* </div> */}
<div className="flex-grow-1 query-manager-input-elem">
<input
type="number"
disabled={!options.showSuccessNotification}
onChange={(e) => optionchanged('notificationDuration', e.target.value)}
placeholder={5}
className="form-control"
value={options.notificationDuration}
data-cy={'notification-duration-input-field'}
/>
</div>
</div>
</div>
);
}

View file

@ -16,6 +16,7 @@ import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles';
import { Button } from '@/_ui/LeftSidebar';
import { Tooltip as ReactTooltip } from 'react-tooltip';
import { authenticationService } from '@/_services';
import Information from '@/_ui/Icon/solidIcons/Information';
import { useCurrentState } from '@/_stores/currentStateStore';
export const Transformation = ({ changeOption, options, darkMode, queryId }) => {
@ -76,10 +77,6 @@ return [row for row in data if row['amount'] > 1000]
useEffect(() => {
const selectedQueryId = localStorage.getItem('selectedQuery') ?? null;
if (queryId === 'draftQuery') {
setState(defaultValue);
return;
}
if (selectedQueryId !== queryId) {
const nonLangdefaultCode = getNonActiveTransformations(options?.transformationLanguage ?? 'javascript');
const finalState = _.merge(
@ -138,9 +135,12 @@ return [row for row in data if row['amount'] > 1000]
};
};
const popover = (
<Popover id="transformation-popover-container">
<p className="transformation-popover" data-cy={`transformation-popover`}>
const labelPopoverContent = (
<Popover
id="transformation-popover-container"
className={`${darkMode && 'popover-dark-themed theme-dark dark-theme tj-dark-mode'} p-0`}
>
<p className={`transformation-popover`} data-cy={`transformation-popover`}>
{t(
'editor.queryManager.transformation.transformationToolTip',
'Transformations can be enabled on queries to transform the query results. ToolJet allows you to transform the query results using two programming languages: JavaScript and Python'
@ -154,9 +154,136 @@ return [row for row in data if row['amount'] > 1000]
</Popover>
);
const popoverForRecommendation = (
<Popover id="transformation-popover-container">
<div className="transformation-popover card text-center">
return (
<div className="field transformation-editor">
<div className="align-items-center gap-2" style={{ display: 'flex', position: 'relative', height: '20px' }}>
<div className="d-flex flex-fill">
<OverlayTrigger
trigger="click"
placement="top"
rootClose
overlay={labelPopoverContent}
container={document.getElementsByClassName('query-details')[0]}
>
<span
className="color-slate9 font-weight-500 form-label"
data-cy={'label-query-transformation'}
style={{ textDecoration: 'underline 2px dashed', textDecorationColor: 'var(--slate8)' }}
>
{t('editor.queryManager.transformation.transformations', 'Transformations')}
</span>
</OverlayTrigger>
<div className="flex-grow-l">
<div className=" d-flex">
<div className="mb-0">
<span className="d-flex">
<CustomToggleSwitch
isChecked={enableTransformation}
toggleSwitchFunction={toggleEnableTransformation}
action="enableTransformation"
darkMode={darkMode}
dataCy={'transformation'}
/>
<span className="ps-1">Enable</span>
</span>
</div>
<EducativeLabel darkMode={darkMode} />
</div>
<div></div>
</div>
</div>
</div>
<br></br>
<div className="d-flex">
<div className="form-label"></div>
<div className="col flex-grow-1">
{enableTransformation && (
<div
className="rounded-3"
style={{ marginBottom: '20px', background: `${darkMode ? '#272822' : '#F8F9FA'}` }}
>
<div className="py-3 px-3 d-flex justify-content-between">
<div className="d-flex">
<div className="d-flex align-items-center border transformation-language-select-wrapper">
<span className="px-2">Language</span>
</div>
<Select
options={[
{ name: 'JavaScript', value: 'javascript' },
{ name: 'Python', value: 'python' },
]}
value={lang}
search={true}
onChange={(value) => {
setLang(value);
changeOption('transformationLanguage', value);
changeOption('transformation', state[value]);
}}
placeholder={t('globals.select', 'Select') + '...'}
styles={computeSelectStyles(darkMode, 140)}
useCustomStyles={true}
/>
</div>
<div
data-tooltip-id="tooltip-for-active-copilot"
data-tooltip-content="Activate Copilot in the workspace settings"
>
<Button
onClick={handleCallToGPT}
darkMode={darkMode}
size="sm"
classNames={`${fetchingRecommendation ? (darkMode ? 'btn-loading' : 'button-loading') : ''}`}
styles={{
width: '100%',
fontSize: '12px',
fontWeight: 500,
borderColor: darkMode && 'transparent',
}}
disabled={!isCopilotEnabled}
>
<Button.Content title={'Generate code'} />
</Button>
</div>
{!isCopilotEnabled && (
<ReactTooltip
id="tooltip-for-active-copilot"
className="tooltip"
style={{ backgroundColor: '#e6eefe', color: '#222' }}
/>
)}
</div>
<div className="border-top mx-3"></div>
<CodeHinter
initialValue={state[lang]}
mode={lang}
theme={darkMode ? 'monokai' : 'base16-light'}
lineNumbers={true}
height={'300px'}
className="query-hinter"
ignoreBraces={true}
onChange={(value) => changeOption('transformation', value)}
componentName={`transformation`}
cyLabel={'transformation-input'}
callgpt={handleCallToGPT}
isCopilotEnabled={isCopilotEnabled}
/>
</div>
)}
</div>
</div>
</div>
);
};
const EducativeLabel = ({ darkMode }) => {
const popoverContent = (
<Popover
id={`transformation-popover-container`}
className={`${darkMode && 'popover-dark-themed theme-dark dark-theme'} p-0`}
>
<div className={`transformation-popover card text-center ${darkMode && 'tj-dark-mode'}`}>
<img src="/assets/images/icons/copilot.svg" alt="AI copilot" height={64} width={64} />
<div className="d-flex flex-column card-body">
<h4 className="mb-2">ToolJet x OpenAI</h4>
@ -179,149 +306,30 @@ return [row for row in data if row['amount'] > 1000]
</Popover>
);
const EducativeLebel = () => {
const title = () => {
return (
<>
Powered by <strong style={{ fontWeight: 700, color: '#3E63DD' }}>AI copilot</strong>
</>
);
};
const title = () => {
return (
<div className="d-flex">
<Button.UnstyledButton styles={{ height: '28px' }} darkMode={darkMode} classNames="mx-1">
<Button.Content title={title} iconSrc={'assets/images/icons/flash.svg'} direction="left" />
</Button.UnstyledButton>
<OverlayTrigger trigger="click" placement="left" overlay={popoverForRecommendation} rootClose>
<svg
width="16.7"
height="16.7"
viewBox="0 0 20 21"
fill="#3E63DD"
xmlns="http://www.w3.org/2000/svg"
style={{ cursor: 'pointer' }}
data-cy={`transformation-info-icon`}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 2.5C5.58172 2.5 2 6.08172 2 10.5C2 14.9183 5.58172 18.5 10 18.5C14.4183 18.5 18 14.9183 18 10.5C18 6.08172 14.4183 2.5 10 2.5ZM0 10.5C0 4.97715 4.47715 0.5 10 0.5C15.5228 0.5 20 4.97715 20 10.5C20 16.0228 15.5228 20.5 10 20.5C4.47715 20.5 0 16.0228 0 10.5ZM9 6.5C9 5.94772 9.44771 5.5 10 5.5H10.01C10.5623 5.5 11.01 5.94772 11.01 6.5C11.01 7.05228 10.5623 7.5 10.01 7.5H10C9.44771 7.5 9 7.05228 9 6.5ZM8 10.5C8 9.94771 8.44772 9.5 9 9.5H10C10.5523 9.5 11 9.94771 11 10.5V13.5C11.5523 13.5 12 13.9477 12 14.5C12 15.0523 11.5523 15.5 11 15.5H10C9.44771 15.5 9 15.0523 9 14.5V11.5C8.44772 11.5 8 11.0523 8 10.5Z"
fill="#3E63DD"
/>
</svg>
</OverlayTrigger>
</div>
<>
Powered by <strong style={{ fontWeight: 700, color: '#3E63DD' }}>AI copilot</strong>
</>
);
};
return (
<div className="field transformation-editor">
<div className="align-items-center gap-2" style={{ display: 'flex', position: 'relative', height: '20px' }}>
<div className="d-flex flex-fill">
<div className="mb-0">
<CustomToggleSwitch
isChecked={enableTransformation}
toggleSwitchFunction={toggleEnableTransformation}
action="enableTransformation"
darkMode={darkMode}
dataCy={'transformation'}
/>
</div>
<span className="mx-1 font-weight-400 tranformation-label" data-cy={'label-query-transformation'}>
{t('editor.queryManager.transformation.transformations', 'Transformations')}
</span>
<OverlayTrigger trigger="click" placement="top" overlay={popover} rootClose>
<svg
width="16.7"
height="16.7"
viewBox="0 0 20 21"
fill="#3E63DD"
xmlns="http://www.w3.org/2000/svg"
style={{ cursor: 'pointer' }}
data-cy={`transformation-info-icon`}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 2.5C5.58172 2.5 2 6.08172 2 10.5C2 14.9183 5.58172 18.5 10 18.5C14.4183 18.5 18 14.9183 18 10.5C18 6.08172 14.4183 2.5 10 2.5ZM0 10.5C0 4.97715 4.47715 0.5 10 0.5C15.5228 0.5 20 4.97715 20 10.5C20 16.0228 15.5228 20.5 10 20.5C4.47715 20.5 0 16.0228 0 10.5ZM9 6.5C9 5.94772 9.44771 5.5 10 5.5H10.01C10.5623 5.5 11.01 5.94772 11.01 6.5C11.01 7.05228 10.5623 7.5 10.01 7.5H10C9.44771 7.5 9 7.05228 9 6.5ZM8 10.5C8 9.94771 8.44772 9.5 9 9.5H10C10.5523 9.5 11 9.94771 11 10.5V13.5C11.5523 13.5 12 13.9477 12 14.5C12 15.0523 11.5523 15.5 11 15.5H10C9.44771 15.5 9 15.0523 9 14.5V11.5C8.44772 11.5 8 11.0523 8 10.5Z"
fill="#3E63DD"
/>
</svg>
</OverlayTrigger>
</div>
<EducativeLebel />
</div>
<br></br>
{enableTransformation && (
<div
className="rounded-3"
style={{ marginLeft: '3rem', marginBottom: '20px', background: `${darkMode ? '#272822' : '#F8F9FA'}` }}
>
<div className="py-3 px-3 d-flex justify-content-between">
<div className="d-flex">
<div className="d-flex align-items-center border transformation-language-select-wrapper">
<span className="px-2">Language</span>
</div>
<Select
options={[
{ name: 'JavaScript', value: 'javascript' },
{ name: 'Python', value: 'python' },
]}
value={lang}
search={true}
onChange={(value) => {
setLang(value);
changeOption('transformationLanguage', value);
changeOption('transformation', state[value]);
}}
placeholder={t('globals.select', 'Select') + '...'}
styles={computeSelectStyles(darkMode, 140)}
useCustomStyles={true}
/>
</div>
<div
data-tooltip-id="tooltip-for-active-copilot"
data-tooltip-content="Activate Copilot in the workspace settings"
>
<Button
onClick={handleCallToGPT}
darkMode={darkMode}
size="sm"
classNames={`${fetchingRecommendation ? (darkMode ? 'btn-loading' : 'button-loading') : ''}`}
styles={{ width: '100%', fontSize: '12px', fontWeight: 500, borderColor: darkMode && 'transparent' }}
disabled={!isCopilotEnabled}
>
<Button.Content title={'Generate code'} />
</Button>
</div>
{!isCopilotEnabled && (
<ReactTooltip
id="tooltip-for-active-copilot"
className="tooltip"
style={{ backgroundColor: '#e6eefe', color: '#222' }}
/>
)}
</div>
<div className="border-top mx-3"></div>
<CodeHinter
initialValue={state[lang]}
mode={lang}
theme={darkMode ? 'monokai' : 'base16-light'}
lineNumbers={true}
height={'300px'}
className="query-hinter"
ignoreBraces={true}
onChange={(value) => changeOption('transformation', value)}
componentName={`transformation`}
cyLabel={'transformation-input'}
callgpt={handleCallToGPT}
isCopilotEnabled={isCopilotEnabled}
/>
</div>
)}
<div className="d-flex">
<Button.UnstyledButton styles={{ height: '28px' }} darkMode={darkMode} classNames="mx-1">
<Button.Content title={title} iconSrc={'assets/images/icons/flash.svg'} direction="left" />
</Button.UnstyledButton>
<OverlayTrigger
overlay={popoverContent}
rootClose
trigger="click"
placement="right"
container={document.getElementsByClassName('query-details')[0]}
>
<span style={{ cursor: 'pointer' }} data-cy={`transformation-info-icon`} className="lh-1">
<Information width={18} fill={'var(--indigo9)'} />
</span>
</OverlayTrigger>
</div>
);
};

View file

@ -8,9 +8,11 @@ export const BaseUrl = ({ dataSourceURL, theme }) => {
style={{
padding: '5px',
border: theme === 'default' ? '1px solid rgb(217 220 222)' : '1px solid white',
borderRightWidth: 0,
background: theme === 'default' ? 'rgb(246 247 251)' : '#20211e',
color: theme === 'default' ? '#9ca1a6' : '#9e9e9e',
height: '32px',
borderRadius: '6px 0 0 6px',
}}
>
{dataSourceURL}

View file

@ -1,5 +1,4 @@
import React from 'react';
import GroupHeader from './GroupHeader';
import TabContent from './TabContent';
export default ({
@ -12,11 +11,9 @@ export default ({
onJsonBodyChange,
componentName,
bodyToggle,
setBodyToggle,
}) => {
return (
<>
<GroupHeader paramType={'body'} descText={'Raw JSON'} bodyToggle={bodyToggle} setBodyToggle={setBodyToggle} />
<TabContent
options={options}
theme={theme}

View file

@ -1,5 +1,9 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CodeHinter } from '../../../CodeBuilder/CodeHinter';
import AddRectangle from '@/_ui/Icon/bulkIcons/AddRectangle';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import Trash from '@/_ui/Icon/solidIcons/Trash';
export default ({
options = [],
@ -14,6 +18,7 @@ export default ({
bodyToggle,
addNewKeyValuePair,
}) => {
const { t } = useTranslation();
const darkMode = localStorage.getItem('darkMode') === 'true';
return (
@ -22,10 +27,9 @@ export default ({
options.map((option, index) => {
return (
<>
<div className="row-container border-bottom query-manager-border-color" key={index}>
<div className="fields-container ">
<div className="d-flex justify-content-center align-items-center query-number">{index + 1}</div>
<div className="field col-4 overflow-hidden">
<div className="row-container query-manager-border-color" key={index}>
<div className="fields-container mb-2">
<div className="field col-4 overflow-hidden border-top border-bottom border-start rounded-start">
<CodeHinter
initialValue={option[0]}
theme={theme}
@ -35,7 +39,7 @@ export default ({
componentName={`${componentName}/${tabType}::key::${index}`}
/>
</div>
<div className="field col overflow-hidden">
<div className="field col overflow-hidden border ">
<CodeHinter
initialValue={option[1]}
theme={theme}
@ -45,30 +49,17 @@ export default ({
componentName={`${componentName}/${tabType}::value::${index}`}
/>
</div>
<div
className="d-flex justify-content-center align-items-center delete-field-option"
<button
className={`d-flex justify-content-center align-items-center delete-field-option bg-transparent border-0 rounded-0 border-top border-bottom border-end rounded-end ${
darkMode ? 'delete-field-option-dark' : ''
}`}
role="button"
onClick={() => {
removeKeyValuePair(paramType, index);
}}
>
<span className="rest-api-delete-field-option query-icon-wrapper d-flex">
<svg
width="auto"
height="auto"
viewBox="0 0 18 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.58579 0.585786C5.96086 0.210714 6.46957 0 7 0H11C11.5304 0 12.0391 0.210714 12.4142 0.585786C12.7893 0.960859 13 1.46957 13 2V4H15.9883C15.9953 3.99993 16.0024 3.99993 16.0095 4H17C17.5523 4 18 4.44772 18 5C18 5.55228 17.5523 6 17 6H16.9201L15.9997 17.0458C15.9878 17.8249 15.6731 18.5695 15.1213 19.1213C14.5587 19.6839 13.7957 20 13 20H5C4.20435 20 3.44129 19.6839 2.87868 19.1213C2.32687 18.5695 2.01223 17.8249 2.00035 17.0458L1.07987 6H1C0.447715 6 0 5.55228 0 5C0 4.44772 0.447715 4 1 4H1.99054C1.9976 3.99993 2.00466 3.99993 2.0117 4H5V2C5 1.46957 5.21071 0.960859 5.58579 0.585786ZM3.0868 6L3.99655 16.917C3.99885 16.9446 4 16.9723 4 17C4 17.2652 4.10536 17.5196 4.29289 17.7071C4.48043 17.8946 4.73478 18 5 18H13C13.2652 18 13.5196 17.8946 13.7071 17.7071C13.8946 17.5196 14 17.2652 14 17C14 16.9723 14.0012 16.9446 14.0035 16.917L14.9132 6H3.0868ZM11 4H7V2H11V4ZM6.29289 10.7071C5.90237 10.3166 5.90237 9.68342 6.29289 9.29289C6.68342 8.90237 7.31658 8.90237 7.70711 9.29289L9 10.5858L10.2929 9.29289C10.6834 8.90237 11.3166 8.90237 11.7071 9.29289C12.0976 9.68342 12.0976 10.3166 11.7071 10.7071L10.4142 12L11.7071 13.2929C12.0976 13.6834 12.0976 14.3166 11.7071 14.7071C11.3166 15.0976 10.6834 15.0976 10.2929 14.7071L9 13.4142L7.70711 14.7071C7.31658 15.0976 6.68342 15.0976 6.29289 14.7071C5.90237 14.3166 5.90237 13.6834 6.29289 13.2929L7.58579 12L6.29289 10.7071Z"
fill="#DB4324"
/>
</svg>
</span>
</div>
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
</button>
</div>
</div>
</>
@ -88,25 +79,11 @@ export default ({
/>
</div>
) : (
<div className="d-flex" style={{ maxHeight: '32px' }}>
<div
className="d-flex align-items-center justify-content-center add-tabs "
style={{ flex: '0 0 32px', background: darkMode ? 'inherit' : '#F8F9FA', height: '32px' }}
onClick={() => addNewKeyValuePair(paramType)}
role="button"
>
<span className="rest-api-add-field-svg">
<svg width="auto" height="auto" viewBox="0 0 24 25" fill="#5677E1" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 4.5C12.5523 4.5 13 4.94772 13 5.5V11.5H19C19.5523 11.5 20 11.9477 20 12.5C20 13.0523 19.5523 13.5 19 13.5H13V19.5C13 20.0523 12.5523 20.5 12 20.5C11.4477 20.5 11 20.0523 11 19.5V13.5H5C4.44772 13.5 4 13.0523 4 12.5C4 11.9477 4.44772 11.5 5 11.5H11V5.5C11 4.94772 11.4477 4.5 12 4.5Z"
fill="#3E63DD"
/>
</svg>
</span>
</div>
<div className="col" style={{ flex: '1', background: darkMode ? '' : '#ffffff' }}></div>
<div className="d-flex mb-2" style={{ maxHeight: '32px' }}>
<ButtonSolid variant="ghostBlue" size="sm" onClick={() => addNewKeyValuePair(paramType)}>
<AddRectangle width="15" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />
&nbsp;&nbsp;{t('editor.inspector.eventManager.addKeyValueParam', 'Add more')}
</ButtonSolid>
</div>
)}
</div>

View file

@ -1,11 +1,9 @@
import React from 'react';
import TabContent from './TabContent';
import GroupHeader from './GroupHeader';
export default ({ options = [], theme, removeKeyValuePair, addNewKeyValuePair, onChange, componentName }) => {
return (
<>
<GroupHeader paramType={'headers'} descText="Query Headers" />
<TabContent
options={options}
theme={theme}

View file

@ -1,11 +1,9 @@
import React from 'react';
import GroupHeader from './GroupHeader';
import TabContent from './TabContent';
export default ({ options = [], theme, removeKeyValuePair, addNewKeyValuePair, onChange, componentName }) => {
return (
<>
<GroupHeader paramType={'url_params'} descText={'Query Parameters'} />
<TabContent
options={options}
theme={theme}

View file

@ -3,6 +3,7 @@ import Headers from './TabHeaders';
import Params from './TabParams';
import Body from './TabBody';
import { Tab, ListGroup, Row } from 'react-bootstrap';
import { CustomToggleSwitch } from '@/Editor/QueryManager/Components/CustomToggleSwitch';
function ControlledTabs({
options,
@ -22,21 +23,29 @@ function ControlledTabs({
return (
<Tab.Container activeKey={key} onSelect={(k) => setKey(k)} defaultActiveKey="headers">
<Row>
<div className="keys">
<ListGroup className="query-pane-rest-api-keys-list-group mx-1" variant="flush">
<div className="keys d-flex justify-content-between">
<ListGroup className="query-pane-rest-api-keys-list-group mx-1 mb-2" variant="flush">
{tabs.map((tab) => (
<ListGroup.Item key={tab} eventKey={tab.toLowerCase()}>
<span>{tab}</span>
</ListGroup.Item>
))}
</ListGroup>
{key === 'body' && (
<div className="text-nowrap d-flex align-items-center">
Raw JSON&nbsp;&nbsp;
<CustomToggleSwitch
toggleSwitchFunction={setBodyToggle}
action="bodyToggle"
darkMode={darkMode}
isChecked={bodyToggle}
/>
</div>
)}
</div>
<div className={`col ${darkMode && 'theme-dark'}`}>
<Tab.Content
bsPrefix="rest-api-tab-content"
className="border overflow-hidden query-manager-border-color rounded"
>
<Tab.Content bsPrefix="rest-api-tab-content" className="query-manager-border-color rounded">
<Tab.Pane eventKey="headers" t bsPrefix="rest-api-tabpanes" transition={false}>
<Headers
removeKeyValuePair={removeKeyValuePair}
@ -69,7 +78,6 @@ function ControlledTabs({
jsonBody={options['json_body']}
theme={theme}
bodyToggle={bodyToggle}
setBodyToggle={setBodyToggle}
darkMode={darkMode}
componentName={componentName}
/>

View file

@ -14,7 +14,7 @@ class Restapi extends React.Component {
super(props);
const options = defaults(
{ ...props.options },
{ headers: [], url_params: [], body: [], json_body: null, body_toggle: false }
{ headers: [['', '']], url_params: [], body: [], json_body: null, body_toggle: false }
);
this.state = {
options,
@ -26,6 +26,9 @@ class Restapi extends React.Component {
if (isEmpty(this.state.options['headers'])) {
this.addNewKeyValuePair('headers');
}
if (isEmpty(this.state.options['method'])) {
changeOption(this, 'method', 'get');
}
setTimeout(() => {
if (isEmpty(this.state.options['url_params'])) {
this.addNewKeyValuePair('url_params');
@ -54,7 +57,10 @@ class Restapi extends React.Component {
const newOptions = { ...options, [option]: [...options[option], ['', '']] };
this.setState({ options: newOptions }, () => {
this.props.optionsChanged(newOptions);
//these values are set to empty array so that user can type in directly without adding new entry, hence no need to pass to parent state
if (!['headers', 'url_params', 'body'].includes(option)) {
this.props.optionsChanged(newOptions);
}
});
};
@ -88,7 +94,9 @@ class Restapi extends React.Component {
handleChange = (key, keyIndex, idx) => (value) => {
const lastPair = this.state.options[key][idx];
if (this.state.options[key].length - 1 === idx && (lastPair[0] || lastPair[1])) this.addNewKeyValuePair(key);
if (this.state.options[key].length - 1 === idx && (lastPair[0] || lastPair[1])) {
this.addNewKeyValuePair(key);
}
this.keyValuePairValueChanged(value, keyIndex, key, idx);
};
@ -98,11 +106,11 @@ class Restapi extends React.Component {
control: (provided) => ({
...provided,
boxShadow: 'none',
backgroundColor: darkMode ? '#2b3547' : '#F1F3F5',
borderRadius: '6px 0 0 6px',
...(darkMode && { backgroundColor: '#2b3547' }),
borderRadius: '6px',
height: 32,
minHeight: 32,
borderColor: darkMode ? 'inherit' : ' #D7DBDF',
borderColor: 'var(--slate7)',
borderWidth: '1px',
'&:hover': {
backgroundColor: darkMode ? '' : '#F8F9FA',
@ -125,64 +133,72 @@ class Restapi extends React.Component {
const currentValue = { label: options.method?.toUpperCase(), value: options.method };
return (
<div>
<div className="rest-api-methods-select-element-container">
<div className={`${this.props.darkMode && 'dark'}`} style={{ width: '90px', height: '32px' }}>
<Select
options={[
{ label: 'GET', value: 'get' },
{ label: 'POST', value: 'post' },
{ label: 'PUT', value: 'put' },
{ label: 'PATCH', value: 'patch' },
{ label: 'DELETE', value: 'delete' },
]}
onChange={(value) => {
changeOption(this, 'method', value);
}}
value={currentValue}
defaultValue={{ label: 'GET', value: 'get' }}
placeholder="Method"
width={100}
height={32}
styles={this.customSelectStyles(this.props.darkMode, 91)}
useCustomStyles={true}
/>
</div>
<div className={`col field w-100 d-flex rest-methods-url ${this.props.darkMode && 'dark'}`}>
{dataSourceURL && (
<BaseUrl theme={this.props.darkMode ? 'monokai' : 'default'} dataSourceURL={dataSourceURL} />
)}
<div className="col">
<CodeHinter
initialValue={options.url}
theme={this.props.darkMode ? 'monokai' : 'default'}
<div className={`d-flex`}>
<div className="form-label">Request</div>
<div className="flex-grow-1">
<div className="rest-api-methods-select-element-container">
<div className={`me-2`} style={{ width: '90px', height: '32px' }}>
<label className="font-weight-bold color-slate12">Method</label>
<Select
options={[
{ label: 'GET', value: 'get' },
{ label: 'POST', value: 'post' },
{ label: 'PUT', value: 'put' },
{ label: 'PATCH', value: 'patch' },
{ label: 'DELETE', value: 'delete' },
]}
onChange={(value) => {
changeOption(this, 'url', value);
changeOption(this, 'method', value);
}}
placeholder="Enter request URL"
componentName={`${queryName}::url`}
mode="javascript"
lineNumbers={false}
height={'32px'}
value={currentValue}
defaultValue={{ label: 'GET', value: 'get' }}
placeholder="Method"
width={100}
height={32}
styles={this.customSelectStyles(this.props.darkMode, 91)}
useCustomStyles={true}
/>
</div>
</div>
</div>
<div className={`query-pane-restapi-tabs ${this.props.darkMode ? 'dark' : ''}`}>
<Tabs
theme={this.props.darkMode ? 'monokai' : 'default'}
options={this.state.options}
onChange={this.handleChange}
onJsonBodyChange={this.handleJsonBodyChanged}
removeKeyValuePair={this.removeKeyValuePair}
addNewKeyValuePair={this.addNewKeyValuePair}
darkMode={this.props.darkMode}
componentName={queryName}
bodyToggle={this.state.options.body_toggle}
setBodyToggle={this.onBodyToggleChanged}
/>
<div className={`field w-100 rest-methods-url`}>
<div className="font-weight-bold color-slate12">URL</div>
<div className="d-flex">
{dataSourceURL && (
<BaseUrl theme={this.props.darkMode ? 'monokai' : 'default'} dataSourceURL={dataSourceURL} />
)}
<div className={`flex-grow-1 ${dataSourceURL ? 'url-input-group' : ''}`}>
<CodeHinter
currentState={this.props.currentState}
initialValue={options.url}
theme={this.props.darkMode ? 'monokai' : 'default'}
onChange={(value) => {
changeOption(this, 'url', value);
}}
placeholder={dataSourceURL ? 'Enter request endpoint' : 'Enter request URL'}
componentName={`${queryName}::url`}
mode="javascript"
lineNumbers={false}
height={'32px'}
/>
</div>
</div>
</div>
</div>
<div className={`query-pane-restapi-tabs ${this.props.darkMode ? 'dark' : ''}`}>
<Tabs
theme={this.props.darkMode ? 'monokai' : 'default'}
options={this.state.options}
onChange={this.handleChange}
onJsonBodyChange={this.handleJsonBodyChanged}
removeKeyValuePair={this.removeKeyValuePair}
addNewKeyValuePair={this.addNewKeyValuePair}
darkMode={this.props.darkMode}
componentName={queryName}
bodyToggle={this.state.options.body_toggle}
setBodyToggle={this.onBodyToggleChanged}
/>
</div>
</div>
</div>
);

View file

@ -81,6 +81,7 @@ const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRe
onClick={() => setShowModal((show) => !show)}
className="ms-2"
id="runjs-param-add-btn"
data-cy={`runjs-add-param-button`}
>
<span className="m-0">
<PlusRectangle fill={'#3E63DD'} width={15} />
@ -93,11 +94,15 @@ const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRe
);
};
export const PillButton = ({ name, onClick, onRemove, marginBottom }) => (
<ButtonGroup aria-label="Parameter" className={cx('ms-2', { 'mb-2': marginBottom })}>
export const PillButton = ({ name, onClick, onRemove, marginBottom, className, size }) => (
<ButtonGroup
aria-label="Parameter"
className={cx('ms-2 bg-slate3', { 'mb-2': marginBottom, ...(className && { [className]: true }) })}
style={{ borderRadius: '15px' }}
>
<Button
size="sm"
className="bg-slate3 color-slate12 runjs-parameter-badge"
className={cx('bg-transparent color-slate12 runjs-parameter-badge', { 'py-0 px-2': size === 'sm' })}
onClick={onClick}
style={{
borderTopLeftRadius: '15px',
@ -108,13 +113,16 @@ export const PillButton = ({ name, onClick, onRemove, marginBottom }) => (
...(!onRemove && { borderRadius: '15px' }),
}}
>
<span className="text-truncate">{name}</span>
<span data-cy={`query-param-${String(name).toLowerCase()}`} className="text-truncate">
{name}
</span>
</Button>
{onRemove && (
<Button
data-cy={`query-param-${String(name).toLowerCase()}-remove-button`}
onClick={onRemove}
size="sm"
className="bg-slate3 color-slate12"
className={cx('bg-transparent color-slate12', { 'p-0 pe-1': size === 'sm' })}
style={{
borderTopRightRadius: '15px',
borderBottomRightRadius: '15px',

View file

@ -50,7 +50,7 @@ const Runjs = (props) => {
};
return (
<Card className="runjs-editor">
<Card className="runjs-editor mb-3">
{(options.hasParamSupport || props.mode === 'create') && (
<ParameterList
parameters={options.parameters}

View file

@ -16,7 +16,7 @@ export class Runpy extends React.Component {
render() {
return (
<div>
<div className="runps-editor">
<CodeHinter
initialValue={this.props.options.code}
mode="python"

View file

@ -186,11 +186,11 @@ class StripeComponent extends React.Component {
{options && !loadingSpec && (
<div>
<div className="row g-2">
<div className="col-12">
<div className="d-flex g-2">
<div className="col-12 form-label">
<label className="form-label">{this.props.t('globals.operation', 'Operation')}</label>
</div>
<div className="col stripe-operation-options" style={{ width: '90px', marginTop: 0 }}>
<div className="col stripe-operation-options flex-grow-1" style={{ width: '90px', marginTop: 0 }}>
<Select
options={this.computeOperationSelectionOptions(specJson)}
value={currentValue}
@ -325,13 +325,13 @@ class StripeComponent extends React.Component {
{requestBody.schema.properties && (
<div
className={`request-body-fields ${
className={`request-body-fields d-flex ${
Object.keys(requestBody.schema.properties).length === 0 && 'd-none'
} `}
>
<h5 className="text-heading">{this.props.t('globals.requestBody', 'REQUEST BODY')}</h5>
<h5 className="text-heading form-label">{this.props.t('globals.requestBody', 'REQUEST BODY')}</h5>
<div
className={`${
className={`flex-grow-1 ${
Object.keys(requestBody.schema.properties).length >= 1 && 'input-group-parent-container'
}`}
>

View file

@ -2,8 +2,9 @@ import React, { useState, useEffect, useContext } from 'react';
import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import Select from '@/_ui/Select';
import { uniqueId } from 'lodash';
import { isEmpty, uniqueId } from 'lodash';
import { useMounted } from '@/_hooks/use-mount';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
export const CreateRow = React.memo(({ optionchanged, options, darkMode }) => {
const mounted = useMounted();
@ -42,105 +43,35 @@ export const CreateRow = React.memo(({ optionchanged, options, darkMode }) => {
handleColumnOptionChange({ ...existingColumnOption, ...{ [uniqueId()]: emptyColumnOption } });
}
const RenderColumnOptions = ({ column, value, id }) => {
const filteredColumns = columns.filter(({ isPrimaryKey }) => !isPrimaryKey);
const existingColumnOption = Object.values ? Object.values(columnOptions) : [];
let displayColumns = filteredColumns.map(({ accessor }) => ({
value: accessor,
label: accessor,
}));
if (existingColumnOption.length > 0) {
displayColumns = displayColumns.filter(
({ value }) => !existingColumnOption.map((item) => item.column !== column && item.column).includes(value)
);
}
const handleColumnChange = (selectedOption) => {
const updatedOption = {
...columnOptions[id],
column: selectedOption,
};
const newColumnOptions = { ...columnOptions, [id]: updatedOption };
handleColumnOptionChange(newColumnOptions);
};
const handleValueChange = (newValue) => {
const updatedOption = {
...columnOptions[id],
value: newValue,
};
const newColumnOptions = { ...columnOptions, [id]: updatedOption };
handleColumnOptionChange(newColumnOptions);
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col-4">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
customWrap={true}
/>
</div>
<div className="field col-4">
<CodeHinter
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
theme={darkMode ? 'monokai' : 'default'}
height={'32px'}
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
</div>
<div className="col cursor-pointer m-1 mx-3">
<svg
onClick={() => {
removeColumnOptionsPair(id);
}}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
);
};
return (
<div className="row tj-db-field-wrapper">
<div className="tab-content-wrapper mt-2">
<div className="tab-content-wrapper mt-2 d-flex">
<label className="form-label" data-cy="label-column-filter">
Columns
</label>
<div className="field-container">
{Object.entries(columnOptions).map(([key, value]) => {
return <RenderColumnOptions key={key} column={value.column} value={value.value} id={key} />;
})}
<div className="field-container flex-grow-1">
{Object.entries(columnOptions).map(([key, value]) => (
<RenderColumnOptions
key={key}
columnOptions={columnOptions}
column={value.column}
columns={columns}
value={value.value}
handleColumnOptionChange={handleColumnOptionChange}
darkMode={darkMode}
removeColumnOptionsPair={removeColumnOptionsPair}
id={key}
/>
))}
{Object.keys(columnOptions).length !== columns.length && (
<div className="cursor-pointer pb-3 fit-content" onClick={addNewColumnOptionsPair}>
<ButtonSolid
variant="ghostBlue"
size="sm"
onClick={addNewColumnOptionsPair}
className={isEmpty(columnOptions) ? '' : 'mt-2'}
>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.34554 10.0207C5.15665 10.0207 4.99832 9.95678 4.87054 9.829C4.74276 9.70123 4.67887 9.54289 4.67887 9.354V5.854H1.17887C0.989985 5.854 0.831651 5.79011 0.703874 5.66234C0.576096 5.53456 0.512207 5.37623 0.512207 5.18734C0.512207 4.99845 0.576096 4.84012 0.703874 4.71234C0.831651 4.58456 0.989985 4.52067 1.17887 4.52067H4.67887V1.02067C4.67887 0.831782 4.74276 0.673448 4.87054 0.54567C4.99832 0.417893 5.15665 0.354004 5.34554 0.354004C5.53443 0.354004 5.69276 0.417893 5.82054 0.54567C5.94832 0.673448 6.01221 0.831782 6.01221 1.02067V4.52067H9.51221C9.7011 4.52067 9.85943 4.58456 9.98721 4.71234C10.115 4.84012 10.1789 4.99845 10.1789 5.18734C10.1789 5.37623 10.115 5.53456 9.98721 5.66234C9.85943 5.79011 9.7011 5.854 9.51221 5.854H6.01221V9.354C6.01221 9.54289 5.94832 9.70123 5.82054 9.829C5.69276 9.95678 5.53443 10.0207 5.34554 10.0207Z"
@ -148,10 +79,104 @@ export const CreateRow = React.memo(({ optionchanged, options, darkMode }) => {
/>
</svg>
&nbsp; Add column
</div>
</ButtonSolid>
)}
</div>
</div>
</div>
);
});
const RenderColumnOptions = ({
column,
value,
id,
columns,
columnOptions,
handleColumnOptionChange,
darkMode,
removeColumnOptionsPair,
}) => {
const filteredColumns = columns.filter(({ isPrimaryKey }) => !isPrimaryKey);
const existingColumnOption = Object.values ? Object.values(columnOptions) : [];
let displayColumns = filteredColumns.map(({ accessor }) => ({
value: accessor,
label: accessor,
}));
if (existingColumnOption.length > 0) {
displayColumns = displayColumns.filter(
({ value }) => !existingColumnOption.map((item) => item.column !== column && item.column).includes(value)
);
}
const handleColumnChange = (selectedOption) => {
const updatedOption = {
...columnOptions[id],
column: selectedOption,
};
const newColumnOptions = { ...columnOptions, [id]: updatedOption };
handleColumnOptionChange(newColumnOptions);
};
const handleValueChange = (newValue) => {
const updatedOption = {
...columnOptions[id],
value: newValue,
};
const newColumnOptions = { ...columnOptions, [id]: updatedOption };
handleColumnOptionChange(newColumnOptions);
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col-4 me-3">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
customWrap={true}
/>
</div>
<div className="field col-6 mx-1">
<CodeHinter
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
theme={darkMode ? 'monokai' : 'default'}
height={'32px'}
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
</div>
<div className="col cursor-pointer m-1 mx-3">
<svg
onClick={() => {
removeColumnOptionsPair(id);
}}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
);
};

View file

@ -1,10 +1,11 @@
import React, { useContext } from 'react';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import { uniqueId } from 'lodash';
import { isEmpty, uniqueId } from 'lodash';
import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
import Select from '@/_ui/Select';
import { operators } from '@/TooljetDatabase/constants';
import { isOperatorOptions } from './util';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
export const DeleteRows = React.memo(({ darkMode }) => {
const { columns, deleteOperationLimitOptionChanged, deleteRowsOptions, handleDeleteRowsOptionsChange } =
@ -44,118 +45,49 @@ export const DeleteRows = React.memo(({ darkMode }) => {
handleWhereFiltersChange(updatedFiltersObject);
}
const RenderFilterFields = ({ column, operator, value, id }) => {
let displayColumns = columns.map(({ accessor }) => ({
value: accessor,
label: accessor,
}));
const handleColumnChange = (selectedOption) => {
updateFilterOptionsChanged({ ...deleteRowsOptions?.where_filters[id], ...{ column: selectedOption } });
};
const handleOperatorChange = (selectedOption) => {
updateFilterOptionsChanged({ ...deleteRowsOptions?.where_filters[id], ...{ operator: selectedOption } });
};
const handleValueChange = (newValue) => {
updateFilterOptionsChanged({ ...deleteRowsOptions?.where_filters[id], ...{ value: newValue } });
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
/>
</div>
<div className="field col mx-1">
<Select
useMenuPortal={true}
placeholder="Select operation"
value={operator}
options={operators}
onChange={handleOperatorChange}
/>
</div>
<div className="field col-4">
{operator === 'is' ? (
<Select
useMenuPortal={true}
placeholder="Select value"
value={value}
options={isOperatorOptions}
onChange={handleValueChange}
/>
) : (
<CodeHinter
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
theme={darkMode ? 'monokai' : 'default'}
height={'32px'}
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
)}
</div>
<div className="col-1 cursor-pointer m-1 mr-2">
<svg
onClick={() => removeFilterConditionPair(id)}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
);
};
return (
<div className="tab-content-wrapper tj-db-field-wrapper mt-2">
<label className="form-label" data-cy="label-column-filter">
Filter
</label>
<div className="d-flex">
<label className="form-label" data-cy="label-column-filter">
Filter
</label>
<div className="field-container">
{Object.values(deleteRowsOptions?.where_filters || {}).map((filter) => (
<RenderFilterFields key={filter.id} {...filter} />
))}
<div
className="cursor-pointer pb-3 fit-content"
onClick={() => {
addNewFilterConditionPair();
}}
>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.34554 10.0207C5.15665 10.0207 4.99832 9.95678 4.87054 9.829C4.74276 9.70123 4.67887 9.54289 4.67887 9.354V5.854H1.17887C0.989985 5.854 0.831651 5.79011 0.703874 5.66234C0.576096 5.53456 0.512207 5.37623 0.512207 5.18734C0.512207 4.99845 0.576096 4.84012 0.703874 4.71234C0.831651 4.58456 0.989985 4.52067 1.17887 4.52067H4.67887V1.02067C4.67887 0.831782 4.74276 0.673448 4.87054 0.54567C4.99832 0.417893 5.15665 0.354004 5.34554 0.354004C5.53443 0.354004 5.69276 0.417893 5.82054 0.54567C5.94832 0.673448 6.01221 0.831782 6.01221 1.02067V4.52067H9.51221C9.7011 4.52067 9.85943 4.58456 9.98721 4.71234C10.115 4.84012 10.1789 4.99845 10.1789 5.18734C10.1789 5.37623 10.115 5.53456 9.98721 5.66234C9.85943 5.79011 9.7011 5.854 9.51221 5.854H6.01221V9.354C6.01221 9.54289 5.94832 9.70123 5.82054 9.829C5.69276 9.95678 5.53443 10.0207 5.34554 10.0207Z"
fill="#466BF2"
<div className="field-container flex-grow-1 mb-2">
{Object.values(deleteRowsOptions?.where_filters || {}).map((filter) => (
<RenderFilterFields
key={filter.id}
{...filter}
removeFilterConditionPair={removeFilterConditionPair}
updateFilterOptionsChanged={updateFilterOptionsChanged}
deleteRowsOptions={deleteRowsOptions}
columns={columns}
darkMode={darkMode}
/>
</svg>
&nbsp;Add Condition
))}
<ButtonSolid
variant="ghostBlue"
size="sm"
onClick={() => {
addNewFilterConditionPair();
}}
className={isEmpty(deleteRowsOptions?.where_filters || {}) ? '' : 'mt-2'}
>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.34554 10.0207C5.15665 10.0207 4.99832 9.95678 4.87054 9.829C4.74276 9.70123 4.67887 9.54289 4.67887 9.354V5.854H1.17887C0.989985 5.854 0.831651 5.79011 0.703874 5.66234C0.576096 5.53456 0.512207 5.37623 0.512207 5.18734C0.512207 4.99845 0.576096 4.84012 0.703874 4.71234C0.831651 4.58456 0.989985 4.52067 1.17887 4.52067H4.67887V1.02067C4.67887 0.831782 4.74276 0.673448 4.87054 0.54567C4.99832 0.417893 5.15665 0.354004 5.34554 0.354004C5.53443 0.354004 5.69276 0.417893 5.82054 0.54567C5.94832 0.673448 6.01221 0.831782 6.01221 1.02067V4.52067H9.51221C9.7011 4.52067 9.85943 4.58456 9.98721 4.71234C10.115 4.84012 10.1789 4.99845 10.1789 5.18734C10.1789 5.37623 10.115 5.53456 9.98721 5.66234C9.85943 5.79011 9.7011 5.854 9.51221 5.854H6.01221V9.354C6.01221 9.54289 5.94832 9.70123 5.82054 9.829C5.69276 9.95678 5.53443 10.0207 5.34554 10.0207Z"
fill="#466BF2"
/>
</svg>
&nbsp;Add Condition
</ButtonSolid>
</div>
</div>
<div className="field-container ">
<div className="field-container d-flex">
<label className="form-label" data-cy="label-column-limit">
Limit
</label>
<div className="field col-4">
<div className="field flex-grow-1">
<CodeHinter
initialValue={deleteRowsOptions?.limit ?? 1}
className="codehinter-plugins"
@ -169,3 +101,97 @@ export const DeleteRows = React.memo(({ darkMode }) => {
</div>
);
});
const RenderFilterFields = ({
column,
operator,
value,
id,
removeFilterConditionPair,
columns,
updateFilterOptionsChanged,
deleteRowsOptions,
darkMode,
}) => {
let displayColumns = columns.map(({ accessor }) => ({
value: accessor,
label: accessor,
}));
const handleColumnChange = (selectedOption) => {
updateFilterOptionsChanged({ ...deleteRowsOptions?.where_filters[id], ...{ column: selectedOption } });
};
const handleOperatorChange = (selectedOption) => {
updateFilterOptionsChanged({ ...deleteRowsOptions?.where_filters[id], ...{ operator: selectedOption } });
};
const handleValueChange = (newValue) => {
updateFilterOptionsChanged({ ...deleteRowsOptions?.where_filters[id], ...{ value: newValue } });
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
width="auto"
/>
</div>
<div className="field col mx-1">
<Select
useMenuPortal={true}
placeholder="Select operation"
value={operator}
options={operators}
onChange={handleOperatorChange}
width="auto"
/>
</div>
<div className="field col-4">
{operator === 'is' ? (
<Select
useMenuPortal={true}
placeholder="Select value"
value={value}
options={isOperatorOptions}
onChange={handleValueChange}
width="auto"
/>
) : (
<CodeHinter
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
theme={darkMode ? 'monokai' : 'default'}
height={'32px'}
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
)}
</div>
<div className="col-1 cursor-pointer m-1 mr-2">
<svg
onClick={() => removeFilterConditionPair(id)}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
);
};

View file

@ -1,10 +1,11 @@
import React, { useContext } from 'react';
import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import { uniqueId } from 'lodash';
import { isEmpty, uniqueId } from 'lodash';
import Select from '@/_ui/Select';
import { operators } from '@/TooljetDatabase/constants';
import { isOperatorOptions } from './util';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
export const ListRows = React.memo(({ darkMode }) => {
const { columns, listRowsOptions, limitOptionChanged, handleOptionsChange } = useContext(TooljetDatabaseContext);
@ -77,197 +78,34 @@ export const ListRows = React.memo(({ darkMode }) => {
handleOrderFiltersChange(updatedFiltersObject);
}
const RenderFilterFields = ({ column, operator, value, id }) => {
let displayColumns = columns.map(({ accessor }) => ({
value: accessor,
label: accessor,
}));
const handleColumnChange = (selectedOption) => {
updateFilterOptionsChanged({ ...listRowsOptions?.where_filters[id], ...{ column: selectedOption } });
};
const handleOperatorChange = (selectedOption) => {
updateFilterOptionsChanged({ ...listRowsOptions?.where_filters[id], ...{ operator: selectedOption } });
};
const handleValueChange = (newValue) => {
updateFilterOptionsChanged({ ...listRowsOptions?.where_filters[id], ...{ value: newValue } });
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
/>
</div>
<div className="field col mx-1">
<Select
useMenuPortal={true}
placeholder="Select operation"
value={operator}
options={operators}
onChange={handleOperatorChange}
/>
</div>
<div className="field col-4">
{operator === 'is' ? (
<Select
useMenuPortal={true}
placeholder="Select value"
value={value}
options={isOperatorOptions}
onChange={handleValueChange}
/>
) : (
<CodeHinter
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
theme={darkMode ? 'monokai' : 'default'}
height={'32px'}
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
)}
</div>
<div className="col-1 cursor-pointer m-1 mr-2">
<svg
onClick={() => removeFilterConditionPair(id)}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
);
};
const RenderSortFields = ({ column, order, id }) => {
const orders = [
{ value: 'asc', label: 'Ascending' },
{ value: 'desc', label: 'Descending' },
];
const existingColumnOptions = Object.values(listRowsOptions?.order_filters).map((item) => item.column);
let displayColumns = columns.map(({ accessor }) => ({
value: accessor,
label: accessor,
}));
if (existingColumnOptions.length > 0) {
displayColumns = displayColumns.filter(
({ value }) => !existingColumnOptions.map((item) => item !== column && item).includes(value)
);
}
const handleColumnChange = (selectedOption) => {
updateSortOptionsChanged({ ...listRowsOptions?.order_filters[id], ...{ column: selectedOption } });
};
const handleDirectionChange = (selectedOption) => {
updateSortOptionsChanged({ ...listRowsOptions?.order_filters[id], ...{ order: selectedOption } });
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col-4">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
/>
</div>
<div className="field col-4">
<Select
useMenuPortal={true}
placeholder="Select direction"
value={order}
options={orders}
onChange={handleDirectionChange}
/>
</div>
<div className="col cursor-pointer m-1 mr-2">
<svg
onClick={() => removeSortConditionPair(id)}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
);
};
return (
<div>
<div className="row my-2 tj-db-field-wrapper">
<div className="tab-content-wrapper">
<label className="form-label" data-cy="label-column-filter">
Filter
</label>
<div className="field-container">
{Object.values(listRowsOptions?.where_filters || {}).map((filter) => (
<RenderFilterFields key={filter.id} {...filter} />
))}
<div
className="cursor-pointer pb-3 fit-content"
onClick={() => {
addNewFilterConditionPair();
}}
>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.34554 10.0207C5.15665 10.0207 4.99832 9.95678 4.87054 9.829C4.74276 9.70123 4.67887 9.54289 4.67887 9.354V5.854H1.17887C0.989985 5.854 0.831651 5.79011 0.703874 5.66234C0.576096 5.53456 0.512207 5.37623 0.512207 5.18734C0.512207 4.99845 0.576096 4.84012 0.703874 4.71234C0.831651 4.58456 0.989985 4.52067 1.17887 4.52067H4.67887V1.02067C4.67887 0.831782 4.74276 0.673448 4.87054 0.54567C4.99832 0.417893 5.15665 0.354004 5.34554 0.354004C5.53443 0.354004 5.69276 0.417893 5.82054 0.54567C5.94832 0.673448 6.01221 0.831782 6.01221 1.02067V4.52067H9.51221C9.7011 4.52067 9.85943 4.58456 9.98721 4.71234C10.115 4.84012 10.1789 4.99845 10.1789 5.18734C10.1789 5.37623 10.115 5.53456 9.98721 5.66234C9.85943 5.79011 9.7011 5.854 9.51221 5.854H6.01221V9.354C6.01221 9.54289 5.94832 9.70123 5.82054 9.829C5.69276 9.95678 5.53443 10.0207 5.34554 10.0207Z"
fill="#466BF2"
/>
</svg>
&nbsp;Add Condition
</div>
</div>
{/* sort */}
<div className="fields-container">
<label className="form-label" data-cy="label-column-sort">
Sort
<div className="d-flex mb-2">
<label className="form-label" data-cy="label-column-filter">
Filter
</label>
<div className="field-container">
{Object.values(listRowsOptions?.order_filters || {}).map((filter) => (
<RenderSortFields key={filter.id} {...filter} />
<div className="field-container flex-grow-1">
{Object.values(listRowsOptions?.where_filters || {}).map((filter) => (
<RenderFilterFields
key={filter.id}
{...filter}
columns={columns}
listRowsOptions={listRowsOptions}
updateFilterOptionsChanged={updateFilterOptionsChanged}
darkMode={darkMode}
removeFilterConditionPair={removeFilterConditionPair}
/>
))}
<div
className="cursor-pointer pb-3 fit-content"
<ButtonSolid
variant="ghostBlue"
size="sm"
onClick={() => {
addNewSortConditionPair();
addNewFilterConditionPair();
}}
className={isEmpty(listRowsOptions?.where_filters || {}) ? '' : 'mt-2'}
>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -276,16 +114,51 @@ export const ListRows = React.memo(({ darkMode }) => {
/>
</svg>
&nbsp;Add Condition
</div>
</ButtonSolid>
</div>
</div>
{/* sort */}
<div className="fields-container d-flex mb-2">
<label className="form-label" data-cy="label-column-sort">
Sort
</label>
<div className="field-container flex-grow-1">
{Object.values(listRowsOptions?.order_filters || {}).map((filter) => (
<RenderSortFields
key={filter.id}
{...filter}
removeSortConditionPair={removeSortConditionPair}
listRowsOptions={listRowsOptions}
columns={columns}
updateSortOptionsChanged={updateSortOptionsChanged}
/>
))}
<ButtonSolid
variant="ghostBlue"
size="sm"
onClick={() => {
addNewSortConditionPair();
}}
className={isEmpty(listRowsOptions?.order_filters || {}) ? '' : 'mt-2'}
>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.34554 10.0207C5.15665 10.0207 4.99832 9.95678 4.87054 9.829C4.74276 9.70123 4.67887 9.54289 4.67887 9.354V5.854H1.17887C0.989985 5.854 0.831651 5.79011 0.703874 5.66234C0.576096 5.53456 0.512207 5.37623 0.512207 5.18734C0.512207 4.99845 0.576096 4.84012 0.703874 4.71234C0.831651 4.58456 0.989985 4.52067 1.17887 4.52067H4.67887V1.02067C4.67887 0.831782 4.74276 0.673448 4.87054 0.54567C4.99832 0.417893 5.15665 0.354004 5.34554 0.354004C5.53443 0.354004 5.69276 0.417893 5.82054 0.54567C5.94832 0.673448 6.01221 0.831782 6.01221 1.02067V4.52067H9.51221C9.7011 4.52067 9.85943 4.58456 9.98721 4.71234C10.115 4.84012 10.1789 4.99845 10.1789 5.18734C10.1789 5.37623 10.115 5.53456 9.98721 5.66234C9.85943 5.79011 9.7011 5.854 9.51221 5.854H6.01221V9.354C6.01221 9.54289 5.94832 9.70123 5.82054 9.829C5.69276 9.95678 5.53443 10.0207 5.34554 10.0207Z"
fill="#466BF2"
/>
</svg>
&nbsp;Add Condition
</ButtonSolid>
</div>
</div>
{/* Limit */}
<div className="field-container ">
<div className="field-container d-flex">
<label className="form-label" data-cy="label-column-limit">
Limit
</label>
<div className="field col-4">
<div className="field flex-grow-1">
<CodeHinter
initialValue={listRowsOptions?.limit ?? ''}
className="codehinter-plugins"
@ -301,3 +174,175 @@ export const ListRows = React.memo(({ darkMode }) => {
</div>
);
});
const RenderSortFields = ({
column,
order,
id,
removeSortConditionPair,
listRowsOptions,
columns,
updateSortOptionsChanged,
}) => {
const orders = [
{ value: 'asc', label: 'Ascending' },
{ value: 'desc', label: 'Descending' },
];
const existingColumnOptions = Object.values(listRowsOptions?.order_filters).map((item) => item.column);
let displayColumns = columns.map(({ accessor }) => ({
value: accessor,
label: accessor,
}));
if (existingColumnOptions.length > 0) {
displayColumns = displayColumns.filter(
({ value }) => !existingColumnOptions.map((item) => item !== column && item).includes(value)
);
}
const handleColumnChange = (selectedOption) => {
updateSortOptionsChanged({ ...listRowsOptions?.order_filters[id], ...{ column: selectedOption } });
};
const handleDirectionChange = (selectedOption) => {
updateSortOptionsChanged({ ...listRowsOptions?.order_filters[id], ...{ order: selectedOption } });
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container mb-2">
<div className="field col">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
/>
</div>
<div className="field col mx-1">
<Select
useMenuPortal={true}
placeholder="Select direction"
value={order}
options={orders}
onChange={handleDirectionChange}
/>
</div>
<div className="col cursor-pointer m-1 ms-1">
<svg
onClick={() => removeSortConditionPair(id)}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
);
};
const RenderFilterFields = ({
column,
operator,
value,
id,
columns,
listRowsOptions,
updateFilterOptionsChanged,
removeFilterConditionPair,
darkMode,
}) => {
let displayColumns = columns.map(({ accessor }) => ({
value: accessor,
label: accessor,
}));
const handleColumnChange = (selectedOption) => {
updateFilterOptionsChanged({ ...listRowsOptions?.where_filters[id], ...{ column: selectedOption } });
};
const handleOperatorChange = (selectedOption) => {
updateFilterOptionsChanged({ ...listRowsOptions?.where_filters[id], ...{ operator: selectedOption } });
};
const handleValueChange = (newValue) => {
updateFilterOptionsChanged({ ...listRowsOptions?.where_filters[id], ...{ value: newValue } });
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
// useCustomStyles
// styles={{ container: (styles) => ({ width: 'auto', ...styles }) }}
width={'auto'}
/>
</div>
<div className="field col mx-1">
<Select
useMenuPortal={true}
placeholder="Select operation"
value={operator}
options={operators}
onChange={handleOperatorChange}
width={'auto'}
/>
</div>
<div className="field col-4">
{operator === 'is' ? (
<Select
useMenuPortal={true}
placeholder="Select value"
value={value}
options={isOperatorOptions}
onChange={handleValueChange}
width={'auto'}
/>
) : (
<CodeHinter
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
theme={darkMode ? 'monokai' : 'default'}
height={'32px'}
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
)}
</div>
<div className="col-1 cursor-pointer m-1 mr-2">
<svg
onClick={() => removeFilterConditionPair(id)}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
);
};

View file

@ -1,4 +1,5 @@
import React, { useState, useEffect, useMemo } from 'react';
import cx from 'classnames';
import { tooljetDatabaseService, authenticationService } from '@/_services';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import { ListRows } from './ListRows';
@ -11,7 +12,7 @@ import { queryManagerSelectComponentStyle } from '@/_ui/Select/styles';
import { useMounted } from '@/_hooks/use-mount';
import { useCurrentState } from '@/_stores/currentStateStore';
const ToolJetDbOperations = ({ optionchanged, options, darkMode }) => {
const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLayout }) => {
const computeSelectStyles = (darkMode, width) => {
return queryManagerSelectComponentStyle(darkMode, width);
};
@ -178,40 +179,46 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode }) => {
return (
<TooljetDatabaseContext.Provider value={value}>
{/* table name dropdown */}
<div className="row">
<div className="col-4">
<label className="form-label">Table name</label>
<Select
options={generateListForDropdown(tables)}
value={selectedTable}
onChange={(value) => handleTableNameSelect(value)}
width="100%"
// useMenuPortal={false}
useCustomStyles={true}
styles={computeSelectStyles(darkMode, '100%')}
/>
<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
options={generateListForDropdown(tables)}
value={selectedTable}
onChange={(value) => handleTableNameSelect(value)}
width="100%"
// useMenuPortal={false}
useCustomStyles={true}
styles={computeSelectStyles(darkMode, '100%')}
/>
</div>
</div>
</div>
{/* operation selection dropdown */}
<div className="row">
<div className="my-2 col-4">
<label className="form-label">Operations</label>
<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('my-3 py-1', { row: !isHorizontalLayout })}>
<div
/* className="my-2 col-4" */
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>
</div>
</div>

View file

@ -3,8 +3,9 @@ import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import Select from '@/_ui/Select';
import { operators } from '@/TooljetDatabase/constants';
import { uniqueId } from 'lodash';
import { isEmpty, uniqueId } from 'lodash';
import { isOperatorOptions } from './util';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
export const UpdateRows = React.memo(({ darkMode }) => {
const { columns, updateRowsOptions, handleUpdateRowsOptionsChange } = useContext(TooljetDatabaseContext);
@ -70,139 +71,150 @@ export const UpdateRows = React.memo(({ darkMode }) => {
handleWhereFiltersChange(updatedFiltersObject);
}
const RenderFilterFields = ({ column, operator, value, id }) => {
let displayColumns = columns.map(({ accessor }) => ({
value: accessor,
label: accessor,
}));
return (
<div className="tab-content-wrapper tj-db-field-wrapper mt-2">
<div className="d-flex mb-2">
<label className="form-label" data-cy="label-column-filter">
Filter
</label>
const handleColumnChange = (selectedOption) => {
updateFilterOptionsChanged({ ...updateRowsOptions?.where_filters[id], ...{ column: selectedOption } });
};
const handleOperatorChange = (selectedOption) => {
updateFilterOptionsChanged({ ...updateRowsOptions?.where_filters[id], ...{ operator: selectedOption } });
};
const handleValueChange = (newValue) => {
updateFilterOptionsChanged({ ...updateRowsOptions?.where_filters[id], ...{ value: newValue } });
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
<div className="field-container flex-grow-1">
{Object.values(updateRowsOptions?.where_filters || {}).map((filter) => (
<RenderFilterFields
key={filter.id}
{...filter}
columns={columns}
updateFilterOptionsChanged={updateFilterOptionsChanged}
updateRowsOptions={updateRowsOptions}
darkMode={darkMode}
removeFilterConditionPair={removeFilterConditionPair}
/>
</div>
<div className="field col mx-1">
<Select
useMenuPortal={true}
placeholder="Select operation"
value={operator}
options={operators}
onChange={handleOperatorChange}
/>
</div>
<div className="field col-4">
{operator === 'is' ? (
<Select
useMenuPortal={true}
placeholder="Select value"
value={value}
options={isOperatorOptions}
onChange={handleValueChange}
/>
) : (
<CodeHinter
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
theme={darkMode ? 'monokai' : 'default'}
height={'32px'}
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
)}
</div>
<div className="col-1 cursor-pointer m-1 mr-2">
<svg
onClick={() => removeFilterConditionPair(id)}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
))}
<ButtonSolid
variant="ghostBlue"
size="sm"
onClick={() => {
addNewFilterConditionPair();
}}
className={`cursor-pointer fit-content ${isEmpty(updateRowsOptions?.where_filters) ? '' : 'mt-2'}`}
>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
d="M5.34554 10.0207C5.15665 10.0207 4.99832 9.95678 4.87054 9.829C4.74276 9.70123 4.67887 9.54289 4.67887 9.354V5.854H1.17887C0.989985 5.854 0.831651 5.79011 0.703874 5.66234C0.576096 5.53456 0.512207 5.37623 0.512207 5.18734C0.512207 4.99845 0.576096 4.84012 0.703874 4.71234C0.831651 4.58456 0.989985 4.52067 1.17887 4.52067H4.67887V1.02067C4.67887 0.831782 4.74276 0.673448 4.87054 0.54567C4.99832 0.417893 5.15665 0.354004 5.34554 0.354004C5.53443 0.354004 5.69276 0.417893 5.82054 0.54567C5.94832 0.673448 6.01221 0.831782 6.01221 1.02067V4.52067H9.51221C9.7011 4.52067 9.85943 4.58456 9.98721 4.71234C10.115 4.84012 10.1789 4.99845 10.1789 5.18734C10.1789 5.37623 10.115 5.53456 9.98721 5.66234C9.85943 5.79011 9.7011 5.854 9.51221 5.854H6.01221V9.354C6.01221 9.54289 5.94832 9.70123 5.82054 9.829C5.69276 9.95678 5.53443 10.0207 5.34554 10.0207Z"
fill="#466BF2"
/>
</svg>
</div>
&nbsp;Add Condition
</ButtonSolid>
</div>
</div>
);
<div className="fields-container d-flex">
<label className="form-label" data-cy="label-column-filter">
Columns
</label>
<div className="field-container flex-grow-1">
{Object.entries(updateRowsOptions?.columns).map(([key, value]) => {
return (
<RenderColumnOptions
key={key}
column={value.column}
value={value.value}
id={key}
columns={columns}
updateRowsOptions={updateRowsOptions}
handleColumnOptionChange={handleColumnOptionChange}
darkMode={darkMode}
removeColumnOptionsPair={removeColumnOptionsPair}
/>
);
})}
{Object.keys(updateRowsOptions?.columns).length !== columns.length && (
<ButtonSolid
variant="ghostBlue"
size="sm"
onClick={addNewColumnOptionsPair}
className={`cursor-pointer fit-content ${isEmpty(updateRowsOptions?.columns) ? '' : 'mt-2'}`}
>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.34554 10.0207C5.15665 10.0207 4.99832 9.95678 4.87054 9.829C4.74276 9.70123 4.67887 9.54289 4.67887 9.354V5.854H1.17887C0.989985 5.854 0.831651 5.79011 0.703874 5.66234C0.576096 5.53456 0.512207 5.37623 0.512207 5.18734C0.512207 4.99845 0.576096 4.84012 0.703874 4.71234C0.831651 4.58456 0.989985 4.52067 1.17887 4.52067H4.67887V1.02067C4.67887 0.831782 4.74276 0.673448 4.87054 0.54567C4.99832 0.417893 5.15665 0.354004 5.34554 0.354004C5.53443 0.354004 5.69276 0.417893 5.82054 0.54567C5.94832 0.673448 6.01221 0.831782 6.01221 1.02067V4.52067H9.51221C9.7011 4.52067 9.85943 4.58456 9.98721 4.71234C10.115 4.84012 10.1789 4.99845 10.1789 5.18734C10.1789 5.37623 10.115 5.53456 9.98721 5.66234C9.85943 5.79011 9.7011 5.854 9.51221 5.854H6.01221V9.354C6.01221 9.54289 5.94832 9.70123 5.82054 9.829C5.69276 9.95678 5.53443 10.0207 5.34554 10.0207Z"
fill="#466BF2"
/>
</svg>
&nbsp; Add column
</ButtonSolid>
)}
</div>
</div>
</div>
);
});
const RenderFilterFields = ({
column,
operator,
value,
id,
columns,
updateFilterOptionsChanged,
updateRowsOptions,
darkMode,
removeFilterConditionPair,
}) => {
let displayColumns = columns.map(({ accessor }) => ({
value: accessor,
label: accessor,
}));
const handleColumnChange = (selectedOption) => {
updateFilterOptionsChanged({ ...updateRowsOptions?.where_filters[id], ...{ column: selectedOption } });
};
const RenderColumnOptions = ({ column, value, id }) => {
const filteredColumns = columns.filter(({ isPrimaryKey }) => !isPrimaryKey);
const existingColumnOptions = Object.values(updateRowsOptions?.columns).map(({ column }) => column);
let displayColumns = filteredColumns.map(({ accessor }) => ({
value: accessor,
label: accessor,
}));
const handleOperatorChange = (selectedOption) => {
updateFilterOptionsChanged({ ...updateRowsOptions?.where_filters[id], ...{ operator: selectedOption } });
};
if (existingColumnOptions.length > 0) {
displayColumns = displayColumns.filter(
({ value }) => !existingColumnOptions.map((item) => item !== column && item).includes(value)
);
}
const handleValueChange = (newValue) => {
updateFilterOptionsChanged({ ...updateRowsOptions?.where_filters[id], ...{ value: newValue } });
};
const handleColumnChange = (selectedOption) => {
const columnOptions = updateRowsOptions?.columns;
const updatedOption = {
...columnOptions[id],
column: selectedOption,
};
const newColumnOptions = { ...columnOptions, [id]: updatedOption };
handleColumnOptionChange(newColumnOptions);
};
const handleValueChange = (newValue) => {
const columnOptions = updateRowsOptions?.columns;
const updatedOption = {
...columnOptions[id],
value: newValue,
};
const newColumnOptions = { ...columnOptions, [id]: updatedOption };
handleColumnOptionChange(newColumnOptions);
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col-4">
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
width="auto"
/>
</div>
<div className="field col mx-1">
<Select
useMenuPortal={true}
placeholder="Select operation"
value={operator}
options={operators}
onChange={handleOperatorChange}
width="auto"
/>
</div>
<div className="field col-4">
{operator === 'is' ? (
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
placeholder="Select value"
value={value}
options={isOperatorOptions}
onChange={handleValueChange}
width="auto"
/>
</div>
<div className="field col-4">
) : (
<CodeHinter
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
@ -211,81 +223,121 @@ export const UpdateRows = React.memo(({ darkMode }) => {
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
</div>
<div className="col cursor-pointer m-1 mx-3">
<svg
onClick={() => {
removeColumnOptionsPair(id);
}}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
)}
</div>
</div>
);
};
return (
<div className="tab-content-wrapper tj-db-field-wrapper mt-2">
<label className="form-label" data-cy="label-column-filter">
Filter
</label>
<div className="field-container">
{Object.values(updateRowsOptions?.where_filters || {}).map((filter) => (
<RenderFilterFields key={filter.id} {...filter} />
))}
<div
className="cursor-pointer pb-3 fit-content"
onClick={() => {
addNewFilterConditionPair();
}}
>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<div className="col-1 cursor-pointer m-1 mr-2">
<svg
onClick={() => removeFilterConditionPair(id)}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.34554 10.0207C5.15665 10.0207 4.99832 9.95678 4.87054 9.829C4.74276 9.70123 4.67887 9.54289 4.67887 9.354V5.854H1.17887C0.989985 5.854 0.831651 5.79011 0.703874 5.66234C0.576096 5.53456 0.512207 5.37623 0.512207 5.18734C0.512207 4.99845 0.576096 4.84012 0.703874 4.71234C0.831651 4.58456 0.989985 4.52067 1.17887 4.52067H4.67887V1.02067C4.67887 0.831782 4.74276 0.673448 4.87054 0.54567C4.99832 0.417893 5.15665 0.354004 5.34554 0.354004C5.53443 0.354004 5.69276 0.417893 5.82054 0.54567C5.94832 0.673448 6.01221 0.831782 6.01221 1.02067V4.52067H9.51221C9.7011 4.52067 9.85943 4.58456 9.98721 4.71234C10.115 4.84012 10.1789 4.99845 10.1789 5.18734C10.1789 5.37623 10.115 5.53456 9.98721 5.66234C9.85943 5.79011 9.7011 5.854 9.51221 5.854H6.01221V9.354C6.01221 9.54289 5.94832 9.70123 5.82054 9.829C5.69276 9.95678 5.53443 10.0207 5.34554 10.0207Z"
fill="#466BF2"
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
&nbsp;Add Condition
</div>
</div>
<div className="fields-container">
<label className="form-label" data-cy="label-column-filter">
Columns
</label>
<div className="field-container">
{Object.entries(updateRowsOptions?.columns).map(([key, value]) => {
return <RenderColumnOptions key={key} column={value.column} value={value.value} id={key} />;
})}
{Object.keys(updateRowsOptions?.columns).length !== columns.length && (
<div className="cursor-pointer pb-3 fit-content" onClick={addNewColumnOptionsPair}>
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.34554 10.0207C5.15665 10.0207 4.99832 9.95678 4.87054 9.829C4.74276 9.70123 4.67887 9.54289 4.67887 9.354V5.854H1.17887C0.989985 5.854 0.831651 5.79011 0.703874 5.66234C0.576096 5.53456 0.512207 5.37623 0.512207 5.18734C0.512207 4.99845 0.576096 4.84012 0.703874 4.71234C0.831651 4.58456 0.989985 4.52067 1.17887 4.52067H4.67887V1.02067C4.67887 0.831782 4.74276 0.673448 4.87054 0.54567C4.99832 0.417893 5.15665 0.354004 5.34554 0.354004C5.53443 0.354004 5.69276 0.417893 5.82054 0.54567C5.94832 0.673448 6.01221 0.831782 6.01221 1.02067V4.52067H9.51221C9.7011 4.52067 9.85943 4.58456 9.98721 4.71234C10.115 4.84012 10.1789 4.99845 10.1789 5.18734C10.1789 5.37623 10.115 5.53456 9.98721 5.66234C9.85943 5.79011 9.7011 5.854 9.51221 5.854H6.01221V9.354C6.01221 9.54289 5.94832 9.70123 5.82054 9.829C5.69276 9.95678 5.53443 10.0207 5.34554 10.0207Z"
fill="#466BF2"
/>
</svg>
&nbsp; Add column
</div>
)}
</div>
</div>
</div>
);
});
};
const RenderColumnOptions = ({
column,
value,
id,
columns,
updateRowsOptions,
handleColumnOptionChange,
darkMode,
removeColumnOptionsPair,
}) => {
const filteredColumns = columns.filter(({ isPrimaryKey }) => !isPrimaryKey);
const existingColumnOptions = Object.values(updateRowsOptions?.columns).map(({ column }) => column);
let displayColumns = filteredColumns.map(({ accessor }) => ({
value: accessor,
label: accessor,
}));
if (existingColumnOptions.length > 0) {
displayColumns = displayColumns.filter(
({ value }) => !existingColumnOptions.map((item) => item !== column && item).includes(value)
);
}
const handleColumnChange = (selectedOption) => {
const columnOptions = updateRowsOptions?.columns;
const updatedOption = {
...columnOptions[id],
column: selectedOption,
};
const newColumnOptions = { ...columnOptions, [id]: updatedOption };
handleColumnOptionChange(newColumnOptions);
};
const handleValueChange = (newValue) => {
const columnOptions = updateRowsOptions?.columns;
const updatedOption = {
...columnOptions[id],
value: newValue,
};
const newColumnOptions = { ...columnOptions, [id]: updatedOption };
handleColumnOptionChange(newColumnOptions);
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col-4 me-3">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
/>
</div>
<div className="field col-6 mx-1">
<CodeHinter
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
theme={darkMode ? 'monokai' : 'default'}
height={'32px'}
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
</div>
<div className="col cursor-pointer m-1 mx-3">
<svg
onClick={() => {
removeColumnOptionsPair(id);
}}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
);
};

View file

@ -22,12 +22,17 @@ export const allSources = {
...Object.keys(allOperations).reduce((accumulator, currentValue) => {
accumulator[currentValue] = (props) => (
<div className="query-editor-dynamic-form-container">
<DynamicForm schema={allOperations[currentValue]} {...props} computeSelectStyles={computeSelectStyles} />
<DynamicForm
schema={allOperations[currentValue]}
{...props}
computeSelectStyles={computeSelectStyles}
layout="horizontal"
/>
</div>
);
return accumulator;
}, {}),
Tooljetdb: (props) => <DynamicForm schema={tooljetDbOperations} {...props} />,
Tooljetdb: (props) => <DynamicForm schema={tooljetDbOperations} {...props} layout="horizontal" />,
Restapi,
Runjs,
Runpy,
@ -38,6 +43,6 @@ export const allSources = {
export const source = (props) => (
<div className="query-editor-dynamic-form-container">
<DynamicForm schema={props.pluginSchema} {...props} computeSelectStyles={computeSelectStyles} />
<DynamicForm schema={props.pluginSchema} {...props} computeSelectStyles={computeSelectStyles} layout="horizontal" />
</div>
);

View file

@ -7,44 +7,22 @@ import { defaultSources } from './constants';
import { useQueryCreationLoading, useQueryUpdationLoading } from '@/_stores/dataQueriesStore';
import { useDataSources, useGlobalDataSources, useLoadingDataSources } from '@/_stores/dataSourcesStore';
import {
useQueryToBeRun,
usePreviewLoading,
usePreviewData,
useSelectedQuery,
useQueryPanelActions,
} from '@/_stores/queryPanelStore';
import { useQueryToBeRun, useSelectedQuery, useQueryPanelActions } from '@/_stores/queryPanelStore';
const QueryManager = ({
addNewQueryAndDeselectSelectedQuery,
toggleQueryEditor,
mode,
dataQueriesChanged,
appId,
darkMode,
apps,
allComponents,
dataSourceModalHandler,
appDefinition,
editorRef,
createDraftQuery,
updateDraftQueryName,
}) => {
const QueryManager = ({ mode, dataQueriesChanged, appId, darkMode, apps, allComponents, appDefinition, editorRef }) => {
const loadingDataSources = useLoadingDataSources();
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const queryToBeRun = useQueryToBeRun();
const isCreationInProcess = useQueryCreationLoading();
const isUpdationInProcess = useQueryUpdationLoading();
const previewLoading = usePreviewLoading();
const queryPreviewData = usePreviewData();
const selectedQuery = useSelectedQuery();
const { setSelectedDataSource, setQueryToBeRun } = useQueryPanelActions();
const [options, setOptions] = useState({});
const mounted = useRef(false);
const previewPanelRef = useRef(null);
/** TODO: Below effect primarily used only for websocket invocation post update. Can be removed onece websocket logic is revamped */
useEffect(() => {
if (mounted.current && !isCreationInProcess && !isUpdationInProcess) {
return dataQueriesChanged();
@ -67,16 +45,20 @@ const QueryManager = ({
useEffect(() => {
if (selectedQuery) {
if (selectedQuery?.kind in defaultSources && !selectedQuery?.data_source_id) {
const selectedDS = [...dataSources, ...globalDataSources].find(
(datasource) => datasource.id === selectedQuery?.data_source_id
);
//TODO: currently type is not taken into account. May create issues in importing REST apis. to be revamped when import app is revamped
if (
selectedQuery?.kind in defaultSources &&
(!selectedQuery?.data_source_id || ['runjs', 'runpy'].includes(selectedQuery?.data_source_id) || !selectedDS)
) {
return setSelectedDataSource(defaultSources[selectedQuery?.kind]);
}
mode === 'edit' &&
setSelectedDataSource(
[...dataSources, ...globalDataSources].find(
(datasource) => datasource.id === selectedQuery?.data_source_id
) || null
);
} else if (selectedQuery === null) setSelectedDataSource(null);
setSelectedDataSource(selectedDS || null);
} else if (selectedQuery === null) {
setSelectedDataSource(null);
}
}, [selectedQuery, dataSources, globalDataSources, setSelectedDataSource, mode]);
return (
@ -85,31 +67,15 @@ const QueryManager = ({
'd-none': loadingDataSources,
})}
>
<QueryManagerHeader
darkMode={darkMode}
mode={mode}
addNewQueryAndDeselectSelectedQuery={addNewQueryAndDeselectSelectedQuery}
updateDraftQueryName={updateDraftQueryName}
toggleQueryEditor={toggleQueryEditor}
previewLoading={previewLoading}
options={options}
appId={appId}
ref={previewPanelRef}
editorRef={editorRef}
/>
<QueryManagerHeader darkMode={darkMode} options={options} editorRef={editorRef} appId={appId} />
<QueryManagerBody
darkMode={darkMode}
mode={mode}
dataSourceModalHandler={dataSourceModalHandler}
options={options}
previewLoading={previewLoading}
queryPreviewData={queryPreviewData}
allComponents={allComponents}
apps={apps}
appId={appId}
appDefinition={appDefinition}
createDraftQuery={createDraftQuery}
setOptions={setOptions}
ref={previewPanelRef}
/>
</div>
);

View file

@ -1,8 +1,8 @@
export const staticDataSources = [
{ kind: 'tooljetdb', id: 'null', name: 'Tooljet Database' },
{ kind: 'restapi', id: 'null', name: 'REST API' },
{ kind: 'runjs', id: 'runjs', name: 'Run JavaScript code' },
{ kind: 'runpy', id: 'runpy', name: 'Run Python code' },
{ kind: 'restapi', id: 'null', name: 'REST API', shortName: 'REST API' },
{ kind: 'runjs', id: 'runjs', name: 'Run JavaScript code', shortName: 'JavaScript' },
{ kind: 'runpy', id: 'runpy', name: 'Run Python code', shortName: 'Python' },
{ kind: 'tooljetdb', id: 'null', name: 'Tooljet Database', shortName: 'ToolJet DB' },
];
export const tabs = ['JSON', 'Raw'];
@ -53,7 +53,7 @@ export const customToggles = {
export const mockDataQueryAsComponent = (events) => {
return {
component: { component: { definition: { events } } },
component: { component: { definition: { events: events } } },
componentMeta: {
events: {
onDataQuerySuccess: { displayName: 'Query Success' },

View file

@ -0,0 +1,318 @@
import React, { useEffect, useRef, useState } from 'react';
import { OverlayTrigger, Popover, Form } from 'react-bootstrap';
import cx from 'classnames';
import { Button } from '@/_ui/LeftSidebar';
import { useDataSources, useGlobalDataSources } from '@/_stores/dataSourcesStore';
import Filter from '@/_ui/Icon/solidIcons/Filter';
import Arrowleft from '@/_ui/Icon/bulkIcons/Arrowleft';
import { useDataQueriesActions, useDataQueriesStore } from '@/_stores/dataQueriesStore';
import Tick from '@/_ui/Icon/solidIcons/Tick';
import useShowPopover from '@/_hooks/useShowPopover';
import DataSourceIcon from '../QueryManager/Components/DataSourceIcon';
import { staticDataSources } from '../QueryManager/constants';
import { Tooltip } from 'react-tooltip';
import { PillButton } from '../QueryManager/QueryEditors/Runjs/ParameterDetails';
const FilterandSortPopup = ({ darkMode, selectedDataSources, onFilterDatasourcesChange, clearSelectedDataSources }) => {
const [showMenu, setShowMenu] = useShowPopover(false, '#query-sort-filter-popover', '#query-sort-filter-popover-btn');
const closeMenu = () => setShowMenu(false);
const [action, setAction] = useState();
const [search, setSearch] = useState('');
const { sortDataQueries } = useDataQueriesActions();
const dataSources = useDataSources();
const globalDataSources = useGlobalDataSources();
const [sources, setSources] = useState();
const searchBoxRef = useRef(null);
const { sortBy, sortOrder, dataQueries } = useDataQueriesStore();
useState(() => {
if (action === 'filter-by-datasource' && searchBoxRef.current) {
searchBoxRef.current.focus();
}
}, [action]);
useEffect(() => {
if (showMenu) {
const seen = new Set();
const createdSources = dataQueries.map((query) => {
const globalDS = [...dataSources, ...globalDataSources].find((source) => source.id === query.data_source_id);
if (globalDS) {
return globalDS;
}
return {
...staticDataSources.find((source) => source.kind === query.kind),
id: null,
};
});
setSearch('');
setSources(
createdSources
.filter((source) => {
const key = source.kind + '-' + source.id;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
})
.sort((a, b) => {
const aChecked = selectedDataSources.some((item) => item.id === a.id && item.kind === a.kind);
const bChecked = selectedDataSources.some((item) => item.id === b.id && item.kind === b.kind);
if (aChecked && !bChecked) {
return -1;
}
if (!aChecked && bChecked) {
return 1;
}
return 0;
})
);
} else {
setAction();
}
}, [dataQueries, dataSources, globalDataSources, showMenu]);
const handlePageCallback = (action) => {
setAction(action);
};
const handleSort = (sortBy, sortOrder) => {
sortDataQueries(sortBy, sortOrder);
closeMenu();
};
const renderPopupComponent = (action) => {
switch (action) {
case 'filter-by-datasource':
return (
<DataSourceSelector
search={search}
setSearch={setSearch}
sources={sources}
onFilterDatasourcesChange={onFilterDatasourcesChange}
onBackBtnClick={() => setAction()}
selectedDataSources={selectedDataSources}
/>
);
default:
return (
<div
className="card-body p-0 tj-scrollbar query-editor-sort-filter-popup"
style={{ height: '315px', overflowY: 'auto' }}
>
<div className="color-slate9 px-3 pb-2 w-100">
<small data-cy="label-filter-by">Filter By</small>
</div>
<div className={`tj-list-btn mx-1 ${selectedDataSources.length ? 'd-flex' : ''}`}>
<MenuButton
id="filter-by-datasource"
text="Data Source"
callback={handlePageCallback}
disabled={dataQueries.length === 0}
noMargin
/>
{selectedDataSources.length ? (
<PillButton
name={selectedDataSources.length}
onRemove={clearSelectedDataSources}
onClick={() => handlePageCallback('filter-by-datasource')}
className="m-1 bg-slate6"
size="sm"
/>
) : (
''
)}
</div>
<div class="border-bottom mt-1"></div>
<div className="color-slate9 px-3 pb-2 pt-1 w-100">
<small data-cy="label-sort-by">Sort By</small>
</div>
<MenuButton
id="name"
order="asc"
text="Name: A-Z"
callback={handleSort}
active={sortBy === 'name' && sortOrder === 'asc'}
/>
<MenuButton
id="name"
order="desc"
text="Name: Z-A"
callback={handleSort}
active={sortBy === 'name' && sortOrder === 'desc'}
/>
<MenuButton
id="kind"
order="asc"
text="Type: A-Z"
callback={handleSort}
active={sortBy === 'kind' && sortOrder === 'asc'}
/>
<MenuButton
id="kind"
order="desc"
text="Type: Z-A"
callback={handleSort}
active={sortBy === 'kind' && sortOrder === 'desc'}
/>
<MenuButton
id="updated_at"
order="asc"
text="Last modified: oldest first"
callback={handleSort}
active={sortBy === 'updated_at' && sortOrder === 'asc'}
/>
<MenuButton
id="updated_at"
order="desc"
text="Last modified: newest first"
callback={handleSort}
active={sortBy === 'updated_at' && sortOrder === 'desc'}
/>
</div>
);
}
};
return (
<OverlayTrigger
placement="right-end"
// rootClose={true}
show={showMenu}
overlay={
<Popover
key={'page.i'}
id="query-sort-filter-popover"
className={`query-manager-sort-filter-popup ${darkMode && 'popover-dark-themed dark-theme tj-dark-mode'}`}
>
<Popover.Body key={'1'} bsPrefix="popover-body" className="pt-1 p-0">
{renderPopupComponent(action)}
</Popover.Body>
</Popover>
}
>
<span>
<button
id="query-sort-filter-popover-btn"
onClick={(e) => {
e.stopPropagation();
setShowMenu((showMenu) => !showMenu);
}}
className={cx('position-relative btn-query-panel-header', {
active: showMenu,
})}
style={{ ...(showMenu && { background: 'var(--slate5)' }) }}
data-tooltip-id="tooltip-for-open-filter"
data-tooltip-content="Show sort/filter"
data-cy={`query-filter-button`}
>
<Filter width="13" height="13" fill="var(--slate12)" />
{selectedDataSources.length > 0 && <div className="notification-dot"></div>}
</button>
<Tooltip id="tooltip-for-open-filter" className="tooltip" />
</span>
</OverlayTrigger>
);
};
const DataSourceSelector = ({
sources: _sources,
search,
setSearch,
onFilterDatasourcesChange,
onBackBtnClick,
selectedDataSources,
}) => {
const searchBoxRef = useRef(null);
const [sources, setSources] = useState([]);
useEffect(() => {
searchBoxRef.current.focus();
}, []);
useEffect(() => {
setSources(
_sources.filter((source) => {
if (!search || !source?.name) {
return true;
}
return source.name.toLowerCase().includes(search.toLowerCase());
})
);
}, [_sources, search]);
return (
<div className="card-body p-0 mt-1">
<div className="border-bottom d-flex px-2">
<div className="d-flex align-items-center mb-1">
<button className="border-0 bg-transparent rounded-0 p-0" onClick={onBackBtnClick}>
<Arrowleft fill="#3E63DD" tailOpacity={1} />
</button>
</div>
<div>
<input
className="bg-transparent border-0 form-control form-control-sm"
placeholder="Select datasource"
onChange={(e) => setSearch(e.target.value)}
ref={searchBoxRef}
value={search}
/>
</div>
</div>
<div className="tj-scrollbar py-2" style={{ height: '281px', overflowY: 'auto' }}>
{sources.map((source) => (
<label
className={cx('px-2 py-2 tj-list-btn d-block mx-1')}
key={source.id || source.kind}
role="button"
for={`default-${source.id || source.kind}`}
>
<Form.Check // prettier-ignore
type={'checkbox'}
id={`default-${source.id || source.kind}`}
onChange={(e) => onFilterDatasourcesChange(source, e.target.value)}
className="m-0"
checked={selectedDataSources.some((item) => item.id === source.id && item.kind === source.kind)}
label={
<div className="d-flex align-items-center">
<DataSourceIcon source={source} height={12} styles={{ minWidth: 12 }} />
&nbsp;<span className="ms-1 text-truncate">{source.name}</span>
</div>
}
/>
</label>
))}
</div>
</div>
);
};
const MenuButton = ({
id,
order,
text,
iconSrc,
disabled = false,
callback = () => null,
active,
noMargin = false,
}) => {
const handleOnClick = (e) => {
e.stopPropagation();
callback(id, order);
};
return (
<div className={`field p-2 ${noMargin ? '' : 'mx-1'} tj-list-btn`}>
<Button.UnstyledButton onClick={handleOnClick} disabled={disabled} classNames="d-flex justify-content-between">
<Button.Content title={text} iconSrc={iconSrc} direction="left" />
{active && <Tick width="20" height="20" viewBox="0 0 22 22" fill="var(--indigo9)" />}
</Button.UnstyledButton>
</div>
);
};
export default FilterandSortPopup;

View file

@ -1,49 +1,33 @@
import React, { useState } from 'react';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Tooltip from 'react-bootstrap/Tooltip';
import { getSvgIcon, checkExistingQueryName } from '@/_helpers/appUtils';
import { DataSourceTypes } from '../DataSourceManager/SourceComponents';
import { Tooltip } from 'react-tooltip';
import { checkExistingQueryName } from '@/_helpers/appUtils';
import { Confirm } from '../Viewer/Confirm';
import { toast } from 'react-hot-toast';
import { useDataQueriesActions, useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { useQueryPanelActions, useSelectedQuery, useUnsavedChanges } from '@/_stores/queryPanelStore';
import { useQueryPanelActions, useSelectedQuery } from '@/_stores/queryPanelStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import Copy from '@/_ui/Icon/solidIcons/Copy';
import DataSourceIcon from '../QueryManager/Components/DataSourceIcon';
import { isQueryRunnable } from '@/_helpers/utils';
export const QueryCard = ({
dataQuery,
setSaveConfirmation,
setCancelData,
setDraftQuery,
darkMode = false,
editorRef,
}) => {
export const QueryCard = ({ dataQuery, darkMode = false, editorRef, appId }) => {
const selectedQuery = useSelectedQuery();
const isUnsavedChangesAvailable = useUnsavedChanges();
const { isDeletingQueryInProcess } = useDataQueriesStore();
const { deleteDataQueries, renameQuery } = useDataQueriesActions();
const { setSelectedQuery, setSelectedDataSource, setUnSavedChanges } = useQueryPanelActions();
const { deleteDataQueries, renameQuery, duplicateQuery } = useDataQueriesActions();
const { setSelectedQuery, setPreviewData } = useQueryPanelActions();
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
const { isVersionReleased } = useAppVersionStore(
(state) => ({
isVersionReleased: state.isVersionReleased,
}),
shallow
);
const [renamingQuery, setRenamingQuery] = useState(false);
const getSourceMetaData = (dataSource) => {
if (dataSource?.plugin_id) {
return dataSource.plugin?.manifest_file?.data.source;
}
return DataSourceTypes.find((source) => source.kind === dataSource.kind);
};
const sourceMeta = getSourceMetaData(dataQuery);
const iconFile = dataQuery?.plugin?.iconFile?.data || dataQuery?.plugin?.icon_file?.data;
const icon = getSvgIcon(sourceMeta?.kind.toLowerCase(), 20, 20, iconFile);
let isSeletedQuery = false;
if (selectedQuery) {
isSeletedQuery = dataQuery.id === selectedQuery.id;
@ -54,24 +38,14 @@ export const QueryCard = ({
setShowDeleteConfirmation(true);
};
const cancelDeleteDataQuery = () => {
setShowDeleteConfirmation(false);
};
const updateQueryName = (selectedQuery, newName) => {
const { id, name } = selectedQuery;
const { name } = selectedQuery;
if (name === newName) {
return setRenamingQuery(false);
}
const isNewQueryNameAlreadyExists = checkExistingQueryName(newName);
if (newName && !isNewQueryNameAlreadyExists) {
if (id === 'draftQuery') {
toast.success('Query Name Updated');
setDraftQuery((query) => ({ ...query, name: newName }));
setSelectedQuery('draftQuery', { ...dataQuery, name: newName });
} else {
renameQuery(dataQuery?.id, newName, editorRef);
}
renameQuery(dataQuery?.id, newName, editorRef);
setRenamingQuery(false);
} else {
if (isNewQueryNameAlreadyExists) {
@ -83,36 +57,24 @@ export const QueryCard = ({
const executeDataQueryDeletion = () => {
setShowDeleteConfirmation(false);
if (dataQuery?.id === 'draftQuery') {
toast.success('Query Deleted');
setDraftQuery(null);
setSelectedQuery(null);
setUnSavedChanges(false);
setSelectedDataSource(null);
return;
}
deleteDataQueries(dataQuery?.id, editorRef);
};
return (
<>
<div
className={'row query-row' + (isSeletedQuery ? ' query-row-selected' : '')}
className={'row query-row pe-2' + (isSeletedQuery ? ' query-row-selected' : '')}
key={dataQuery.id}
onClick={() => {
if (selectedQuery?.id === dataQuery?.id) return;
const stateToBeUpdated = { editingQuery: true, selectedQuery: dataQuery, draftQuery: null };
if (isUnsavedChangesAvailable) {
setSaveConfirmation(true);
setCancelData(stateToBeUpdated);
} else {
setSelectedQuery(dataQuery?.id);
setDraftQuery(null);
}
setSelectedQuery(dataQuery?.id);
setPreviewData(null);
}}
role="button"
>
<div className="col-auto query-icon d-flex">{icon}</div>
<div className="col-auto query-icon d-flex">
<DataSourceIcon source={dataQuery} height={16} />
</div>
<div className="col query-row-query-name">
{renamingQuery ? (
<input
@ -123,6 +85,11 @@ export const QueryCard = ({
type="text"
defaultValue={dataQuery.name}
autoFocus={true}
onKeyDown={({ target, key }) => {
if (key === 'Enter') {
updateQueryName(selectedQuery, target.value);
}
}}
onBlur={({ target }) => {
updateQueryName(selectedQuery, target.value);
}}
@ -135,7 +102,15 @@ export const QueryCard = ({
overlay={<Tooltip id="button-tooltip">{dataQuery.name}</Tooltip>}
>
<div className="query-name" data-cy={`list-query-${dataQuery.name.toLowerCase()}`}>
{dataQuery.name}
<span
className="text-truncate"
data-tooltip-id="query-card-name-tooltip"
data-tooltip-content={dataQuery.name}
>
{dataQuery.name}
</span>{' '}
<Tooltip id="query-card-name-tooltip" className="tooltip query-manager-tooltip" />
{!isQueryRunnable(dataQuery) && <small className="mx-2 text-secondary">Draft</small>}
</div>
</OverlayTrigger>
)}
@ -147,7 +122,7 @@ export const QueryCard = ({
className={`col-auto ${renamingQuery && 'display-none'} rename-query`}
onClick={() => setRenamingQuery(true)}
>
<span className="d-flex">
<span className="d-flex" data-tooltip-id="query-card-btn-tooltip" data-tooltip-content="Rename query">
<svg
data-cy={`edit-query-${dataQuery.name.toLowerCase()}`}
width="auto"
@ -165,13 +140,23 @@ export const QueryCard = ({
</svg>
</span>
</div>
<div className={`col-auto rename-query`} onClick={() => duplicateQuery(dataQuery?.id, appId)}>
<span className="d-flex" data-tooltip-id="query-card-btn-tooltip" data-tooltip-content="Duplicate query">
<Copy height={16} width={16} viewBox="0 5 20 20" />
</span>
</div>
<div className="col-auto">
{isDeletingQueryInProcess ? (
<div className="px-2">
<div className="text-center spinner-border spinner-border-sm" role="status"></div>
</div>
) : (
<span className="delete-query" onClick={deleteDataQuery} disabled={dataQuery?.id === 'draftQuery'}>
<span
className="delete-query"
onClick={deleteDataQuery}
data-tooltip-id="query-card-btn-tooltip"
data-tooltip-content="Delete query"
>
<span className="d-flex">
<svg
data-cy={`delete-query-${dataQuery.name.toLowerCase()}`}
@ -192,19 +177,18 @@ export const QueryCard = ({
</span>
)}
</div>
<Tooltip id="query-card-btn-tooltip" className="tooltip" />
</div>
)}
</div>
{showDeleteConfirmation ? (
<Confirm
show={showDeleteConfirmation}
message={'Do you really want to delete this query?'}
confirmButtonLoading={isDeletingQueryInProcess}
onConfirm={executeDataQueryDeletion}
onCancel={cancelDeleteDataQuery}
darkMode={darkMode}
/>
) : null}
<Confirm
show={showDeleteConfirmation}
message={'Do you really want to delete this query?'}
confirmButtonLoading={isDeletingQueryInProcess}
onConfirm={executeDataQueryDeletion}
onCancel={() => setShowDeleteConfirmation(false)}
darkMode={darkMode}
/>
</>
);
};

View file

@ -1,99 +1,157 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { isEmpty } from 'lodash';
import { SearchBox } from '@/_components/SearchBox';
import Minimize from '@/_ui/Icon/solidIcons/Minimize';
import Search from '@/_ui/Icon/solidIcons/Search';
import Skeleton from 'react-loading-skeleton';
import EmptyQueriesIllustration from '@assets/images/icons/no-queries-added.svg';
import { QueryCard } from './QueryCard';
import Fuse from 'fuse.js';
import cx from 'classnames';
import { Tooltip } from 'react-tooltip';
import { useDataQueriesStore, useDataQueries } from '@/_stores/dataQueriesStore';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
import FilterandSortPopup from './FilterandSortPopup';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import Plus from '@/_ui/Icon/solidIcons/Plus';
import useShowPopover from '@/_hooks/useShowPopover';
import DataSourceSelect from '../QueryManager/Components/DataSourceSelect';
import { OverlayTrigger, Popover } from 'react-bootstrap';
import FolderEmpty from '@/_ui/Icon/solidIcons/FolderEmpty';
export const QueryDataPane = ({
setSaveConfirmation,
setCancelData,
draftQuery,
handleAddNewQuery,
setDraftQuery,
darkMode,
fetchDataQueries,
editorRef,
}) => {
export const QueryDataPane = ({ darkMode, fetchDataQueries, editorRef, appId, toggleQueryEditor }) => {
const { t } = useTranslation();
const { loadingDataQueries } = useDataQueriesStore();
const dataQueries = useDataQueries();
const [filteredQueries, setFilteredQueries] = useState(dataQueries);
const { isVersionReleased } = useAppVersionStore(
(state) => ({
isVersionReleased: state.isVersionReleased,
}),
shallow
);
useEffect(() => {
setFilteredQueries(dataQueries);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(dataQueries)]);
const [showSearchBox, setShowSearchBox] = useState(false);
const searchBoxRef = useRef(null);
const [dataSourcesForFilters, setDataSourcesForFilters] = useState([]);
const [searchTermForFilters, setSearchTermForFilters] = useState();
useEffect(() => {
// Create a copy of the dataQueries array to perform filtering without modifying the original data.
let filteredDataQueries = [...dataQueries];
// Filter the dataQueries based on the selected data sources (dataSourcesForFilters).
if (!isEmpty(dataSourcesForFilters)) {
const excludedDataSources = ['runjs', 'runpy'];
filteredDataQueries = dataQueries.filter((query) => {
const queryDSId = excludedDataSources.includes(query.data_source_id) ? null : query.data_source_id;
return dataSourcesForFilters.some((source) => source.id == queryDSId && source.kind === query.kind);
});
}
// Apply additional filtering based on the search term (searchTermForFilters).
filterQueries(searchTermForFilters, filteredDataQueries);
const filterQueries = useCallback(
(value) => {
if (value) {
const fuse = new Fuse(dataQueries, { keys: ['name'] });
const results = fuse.search(value);
let filterDataQueries = [];
results.every((result) => {
if (result.item.name === value) {
filterDataQueries = [];
filterDataQueries.push(result.item);
return false;
}
filterDataQueries.push(result.item);
return true;
});
setFilteredQueries(filterDataQueries);
} else {
setFilteredQueries(dataQueries);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[JSON.stringify(dataQueries)]
);
}, [JSON.stringify(dataQueries), dataSourcesForFilters, searchTermForFilters]);
const handleFilterDatasourcesChange = (source) => {
const { id, kind } = source;
setDataSourcesForFilters((dataSourcesForFilters) => {
const exists = dataSourcesForFilters.some((item) => item.id === id && item.kind === kind);
return exists
? dataSourcesForFilters.filter((item) => item.id !== id || item.kind !== kind)
: [...dataSourcesForFilters, source];
});
};
const filterQueries = (value, queries) => {
if (value) {
const fuse = new Fuse(queries, { keys: ['name'] });
const results = fuse.search(value);
let filterDataQueries = [];
results.every((result) => {
if (result.item.name === value) {
filterDataQueries = [];
filterDataQueries.push(result.item);
return false;
}
filterDataQueries.push(result.item);
return true;
});
setFilteredQueries(filterDataQueries);
} else {
setFilteredQueries(queries);
}
};
useEffect(() => {
showSearchBox && searchBoxRef.current.focus();
}, [showSearchBox]);
return (
<div className="data-pane">
<div className={`queries-container ${darkMode && 'theme-dark'}`}>
<div className={`queries-container ${darkMode && 'theme-dark'} d-flex flex-column h-100`}>
<div className="queries-header row d-flex align-items-center justify-content-between">
<div className="col-auto">
<div className={`queries-search ${darkMode && 'theme-dark'}`}>
<div className="col-auto d-flex">
<button
onClick={toggleQueryEditor}
className="btn-query-panel-header"
data-tooltip-id="tooltip-for-query-panel-header-btn"
data-tooltip-content="Hide query panel"
>
<Minimize width="14" height="14" viewBox="0 0 18 20" stroke="var(--slate12)" />
</button>
<button
onClick={() => {
showSearchBox && setSearchTermForFilters('');
setShowSearchBox((showSearchBox) => !showSearchBox);
}}
className={cx('btn-query-panel-header mx-1', {
active: showSearchBox,
})}
data-tooltip-id="tooltip-for-query-panel-header-btn"
data-tooltip-content="Open quick search"
data-cy="query-search-button"
>
<Search width="14" height="14" fill="var(--slate12)" />
</button>
<FilterandSortPopup
onFilterDatasourcesChange={handleFilterDatasourcesChange}
selectedDataSources={dataSourcesForFilters}
clearSelectedDataSources={() => setDataSourcesForFilters([])}
darkMode={darkMode}
/>
<Tooltip id="tooltip-for-query-panel-header-btn" className="tooltip" />
</div>
<AddDataSourceButton darkMode={darkMode} disabled={isEmpty(dataQueries)} />
</div>
<div
className={cx('queries-header row d-flex align-items-center justify-content-between', {
'd-none': !showSearchBox,
})}
>
<div className="col-auto w-100">
<div className={`queries-search d-flex ${darkMode && 'theme-dark'}`}>
<SearchBox
ref={searchBoxRef}
dataCy={`query-manager`}
width="100%"
onSubmit={filterQueries}
initialValue={searchTermForFilters}
callBack={(val) => {
setSearchTermForFilters(val.target.value);
}}
onClearCallback={() => setSearchTermForFilters('')}
placeholder={t('globals.search', 'Search')}
customClass="query-manager-search-box-wrapper"
customClass="query-manager-search-box-wrapper flex-grow-1"
showClearButton
/>
<ButtonSolid
size="sm"
variant="ghostBlue"
className="ms-1"
onClick={() => {
setSearchTermForFilters('');
setShowSearchBox(false);
}}
data-cy={`query-search-close-button`}
>
Close
</ButtonSolid>
</div>
</div>
<button
data-cy={`button-add-new-queries`}
className={cx(`col-auto d-flex align-items-center py-1 rounded default-secondary-button`, {
disabled: isVersionReleased,
'theme-dark': darkMode,
})}
onClick={handleAddNewQuery}
data-tooltip-id="tooltip-for-add-query"
data-tooltip-content="Add new query"
>
<span className={` d-flex query-manager-btn-svg-wrapper align-items-center query-icon-wrapper`}>
<svg width="auto" height="auto" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8 15.25C7.71667 15.25 7.47917 15.1542 7.2875 14.9625C7.09583 14.7708 7 14.5333 7 14.25V9H1.75C1.46667 9 1.22917 8.90417 1.0375 8.7125C0.845833 8.52083 0.75 8.28333 0.75 8C0.75 7.71667 0.845833 7.47917 1.0375 7.2875C1.22917 7.09583 1.46667 7 1.75 7H7V1.75C7 1.46667 7.09583 1.22917 7.2875 1.0375C7.47917 0.845833 7.71667 0.75 8 0.75C8.28333 0.75 8.52083 0.845833 8.7125 1.0375C8.90417 1.22917 9 1.46667 9 1.75V7H14.25C14.5333 7 14.7708 7.09583 14.9625 7.2875C15.1542 7.47917 15.25 7.71667 15.25 8C15.25 8.28333 15.1542 8.52083 14.9625 8.7125C14.7708 8.90417 14.5333 9 14.25 9H9V14.25C9 14.5333 8.90417 14.7708 8.7125 14.9625C8.52083 15.1542 8.28333 15.25 8 15.25Z"
fill="#3E63DD"
/>
</svg>
</span>
<span className="query-manager-btn-name">Add</span>
</button>
</div>
{loadingDataQueries ? (
@ -102,41 +160,26 @@ export const QueryDataPane = ({
<Skeleton height={'36px'} className="skeleton" />
</div>
) : (
<div className="query-list">
<div
className={`query-list tj-scrollbar overflow-auto ${
filteredQueries.length === 0 ? 'flex-grow-1 align-items-center justify-content-center' : ''
}`}
>
<div>
{draftQuery !== null ? (
<QueryCard
key={draftQuery.id}
dataQuery={draftQuery}
setSaveConfirmation={setSaveConfirmation}
setCancelData={setCancelData}
setDraftQuery={setDraftQuery}
fetchDataQueries={fetchDataQueries}
darkMode={darkMode}
editorRef={editorRef}
/>
) : (
''
)}
{filteredQueries.map((query) => (
<QueryCard
key={query.id}
dataQuery={query}
setSaveConfirmation={setSaveConfirmation}
setCancelData={setCancelData}
setDraftQuery={setDraftQuery}
fetchDataQueries={fetchDataQueries}
darkMode={darkMode}
editorRef={editorRef}
appId={appId}
/>
))}
</div>
{filteredQueries.length === 0 && draftQuery === null && (
{filteredQueries.length === 0 && (
<div className=" d-flex flex-column align-items-center justify-content-start">
<EmptyQueriesIllustration />
<span data-cy="no-query-message" className="mute-text pt-3">
{dataQueries.length === 0 ? 'No queries added' : 'No queries found'}
</span>
{filteredQueries.length === 0 ? <EmptyDataSource /> : ''}
<br />
</div>
)}
@ -146,3 +189,67 @@ export const QueryDataPane = ({
</div>
);
};
const EmptyDataSource = () => (
<div>
<div className="text-center">
<span
className="rounded mb-3 bg-slate3 d-flex justify-content-center align-items-center"
style={{ width: '32px', height: '32px' }}
>
<FolderEmpty style={{ height: '16px' }} />
</span>
</div>
<span>No queries have been added. </span>
</div>
);
const AddDataSourceButton = ({ darkMode, disabled }) => {
const [showMenu, setShowMenu] = useShowPopover(false, '#query-add-ds-popover', '#query-add-ds-popover-btn');
const selectRef = useRef();
useEffect(() => {
if (showMenu) {
selectRef.current.focus();
}
}, [showMenu]);
return (
<OverlayTrigger
show={showMenu && !disabled}
placement="right-end"
arrowOffsetTop={90}
arrowOffsetLeft={90}
overlay={
<Popover
key={'page.i'}
id="query-add-ds-popover"
className={`${darkMode && 'popover-dark-themed dark-theme tj-dark-mode'}`}
style={{ width: '244px', maxWidth: '246px' }}
>
<DataSourceSelect selectRef={selectRef} closePopup={() => setShowMenu(false)} />
</Popover>
}
>
<span className="col-auto" id="query-add-ds-popover-btn">
<ButtonSolid
size="sm"
variant="tertiary"
disabled={disabled}
onClick={(e) => {
e.stopPropagation();
if (disabled) {
return;
}
setShowMenu((show) => !show);
}}
className="px-1 pe-3 ps-2 gap-0"
data-cy={`show-ds-popover-button`}
>
<Plus style={{ height: '16px' }} />
Add
</ButtonSolid>
</span>
</OverlayTrigger>
);
};

View file

@ -2,12 +2,14 @@ import React, { useState, useRef, useCallback, useEffect } from 'react';
import { useEventListener } from '@/_hooks/use-event-listener';
import { Tooltip } from 'react-tooltip';
import { QueryDataPane } from './QueryDataPane';
import { Confirm } from '../Viewer/Confirm';
import QueryManager from '../QueryManager/QueryManager';
import useWindowResize from '@/_hooks/useWindowResize';
import { useQueryPanelActions, useUnsavedChanges, useSelectedQuery } from '@/_stores/queryPanelStore';
import { useDataQueries } from '@/_stores/dataQueriesStore';
import { useQueryPanelStore, useQueryPanelActions } from '@/_stores/queryPanelStore';
import { useDataQueriesStore, useDataQueries } from '@/_stores/dataQueriesStore';
import Maximize from '@/_ui/Icon/solidIcons/Maximize';
import { cloneDeep, isEmpty, isEqual } from 'lodash';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
const QueryPanel = ({
dataQueriesChanged,
@ -17,14 +19,11 @@ const QueryPanel = ({
allComponents,
appId,
appDefinition,
dataSourceModalHandler,
editorRef,
onQueryPaneDragging,
handleQueryPaneExpanding,
}) => {
const { setSelectedQuery, updateQueryPanelHeight, setUnSavedChanges, setSelectedDataSource } = useQueryPanelActions();
const isUnsavedQueriesAvailable = useUnsavedChanges();
const selectedQuery = useSelectedQuery();
const { updateQueryPanelHeight } = useQueryPanelActions();
const dataQueries = useDataQueries();
const queryManagerPreferences = useRef(JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {});
const queryPaneRef = useRef(null);
@ -36,28 +35,38 @@ const QueryPanel = ({
: queryManagerPreferences.current?.queryPanelHeight ?? 70
);
const [isTopOfQueryPanel, setTopOfQueryPanel] = useState(false);
const [showSaveConfirmation, setSaveConfirmation] = useState(false);
const [queryCancelData, setCancelData] = useState({});
const [draftQuery, setDraftQuery] = useState(null);
const [editingQuery, setEditingQuery] = useState(dataQueries.length > 0);
const [windowSize, isWindowResizing] = useWindowResize();
useEffect(() => {
if (!editingQuery && selectedQuery !== null && selectedQuery?.id !== 'draftQuery') {
setEditingQuery(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedQuery?.id, editingQuery]);
const queryPanelStoreListner = useQueryPanelStore.subscribe(({ selectedQuery }, prevState) => {
if (isEmpty(prevState?.selectedQuery) || isEmpty(selectedQuery)) {
return;
}
if (prevState?.selectedQuery?.id !== selectedQuery.id) {
return;
}
//removing updated_at since this value changes whenever the data is updated in the BE
const formattedQuery = cloneDeep(selectedQuery);
delete formattedQuery.updated_at;
const formattedPrevQuery = cloneDeep(prevState?.selectedQuery || {});
delete formattedPrevQuery.updated_at;
if (!isEqual(formattedQuery, formattedPrevQuery)) {
useDataQueriesStore.getState().actions.saveData(selectedQuery);
}
});
return queryPanelStoreListner;
}, []);
useEffect(() => {
handleQueryPaneExpanding(isExpanded);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isExpanded]);
useEffect(() => {
setEditingQuery(dataQueries.length > 0);
}, [dataQueries.length]);
useEffect(() => {
onQueryPaneDragging(isDragging);
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -73,14 +82,6 @@ const QueryPanel = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [windowSize.height, isExpanded, isWindowResizing]);
const createDraftQuery = useCallback((queryDetails, source) => {
setSelectedQuery(queryDetails.id, queryDetails);
setDraftQuery(queryDetails);
setSelectedDataSource(source);
setEditingQuery(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onMouseUp = () => {
setDragging(false);
@ -127,27 +128,6 @@ const QueryPanel = ({
useEventListener('mousemove', onMouseMove);
useEventListener('mouseup', onMouseUp);
const handleAddNewQuery = useCallback(() => {
const stateToBeUpdated = {
selectedDataSource: null,
selectedQuery: null,
editingQuery: false,
isSourceSelected: false,
draftQuery: null,
};
if (isUnsavedQueriesAvailable) {
setSaveConfirmation(true);
setCancelData(stateToBeUpdated);
} else {
setSelectedDataSource(null);
setSelectedQuery(null);
setDraftQuery(null);
setEditingQuery(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isUnsavedQueriesAvailable]);
const toggleQueryEditor = useCallback(() => {
queryManagerPreferences.current = { ...queryManagerPreferences.current, isExpanded: !isExpanded };
localStorage.setItem('queryManagerPreferences', JSON.stringify(queryManagerPreferences.current));
@ -161,85 +141,47 @@ const QueryPanel = ({
}, [isExpanded]);
const updateDataQueries = useCallback(() => {
setEditingQuery(true);
setDraftQuery(null);
dataQueriesChanged();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const updateDraftQueryName = useCallback(
(newName) => {
setDraftQuery((query) => ({ ...query, name: newName }));
setSelectedQuery(draftQuery.id, { ...draftQuery, name: newName });
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[draftQuery]
);
return (
<>
<Confirm
show={showSaveConfirmation}
message={`Query ${selectedQuery?.name} has unsaved changes. Are you sure you want to discard changes ?`}
onConfirm={() => {
setSaveConfirmation(false);
setDraftQuery(null);
setSelectedQuery(queryCancelData?.selectedQuery?.id ?? null);
setSelectedDataSource(queryCancelData?.selectedDataSource ?? null);
setUnSavedChanges(false);
if (queryCancelData.hasOwnProperty('editingQuery')) {
setEditingQuery(queryCancelData.editingQuery);
}
}}
onCancel={() => {
setSaveConfirmation(false);
}}
confirmButtonText="Discard changes"
cancelButtonText="Continue editing"
callCancelFnOnConfirm={false}
darkMode={darkMode}
/>
<div
className="query-pane"
style={{
height: 40,
background: '#fff',
padding: '8px 16px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<h5 className="mb-0 font-weight-500 cursor-pointer" onClick={toggleQueryEditor}>
QUERIES
</h5>
<span
<div
style={{ width: '288px', padding: '5px 12px' }}
className="d-flex justify-content- border-end align-items-center"
role="button"
onClick={toggleQueryEditor}
className="cursor-pointer m-1 d-flex"
data-tooltip-id="tooltip-for-show-query-editor"
data-tooltip-content="Show query editor"
>
{isExpanded ? (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8.00013 6.18288C7.94457 6.18288 7.88624 6.17177 7.82513 6.14954C7.76402 6.12732 7.70569 6.08843 7.65013 6.03288L5.3668 3.74954C5.2668 3.64954 5.2168 3.52732 5.2168 3.38288C5.2168 3.23843 5.2668 3.11621 5.3668 3.01621C5.4668 2.91621 5.58346 2.86621 5.7168 2.86621C5.85013 2.86621 5.9668 2.91621 6.0668 3.01621L8.00013 4.94954L9.93346 3.01621C10.0335 2.91621 10.1529 2.86621 10.2918 2.86621C10.4307 2.86621 10.5501 2.91621 10.6501 3.01621C10.7501 3.11621 10.8001 3.23566 10.8001 3.37454C10.8001 3.51343 10.7501 3.63288 10.6501 3.73288L8.35013 6.03288C8.29457 6.08843 8.23902 6.12732 8.18346 6.14954C8.12791 6.17177 8.0668 6.18288 8.00013 6.18288ZM5.3668 12.9662C5.2668 12.8662 5.2168 12.7468 5.2168 12.6079C5.2168 12.469 5.2668 12.3495 5.3668 12.2495L7.65013 9.96621C7.70569 9.91065 7.76402 9.87177 7.82513 9.84954C7.88624 9.82732 7.94457 9.81621 8.00013 9.81621C8.0668 9.81621 8.12791 9.82732 8.18346 9.84954C8.23902 9.87177 8.29457 9.91065 8.35013 9.96621L10.6501 12.2662C10.7501 12.3662 10.8001 12.4829 10.8001 12.6162C10.8001 12.7495 10.7501 12.8662 10.6501 12.9662C10.5501 13.0662 10.4279 13.1162 10.2835 13.1162C10.139 13.1162 10.0168 13.0662 9.9168 12.9662L8.00013 11.0495L6.08346 12.9662C5.98346 13.0662 5.86402 13.1162 5.72513 13.1162C5.58624 13.1162 5.4668 13.0662 5.3668 12.9662Z"
fill="#121212"
/>
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.3668 5.43327C5.2668 5.33327 5.2168 5.21105 5.2168 5.0666C5.2168 4.92216 5.2668 4.79994 5.3668 4.69994L7.65013 2.4166C7.70569 2.36105 7.76124 2.32216 7.8168 2.29993C7.87235 2.27771 7.93346 2.2666 8.00013 2.2666C8.05569 2.2666 8.11402 2.27771 8.17513 2.29993C8.23624 2.32216 8.29457 2.36105 8.35013 2.4166L10.6335 4.69994C10.7335 4.79994 10.7835 4.92216 10.7835 5.0666C10.7835 5.21105 10.7335 5.33327 10.6335 5.43327C10.5335 5.53327 10.4112 5.58327 10.2668 5.58327C10.1224 5.58327 10.0001 5.53327 9.90013 5.43327L8.00013 3.53327L6.10013 5.43327C6.00013 5.53327 5.87791 5.58327 5.73346 5.58327C5.58902 5.58327 5.4668 5.53327 5.3668 5.43327V5.43327ZM8.00013 13.7999C7.94457 13.7999 7.88624 13.7888 7.82513 13.7666C7.76402 13.7444 7.70569 13.7055 7.65013 13.6499L5.3668 11.3666C5.2668 11.2666 5.2168 11.1444 5.2168 10.9999C5.2168 10.8555 5.2668 10.7333 5.3668 10.6333C5.4668 10.5333 5.58902 10.4833 5.73346 10.4833C5.87791 10.4833 6.00013 10.5333 6.10013 10.6333L8.00013 12.5333L9.90013 10.6333C10.0001 10.5333 10.1224 10.4833 10.2668 10.4833C10.4112 10.4833 10.5335 10.5333 10.6335 10.6333C10.7335 10.7333 10.7835 10.8555 10.7835 10.9999C10.7835 11.1444 10.7335 11.2666 10.6335 11.3666L8.35013 13.6499C8.29457 13.7055 8.23902 13.7444 8.18346 13.7666C8.12791 13.7888 8.0668 13.7999 8.00013 13.7999V13.7999Z"
fill="#576574"
/>
</svg>
)}
</span>
<ButtonSolid
variant="ghostBlack"
size="sm"
className="gap-0 p-2 me-2"
data-tooltip-id="tooltip-for-query-panel-footer-btn"
data-tooltip-content="Show query panel"
>
<Maximize stroke="var(--slate9)" style={{ height: '14px', width: '14px' }} viewBox={null} />
</ButtonSolid>
<h5 className="mb-0 font-weight-500 cursor-pointer" onClick={toggleQueryEditor}>
Query Manager
</h5>
</div>
</div>
<div
ref={queryPaneRef}
onMouseDown={onMouseDown}
className="query-pane"
id="query-manager"
style={{
height: `calc(100% - ${isExpanded ? height : 100}%)`,
cursor: isDragging || isTopOfQueryPanel ? 'row-resize' : 'default',
@ -247,42 +189,32 @@ const QueryPanel = ({
>
<div className="row main-row">
<QueryDataPane
showSaveConfirmation={showSaveConfirmation}
setSaveConfirmation={setSaveConfirmation}
setCancelData={setCancelData}
draftQuery={draftQuery}
handleAddNewQuery={handleAddNewQuery}
setDraftQuery={setDraftQuery}
fetchDataQueries={fetchDataQueries}
darkMode={darkMode}
editorRef={editorRef}
appId={appId}
toggleQueryEditor={toggleQueryEditor}
/>
<div className="query-definition-pane-wrapper">
<div className="query-definition-pane">
<div>
<QueryManager
addNewQueryAndDeselectSelectedQuery={handleAddNewQuery}
toggleQueryEditor={toggleQueryEditor}
dataQueries={dataQueries}
mode={editingQuery ? 'edit' : 'create'}
dataQueriesChanged={updateDataQueries}
appId={appId}
darkMode={darkMode}
apps={apps}
allComponents={allComponents}
dataSourceModalHandler={dataSourceModalHandler}
appDefinition={appDefinition}
editorRef={editorRef}
createDraftQuery={createDraftQuery}
isUnsavedQueriesAvailable={isUnsavedQueriesAvailable}
updateDraftQueryName={updateDraftQueryName}
/>
</div>
</div>
</div>
</div>
</div>
<Tooltip id="tooltip-for-show-query-editor" className="tooltip" />
<Tooltip id="tooltip-for-query-panel-footer-btn" className="tooltip" />
</>
);
};

View file

@ -23,6 +23,7 @@ import {
stripTrailingSlash,
getSubpath,
excludeWorkspaceIdFromURL,
isQueryRunnable,
redirectToDashboard,
getWorkspaceId,
} from '@/_helpers/utils';
@ -184,7 +185,7 @@ class ViewerComponent extends React.Component {
runQueries = (data_queries) => {
data_queries.forEach((query) => {
if (query.options.runOnPageLoad) {
if (query.options.runOnPageLoad && isQueryRunnable(query)) {
runQuery(this, query.id, query.name, undefined, 'view');
}
});

View file

@ -22,7 +22,7 @@ export function Confirm({
}, [show]);
const handleClose = () => {
onCancel();
onCancel && onCancel();
setShow(false);
};

View file

@ -33,8 +33,11 @@ const DynamicForm = ({
optionsChanged,
queryName,
computeSelectStyles = false,
onBlur,
layout = 'vertical',
}) => {
const [computedProps, setComputedProps] = React.useState({});
const isHorizontalLayout = layout === 'horizontal';
const currentState = useCurrentState();
const { isEditorActive } = useEditorStore(
@ -184,7 +187,8 @@ const DynamicForm = ({
value: options?.[key]?.value,
...(type === 'textarea' && { rows: rows }),
...(helpText && { helpText }),
onChange: (e) => optionchanged(key, e.target.value),
onChange: (e) => optionchanged(key, e.target.value, true), //shouldNotAutoSave is true because autosave should occur during onBlur, not after each character change (in optionchanged).
onblur: () => onBlur(),
isGDS,
workspaceVariables,
};
@ -356,56 +360,80 @@ const DynamicForm = ({
};
return (
<div className="row">
<div className={`${isHorizontalLayout ? '' : 'row'}`}>
{Object.keys(obj).map((key) => {
const { label, type, encrypted, className } = obj[key];
const Element = getElement(type);
const isSpecificComponent = ['tooljetdb-operations'].includes(type);
return (
<div className={cx('my-2', { 'col-md-12': !className, [className]: !!className })} key={key}>
<div className="d-flex align-items-center">
{label && (
<label
className="form-label"
data-cy={`label-${String(label).toLocaleLowerCase().replace(/\s+/g, '-')}`}
>
{label}
</label>
)}
{(type === 'password' || encrypted) && selectedDataSource?.id && (
<div className="mx-1 col">
<ButtonSolid
className="datasource-edit-btn mb-2"
type="a"
variant="tertiary"
target="_blank"
rel="noreferrer"
onClick={(event) => handleEncryptedFieldsToggle(event, key)}
<div
className={cx('my-2', {
'col-md-12': !className && !isHorizontalLayout,
[className]: !!className,
'd-flex': isHorizontalLayout,
'dynamic-form-row': isHorizontalLayout,
})}
key={key}
>
{!isSpecificComponent && (
<div
className={cx('d-flex', {
'form-label': isHorizontalLayout,
'align-items-center': !isHorizontalLayout,
})}
>
{label && (
<label
className="form-label"
data-cy={`label-${String(label).toLocaleLowerCase().replace(/\s+/g, '-')}`}
>
{computedProps?.[key]?.['disabled'] ? 'Edit' : 'Cancel'}
</ButtonSolid>
</div>
)}
{(type === 'password' || encrypted) && (
<div className="col-auto mb-2">
<small className="text-green">
<img
className="mx-2 encrypted-icon"
src="assets/images/icons/padlock.svg"
width="12"
height="12"
/>
Encrypted
</small>
</div>
)}
{label}
</label>
)}
{(type === 'password' || encrypted) && selectedDataSource?.id && (
<div className="mx-1 col">
<ButtonSolid
className="datasource-edit-btn mb-2"
type="a"
variant="tertiary"
target="_blank"
rel="noreferrer"
onClick={(event) => handleEncryptedFieldsToggle(event, key)}
>
{computedProps?.[key]?.['disabled'] ? 'Edit' : 'Cancel'}
</ButtonSolid>
</div>
)}
{(type === 'password' || encrypted) && (
<div className="col-auto mb-2">
<small className="text-green">
<img
className="mx-2 encrypted-icon"
src="assets/images/icons/padlock.svg"
width="12"
height="12"
/>
Encrypted
</small>
</div>
)}
</div>
)}
<div
className={cx({
'flex-grow-1': isHorizontalLayout && !isSpecificComponent,
'w-100': isHorizontalLayout && type !== 'codehinter',
})}
>
<Element
{...getElementProps(obj[key])}
{...computedProps[key]}
data-cy={`${String(label).toLocaleLowerCase().replace(/\s+/g, '-')}-text-field`}
customWrap={true} //to be removed after whole ui is same
isHorizontalLayout={isHorizontalLayout}
/>
</div>
<Element
{...getElementProps(obj[key])}
{...computedProps[key]}
data-cy={`${String(label).toLocaleLowerCase().replace(/\s+/g, '-')}-text-field`}
customWrap={true} //to be removed after whole ui is same
/>
</div>
);
})}
@ -421,17 +449,19 @@ const DynamicForm = ({
const selector = options?.[flipComponentDropdown?.key]?.value || options?.[flipComponentDropdown?.key];
return (
<>
<div className="row">
<div className={`${isHorizontalLayout ? '' : 'row'}`}>
{flipComponentDropdown.commonFields && getLayout(flipComponentDropdown.commonFields)}
<div
className={cx('my-2', {
'col-md-12': !flipComponentDropdown.className,
'col-md-12': !flipComponentDropdown.className && !isHorizontalLayout,
'd-flex': isHorizontalLayout,
'dynamic-form-row': isHorizontalLayout,
[flipComponentDropdown.className]: !!flipComponentDropdown.className,
})}
>
{flipComponentDropdown.label && (
{(flipComponentDropdown.label || isHorizontalLayout) && (
<label
className="form-label"
className={cx('form-label')}
data-cy={`${String(flipComponentDropdown.label)
.toLocaleLowerCase()
.replace(/\s+/g, '-')}-dropdown-label`}
@ -439,7 +469,7 @@ const DynamicForm = ({
{flipComponentDropdown.label}
</label>
)}
<div data-cy={'query-select-dropdown'}>
<div data-cy={'query-select-dropdown'} className={cx({ 'flex-grow-1': isHorizontalLayout })}>
<Select
{...getElementProps(flipComponentDropdown)}
styles={computeSelectStyles ? computeSelectStyles('100%') : {}}

View file

@ -1,80 +1,93 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, forwardRef } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import useDebounce from '@/_hooks/useDebounce';
import { useMounted } from '@/_hooks/use-mount';
import SolidIcon from '@/_ui/Icon/SolidIcons';
export function SearchBox({
width = '200px',
onSubmit,
className,
debounceDelay = 300,
darkMode = false,
placeholder = 'Search',
customClass = '',
dataCy = '',
callBack,
onClearCallback,
autoFocus = false,
}) {
const [searchText, setSearchText] = useState('');
const debouncedSearchTerm = useDebounce(searchText, debounceDelay);
const [isFocused, setFocussed] = useState(false);
export const SearchBox = forwardRef(
(
{
width = '200px',
onSubmit,
className,
debounceDelay = 300,
darkMode = false,
placeholder = 'Search',
customClass = '',
dataCy = '',
callBack,
onClearCallback,
autoFocus = false,
showClearButton,
initialValue = '',
},
ref
) => {
const [searchText, setSearchText] = useState('');
const debouncedSearchTerm = useDebounce(searchText, debounceDelay);
const [isFocused, setFocussed] = useState(false);
const handleChange = (e) => {
setSearchText(e.target.value);
callBack?.(e);
};
const handleChange = (e) => {
setSearchText(e.target.value);
callBack?.(e);
};
const clearSearchText = () => {
setSearchText('');
onClearCallback?.();
};
const clearSearchText = () => {
setSearchText('');
onClearCallback?.();
};
const mounted = useMounted();
const mounted = useMounted();
useEffect(() => {
if (mounted) {
onSubmit?.(debouncedSearchTerm);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchTerm, onSubmit]);
useEffect(() => {
if (mounted) {
onSubmit?.(debouncedSearchTerm);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchTerm, onSubmit]);
return (
<div className={`search-box-wrapper ${customClass}`}>
<div className="input-icon">
{!isFocused && (
<span className="input-icon-addon">
<SolidIcon name="search" width="14" />
</span>
)}
<input
style={{ width }}
type="text"
value={searchText}
onChange={handleChange}
className={cx('form-control', {
'dark-theme-placeholder': darkMode,
[className]: !!className,
})}
placeholder={placeholder}
onFocus={() => setFocussed(true)}
onBlur={() => setFocussed(false)}
data-cy={`${dataCy}-search-bar`}
autoFocus={autoFocus}
/>
{isFocused && (
<span className="input-icon-addon end" onMouseDown={clearSearchText}>
<div className="d-flex tj-common-search-input-clear-icon" title="clear">
<SolidIcon name="remove" />
</div>
</span>
)}
useEffect(() => {
initialValue !== undefined && setSearchText(initialValue);
}, [initialValue]);
return (
<div className={`search-box-wrapper ${customClass}`}>
<div className="input-icon">
{!isFocused && (
<span className="input-icon-addon">
<SolidIcon name="search" width="14" />
</span>
)}
<input
style={{ width }}
type="text"
value={searchText}
onChange={handleChange}
className={cx('form-control', {
'dark-theme-placeholder': darkMode,
[className]: !!className,
})}
placeholder={placeholder}
onFocus={() => setFocussed(true)}
onBlur={() => setFocussed(false)}
data-cy={`${dataCy}-search-bar`}
autoFocus={autoFocus}
ref={ref}
/>
{(isFocused || showClearButton) && (
<span className="input-icon-addon end" onMouseDown={clearSearchText}>
<div className="d-flex tj-common-search-input-clear-icon" title="clear">
<SolidIcon name="remove" />
</div>
</span>
)}
</div>
</div>
</div>
);
}
);
}
);
SearchBox.propTypes = {
onSubmit: PropTypes.func.isRequired,
debounceDelay: PropTypes.number,

View file

@ -8,6 +8,7 @@ import {
computeComponentName,
generateAppActions,
loadPyodide,
isQueryRunnable,
} from '@/_helpers/utils';
import { dataqueryService } from '@/_services';
import _ from 'lodash';
@ -585,7 +586,6 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
let _self = _ref;
const { customVariables } = options;
if (eventName === 'onPageLoad') {
await executeActionsForEventId(_ref, 'onPageLoad', { definition: { events: [options] } }, mode, customVariables);
}
@ -879,7 +879,7 @@ export function previewQuery(_ref, query, calledFromQuery = false, parameters =
case 'Created':
case 'Accepted':
case 'No Content': {
toast(`Query completed.`, {
toast(`Query ${'(' + query.name + ') ' || ''}completed.`, {
icon: '🚀',
});
break;
@ -958,7 +958,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
getCurrentState()
);
} else {
queryExecutionPromise = dataqueryService.run(queryId, options);
queryExecutionPromise = dataqueryService.run(queryId, options, query?.options);
}
queryExecutionPromise
@ -1600,7 +1600,7 @@ export const checkExistingQueryName = (newName) =>
export const runQueries = (queries, _ref) => {
queries.forEach((query) => {
if (query.options.runOnPageLoad) {
if (query.options.runOnPageLoad && isQueryRunnable(query)) {
runQuery(_ref, query.id, query.name);
}
});
@ -1611,14 +1611,14 @@ export const computeQueryState = (queries, _ref) => {
queries.forEach((query) => {
if (query.plugin?.plugin_id) {
queryState[query.name] = {
...query.plugin.manifest_file.data.source.exposedVariables,
...query.plugin.manifest_file.data?.source?.exposedVariables,
kind: query.plugin.manifest_file.data.source.kind,
...getCurrentState().queries[query.name],
};
} else {
queryState[query.name] = {
...DataSourceTypes.find((source) => source.kind === query.kind).exposedVariables,
kind: DataSourceTypes.find((source) => source.kind === query.kind).kind,
...DataSourceTypes.find((source) => source.kind === query.kind)?.exposedVariables,
kind: DataSourceTypes.find((source) => source.kind === query.kind)?.kind,
...getCurrentState()?.queries[query.name],
};
}

View file

@ -1,6 +1,6 @@
/* eslint-disable no-useless-escape */
import moment from 'moment';
import _ from 'lodash';
import _, { isEmpty } from 'lodash';
import axios from 'axios';
import JSON5 from 'json5';
import { previewQuery, executeAction } from '@/_helpers/appUtils';
@ -9,6 +9,7 @@ import { authenticationService } from '@/_services/authentication.service';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
import { getCurrentState } from '@/_stores/currentStateStore';
import { staticDataSources } from '@/Editor/QueryManager/constants';
export function findProp(obj, prop, defval) {
if (typeof defval === 'undefined') defval = null;
@ -549,7 +550,14 @@ export const generateAppActions = (_ref, queryId, mode, isPreview = false) => {
? Object.entries(_ref.state.appDefinition.pages[currentPageId]?.components)
: {};
const runQuery = (queryName = '', parameters) => {
const query = useDataQueriesStore.getState().dataQueries.find((query) => query.name === queryName);
const query = useDataQueriesStore.getState().dataQueries.find((query) => {
const isFound = query.name === queryName;
if (isPreview) {
return isFound;
} else {
return isFound && isQueryRunnable(query);
}
});
const processedParams = {};
if (_.isEmpty(query) || queryId === query?.id) {
@ -963,6 +971,15 @@ export const handleHttpErrorMessages = ({ statusCode, error }, feature_name) =>
export const defaultAppEnvironments = [{ name: 'production', isDefault: true, priority: 3 }];
/** Check if the query is connected to a DS. */
export const isQueryRunnable = (query) => {
if (staticDataSources.find((source) => query.kind === source.kind)) {
return true;
}
//TODO: both view api and creat/update apis return dataSourceId in two format 1) camelCase 2) snakeCase. Need to unify it.
return !!(query?.data_source_id || query?.dataSourceId || !isEmpty(query?.plugins));
};
export const redirectToDashboard = () => {
const subpath = getSubpath();
window.location = `${subpath ? `${subpath}` : ''}/${getWorkspaceId()}`;

View file

@ -0,0 +1,27 @@
import { useState, useEffect } from 'react';
export default (_show, selector, triggerSelector) => {
const [show, setShow] = useState(_show);
const handleClickOutside = (event) => {
if (triggerSelector && event.target.closest(triggerSelector) !== null) {
return;
}
if (show && event.target.closest(selector) === null) {
setShow(false);
}
};
useEffect(() => {
if (show) {
document.addEventListener('mousedown', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [show]);
return [show, setShow];
};

View file

@ -9,6 +9,7 @@ export const dataqueryService = {
del,
preview,
changeQueryDataSource,
updateStatus,
};
function getAll(appVersionId) {
@ -42,13 +43,23 @@ function update(id, name, options) {
return fetch(`${config.apiUrl}/data_queries/${id}`, requestOptions).then(handleResponse);
}
function updateStatus(id, status) {
const body = {
status,
};
const requestOptions = { method: 'PUT', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/data_queries/${id}/status`, requestOptions).then(handleResponse);
}
function del(id) {
const requestOptions = { method: 'DELETE', headers: authHeader(), credentials: 'include' };
return fetch(`${config.apiUrl}/data_queries/${id}`, requestOptions).then(handleResponse);
}
function run(queryId, options) {
function run(queryId, resolvedOptions, options) {
const body = {
resolvedOptions: resolvedOptions,
options: options,
};

View file

@ -2,6 +2,8 @@ import { create, zustandDevTools } from './utils';
const initialState = {
editingVersion: null,
isSaving: false,
appId: null,
};
export const useAppDataStore = create(
@ -10,6 +12,8 @@ export const useAppDataStore = create(
...initialState,
actions: {
updateEditingVersion: (version) => set(() => ({ editingVersion: version })),
setIsSaving: (isSaving) => set(() => ({ isSaving })),
setAppId: (appId) => set(() => ({ appId })),
},
}),
{ name: 'App Data Store' }
@ -17,4 +21,5 @@ export const useAppDataStore = create(
);
export const useEditingVersion = () => useAppDataStore((state) => state.editingVersion);
export const useIsSaving = () => useAppDataStore((state) => state.isSaving);
export const useUpdateEditingVersion = () => useAppDataStore((state) => state.actions);

View file

@ -1,157 +1,273 @@
import { create, zustandDevTools } from './utils';
import { getDefaultOptions } from './storeHelper';
import { dataqueryService } from '@/_services';
import { toast } from 'react-hot-toast';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import debounce from 'lodash/debounce';
import { useAppDataStore } from '@/_stores/appDataStore';
import { useQueryPanelStore } from '@/_stores/queryPanelStore';
import { runQueries, computeQueryState } from '@/_helpers/appUtils';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { runQueries } from '@/_helpers/appUtils';
import { v4 as uuidv4 } from 'uuid';
import { toast } from 'react-hot-toast';
const initialState = {
dataQueries: [],
sortBy: 'updated_at',
sortOrder: 'desc',
loadingDataQueries: true,
isDeletingQueryInProcess: false,
/** TODO: Below two params are primarily used only for websocket invocation post update. Can be removed onece websocket logic is revamped */
isCreatingQueryInProcess: false,
isUpdatingQueryInProcess: false,
};
export const useDataQueriesStore = create(
zustandDevTools(
(set, get) => ({
(set) => ({
...initialState,
actions: {
// TODO: Remove editor state while changing currentState
fetchDataQueries: async (appId, selectFirstQuery = false, runQueriesOnAppLoad = false, editorRef) => {
set({ loadingDataQueries: true });
await dataqueryService.getAll(appId).then((data) => {
set({
dataQueries: data.data_queries,
loadingDataQueries: false,
});
// Runs query on loading application
if (runQueriesOnAppLoad) runQueries(data.data_queries, editorRef);
// Compute query state to be added in the current state
computeQueryState(data.data_queries, editorRef);
const { actions, selectedQuery } = useQueryPanelStore.getState();
if (selectFirstQuery || selectedQuery?.id === 'draftQuery') {
actions.setSelectedQuery(data.data_queries[0]?.id, data.data_queries[0]);
} else if (selectedQuery?.id) {
const query = data.data_queries.find((query) => query.id === selectedQuery?.id);
actions.setSelectedQuery(query?.id);
}
});
const data = await dataqueryService.getAll(appId);
set((state) => ({
dataQueries: sortByAttribute(data.data_queries, state.sortBy, state.sortOrder),
loadingDataQueries: false,
}));
// Runs query on loading application
if (runQueriesOnAppLoad) runQueries(data.data_queries, editorRef);
// Compute query state to be added in the current state
const { actions, selectedQuery } = useQueryPanelStore.getState();
if (selectFirstQuery) {
actions.setSelectedQuery(data.data_queries[0]?.id, data.data_queries[0]);
} else if (selectedQuery?.id) {
const query = data.data_queries.find((query) => query.id === selectedQuery?.id);
actions.setSelectedQuery(query?.id);
}
},
setDataQueries: (dataQueries) => set({ dataQueries }),
deleteDataQueries: (queryId, editorRef) => {
deleteDataQueries: (queryId) => {
set({ isDeletingQueryInProcess: true });
useAppDataStore.getState().actions.setIsSaving(true);
dataqueryService
.del(queryId)
.then(() => {
toast.success('Query Deleted');
set({
isDeletingQueryInProcess: false,
});
const { actions, selectedQuery } = useQueryPanelStore.getState();
if (queryId === selectedQuery?.id) {
actions.setUnSavedChanges(false);
actions.setSelectedQuery(null);
const { actions } = useQueryPanelStore.getState();
const { dataQueries } = useDataQueriesStore.getState();
const newSelectedQuery = dataQueries.find((query) => query.id !== queryId);
actions.setSelectedQuery(newSelectedQuery?.id || null);
if (!newSelectedQuery?.id) {
actions.setSelectedDataSource(null);
}
get().actions.fetchDataQueries(
useAppVersionStore.getState().editingVersion?.id,
selectedQuery?.id === queryId,
false,
editorRef
);
set((state) => ({
isDeletingQueryInProcess: false,
dataQueries: state.dataQueries.filter((query) => query.id !== queryId),
}));
})
.catch(({ error }) => {
.catch(() => {
set({
isDeletingQueryInProcess: false,
});
toast.error(error);
});
})
.finally(() => useAppDataStore.getState().actions.setIsSaving(false));
},
updateDataQuery: (options, shouldRunQuery) => {
updateDataQuery: (options) => {
set({ isUpdatingQueryInProcess: true });
const { actions, selectedQuery } = useQueryPanelStore.getState();
const { name, id, kind } = selectedQuery;
dataqueryService
.update(id, name, options)
.then((data) => {
const updatedData = { ...data, kind, options };
actions.setUnSavedChanges(false);
localStorage.removeItem('transformation');
toast.success('Query Saved');
set((state) => ({
isUpdatingQueryInProcess: false,
dataQueries: state.dataQueries.map((query) => {
if (query.id === data.id) return updatedData;
return query;
}),
}));
if (shouldRunQuery) actions.setQueryToBeRun(updatedData);
})
.catch(({ error }) => {
actions.setUnSavedChanges(false);
toast.error(error);
set({
isUpdatingQueryInProcess: false,
});
});
set((state) => ({
isUpdatingQueryInProcess: false,
dataQueries: state.dataQueries.map((query) => {
if (query.id === selectedQuery.id) {
return {
...query,
options: { ...options },
};
}
return query;
}),
}));
actions.setSelectedQuery(selectedQuery.id);
},
createDataQuery: (appId, appVersionId, options, shouldRunQuery) => {
// createDataQuery: (appId, appVersionId, options, kind, name, selectedDataSource, shouldRunQuery) => {
createDataQuery: (selectedDataSource, shouldRunQuery) => {
const appVersionId = useAppVersionStore.getState().editingVersion?.id;
const appId = useAppDataStore.getState().appId;
const { options, name } = getDefaultOptions(selectedDataSource);
const kind = selectedDataSource.kind;
set({ isCreatingQueryInProcess: true });
const { actions, selectedQuery, selectedDataSource } = useQueryPanelStore.getState();
const { name, kind } = selectedQuery;
const dataSourceId = selectedDataSource.id === 'null' ? null : selectedDataSource.id;
const { actions, selectedQuery } = useQueryPanelStore.getState();
const dataSourceId = selectedDataSource?.id !== 'null' ? selectedDataSource?.id : null;
const pluginId = selectedDataSource.pluginId || selectedDataSource.plugin_id;
useAppDataStore.getState().actions.setIsSaving(true);
const { dataQueries } = useDataQueriesStore.getState();
const currDataQueries = [...dataQueries];
const tempId = uuidv4();
set(() => ({
dataQueries: [
{
...selectedQuery,
data_source_id: dataSourceId,
app_version_id: appVersionId,
options,
name,
kind,
id: tempId,
plugin: selectedDataSource.plugin,
},
...currDataQueries,
],
}));
actions.setSelectedQuery(tempId);
dataqueryService
.create(appId, appVersionId, name, kind, options, dataSourceId, pluginId)
.then((data) => {
const query = { ...data, kind, options };
actions.setUnSavedChanges(false);
toast.success('Query Added');
set((state) => ({
isCreatingQueryInProcess: false,
dataQueries: [query, ...state.dataQueries],
dataQueries: state.dataQueries.map((query) => {
if (query.id === tempId) {
return { ...query, ...data, data_source_id: dataSourceId };
}
return query;
}),
}));
if (shouldRunQuery) actions.setQueryToBeRun(query);
actions.setSelectedQuery(data.id, data);
actions.setNameInputFocussed(true);
if (shouldRunQuery) actions.setQueryToBeRun(data);
})
.catch(({ error }) => {
actions.setUnSavedChanges(false);
toast.error(error);
set({
.catch((error) => {
set((state) => ({
isCreatingQueryInProcess: false,
});
});
dataQueries: state.dataQueries.filter((query) => query.id !== tempId),
}));
actions.setSelectedQuery(null);
toast.error(`Failed to create query: ${error.message}`);
})
.finally(() => useAppDataStore.getState().actions.setIsSaving(false));
},
renameQuery: (id, newName, editorRef) => {
renameQuery: (id, newName) => {
useAppDataStore.getState().actions.setIsSaving(true);
/**
* Seting name to store before api call for instant UI update and better UX.
* Name is again set to state post api call to handle if renaming fails in backend.
* */
set((state) => ({
dataQueries: state.dataQueries.map((query) => (query.id === id ? { ...query, name: newName } : query)),
}));
dataqueryService
.update(id, newName)
.then(() => {
toast.success('Query Name Updated');
get().actions.fetchDataQueries(useAppVersionStore.getState().editingVersion?.id, false, false, editorRef);
.then((data) => {
set((state) => ({
dataQueries: state.dataQueries.map((query) => {
if (query.id === id) {
return { ...query, name: newName, updated_at: data.updated_at };
}
return query;
}),
}));
useQueryPanelStore.getState().actions.setSelectedQuery(id);
})
.catch(({ error }) => {
toast.error(error);
});
.finally(() => useAppDataStore.getState().actions.setIsSaving(false));
},
changeDataQuery: (newDataSource) => {
const { selectedQuery } = useQueryPanelStore.getState();
set({
isUpdatingQueryInProcess: true,
});
useAppDataStore.getState().actions.setIsSaving(true);
dataqueryService
.changeQueryDataSource(selectedQuery?.id, newDataSource.id)
.then(() => {
set((state) => ({
isUpdatingQueryInProcess: false,
dataQueries: state.dataQueries.map((query) => {
if (query?.id === selectedQuery?.id) {
return { ...query, dataSourceId: newDataSource?.id, data_source_id: newDataSource?.id };
}
return query;
}),
}));
useQueryPanelStore.getState().actions.setSelectedQuery(selectedQuery.id);
useQueryPanelStore.getState().actions.setSelectedDataSource(newDataSource);
})
.catch(() => {
set({
isUpdatingQueryInProcess: false,
});
toast.success('Data source changed');
})
.finally(() => useAppDataStore.getState().actions.setIsSaving(false));
},
duplicateQuery: (id, appId) => {
set({ isCreatingQueryInProcess: true });
const { actions } = useQueryPanelStore.getState();
const { dataQueries } = useDataQueriesStore.getState();
const queryToClone = { ...dataQueries.find((query) => query.id === id) };
let newName = queryToClone.name + '_copy';
const names = dataQueries.map(({ name }) => name);
let count = 0;
while (names.includes(newName)) {
count++;
newName = queryToClone.name + '_copy' + count.toString();
}
queryToClone.name = newName;
delete queryToClone.id;
useAppDataStore.getState().actions.setIsSaving(true);
dataqueryService
.create(
appId,
queryToClone.app_version_id,
queryToClone.name,
queryToClone.kind,
queryToClone.options,
queryToClone.data_source_id,
queryToClone.pluginId
)
.then((data) => {
set((state) => ({
isCreatingQueryInProcess: false,
dataQueries: [{ ...data, data_source_id: queryToClone.data_source_id }, ...state.dataQueries],
}));
actions.setSelectedQuery(data.id, { ...data, data_source_id: queryToClone.data_source_id });
})
.catch((error) => {
toast.error(error);
console.error('error', error);
set({
isCreatingQueryInProcess: false,
});
})
.finally(() => useAppDataStore.getState().actions.setIsSaving(false));
},
saveData: debounce((newValues) => {
useAppDataStore.getState().actions.setIsSaving(true);
set({ isUpdatingQueryInProcess: true });
dataqueryService
.update(newValues?.id, newValues?.name, newValues?.options)
.then((data) => {
localStorage.removeItem('transformation');
set((state) => ({
dataQueries: state.dataQueries.map((query) => {
if (query.id === newValues?.id) {
return { ...query, updated_at: data.updated_at };
}
return query;
}),
isUpdatingQueryInProcess: false,
}));
})
.catch(() => {
set({
isUpdatingQueryInProcess: false,
});
});
})
.finally(() => useAppDataStore.getState().actions.setIsSaving(false));
}, 500),
sortDataQueries: (sortBy, sortOrder) => {
set(({ dataQueries, sortOrder: currSortOrder }) => {
const newSortOrder = sortOrder ? sortOrder : currSortOrder === 'asc' ? 'desc' : 'asc';
return {
sortBy,
sortOrder: newSortOrder,
dataQueries: sortByAttribute(dataQueries, sortBy, newSortOrder),
};
});
},
},
}),
@ -159,6 +275,15 @@ export const useDataQueriesStore = create(
)
);
const sortByAttribute = (data, sortBy, order) => {
if (order === 'asc') {
return data.sort((a, b) => (a[sortBy] > b[sortBy] ? 1 : -1));
}
if (order === 'desc') {
return data.sort((a, b) => (a[sortBy] < b[sortBy] ? 1 : -1));
}
};
export const useDataQueries = () => useDataQueriesStore((state) => state.dataQueries);
export const useDataQueriesActions = () => useDataQueriesStore((state) => state.actions);
export const useQueryCreationLoading = () => useDataQueriesStore((state) => state.isCreatingQueryInProcess);

View file

@ -1,3 +1,4 @@
import { cloneDeep } from 'lodash';
import { create, zustandDevTools } from './utils';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
@ -7,10 +8,11 @@ const initialState = {
queryPanelHeight: queryManagerPreferences?.isExpanded ? queryManagerPreferences?.queryPanelHeight : 95 ?? 70,
selectedQuery: null,
selectedDataSource: null,
isUnsavedChangesAvailable: false,
queryToBeRun: null,
previewLoading: false,
queryPreviewData: null,
showCreateQuery: false,
nameInputFocussed: false,
};
export const useQueryPanelStore = create(
@ -19,22 +21,21 @@ export const useQueryPanelStore = create(
...initialState,
actions: {
updateQueryPanelHeight: (newHeight) => set(() => ({ queryPanelHeight: newHeight })),
setSelectedQuery: (queryId, dataQuery = {}) => {
setSelectedQuery: (queryId) => {
set(() => {
if (queryId === null) {
return { selectedQuery: null };
} else if (queryId === 'draftQuery') {
return { selectedQuery: dataQuery };
}
const query = useDataQueriesStore.getState().dataQueries.find((query) => query.id === queryId);
return { selectedQuery: query ? query : null };
return { selectedQuery: query };
});
},
setSelectedDataSource: (dataSource = null) => set({ selectedDataSource: dataSource }),
setUnSavedChanges: (value) => set({ isUnsavedChangesAvailable: value }),
setQueryToBeRun: (query) => set({ queryToBeRun: query }),
setPreviewLoading: (status) => set({ previewLoading: status }),
setPreviewData: (data) => set({ queryPreviewData: data }),
setShowCreateQuery: (showCreateQuery) => set({ showCreateQuery }),
setNameInputFocussed: (nameInputFocussed) => set({ nameInputFocussed }),
},
}),
{ name: 'Query Panel Store' }
@ -44,8 +45,11 @@ export const useQueryPanelStore = create(
export const usePanelHeight = () => useQueryPanelStore((state) => state.queryPanelHeight);
export const useSelectedQuery = () => useQueryPanelStore((state) => state.selectedQuery);
export const useSelectedDataSource = () => useQueryPanelStore((state) => state.selectedDataSource);
export const useUnsavedChanges = () => useQueryPanelStore((state) => state.isUnsavedChangesAvailable);
export const useQueryToBeRun = () => useQueryPanelStore((state) => state.queryToBeRun);
export const usePreviewLoading = () => useQueryPanelStore((state) => state.previewLoading);
export const usePreviewData = () => useQueryPanelStore((state) => state.queryPreviewData);
export const useQueryPanelActions = () => useQueryPanelStore((state) => state.actions);
export const useShowCreateQuery = () =>
useQueryPanelStore((state) => [state.showCreateQuery, state.actions.setShowCreateQuery]);
export const useNameInputFocussed = () =>
useQueryPanelStore((state) => [state.nameInputFocussed, state.actions.setNameInputFocussed]);

View file

@ -0,0 +1,55 @@
import { schemaUnavailableOptions } from '@/Editor/QueryManager/constants';
import { allOperations } from '@tooljet/plugins/client';
import { capitalize } from 'lodash';
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
export const getDefaultOptions = (source) => {
const isSchemaUnavailable = Object.keys(schemaUnavailableOptions).includes(source.kind);
let options = {};
if (isSchemaUnavailable) {
options = {
...{ ...schemaUnavailableOptions[source.kind] },
...(source?.kind != 'runjs' && {
transformationLanguage: 'javascript',
enableTransformation: false,
}),
};
} else {
const selectedSourceDefault =
source?.plugin?.operationsFile?.data?.defaults ?? allOperations[capitalize(source.kind)]?.defaults;
if (selectedSourceDefault) {
options = {
...{ ...selectedSourceDefault },
...(source?.kind != 'runjs' && {
transformationLanguage: 'javascript',
enableTransformation: false,
}),
};
} else {
options = {
...(source?.kind != 'runjs' && {
transformationLanguage: 'javascript',
enableTransformation: false,
}),
};
}
}
return { options, name: computeQueryName(source.kind) };
};
const computeQueryName = (kind) => {
const dataQueries = useDataQueriesStore.getState().dataQueries;
const currentQueriesForKind = dataQueries.filter((query) => query.kind === kind);
let currentNumber = currentQueriesForKind.length + 1;
// eslint-disable-next-line no-constant-condition
while (true) {
const newName = `${kind}${currentNumber}`;
if (dataQueries.find((query) => query.name === newName) === undefined) {
return newName;
}
currentNumber += 1;
}
};

View file

@ -148,15 +148,46 @@ $primary-light: unquote("rgb(#{$primary-rgb-darker})");
.color-light-gray-c3c3c3{
color: #c3c3c3;
}
.color-light-indigo-09 {
color: $color-light-indigo-09;
}
.bg-color-primary {
background-color: $primary !important;
}
.bg-slate3{
.color-slate9 {
color: var(--slate9) !important;
}
.color-indigo9 {
color: var(--indigo9) !important;
}
.color-slate11 {
color: var(--slate11) !important;
}
.color-slate12 {
color: var(--slate12) !important;
}
.bg-slate2 {
background-color: var(--slate2) !important;
}
.border-slate3 {
border-top: 1px solid var(--slate3) !important;
}
.border-slate3-top {
border-top: 1px solid var(--slate3) !important;
}
.bg-slate3 {
background-color: var(--slate3) !important;
}
.color-slate12{
color: var(--slate12) !important;
.bg-slate6 {
background-color: var(--slate6) !important;
}

View file

@ -17,7 +17,7 @@ $btn-dark-color: #FFFFFF;
transition: all 0.3s ease-in-out;
&:hover {
@if $bg !=none {
@if $bg !=none {
background-color: darken($bg, 10%);
}
}
@ -229,6 +229,22 @@ $btn-dark-color: #FFFFFF;
}
}
.notification-dot {
position: absolute;
top: -4px;
right: -4px;
width: 14px;
height: 14px;
background-color: var(--indigo9);
border-radius: 50%;
border: 3px solid var(--base);
}
.query-manager-sort-filter-popup {
width: 215px !important;
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03) !important;
}
.page-icons {
position: relative;
left: 1rem;
@ -267,7 +283,7 @@ $btn-dark-color: #FFFFFF;
}
.page-handler-input {
border-radius: $base-border-radius !important;
border-radius: $base-border-radius !important;
}
}
@ -329,7 +345,7 @@ $btn-dark-color: #FFFFFF;
#popover-change-scope {
border: 1px solid rgba(101, 109, 119, 0.16);
border: 1px solid rgba(101, 109, 119, 0.16);
box-shadow: 0px 3px 2px rgba(0, 0, 0, 0.25);
}

View file

@ -44,10 +44,10 @@ $border-radius: 4px;
scrollbar-width: none;
}
.query-pane::-webkit-scrollbar {
width: 0;
background: transparent;
}
// .query-pane::-webkit-scrollbar {
// width: 0;
// background: transparent;
// }
.query-pane {
z-index: 1;
@ -101,9 +101,8 @@ $border-radius: 4px;
display: none;
align-items: center;
gap: 8px;
margin-right: 9.33px;
margin-left: 2px;
width: 44px;
width: 66px;
height: 20px;
}
@ -124,15 +123,6 @@ $border-radius: 4px;
.query-icon {
margin: auto 8px auto 12px;
width: 21.33px;
height: 21.33px;
padding: 1.33px;
svg {
width: 20px !important;
height: 20px !important;
}
}
.delete-query,
@ -303,7 +293,7 @@ $border-radius: 4px;
gap: 12px;
.queries-search {
width: 172px !important;
// width: 172px !important;
height: 28px !important;
.query-manager-search-box-wrapper {
@ -339,7 +329,7 @@ $border-radius: 4px;
font-size: 12px;
font-weight: 400;
line-height: 20px;
border: 0;
border: 1px solid var(--slate-07, #D7DBDF);
color: $color-light-slate-12;
padding-left: 12px !important;
@ -418,16 +408,9 @@ $border-radius: 4px;
}
}
.query-list::-webkit-scrollbar {
width: 0;
background: transparent;
}
.query-list {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 8px;
gap: 2px;
width: 100%;
@ -512,11 +495,15 @@ $border-radius: 4px;
}
.delete-field-option {
max-height: 32px;
height: 34px;
flex: 0 0 28px;
background: #ffffff;
max-width: 28px !important;
width: 28px;
width: 34px;
}
.delete-field-option-dark {
background-color: #272822 !important;
border-color: #272822 !important ;
}
.code-hinter.codehinter-default-input {
@ -640,7 +627,7 @@ $border-radius: 4px;
width: 100%;
display: flex;
flex-direction: row;
gap: 4px;
// gap: 4px;
.list-group-item {
border: none !important;
@ -648,10 +635,11 @@ $border-radius: 4px;
font-weight: 500;
font-size: 12px;
padding: 0 !important;
height: 28px;
height: 32px;
color: $color-light-slate-11;
display: flex;
align-items: center;
border-bottom: 1px solid #dee2e6 !important;
span {
padding: 6px 8px;
@ -662,7 +650,6 @@ $border-radius: 4px;
.list-group-item:hover {
color: $color-light-slate-12 !important;
background-color: $color-light-slate-03 !important;
border-radius: 6px;
}
.list-group-item+.list-group-item.active {
@ -709,7 +696,7 @@ $border-radius: 4px;
}
.query-preview-list-group {
width: 100%;
// width: 100%;
display: flex;
flex-direction: row;
gap: 4px;
@ -720,14 +707,15 @@ $border-radius: 4px;
font-weight: 500;
font-size: 12px;
padding: 0 !important;
height: 28px;
height: 24px;
color: $color-light-slate-11;
border-radius: 6px;
display: flex;
align-items: center;
span {
padding: 6px 8px;
// padding: 6px 8px;
padding: 2px 0px;
}
&:hover {
@ -748,11 +736,15 @@ $border-radius: 4px;
.list-group-item.active {
background-color: transparent !important;
color: $color-light-indigo-09 !important;
// color: $color-light-indigo-09 !important;
z-index: inherit !important;
border-bottom: 2px solid $color-light-indigo-09 !important;
// border-bottom: 2px solid $color-light-indigo-09 !important;
border-radius: 0;
transition-delay: 5ms;
span {
background: #ffffff;
}
}
.list-group-item.active:hover {
@ -1108,14 +1100,14 @@ $border-radius: 4px;
.preview-default-container {
user-select: text;
background-color: #F8F9FA;
background-color: #FBFCFD;
border: 0 0 6px 6px;
height: 52px,
}
.tab-pane.active {
div {
background-color: #F8F9FA !important;
background-color: #FBFCFD !important;
border-radius: 0 0 6px 6px !important;
}
@ -1123,6 +1115,10 @@ $border-radius: 4px;
background-color: transparent !important;
}
}
.preview-section-header {
background-color: var(--slate2);
}
}
.query-manager-border-color {
@ -1131,39 +1127,55 @@ $border-radius: 4px;
.query-details {
.form-label {
color: $color-light-slate-12 !important;
color: var(--slate9) !important;
font-size: 12px;
font-weight: 400;
font-weight: 500;
line-height: 20px;
margin-bottom: 0 !important;
width: 100px;
display: flex;
padding: 0;
margin-right: 12px;
}
.preview-data-container {
min-height: 65px;
}
}
.rest-methods-url {
.code-hinter.codehinter-default-input {
border-style: solid !important;
border-color: $color-light-slate-07 !important;
border-radius: 0 6px 6px 0 !important;
.rest-methods-url {
.url-input-group {
.code-hinter.codehinter-default-input {
border-radius: 0 6px 6px 0 !important;
}
}
&:focus-within {
box-shadow: 0px 0px 0px 2px #C6D4F9 !important;
border: 1px solid #3E63DD !important;
background-color: #F8FAFF;
position: relative;
z-index: 1 !important;
.code-hinter.codehinter-default-input {
border-style: solid !important;
border-color: var(--slate7) !important;
// border-radius: 0 6px 6px 0 !important;
border-radius: 6px !important;
&:focus-within {
box-shadow: 0px 0px 0px 2px #C6D4F9 !important;
border: 1px solid #3E63DD !important;
background-color: #F8FAFF;
position: relative;
z-index: 1 !important;
}
}
.CodeMirror.CodeMirror-wrap {
background-color: transparent !important;
.cm-variable,
.cm-comment {
font-size: 12px !important;
color: $color-light-slate-12 !important;
}
}
}
.CodeMirror.CodeMirror-wrap {
background-color: transparent !important;
.cm-variable,
.cm-comment {
font-size: 12px !important;
color: $color-light-slate-12 !important;
}
}
}
.rest-api-methods-select-element-container {
display: flex;
flex-direction: row;
@ -1243,7 +1255,7 @@ $border-radius: 4px;
.inspector-add-button {
background-color: transparent;
font-weight: 400 !important;
font-weight: bold;
font-size: 12px !important;
color: $color-light-indigo-09 !important;
@ -1290,6 +1302,16 @@ $border-radius: 4px;
.CodeMirror-gutters {
background-color: $color-light-slate-02;
}
.CodeMirror-sizer {
margin-left: 29px !important;
}
}
.runpy-editor{
.CodeMirror-sizer {
margin-left: 29px !important;
}
}
.code-hinter,
@ -1445,7 +1467,6 @@ $border-radius: 4px;
.query-details {
.form-label {
color: #f4f6fa !important;
font-size: 12px;
font-weight: 400;
line-height: 20px;
@ -1595,7 +1616,6 @@ $border-radius: 4px;
.rest-methods-url {
.code-hinter.codehinter-default-input {
border: solid inherit !important;
border-radius: 0 6px 6px 0 !important;
.CodeMirror.cm-s-monokai.CodeMirror-wrap {
background-color: transparent !important;
@ -1848,3 +1868,14 @@ $border-radius: 4px;
color: var(--slate11);
}
}
.query-manager-tooltip {
max-width: 800px;
overflow-wrap: break-word;
}
.query-manager-ds-select-tooltip {
max-width: 230px;
overflow-wrap: break-word;
}

View file

@ -165,6 +165,7 @@
src: url('/assets/fonts/ibm-plex-sans-v19-latin/ibm-plex-sans-v19-latin-700italic.woff2') format('woff2');
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
// variables
$border-radius: 4px;
@ -302,7 +303,7 @@ button {
.resizer-select,
.resizer-active {
border: solid 1px $primary !important;
border: solid 1px $primary !important;
.top-right,
.top-left,
@ -332,7 +333,8 @@ button {
.datasource-picker {
margin-bottom: 24px;
padding: 0 32px;
width: 475px;
margin: auto;
}
.header-query-datasource-card-container {
@ -786,7 +788,7 @@ button {
.list-group.list-group-transparent.dark .all-apps-link,
.list-group-item-action.dark.active {
background-color: $dark-background !important;
background-color: $dark-background !important;
}
}
@ -1463,7 +1465,7 @@ button {
.select-search-dark input {
width: 224px !important;
height: 32px !important;
border-radius: $border-radius !important;
border-radius: $border-radius !important;
}
}
@ -1474,7 +1476,7 @@ button {
.select-search__value input,
.select-search-dark input {
height: 32px !important;
border-radius: $border-radius !important;
border-radius: $border-radius !important;
}
}
@ -1535,7 +1537,7 @@ button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: $border-radius !important;
border-radius: $border-radius !important;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
@ -3407,6 +3409,7 @@ input:focus-visible {
.query-pane {
background-color: #1f2936 !important;
border-top: 2px solid var(--slate4) !important;
}
.input-icon .input-icon-addon img {
@ -3861,7 +3864,7 @@ input[type="text"] {
.nav-tabs .nav-link.active {
font-weight: 400 !important;
color: $primary !important;
color: $primary !important;
}
.empty {
@ -4376,7 +4379,7 @@ input[type="text"] {
.tabs-inspector.dark {
.nav-link.active {
border-bottom: 1px solid $primary !important;
border-bottom: 1px solid $primary !important;
}
}
@ -4611,7 +4614,7 @@ input[type="text"] {
}
input {
border-radius: $border-radius !important;
border-radius: $border-radius !important;
padding-left: 1.75rem !important;
}
}
@ -4779,11 +4782,11 @@ input[type="text"] {
}
.modal-content.home-modal-component.dark {
background-color: $bg-dark-light !important;
color: $white !important;
background-color: $bg-dark-light !important;
color: $white !important;
.modal-header {
background-color: $bg-dark-light !important;
background-color: $bg-dark-light !important;
}
.btn-close {
@ -4791,22 +4794,22 @@ input[type="text"] {
}
.form-control {
border-color: $border-grey-dark !important;
border-color: $border-grey-dark !important;
color: inherit;
}
input {
background-color: $bg-dark-light !important;
background-color: $bg-dark-light !important;
}
.form-select {
background-color: $bg-dark !important;
color: $white !important;
border-color: $border-grey-dark !important;
background-color: $bg-dark !important;
color: $white !important;
border-color: $border-grey-dark !important;
}
.text-muted {
color: $white !important;
color: $white !important;
}
}
@ -5101,7 +5104,7 @@ div#driver-page-overlay {
}
.dark-theme-walkthrough#driver-popover-item {
background-color: $bg-dark-light !important;
background-color: $bg-dark-light !important;
border-color: rgba(101, 109, 119, 0.16) !important;
.driver-popover-title {
@ -5109,7 +5112,7 @@ div#driver-page-overlay {
}
.driver-popover-tip {
border-color: transparent transparent transparent $bg-dark-light !important;
border-color: transparent transparent transparent $bg-dark-light !important;
}
.driver-popover-description {
@ -5141,7 +5144,7 @@ div#driver-page-overlay {
.driver-next-btn,
.driver-prev-btn {
color: $primary !important;
color: $primary !important;
}
.driver-disabled {
@ -5167,11 +5170,11 @@ div#driver-page-overlay {
color: #d9dcde !important;
}
.popover-header {
background-color: var(--slate2);
color: var(--slate11);
border-bottom-color: var
}
.popover-header {
background-color: var(--slate2);
color: var(--slate11);
border-bottom-color: var
}
}
.toast-dark-mode {
@ -5200,8 +5203,12 @@ div#driver-page-overlay {
color: $light-gray;
}
.dynamic-form-row {
margin-top: 20px !important;
margin-bottom: 20px !important;
}
#transformation-popover-container {
margin-left: 80px !important;
margin-bottom: -2px !important;
}
@ -5596,7 +5603,7 @@ div#driver-page-overlay {
}
.selected-node {
border-color: $primary-light !important;
border-color: $primary-light !important;
}
.json-tree-icon-container .selected-node>svg:first-child {
@ -5687,7 +5694,7 @@ div#driver-page-overlay {
}
.selected-node {
border-color: $primary-light !important;
border-color: $primary-light !important;
}
.selected-node .group-object-container .badge {
@ -5954,7 +5961,7 @@ div#driver-page-overlay {
//Kanban board
.kanban-container.dark-themed {
background-color: $bg-dark-light !important;
background-color: $bg-dark-light !important;
.kanban-column {
.card-header {
@ -6000,7 +6007,7 @@ div#driver-page-overlay {
}
.dnd-card.card.card-dark {
background-color: $bg-dark !important;
background-color: $bg-dark !important;
}
}
@ -7427,6 +7434,12 @@ tbody {
}
}
&:disabled {
cursor: not-allowed;
pointer-events: none;
opacity: .65;
}
.query-run-svg {
padding: 4px 2.67px;
}
@ -10817,25 +10830,27 @@ tbody {
}
}
}
.generate-cell-value-component-div-wrapper{
.generate-cell-value-component-div-wrapper {
.form-control-plaintext:focus-visible {
outline-color: #dadcde;
border-radius: 4px;
}
.form-control-plaintext:hover {
outline-color: #dadcde;
border-radius: 4px;
}
}
.dark-theme{
.generate-cell-value-component-div-wrapper{
.dark-theme {
.generate-cell-value-component-div-wrapper {
.form-control-plaintext:focus-visible {
filter: invert(-1);
}
.form-control-plaintext:hover {
filter: invert(-1);
}
@ -10938,6 +10953,89 @@ tbody {
display: none;
}
.query-rename-input {
&:focus,
&:active {
box-shadow: 0px 0px 0px 2px #C6D4F9;
border: 1px solid var(--light-indigo-09, #3E63DD);
}
}
.btn-query-panel-header {
height: 28px;
width: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
background-color: transparent;
border: none;
&.active {
background-color: var(--slate5) !important;
}
&:hover,
&:focus {
background-color: var(--slate4) !important;
}
}
.tj-scrollbar {
::-webkit-scrollbar,
&::-webkit-scrollbar {
width: 16px;
border-radius: 8px;
}
::-webkit-scrollbar-thumb,
&::-webkit-scrollbar-thumb {
border: 4px solid var(--base);
border-radius: 8px;
background-color: var(--slate4) !important;
}
::-webkit-scrollbar-track,
&::-webkit-scrollbar-track {
background-color: var(--base);
}
}
.form-check>.form-check-input:not(:checked) {
background-color: var(--base);
border-color: var(--slate7);
}
/*
* remove this once whole app is migrated to new styles. use only `theme-dark` class everywhere.
* This is added since some of the pages are in old theme and making changes to `theme-dark` styles can break UI style somewhere else
*/
.tj-dark-mode {
background-color: var(--base) !important;
color: var(--base-black) !important;
}
.tj-list-btn {
border-radius: 6px;
&:hover {
background-color: var(--slate4);
}
&.active {
background-color: var(--slate5);
}
}
.tj-list-option {
&.active {
background-color: var(--indigo2);
}
}
.runjs-parameter-badge {
max-width: 140px;
}

View file

@ -25,6 +25,7 @@ export const ButtonBase = function ButtonBase(props) {
fill,
iconCustomClass,
iconWidth,
iconViewBox,
...restProps
} = props;
@ -41,7 +42,15 @@ export const ButtonBase = function ButtonBase(props) {
>
{!isLoading && leftIcon && (
<span className="tj-btn-left-icon">
{<SolidIcon fill={fill} className={iconCustomClass} name={leftIcon} width={iconWidth} />}
{
<SolidIcon
fill={fill}
className={iconCustomClass}
name={leftIcon}
width={iconWidth}
viewBox={iconViewBox}
/>
}
</span>
)}
{isLoading ? (

View file

@ -114,26 +114,25 @@
}
.tj-tertiary-btn {
background: var(--base);
background: transparent;
border: 1px solid var(--slate7);
color: var(--slate12);
&:hover {
background: var(--slate8);
background: var(--slate4);
color: var(--slate11);
border: 1px solid var(--slate8);
background: var(--base);
border: 1px solid var(--slate7);
}
&:active {
background: var(--base);
background: transparent;
box-shadow: none;
border: 1px solid var(--slate12);
color: var(--slate12);
}
&:focus-visible {
background: var(--base);
background: transparent;
color: var(--slate11);
outline: 1px solid var(--slate8);
box-shadow: 0px 0px 0px 4px var(--slate6);
@ -149,6 +148,7 @@
&:hover {
color: var(--indigo10);
background-color: var(--indigo4);
}
&:active {
@ -162,6 +162,9 @@
box-shadow: 0px 0px 0px 4px var(--indigo6);
outline: none;
}
&:focus {
box-shadow: 0px 0px 0px 4px var(--indigo6);
}
}
.tj-ghost-black-btn {
@ -171,6 +174,7 @@
&:hover {
color: var(--slate11);
background: var(--slate4, #ECEEF0);
}
&:active {

View file

@ -1,12 +1,14 @@
import React from 'react';
import { CodeHinter } from '@/Editor/CodeBuilder/CodeHinter';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import AddRectangle from '@/_ui/Icon/bulkIcons/AddRectangle';
export default ({ options, addNewKeyValuePair, removeKeyValuePair, keyValuePairValueChanged }) => {
return (
<div>
{options.map((option, index) => {
return (
<div className="d-flex gap-2" key={index}>
<div className="d-flex gap-2 mb-2" key={index}>
<div className="d-flex justify-content-between gap-2 w-100">
<div className="w-100">
<CodeHinter
@ -27,15 +29,27 @@ export default ({ options, addNewKeyValuePair, removeKeyValuePair, keyValuePairV
/>
</div>
</div>
<button type="button" className="btn btn-primary" onClick={() => removeKeyValuePair(index)}>
<img src="assets/images/icons/trash-light.svg" className="h-3" />
</button>
<ButtonSolid variant="ghostBlue" size="sm" className="py-3" onClick={() => removeKeyValuePair(index)}>
{/* <img src="assets/images/icons/trash-light.svg" className="h-3" /> */}
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</ButtonSolid>
</div>
);
})}
<button type="button" className="btn btn-primary" onClick={addNewKeyValuePair}>
{/* <ButtonSolid variant="ghostBlue" size="sm" onClick={addNewKeyValuePair}>
+ Add header
</button>
</ButtonSolid> */}
<ButtonSolid variant="ghostBlue" size="sm" onClick={addNewKeyValuePair}>
<AddRectangle width="15" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />
&nbsp;&nbsp; Add header
</ButtonSolid>
</div>
);
};

View file

@ -6,4 +6,5 @@ const SolidIcon = (props) => {
const { name, ...restProps } = props;
return <Icon {...restProps} name={name} />;
};
export default SolidIcon;

View file

@ -1,6 +1,13 @@
import React from 'react';
const AddRectangle = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
const AddRectangle = ({
fill = '#C1C8CD',
width = '25',
className = '',
viewBox = '0 0 25 25',
opacity = '0.4',
secondaryFill = '#121212',
}) => (
<svg
width={width}
className={className}
@ -10,7 +17,7 @@ const AddRectangle = ({ fill = '#C1C8CD', width = '25', className = '', viewBox
xmlns="http://www.w3.org/2000/svg"
>
<path
opacity="0.4"
opacity={opacity}
d="M6 2H18C20.2091 2 22 3.79086 22 6V18C22 20.2091 20.2091 22 18 22H6C3.79086 22 2 20.2091 2 18V6C2 3.79086 3.79086 2 6 2Z"
fill={fill}
/>
@ -18,7 +25,7 @@ const AddRectangle = ({ fill = '#C1C8CD', width = '25', className = '', viewBox
fillRule="evenodd"
clipRule="evenodd"
d="M12.75 8C12.75 7.58579 12.4142 7.25 12 7.25C11.5858 7.25 11.25 7.58579 11.25 8V11.25H8C7.58579 11.25 7.25 11.5858 7.25 12C7.25 12.4142 7.58579 12.75 8 12.75H11.25V16C11.25 16.4142 11.5858 16.75 12 16.75C12.4142 16.75 12.75 16.4142 12.75 16V12.75H16C16.4142 12.75 16.75 12.4142 16.75 12C16.75 11.5858 16.4142 11.25 16 11.25H12.75V8Z"
fill="#121212"
fill={secondaryFill}
/>
</svg>
);

View file

@ -1,6 +1,6 @@
import React from 'react';
const ArrowLeft = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
const ArrowLeft = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25', tailOpacity = '0.4' }) => (
<svg
width={width}
height={width}
@ -10,7 +10,7 @@ const ArrowLeft = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '
className={className}
>
<path
opacity="0.4"
opacity={tailOpacity}
fillRule="evenodd"
clipRule="evenodd"
d="M5.46967 11.4697C5.17678 11.7626 5.17678 12.2374 5.46967 12.5303L9.46967 16.5303C9.76256 16.8232 10.2374 16.8232 10.5303 16.5303C10.8232 16.2374 10.8232 15.7626 10.5303 15.4697L7.81066 12.75L18 12.75C18.4142 12.75 18.75 12.4142 18.75 12C18.75 11.5858 18.4142 11.25 18 11.25L7.81066 11.25L10.5303 8.53033C10.8232 8.23744 10.8232 7.76256 10.5303 7.46967C10.2374 7.17678 9.76256 7.17678 9.46967 7.46967L5.46967 11.4697Z"

View file

@ -0,0 +1,31 @@
import React from 'react';
const FolderEmpty = ({ width = '16', className = '', viewBox = '0 0 16 16', style = {} }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={width}
viewBox={viewBox}
fill="none"
className={className}
style={style}
>
<path
opacity="0.4"
d="M13.6823 5.33398H2.31431C1.46545 5.33398 0.832714 6.11667 1.01057 6.94669L2.2133 12.5594C2.47677 13.7889 3.56334 14.6673 4.82077 14.6673H11.1759C12.4333 14.6673 13.5199 13.7889 13.7834 12.5594L14.9861 6.94669C15.1639 6.11667 14.5312 5.33398 13.6823 5.33398Z"
fill="#889096"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.23238 8.23238C6.42765 8.03712 6.74423 8.03712 6.93949 8.23238L8.00015 9.29304L9.06081 8.23238C9.25607 8.03712 9.57266 8.03712 9.76792 8.23238C9.96318 8.42765 9.96318 8.74423 9.76792 8.93949L8.70726 10.0002L9.76792 11.0608C9.96318 11.2561 9.96318 11.5727 9.76792 11.7679C9.57266 11.9632 9.25607 11.9632 9.06081 11.7679L8.00015 10.7073L6.93949 11.7679C6.74423 11.9632 6.42765 11.9632 6.23238 11.7679C6.03712 11.5727 6.03712 11.2561 6.23238 11.0608L7.29304 10.0002L6.23238 8.93949C6.03712 8.74423 6.03712 8.42765 6.23238 8.23238Z"
fill="#889096"
/>
<path
d="M13.3346 5.33398V4.66732C13.3346 3.56275 12.4392 2.66732 11.3346 2.66732H9.4663C9.02093 2.66732 8.58831 2.51866 8.237 2.24492L7.61005 1.75638C7.25874 1.48264 6.82612 1.33398 6.38075 1.33398H4.66797C3.5634 1.33398 2.66797 2.22941 2.66797 3.33398V5.33398H13.3346Z"
fill="#889096"
/>
</svg>
);
export default FolderEmpty;

View file

@ -0,0 +1,17 @@
import React from 'react';
function Maximize({ stroke = '#C1C8CD', width = '25', viewBox = '0 0 25 25', style = {} }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={width} fill="none" viewBox={viewBox} style={style}>
<path
stroke={stroke}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M8.5 1H13m0 0v4.5M13 1L1 13m0 0V8.5M1 13h4.5"
></path>
</svg>
);
}
export default Maximize;

View file

@ -0,0 +1,17 @@
import React from 'react';
function Minimize({ stroke = '#C1C8CD', width = '25', viewBox = '0 0 25 25' }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={width} fill="none" viewBox={viewBox}>
<path
stroke={stroke}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M17 1l-7 7m-9 9l7-7m2-2h5m-5 0V3m-2 7v5m0-5H3"
></path>
</svg>
);
}
export default Minimize;

View file

@ -1,18 +1,9 @@
import React from 'react';
const Play = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={width} viewBox={viewBox} fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17.4611 14.5255L9.49228 19.0792C8.15896 19.8411 6.5 18.8783 6.5 17.3427V12.7891V8.23542C6.5 6.69978 8.15896 5.73704 9.49228 6.49894L17.4611 11.0526C18.8048 11.8204 18.8048 13.7578 17.4611 14.5255Z"
d="M11.9044 8.54526L4.81454 12.5966C3.62831 13.2744 2.15234 12.4179 2.15234 11.0517V2.949C2.15234 1.58275 3.62831 0.726218 4.81454 1.40407L11.9044 5.45539C13.0998 6.13849 13.0998 7.86217 11.9044 8.54526Z"
fill={fill}
/>
</svg>

View file

@ -1,6 +1,6 @@
import React from 'react';
const Plus = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25', dataCy = '' }) => (
const Plus = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25', dataCy = '', style }) => (
<svg
width={width}
height={width}
@ -9,6 +9,7 @@ const Plus = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 2
xmlns="http://www.w3.org/2000/svg"
className={className}
data-cy={dataCy}
style={style}
>
<path
fillRule="evenodd"

View file

@ -1,6 +1,6 @@
import React from 'react';
const Search = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
const Search = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25', style }) => (
<svg
width={width}
height={width}
@ -8,6 +8,7 @@ const Search = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
style={style}
>
<path
fillRule="evenodd"

View file

@ -1,6 +1,6 @@
import React from 'react';
const Trash = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25' }) => (
const Trash = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 25 25', style }) => (
<svg
width={width}
height={width}
@ -8,6 +8,7 @@ const Trash = ({ fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
style={style}
>
<path
fillRule="evenodd"

View file

@ -115,6 +115,8 @@ import Lock from './Lock.jsx';
import Mail from './Mail.jsx';
import Logs from './Logs.jsx';
import Marketplace from './Marketplace.jsx';
import Minimize from './Minimize.jsx';
import Maximize from './Maximize.jsx';
import PlusRectangle from './PlusRectangle.jsx';
const Icon = (props) => {
@ -351,6 +353,10 @@ const Icon = (props) => {
return <Mail {...props} />;
case 'marketplace':
return <Marketplace {...props} />;
case 'minimize':
return <Minimize {...props} />;
case 'maximize':
return <Maximize {...props} />;
default:
return <Apps {...props} />;
}

View file

@ -39,7 +39,7 @@ const Button = ({
};
const Content = ({ title = null, iconSrc = null, direction = 'left', dataCy }) => {
const icon = !iconSrc ? (
const Icon = !iconSrc ? (
''
) : (
<img
@ -52,7 +52,7 @@ const Content = ({ title = null, iconSrc = null, direction = 'left', dataCy }) =
.replace(/\s+/g, '-')}-option-icon`}
/>
);
const btnTitle = !title ? (
const BtnTitle = !title ? (
''
) : typeof title === 'function' ? (
title()
@ -66,7 +66,19 @@ const Content = ({ title = null, iconSrc = null, direction = 'left', dataCy }) =
{title}
</span>
);
const content = direction === 'left' ? [icon, btnTitle] : [btnTitle, icon];
const content =
direction === 'left' ? (
<>
{Icon}
{BtnTitle}
</>
) : (
<>
{BtnTitle}
{Icon}
</>
);
return content;
};

View file

@ -8,7 +8,7 @@ export default function styles(darkMode, width = 224, height = 32, styles = {})
}),
control: (provided, state) => ({
...provided,
border: state.isDisabled && darkMode ? 'none' : styles.border ?? '1px solid hsl(0, 0%, 80%)',
border: state.isDisabled && darkMode ? 'none' : styles.border ?? '1px solid var(--slate7)',
boxShadow: 'none',
'&:hover': {
backgroundColor: darkMode ? '' : '#F8F9FA',

View file

@ -152,7 +152,7 @@
"description": "Enter max keys",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins col-6"
"className": "codehinter-plugins"
},
"offset": {
"label": "Offset",

View file

@ -94,7 +94,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -125,7 +124,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -156,7 +154,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -187,7 +184,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -218,7 +214,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -240,7 +235,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -271,7 +265,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -313,7 +306,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -353,7 +345,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -393,7 +384,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -433,7 +423,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -473,7 +462,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -513,7 +501,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -544,7 +531,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -575,7 +561,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -606,7 +591,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"
@ -637,7 +621,6 @@
"type": "codehinter",
"lineNumbers": false,
"description": "Enter collection",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter collection"

View file

@ -152,7 +152,7 @@
"description": "Enter max keys",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins col-6"
"className": "codehinter-plugins"
},
"offset": {
"label": "Offset",
@ -162,7 +162,7 @@
"description": "Enter offset",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins col-6"
"className": "codehinter-plugins"
},
"NextContinuationToken": {
"label": "Next Continuation Token",

View file

@ -1 +1 @@
2.12.0
2.13.0

View file

@ -27,6 +27,7 @@ import { EntityManager } from 'typeorm';
import { DataSource } from 'src/entities/data_source.entity';
import { DataSourceScopes, DataSourceTypes } from 'src/helpers/data_source.constants';
import { App } from 'src/entities/app.entity';
import { isEmpty } from 'class-validator';
@Controller('data_queries')
export class DataQueriesController {
@ -127,7 +128,12 @@ export class DataQueriesController {
appVersionId,
manager
);
return decamelizeKeys(dataQuery);
const decamelizedQuery = decamelizeKeys({ ...dataQuery, kind });
decamelizedQuery['options'] = dataQuery.options;
return decamelizedQuery;
});
}
@ -144,7 +150,9 @@ export class DataQueriesController {
}
const result = await this.dataQueriesService.update(dataQueryId, name, options);
return decamelizeKeys(result);
const decamelizedQuery = decamelizeKeys({ ...dataQuery, ...result });
decamelizedQuery['options'] = result.options;
return decamelizedQuery;
}
@UseGuards(JwtAuthGuard)
@ -169,7 +177,7 @@ export class DataQueriesController {
@Param('environmentId') environmentId,
@Body() updateDataQueryDto: UpdateDataQueryDto
) {
const { options } = updateDataQueryDto;
const { options, resolvedOptions } = updateDataQueryDto;
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
@ -179,12 +187,17 @@ export class DataQueriesController {
if (!ability.can('runQuery', dataQuery.app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
}
if (ability.can('updateQuery', dataQuery.app) && !isEmpty(options)) {
await this.dataQueriesService.update(dataQueryId, dataQuery.name, options);
dataQuery['options'] = options;
}
}
let result = {};
try {
result = await this.dataQueriesService.runQuery(user, dataQuery, options, environmentId);
result = await this.dataQueriesService.runQuery(user, dataQuery, resolvedOptions, environmentId);
} catch (error) {
if (error.constructor.name === 'QueryError') {
result = {

View file

@ -31,6 +31,10 @@ export class CreateDataQueryDto {
@IsObject()
options: object;
@IsObject()
@IsOptional()
resolvedOptions: object;
}
export class UpdateDataQueryDto extends PartialType(CreateDataQueryDto) {}

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