Merge branch 'release' into fix/disable-cancel-button-on-a-disabled-select-widget
This commit is contained in:
commit
fa48a026ad
2
.github/workflows/TestReuseActions.yml
vendored
2
.github/workflows/TestReuseActions.yml
vendored
|
|
@ -459,7 +459,7 @@ jobs:
|
|||
|
||||
# In case this is second attempt try restoring status of the prior attempt from cache
|
||||
- name: Restore the previous run result
|
||||
uses: martijnhols/actions-cache@v3
|
||||
uses: martijnhols/actions-cache@v3.0.2
|
||||
with:
|
||||
path: |
|
||||
~/run_result
|
||||
|
|
|
|||
|
|
@ -471,7 +471,7 @@ jobs:
|
|||
|
||||
# In case this is second attempt try restoring status of the prior attempt from cache
|
||||
- name: Restore the previous run result
|
||||
uses: martijnhols/actions-cache@v3
|
||||
uses: martijnhols/actions-cache@v3.0.2
|
||||
with:
|
||||
path: |
|
||||
~/run_result
|
||||
|
|
@ -806,7 +806,7 @@ jobs:
|
|||
|
||||
# In case this is second attempt try restoring status of the prior attempt from cache
|
||||
- name: Restore the previous run result
|
||||
uses: martijnhols/actions-cache@v3
|
||||
uses: martijnhols/actions-cache@v3.0.2
|
||||
with:
|
||||
path: |
|
||||
~/run_result
|
||||
|
|
@ -1218,12 +1218,11 @@ jobs:
|
|||
summary: "https://github.com/" + process.env.repository + "/actions/runs/" + process.env.run_id
|
||||
}
|
||||
});
|
||||
console.log({ result });
|
||||
return result;
|
||||
} catch(e) {
|
||||
console.error({ error: e.message });
|
||||
}
|
||||
|
||||
console.log({ result });
|
||||
return result;
|
||||
}
|
||||
|
||||
- name: Dump the client payload context
|
||||
|
|
|
|||
|
|
@ -446,7 +446,7 @@ jobs:
|
|||
|
||||
# In case this is second attempt try restoring status of the prior attempt from cache
|
||||
- name: Restore the previous run result
|
||||
uses: martijnhols/actions-cache@v3
|
||||
uses: martijnhols/actions-cache@v3.0.2
|
||||
with:
|
||||
path: |
|
||||
~/run_result
|
||||
|
|
@ -785,7 +785,7 @@ jobs:
|
|||
|
||||
# In case this is second attempt try restoring status of the prior attempt from cache
|
||||
- name: Restore the previous run result
|
||||
uses: martijnhols/actions-cache@v3
|
||||
uses: martijnhols/actions-cache@v3.0.2
|
||||
with:
|
||||
path: |
|
||||
~/run_result
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ jobs:
|
|||
|
||||
# In case this is second attempt try restoring status of the prior attempt from cache
|
||||
- name: Restore the previous run result
|
||||
uses: martijnhols/actions-cache@v3
|
||||
uses: martijnhols/actions-cache@v3.0.2
|
||||
with:
|
||||
path: |
|
||||
~/run_result
|
||||
|
|
@ -829,7 +829,7 @@ jobs:
|
|||
|
||||
# In case this is second attempt try restoring status of the prior attempt from cache
|
||||
- name: Restore the previous run result
|
||||
uses: martijnhols/actions-cache@v3
|
||||
uses: martijnhols/actions-cache@v3.0.2
|
||||
with:
|
||||
path: |
|
||||
~/run_result
|
||||
|
|
|
|||
10
Dockerfile
10
Dockerfile
|
|
@ -16,18 +16,15 @@ RUN apt-get update \
|
|||
supervisor curl cron certbot nginx gnupg wget netcat openssh-client \
|
||||
software-properties-common gettext openjdk-11-jre \
|
||||
python3-pip python-setuptools git \
|
||||
&& add-apt-repository ppa:redislabs/redis \
|
||||
&& pip install --no-cache-dir git+https://github.com/coderanger/supervisor-stdout@973ba19967cdaf46d9c1634d1675fc65b9574f6e \
|
||||
&& apt-get remove -y git python3-pip \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& apt-get remove -y git python3-pip
|
||||
|
||||
# Install MongoDB v4.0.5, Redis, NodeJS - Service Layer
|
||||
RUN wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add -
|
||||
RUN echo "deb [ arch=amd64,arm64 ]http://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list \
|
||||
&& apt-get remove wget -y
|
||||
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \
|
||||
&& apt-get -y install --no-install-recommends -y mongodb-org=4.4.6 nodejs redis build-essential \
|
||||
&& apt-get install --no-install-recommends -y mongodb-org=4.4.6 nodejs redis build-essential \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
|
@ -88,6 +85,9 @@ RUN chmod 0644 /etc/cron.d/*
|
|||
|
||||
RUN chmod +x entrypoint.sh renew-certificate.sh
|
||||
|
||||
# Disable setuid/setgid bits for the files inside container.
|
||||
RUN find / \( -path /proc -prune \) -o \( \( -perm -2000 -o -perm -4000 \) -print -exec chmod -s '{}' + \) || true
|
||||
|
||||
# Update path to load appsmith utils tool as default
|
||||
ENV PATH /opt/appsmith/utils/node_modules/.bin:$PATH
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"githubClientId": "",
|
||||
"githubClientSecret": ""
|
||||
}
|
||||
|
|
@ -25,17 +25,27 @@ describe("AForce - Community Issues page validations", function () {
|
|||
|
||||
let reconnect = true, selectedRow: number;
|
||||
it("1. Import application json and validate headers", () => {
|
||||
|
||||
homePage.ImportApp("AForceMigrationExport.json", reconnect)
|
||||
if (reconnect)
|
||||
dataSources.ReconnectDataSourcePostgres("AForceDB")
|
||||
//Validate table is not empty!
|
||||
table.WaitUntilTableLoad()
|
||||
//Validating order of header columns!
|
||||
table.AssertTableHeaderOrder("TypeTitleStatus+1CommentorsVotesAnswerUpVoteStatesupvote_ididgithub_issue_idauthorcreated_atdescriptionlabelsstatelinkupdated_at")
|
||||
//Validating hidden columns:
|
||||
table.AssertHiddenColumns(['States', 'upvote_id', 'id', 'github_issue_id', 'author', 'created_at', 'description', 'labels', 'state', 'link', 'updated_at'])
|
||||
|
||||
cy.visit("/applications");
|
||||
homePage.ImportApp("AForceMigrationExport.json", reconnect);
|
||||
cy.wait("@importNewApplication").then((interception) => {
|
||||
cy.wait(100);
|
||||
const { isPartialImport } = interception.response.body.data;
|
||||
if (isPartialImport) {
|
||||
// should reconnect modal
|
||||
dataSources.ReconnectDataSourcePostgres("AForceDB")
|
||||
} else {
|
||||
cy.get(homePage.toastMessage).should(
|
||||
"contain",
|
||||
"Application imported successfully",
|
||||
);
|
||||
}
|
||||
//Validate table is not empty!
|
||||
table.WaitUntilTableLoad()
|
||||
//Validating order of header columns!
|
||||
table.AssertTableHeaderOrder("TypeTitleStatus+1CommentorsVotesAnswerUpVoteStatesupvote_ididgithub_issue_idauthorcreated_atdescriptionlabelsstatelinkupdated_at")
|
||||
//Validating hidden columns:
|
||||
table.AssertHiddenColumns(['States', 'upvote_id', 'id', 'github_issue_id', 'author', 'created_at', 'description', 'labels', 'state', 'link', 'updated_at'])
|
||||
});
|
||||
});
|
||||
|
||||
it("2. Validate table navigation with Server Side pagination enabled with Default selected row", () => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ describe("Reconnect Datasource Modal validation while importing application", fu
|
|||
let appid;
|
||||
let newOrganizationName;
|
||||
let appName;
|
||||
it("Import application from json with one postgres", function() {
|
||||
it("Import application from json with one postgres and success modal", function() {
|
||||
cy.NavigateToHome();
|
||||
// import application
|
||||
cy.generateUUID().then((uid) => {
|
||||
|
|
@ -49,16 +49,32 @@ describe("Reconnect Datasource Modal validation while importing application", fu
|
|||
cy.get(
|
||||
"[data-cy='datasourceConfiguration.connection.ssl.authType']",
|
||||
).should("contain", "Default");
|
||||
cy.get(reconnectDatasourceModal.SkipToAppBtn).click({
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.ReconnectDatasource("Untitled Datasource");
|
||||
cy.wait(1000);
|
||||
cy.fillPostgresDatasourceForm();
|
||||
cy.testSaveDatasource();
|
||||
cy.wait(2000);
|
||||
|
||||
// cy.get(reconnectDatasourceModal.SkipToAppBtn).click({
|
||||
// force: true,
|
||||
// });
|
||||
// cy.wait(2000);
|
||||
} else {
|
||||
cy.get(homePage.toastMessage).should(
|
||||
"contain",
|
||||
"Application imported successfully",
|
||||
);
|
||||
}
|
||||
// check datasource configured success modal
|
||||
cy.get(".t--import-app-success-modal").should("be.visible");
|
||||
cy.get(".t--import-app-success-modal").should(
|
||||
"contain",
|
||||
"All your datasources are configuered and ready to use.",
|
||||
);
|
||||
cy.get(".t--import-success-modal-got-it").click({ force: true });
|
||||
cy.get(".t--import-app-success-modal").should("not.exist");
|
||||
|
||||
const uuid = () => Cypress._.random(0, 1e4);
|
||||
const name = uuid();
|
||||
appName = `app${name}`;
|
||||
|
|
|
|||
|
|
@ -73,6 +73,11 @@ describe("Test Create Api and Bind to Table widget", function() {
|
|||
cy.wait(500);
|
||||
cy.wait("@postExecute");
|
||||
cy.wait(500);
|
||||
cy.get(".show-page-items").should("contain", "20 Records");
|
||||
cy.get(".page-item")
|
||||
.next()
|
||||
.should("contain", "of 2");
|
||||
|
||||
cy.get(".t--table-widget-next-page").should("not.have.attr", "disabled");
|
||||
cy.ValidateTableData("1");
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ describe("JSON Form Widget Field Change", () => {
|
|||
cy.get(`${fieldPrefix}-name`)
|
||||
.find("button")
|
||||
.should("have.length", 2);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/);
|
||||
cy.closePropertyPane();
|
||||
});
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ describe("JSON Form Widget Field Change", () => {
|
|||
.find("input")
|
||||
.invoke("attr", "type")
|
||||
.should("contain", "checkbox");
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/);
|
||||
cy.closePropertyPane();
|
||||
});
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ describe("JSON Form Widget Field Change", () => {
|
|||
.find("input")
|
||||
.click({ force: true });
|
||||
cy.get(".bp3-popover.bp3-dateinput-popover").should("exist");
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/);
|
||||
cy.closePropertyPane();
|
||||
});
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ describe("JSON Form Widget Field Change", () => {
|
|||
.find(".bp3-control.bp3-switch")
|
||||
.should("exist");
|
||||
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/);
|
||||
cy.closePropertyPane();
|
||||
});
|
||||
|
||||
|
|
@ -82,12 +82,12 @@ describe("JSON Form Widget Field Change", () => {
|
|||
cy.get(".bp3-select-popover.select-popover-wrapper").should("not.exist");
|
||||
|
||||
cy.openFieldConfiguration("name");
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Select$/);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Select/);
|
||||
|
||||
cy.get(`${fieldPrefix}-name label`).click({ force: true });
|
||||
cy.get(".bp3-select-popover.select-popover-wrapper").should("exist");
|
||||
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/);
|
||||
cy.closePropertyPane();
|
||||
});
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ describe("JSON Form Widget Field Change", () => {
|
|||
.find(".rc-select-multiple")
|
||||
.should("exist");
|
||||
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/);
|
||||
cy.closePropertyPane();
|
||||
});
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ describe("JSON Form Widget Field Change", () => {
|
|||
.should("exist")
|
||||
.should("have.length", 2);
|
||||
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/);
|
||||
cy.closePropertyPane();
|
||||
});
|
||||
|
||||
|
|
@ -139,7 +139,7 @@ describe("JSON Form Widget Field Change", () => {
|
|||
.find(".t--jsonformfield-array-add-btn")
|
||||
.should("exist");
|
||||
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/);
|
||||
cy.closePropertyPane();
|
||||
});
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ describe("JSON Form Widget Field Change", () => {
|
|||
.find("input")
|
||||
.should("exist");
|
||||
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/);
|
||||
cy.closePropertyPane();
|
||||
});
|
||||
|
||||
|
|
@ -185,7 +185,7 @@ describe("JSON Form Widget Field Change", () => {
|
|||
.should("have.length", 2);
|
||||
});
|
||||
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/);
|
||||
cy.closePropertyPane();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ describe("Select Field Property Control", () => {
|
|||
cy.openPropertyPane("jsonformwidget");
|
||||
cy.testJsontext("sourcedata", JSON.stringify(schema));
|
||||
cy.openFieldConfiguration("state");
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Select$/);
|
||||
cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Select/);
|
||||
});
|
||||
|
||||
it("has valid default value", () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const pages = require("../../../../locators/Pages.json");
|
|||
const generatePage = require("../../../../locators/GeneratePage.json");
|
||||
import homePage from "../../../../locators/HomePage";
|
||||
const datasource = require("../../../../locators/DatasourcesEditor.json");
|
||||
const commonlocators = require("../../../../locators/commonlocators.json");
|
||||
|
||||
describe("Generate New CRUD Page Inside from Mongo as Data Source", function() {
|
||||
let datasourceName;
|
||||
|
|
@ -91,6 +92,9 @@ describe("Generate New CRUD Page Inside from Mongo as Data Source", function() {
|
|||
"response.body.responseMeta.status",
|
||||
200,
|
||||
);
|
||||
cy.get(commonlocators.toastAction)
|
||||
.should("have.length", 1)
|
||||
.should("have.text", "Successfully generated a page");
|
||||
cy.get("span:contains('GOT IT')").click();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ describe("Git import flow", function() {
|
|||
});
|
||||
});
|
||||
it("Import an app from JSON with Postgres, MySQL, Mongo db", () => {
|
||||
cy.get(homePage.homeIcon).click();
|
||||
cy.NavigateToHome();
|
||||
cy.get(homePage.optionsIcon)
|
||||
.first()
|
||||
.click();
|
||||
|
|
@ -54,6 +54,10 @@ describe("Git import flow", function() {
|
|||
"contain",
|
||||
"Application imported successfully",
|
||||
); */
|
||||
cy.get(reconnectDatasourceModal.ImportSuccessModal).should("be.visible");
|
||||
cy.get(reconnectDatasourceModal.ImportSuccessModalCloseBtn).click({
|
||||
force: true,
|
||||
});
|
||||
cy.wait(1000);
|
||||
cy.generateUUID().then((uid) => {
|
||||
repoName = uid;
|
||||
|
|
@ -98,6 +102,11 @@ describe("Git import flow", function() {
|
|||
cy.get(datasourceEditor.sectionAuthentication).click();
|
||||
cy.testSaveDatasource();
|
||||
cy.wait(2000);
|
||||
cy.get(reconnectDatasourceModal.ImportSuccessModal).should("be.visible");
|
||||
cy.get(reconnectDatasourceModal.ImportSuccessModalCloseBtn).click({
|
||||
force: true,
|
||||
});
|
||||
cy.wait(1000);
|
||||
/* cy.get(homePage.toastMessage).should(
|
||||
"contain",
|
||||
"Application imported successfully",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
const dsl = require("../../../../fixtures/jsonFormDslWithSchema.json");
|
||||
const { ObjectsRegistry } = require("../../../../support/Objects/Registry");
|
||||
let ee = ObjectsRegistry.EntityExplorer;
|
||||
|
||||
describe("Property pane js enabled field", function() {
|
||||
before(() => {
|
||||
cy.addDsl(dsl);
|
||||
});
|
||||
|
||||
it("Ensure text is visible for js enabled field when a section is collapsed by default", function() {
|
||||
cy.openPropertyPane("jsonformwidget");
|
||||
|
||||
cy.get(".t--property-pane-section-collapse-submitbuttonstyles").click();
|
||||
cy.get(".t--property-control-buttonvariant")
|
||||
.find(".t--js-toggle")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
cy.get(".t--property-control-buttonvariant")
|
||||
.find(".t--js-toggle")
|
||||
.first()
|
||||
.should("have.class", "is-active");
|
||||
|
||||
cy.get(".t--property-control-buttonvariant .CodeMirror-code").type(
|
||||
"PRIMARY",
|
||||
);
|
||||
cy.get(".t--property-control-buttonvariant")
|
||||
.find(".CodeMirror-code")
|
||||
.invoke("text")
|
||||
.should("equal", "PRIMARY");
|
||||
|
||||
cy.closePropertyPane();
|
||||
cy.wait(1000);
|
||||
|
||||
cy.openPropertyPane("jsonformwidget");
|
||||
cy.get(".t--property-pane-section-collapse-submitbuttonstyles").click();
|
||||
cy.get(".t--property-control-buttonvariant")
|
||||
.find(".CodeMirror-code")
|
||||
.invoke("text")
|
||||
.should("equal", "PRIMARY");
|
||||
});
|
||||
});
|
||||
|
|
@ -131,15 +131,15 @@ describe("Undo/Redo functionality", function() {
|
|||
.first()
|
||||
.focus()
|
||||
.type("{downarrow}{downarrow}{downarrow} ")
|
||||
.type("test:()=>{},");
|
||||
.type("testJSFunction:()=>{},");
|
||||
cy.get("body").type(`{${modifierKey}}z{${modifierKey}}z{${modifierKey}}z`);
|
||||
// verifying test function is not visible in response tab after undo
|
||||
cy.get(".function-name").should("not.contain.text", "test");
|
||||
// verifying testJSFunction is not visible on page after undo
|
||||
cy.contains("testJSFunction").should("not.exist");
|
||||
cy.get("body").type(
|
||||
`{${modifierKey}}{shift}z{${modifierKey}}{shift}z{${modifierKey}}{shift}z`,
|
||||
);
|
||||
// verifying test function is visible in response tab after redo
|
||||
cy.get(".function-name").should("contain.text", "test");
|
||||
// verifying testJSFunction is visible on page after redo
|
||||
cy.contains("testJSFunction").should("exist");
|
||||
// performing undo from app menu
|
||||
cy.get(".t--application-name").click({ force: true });
|
||||
cy.get("li:contains(Edit)")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ObjectsRegistry } from "../../../../support/Objects/Registry";
|
||||
|
||||
let guid: any, jsName : any;
|
||||
let guid: any, jsName: any;
|
||||
const agHelper = ObjectsRegistry.AggregateHelper,
|
||||
ee = ObjectsRegistry.EntityExplorer,
|
||||
dataSources = ObjectsRegistry.DataSources,
|
||||
|
|
@ -24,7 +24,7 @@ describe("JSObjects OnLoad Actions tests", function() {
|
|||
true,
|
||||
false,
|
||||
);
|
||||
jsEditor.EnableOnPageLoad("getId", false, true);
|
||||
jsEditor.AddJSFunctionSettings("getId", false, true);
|
||||
agHelper.GenerateUUID();
|
||||
cy.get("@guid").then((uid) => {
|
||||
dataSources.NavigateToDSCreateNew();
|
||||
|
|
@ -39,17 +39,19 @@ describe("JSObjects OnLoad Actions tests", function() {
|
|||
cy.get("@jsObjName").then((jsObjName) => {
|
||||
jsName = jsObjName;
|
||||
agHelper.EnterValue(
|
||||
"SELECT * FROM public.users where id = {{" + jsObjName + ".getId.data}}",
|
||||
);;
|
||||
})
|
||||
"SELECT * FROM public.users where id = {{" +
|
||||
jsObjName +
|
||||
".getId.data}}",
|
||||
);
|
||||
});
|
||||
});
|
||||
ee.SelectEntityByName("Table1", 'WIDGETS');
|
||||
ee.SelectEntityByName("Table1", "WIDGETS");
|
||||
jsEditor.EnterJSContext("Table Data", "{{GetUser.data}}");
|
||||
|
||||
agHelper.DeployApp();
|
||||
agHelper.AssertElementPresence(jsEditor._dialog("Confirmation Dialog"));
|
||||
agHelper.ClickButton("Yes");
|
||||
agHelper.Sleep(1000)
|
||||
agHelper.Sleep(1000);
|
||||
agHelper.ValidateNetworkExecutionSuccess("@postExecute");
|
||||
|
||||
table.ReadTableRowColumnData(0, 0).then((cellData) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"runButton": ".run-button",
|
||||
"runButton": ".run-js-action",
|
||||
"editNameField": ".bp3-editable-text-input",
|
||||
"outputConsole": ".CodeEditorTarget",
|
||||
"jsObjectName": ".t--action-name-edit-field",
|
||||
|
|
|
|||
|
|
@ -2,4 +2,6 @@ export default {
|
|||
Modal: ".reconnect-datasource-modal",
|
||||
ClostBtn: ".t--reconnect-close-btn",
|
||||
SkipToAppBtn: ".t--skip-to-application-btn",
|
||||
ImportSuccessModal: ".t--import-app-success-modal",
|
||||
ImportSuccessModalCloseBtn: ".t--import-success-modal-got-it",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ require("cypress-file-upload");
|
|||
const googleForm = require("../locators/GoogleForm.json");
|
||||
const googleData = require("../fixtures/googleSource.json");
|
||||
const githubForm = require("../locators/GithubForm.json");
|
||||
const githubData = require("../fixtures/githubSource.json");
|
||||
|
||||
Cypress.Commands.add("fillGoogleFormPartly", () => {
|
||||
cy.get(googleForm.googleClientId).type(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ export class DataSources {
|
|||
private _datasourceCard = ".t--datasource"
|
||||
_templateMenu = ".t--template-menu"
|
||||
private _createQuery = ".t--create-query"
|
||||
private _importSuccessModal = ".t--import-app-success-modal"
|
||||
private _importSuccessModalClose = ".t--import-success-modal-got-it"
|
||||
_visibleTextSpan = (spanText: string) => "//span[contains(text(),'" + spanText + "')]"
|
||||
_dropdownTitle = (ddTitle: string) => "//p[contains(text(),'" + ddTitle + "')]/parent::label/following-sibling::div/div/div"
|
||||
_reconnectModal = "div.reconnect-datasource-modal"
|
||||
|
|
@ -112,6 +114,8 @@ export class DataSources {
|
|||
this.ValidateNSelectDropdown("Connection Mode", "", "Read / Write")
|
||||
this.FillPostgresDSForm()
|
||||
cy.get(this._saveDs).click();
|
||||
cy.get(this._importSuccessModal).should("be.visible");
|
||||
cy.get(this._importSuccessModalClose).click({ force: true });
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -5,18 +5,39 @@ export class JSEditor {
|
|||
public locator = ObjectsRegistry.CommonLocators;
|
||||
public ee = ObjectsRegistry.EntityExplorer;
|
||||
|
||||
private _runButton = "//li//*[local-name() = 'svg' and @class='run-button']";
|
||||
private _runButton = "button.run-js-action";
|
||||
private _settingsTab = ".tab-title:contains('Settings')";
|
||||
private _codeTab = ".tab-title:contains('Code')";
|
||||
private _onPageLoadRadioButton = (functionName: string, onLoad: boolean) =>
|
||||
`.${functionName}-on-page-load-setting label:contains(${
|
||||
onLoad ? "Yes" : "No"
|
||||
}) span.checkbox`;
|
||||
private _confirmBeforeExecuteRadioButton = (
|
||||
functionName: string,
|
||||
shouldConfirm: boolean,
|
||||
) =>
|
||||
`.${functionName}-confirm-before-execute label:contains(${
|
||||
shouldConfirm ? "Yes" : "No"
|
||||
}) span.checkbox`;
|
||||
private _outputConsole = ".CodeEditorTarget";
|
||||
private _jsObjName = ".t--js-action-name-edit-field span";
|
||||
private _jsObjTxt = ".t--js-action-name-edit-field input";
|
||||
private _newJSobj = "span:contains('New JS Object')"
|
||||
private _bindingsClose = ".t--entity-property-close"
|
||||
private _propertyList = ".t--entity-property"
|
||||
private _responseTabAction = (funName: string) => "//div[@class='function-name'][text()='" + funName + "']/following-sibling::div//*[local-name()='svg']"
|
||||
private _functionSetting = (settingTxt: string) => "//span[text()='" + settingTxt + "']/parent::div/following-sibling::input[@type='checkbox']"
|
||||
_dialog = (dialogHeader: string) => "//div[contains(@class, 'bp3-dialog')]//h4[contains(text(), '" + dialogHeader + "')]"
|
||||
private _closeSettings = "span[icon='small-cross']"
|
||||
|
||||
private _newJSobj = "span:contains('New JS Object')";
|
||||
private _bindingsClose = ".t--entity-property-close";
|
||||
private _propertyList = ".t--entity-property";
|
||||
private _responseTabAction = (funName: string) =>
|
||||
"//div[@class='function-name'][text()='" +
|
||||
funName +
|
||||
"']/following-sibling::div//*[local-name()='svg']";
|
||||
private _functionSetting = (settingTxt: string) =>
|
||||
"//span[text()='" +
|
||||
settingTxt +
|
||||
"']/parent::div/following-sibling::input[@type='checkbox']";
|
||||
_dialog = (dialogHeader: string) =>
|
||||
"//div[contains(@class, 'bp3-dialog')]//h4[contains(text(), '" +
|
||||
dialogHeader +
|
||||
"')]";
|
||||
private _closeSettings = "span[icon='small-cross']";
|
||||
|
||||
public NavigateToJSEditor() {
|
||||
cy.get(this.locator._createNew)
|
||||
|
|
@ -87,7 +108,7 @@ export class JSEditor {
|
|||
if (toRun) {
|
||||
//clicking 1 times & waits for 3 second for result to be populated!
|
||||
Cypress._.times(1, () => {
|
||||
cy.xpath(this._runButton)
|
||||
cy.get(this._runButton)
|
||||
.first()
|
||||
.click()
|
||||
.wait(3000);
|
||||
|
|
@ -109,13 +130,21 @@ export class JSEditor {
|
|||
this.agHelper.AssertAutoSave(); //Ample wait due to open bug # 10284
|
||||
}
|
||||
|
||||
public EnterJSContext(endp: string, value: string, paste = true, toToggleOnJS = false, notField = false) {
|
||||
public EnterJSContext(
|
||||
endp: string,
|
||||
value: string,
|
||||
paste = true,
|
||||
toToggleOnJS = false,
|
||||
notField = false,
|
||||
) {
|
||||
if (toToggleOnJS) {
|
||||
cy.get(this.locator._jsToggle(endp.replace(/ +/g, "").toLowerCase()))
|
||||
.invoke("attr", "class")
|
||||
.then((classes: any) => {
|
||||
if (!classes.includes("is-active")) {
|
||||
cy.get(this.locator._jsToggle(endp.replace(/ +/g, "").toLowerCase()))
|
||||
cy.get(
|
||||
this.locator._jsToggle(endp.replace(/ +/g, "").toLowerCase()),
|
||||
)
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
|
|
@ -131,20 +160,23 @@ export class JSEditor {
|
|||
// .type("{del}", { force: true });
|
||||
|
||||
if (paste) {
|
||||
this.agHelper.EnterValue(value, endp, notField)
|
||||
}
|
||||
else {
|
||||
cy.get(this.locator._propertyControl + endp.replace(/ +/g, "").toLowerCase() + " " + this.locator._codeMirrorTextArea)
|
||||
this.agHelper.EnterValue(value, endp, notField);
|
||||
} else {
|
||||
cy.get(
|
||||
this.locator._propertyControl +
|
||||
endp.replace(/ +/g, "").toLowerCase() +
|
||||
" " +
|
||||
this.locator._codeMirrorTextArea,
|
||||
)
|
||||
.first()
|
||||
.then((el: any) => {
|
||||
const input = cy.get(el);
|
||||
input.type(value, {
|
||||
parseSpecialCharSequences: false,
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// cy.focused().then(($cm: any) => {
|
||||
// if ($cm.contents != "") {
|
||||
// cy.log("The field is not empty");
|
||||
|
|
@ -175,18 +207,22 @@ export class JSEditor {
|
|||
// });
|
||||
// });
|
||||
|
||||
this.agHelper.AssertAutoSave()//Allowing time for Evaluate value to capture value
|
||||
|
||||
this.agHelper.AssertAutoSave(); //Allowing time for Evaluate value to capture value
|
||||
}
|
||||
|
||||
public RemoveText(endp: string) {
|
||||
cy.get(this.locator._propertyControl + endp + " " + this.locator._codeMirrorTextArea)
|
||||
cy.get(
|
||||
this.locator._propertyControl +
|
||||
endp +
|
||||
" " +
|
||||
this.locator._codeMirrorTextArea,
|
||||
)
|
||||
.first()
|
||||
.focus()
|
||||
.type("{uparrow}", { force: true })
|
||||
.type("{ctrl}{shift}{downarrow}", { force: true })
|
||||
.type("{del}", { force: true });
|
||||
this.agHelper.AssertAutoSave()
|
||||
this.agHelper.AssertAutoSave();
|
||||
}
|
||||
|
||||
public RenameJSObjFromForm(renameVal: string) {
|
||||
|
|
@ -216,7 +252,7 @@ export class JSEditor {
|
|||
|
||||
public validateDefaultJSObjProperties(jsObjName: string) {
|
||||
this.ee.ActionContextMenuByEntityName(jsObjName, "Show Bindings");
|
||||
cy.get(this._propertyList).then(function ($lis) {
|
||||
cy.get(this._propertyList).then(function($lis) {
|
||||
const bindingsLength = $lis.length;
|
||||
expect(bindingsLength).to.be.at.least(4);
|
||||
expect($lis.eq(0).text()).to.be.oneOf([
|
||||
|
|
@ -239,17 +275,22 @@ export class JSEditor {
|
|||
cy.get(this._bindingsClose).click({ force: true });
|
||||
}
|
||||
|
||||
|
||||
public EnableOnPageLoad(funName: string, onLoad = true, bfrCalling = true) {
|
||||
|
||||
this.agHelper.GetNClick(this._responseTabAction(funName))
|
||||
this.agHelper.AssertElementPresence(this._dialog('Function settings'))
|
||||
if (onLoad)
|
||||
this.agHelper.CheckUncheck(this._functionSetting(Cypress.env("MESSAGES").JS_SETTINGS_ONPAGELOAD()), true)
|
||||
if (bfrCalling)
|
||||
this.agHelper.CheckUncheck(this._functionSetting(Cypress.env("MESSAGES").JS_SETTINGS_CONFIRM_EXECUTION()), true)
|
||||
|
||||
this.agHelper.GetNClick(this._closeSettings)
|
||||
public AddJSFunctionSettings(
|
||||
funName: string,
|
||||
onLoad = true,
|
||||
bfrCalling = true,
|
||||
) {
|
||||
// Navigate to Settings tab
|
||||
this.agHelper.GetNClick(this._settingsTab);
|
||||
// Set onPageLoad
|
||||
cy.get(this._onPageLoadRadioButton(funName, onLoad))
|
||||
.first()
|
||||
.click();
|
||||
// Set confirmBeforeExecute
|
||||
cy.get(this._confirmBeforeExecuteRadioButton(funName, bfrCalling))
|
||||
.first()
|
||||
.click();
|
||||
// Return to code tab
|
||||
this.agHelper.GetNClick(this._codeTab);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
} from "@appsmith/constants/ReduxActionConstants";
|
||||
import { JSCollection, JSAction } from "entities/JSCollection";
|
||||
import { RefactorAction, SetFunctionPropertyPayload } from "api/JSActionAPI";
|
||||
|
||||
export const createNewJSCollection = (
|
||||
pageId: string,
|
||||
): ReduxAction<{ pageId: string }> => ({
|
||||
|
|
@ -89,3 +90,13 @@ export const updateJSFunction = (payload: SetFunctionPropertyPayload) => {
|
|||
payload,
|
||||
};
|
||||
};
|
||||
|
||||
export const setActiveJSAction = (payload: {
|
||||
jsCollectionId: string;
|
||||
jsActionId: string;
|
||||
}) => {
|
||||
return {
|
||||
type: ReduxActionTypes.SET_ACTIVE_JS_ACTION,
|
||||
payload,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -699,6 +699,7 @@ export const ReduxActionTypes = {
|
|||
GET_TEMPLATE_SUCCESS: "GET_TEMPLATES_SUCCESS",
|
||||
START_EXECUTE_JS_FUNCTION: "START_EXECUTE_JS_FUNCTION",
|
||||
RESET_PAGE_LIST: "RESET_PAGE_LIST",
|
||||
SET_ACTIVE_JS_ACTION: "SET_ACTIVE_JS_ACTION",
|
||||
};
|
||||
|
||||
export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes];
|
||||
|
|
|
|||
|
|
@ -392,6 +392,8 @@ export const ACTION_CONFIGURATION_UPDATED = () => "Configuration updated";
|
|||
export const WIDGET_PROPERTIES_UPDATED = () => "Widget properties were updated";
|
||||
export const EMPTY_RESPONSE_FIRST_HALF = () => "🙌 Click on";
|
||||
export const EMPTY_RESPONSE_LAST_HALF = () => "to get a response";
|
||||
export const EMPTY_JS_RESPONSE_LAST_HALF = () =>
|
||||
"to view response of selected function";
|
||||
export const INVALID_EMAIL = () => "Please enter a valid email";
|
||||
export const DEBUGGER_INTERCOM_TEXT = (text: string) =>
|
||||
`Hi, \nI'm facing the following error on Appsmith, can you please help? \n\n${text}`;
|
||||
|
|
@ -451,6 +453,8 @@ export const JS_EXECUTION_FAILURE = () => "JS Function execution failed";
|
|||
export const JS_EXECUTION_FAILURE_TOASTER = () =>
|
||||
"There was an error while executing function";
|
||||
export const JS_SETTINGS_ONPAGELOAD = () => "Run function on page load (Beta)";
|
||||
export const JS_EXECUTION_SUCCESS_TOASTER = (actionName: string) =>
|
||||
`${actionName} ran successfully`;
|
||||
export const JS_SETTINGS_ONPAGELOAD_SUBTEXT = () =>
|
||||
"Will refresh data every time page is reloaded";
|
||||
export const JS_SETTINGS_CONFIRM_EXECUTION = () =>
|
||||
|
|
@ -459,6 +463,13 @@ export const JS_SETTINGS_CONFIRM_EXECUTION_SUBTEXT = () =>
|
|||
"Ask confirmation from the user every time before refreshing data";
|
||||
export const JS_SETTINGS_EXECUTE_TIMEOUT = () =>
|
||||
"Function Timeout (in milliseconds)";
|
||||
export const ASYNC_FUNCTION_SETTINGS_HEADING = () => "Async Function Settings";
|
||||
export const NO_ASYNC_FUNCTIONS = () =>
|
||||
"There is no asynchronous function in this JSObject";
|
||||
export const NO_JS_FUNCTION_TO_RUN = (JSObjectName: string) =>
|
||||
`${JSObjectName} has no function`;
|
||||
export const NO_JS_FUNCTION_RETURN_VALUE = (JSFunctionName: string) =>
|
||||
`${JSFunctionName} did not return any data. Did you add a return statement?`;
|
||||
|
||||
// Import/Export Application features
|
||||
export const IMPORT_APPLICATION_MODAL_TITLE = () => "Import application";
|
||||
|
|
@ -688,6 +699,10 @@ export const CONTACT_SALES_MESSAGE_ON_INTERCOM = (orgName: string) =>
|
|||
export const REPOSITORY_LIMIT_REACHED = () => "Repository Limit Reached";
|
||||
export const REPOSITORY_LIMIT_REACHED_INFO = () =>
|
||||
"Adding and using upto 3 repositories is free. To add more repositories kindly upgrade.";
|
||||
export const APPLICATION_IMPORT_SUCCESS = (username: string) =>
|
||||
`${username}! Your application is ready to use.`;
|
||||
export const APPLICATION_IMPORT_SUCCESS_DESCRIPTION = () =>
|
||||
"All your datasources are configuered and ready to use.";
|
||||
export const NONE_REVERSIBLE_MESSAGE = () =>
|
||||
"This action is non reversible. Proceed with caution.";
|
||||
export const CONTACT_SUPPORT_TO_UPGRADE = () =>
|
||||
|
|
@ -999,6 +1014,9 @@ export const TEST_EMAIL_SUCCESS = (email: string) => () =>
|
|||
`Test email sent, please check the inbox of ${email}`;
|
||||
export const TEST_EMAIL_SUCCESS_TROUBLESHOOT = () => "Troubleshoot";
|
||||
export const TEST_EMAIL_FAILURE = () => "Sending Test Email Failed";
|
||||
export const DISCONNECT_AUTH_ERROR = () =>
|
||||
"Cannot disconnect the only connected authentication method.";
|
||||
export const MANDATORY_FIELDS_ERROR = () => "Mandatory fields cannot be empty";
|
||||
//Reflow Beta Screen
|
||||
export const REFLOW_BETA_CHECKBOX_LABEL = () =>
|
||||
"Turn on new drag & drop experience";
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import Icon from "components/ads/Icon";
|
|||
import TooltipComponent from "components/ads/Tooltip";
|
||||
import { Position } from "@blueprintjs/core";
|
||||
import { adminSettingsCategoryUrl } from "RouteBuilder";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
|
||||
const { intercomAppID } = getAppsmithConfigs();
|
||||
|
||||
|
|
@ -149,6 +150,30 @@ export function AuthPage({ authMethods }: { authMethods: AuthMethodType[] }) {
|
|||
}
|
||||
};
|
||||
|
||||
const onClickHandler = (method: AuthMethodType) => {
|
||||
if (!method.needsUpgrade || method.isConnected) {
|
||||
AnalyticsUtil.logEvent(
|
||||
method.isConnected
|
||||
? "ADMIN_SETTINGS_EDIT_AUTH_METHOD"
|
||||
: "ADMIN_SETTINGS_ENABLE_AUTH_METHOD",
|
||||
{
|
||||
method: method.label,
|
||||
},
|
||||
);
|
||||
history.push(
|
||||
adminSettingsCategoryUrl({
|
||||
category: SettingCategories.AUTHENTICATION,
|
||||
subCategory: method.category,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
AnalyticsUtil.logEvent("ADMIN_SETTINGS_UPGRADE_AUTH_METHOD", {
|
||||
method: method.label,
|
||||
});
|
||||
triggerIntercom(method.label);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<SettingsFormWrapper>
|
||||
|
|
@ -211,16 +236,7 @@ export function AuthPage({ authMethods }: { authMethods: AuthMethodType[] }) {
|
|||
: method.category
|
||||
}`}
|
||||
data-cy="btn-auth-account"
|
||||
onClick={() =>
|
||||
!method.needsUpgrade || method.isConnected
|
||||
? history.push(
|
||||
adminSettingsCategoryUrl({
|
||||
category: SettingCategories.AUTHENTICATION,
|
||||
subCategory: method.category,
|
||||
}),
|
||||
)
|
||||
: triggerIntercom(method.label)
|
||||
}
|
||||
onClick={() => onClickHandler(method)}
|
||||
text={createMessage(
|
||||
method.isConnected
|
||||
? EDIT
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export enum SettingTypes {
|
|||
UNEDITABLEFIELD = "UNEDITABLEFIELD",
|
||||
ACCORDION = "ACCORDION",
|
||||
TAGINPUT = "TAGINPUT",
|
||||
DROPDOWN = "DROPDOWN",
|
||||
}
|
||||
|
||||
export enum SettingSubtype {
|
||||
|
|
@ -52,6 +53,7 @@ export interface Setting {
|
|||
isRequired?: boolean;
|
||||
formName?: string;
|
||||
fieldName?: string;
|
||||
dropdownOptions?: Array<{ id: string; value: string; label?: string }>;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
|
|
|
|||
|
|
@ -424,7 +424,7 @@ const ButtonStyles = css<ThemeProp & ButtonProps>`
|
|||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled("button")`
|
||||
export const StyledButton = styled("button")`
|
||||
${ButtonStyles}
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@ const Wrapper = styled.div<{ offset?: string }>`
|
|||
}
|
||||
`;
|
||||
|
||||
function CopyToClipboard(props: { copyText: string; btnWidth?: string }) {
|
||||
function CopyToClipboard(props: {
|
||||
className?: string;
|
||||
copyText: string;
|
||||
btnWidth?: string;
|
||||
}) {
|
||||
const { copyText } = props;
|
||||
const copyURLInput = createRef<HTMLInputElement>();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
|
@ -38,7 +42,7 @@ function CopyToClipboard(props: { copyText: string; btnWidth?: string }) {
|
|||
}
|
||||
};
|
||||
return (
|
||||
<Wrapper offset={props.btnWidth}>
|
||||
<Wrapper className={props.className} offset={props.btnWidth}>
|
||||
<TextInput
|
||||
defaultValue={copyText}
|
||||
onChange={() => {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ const StyledDialog = styled(Dialog)<{
|
|||
padding: 0;
|
||||
background: ${(props) => props.theme.colors.modal.bg};
|
||||
box-shadow: none;
|
||||
.${Classes.ICON} {
|
||||
color: ${(props) => props.theme.colors.modal.iconColor};
|
||||
min-height: unset;
|
||||
svg {
|
||||
color: ${Colors.GREY_800};
|
||||
}
|
||||
|
||||
.${Classes.BUTTON}.${Classes.MINIMAL}:hover {
|
||||
|
|
@ -39,19 +40,23 @@ const StyledDialog = styled(Dialog)<{
|
|||
color: ${(props) => props.theme.colors.modal.headerText};
|
||||
font-weight: ${(props) => props.theme.typography.h1.fontWeight};
|
||||
font-size: ${(props) => props.theme.typography.h1.fontSize}px;
|
||||
line-height: ${(props) => props.theme.typography.h1.lineHeight}px;
|
||||
line-height: unset;
|
||||
letter-spacing: ${(props) => props.theme.typography.h1.letterSpacing};
|
||||
}
|
||||
|
||||
.${Classes.DIALOG_CLOSE_BUTTON} {
|
||||
color: ${Colors.CHARCOAL};
|
||||
color: ${Colors.SCORPION};
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
|
||||
svg {
|
||||
fill: ${Colors.CHARCOAL};
|
||||
fill: ${Colors.SCORPION};
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
&:hover {
|
||||
fill: ${Colors.COD_GRAY};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +83,7 @@ const StyledDialog = styled(Dialog)<{
|
|||
|
||||
& .${Classes.DIALOG_BODY} {
|
||||
margin: 0;
|
||||
margin-top: ${(props) => (props.noModalBodyMarginTop ? "0px" : "24px")};
|
||||
margin-top: ${(props) => (props.noModalBodyMarginTop ? "0px" : "16px")};
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ export function DraggableListCard(props: RenderComponentProps) {
|
|||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const debouncedUpdate = _.debounce(updateOption, 1000);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibility(item.isVisible);
|
||||
}, [item.isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditing && item && item.label) setValue(item.label);
|
||||
}, [item?.label, isEditing]);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import Dropdown from "./Dropdown";
|
|||
import { lightTheme } from "selectors/themeSelectors";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
const props = {
|
||||
const optionsProps: any = {
|
||||
options: [
|
||||
{ label: "Primary", value: "PRIMARY" },
|
||||
{ label: "Secondary", value: "SECONDARY" },
|
||||
|
|
@ -24,8 +24,13 @@ const props = {
|
|||
showLabelOnly: true,
|
||||
};
|
||||
|
||||
const noOptionsProps = {
|
||||
options: [],
|
||||
};
|
||||
|
||||
const getTestComponent = (
|
||||
handleOnSelect: any = undefined,
|
||||
props = optionsProps,
|
||||
allowDeselection?: boolean,
|
||||
) => (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
|
|
@ -135,15 +140,15 @@ describe("<Dropdown /> - Keyboard Navigation", () => {
|
|||
userEvent.keyboard("{ArrowDown}");
|
||||
userEvent.keyboard("{Enter}");
|
||||
expect(handleOnSelect).toHaveBeenLastCalledWith(
|
||||
props.options[1].value,
|
||||
props.options[1],
|
||||
optionsProps.options[1].value,
|
||||
optionsProps.options[1],
|
||||
);
|
||||
userEvent.keyboard("{Enter}");
|
||||
userEvent.keyboard("{ArrowDown}");
|
||||
userEvent.keyboard(" ");
|
||||
expect(handleOnSelect).toHaveBeenLastCalledWith(
|
||||
props.options[2].value,
|
||||
props.options[2],
|
||||
optionsProps.options[2].value,
|
||||
optionsProps.options[2],
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -179,26 +184,26 @@ describe("<Dropdown /> - allowDeselection behaviour", () => {
|
|||
// click on Second Item
|
||||
fireEvent.click(screen.queryAllByRole("option")[1]);
|
||||
expect(screen.getByRole("option", { selected: true })).toHaveTextContent(
|
||||
props.options[1].label,
|
||||
optionsProps.options[1].label,
|
||||
);
|
||||
expect(screen.getByRole("listbox")).toHaveTextContent(
|
||||
props.options[1].label,
|
||||
optionsProps.options[1].label,
|
||||
);
|
||||
|
||||
// click on Second Item Again
|
||||
fireEvent.click(screen.queryAllByRole("option")[1]);
|
||||
expect(screen.getByRole("option", { selected: true })).toHaveTextContent(
|
||||
props.options[1].label,
|
||||
optionsProps.options[1].label,
|
||||
);
|
||||
expect(screen.getByRole("listbox")).toHaveTextContent(
|
||||
props.options[1].label,
|
||||
optionsProps.options[1].label,
|
||||
);
|
||||
expect(screen.getByRole("option", { selected: true })).not.toBeNull();
|
||||
});
|
||||
|
||||
it("Test allowDeselection = true behaviour", async () => {
|
||||
const handleOnSelect = jest.fn();
|
||||
render(getTestComponent(handleOnSelect, true));
|
||||
render(getTestComponent(handleOnSelect, optionsProps, true));
|
||||
expect(screen.getByRole("listbox")).toHaveTextContent("Primary");
|
||||
|
||||
const dropdown = screen
|
||||
|
|
@ -213,10 +218,10 @@ describe("<Dropdown /> - allowDeselection behaviour", () => {
|
|||
// click on Third Item
|
||||
fireEvent.click(screen.queryAllByRole("option")[2]);
|
||||
expect(screen.getByRole("option", { selected: true })).toHaveTextContent(
|
||||
props.options[2].label,
|
||||
optionsProps.options[2].label,
|
||||
);
|
||||
expect(screen.getByRole("listbox")).toHaveTextContent(
|
||||
props.options[2].label,
|
||||
optionsProps.options[2].label,
|
||||
);
|
||||
|
||||
// click on Third Item Again, that should unselect everything
|
||||
|
|
@ -224,3 +229,19 @@ describe("<Dropdown /> - allowDeselection behaviour", () => {
|
|||
expect(screen.queryByRole("option", { selected: true })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("<Dropdown /> - when the options is an empty array", () => {
|
||||
it("Hide options renderer when option list is empty", () => {
|
||||
const handleOnSelect = jest.fn();
|
||||
render(getTestComponent(handleOnSelect, noOptionsProps));
|
||||
|
||||
const dropdown = screen
|
||||
.getByRole("listbox")
|
||||
.querySelector(".bp3-popover-target");
|
||||
expect(dropdown).not.toBeNull();
|
||||
|
||||
// open dropdown
|
||||
fireEvent.click(dropdown as Element);
|
||||
expect(screen.queryByTestId("dropdown-options-wrapper")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export type DropdownOption = {
|
|||
onSelect?: DropdownOnSelect;
|
||||
data?: any;
|
||||
isSectionHeader?: boolean;
|
||||
hasCustomBadge?: boolean;
|
||||
};
|
||||
export interface DropdownSearchProps {
|
||||
enableSearch?: boolean;
|
||||
|
|
@ -101,6 +102,8 @@ export type DropdownProps = CommonComponentProps &
|
|||
defaultIcon?: IconName;
|
||||
allowDeselection?: boolean; //prevents de-selection of the selected option
|
||||
truncateOption?: boolean; // enabled wrapping and adding tooltip on option item of dropdown menu
|
||||
customBadge?: JSX.Element;
|
||||
selectedHighlightBg?: string;
|
||||
};
|
||||
export interface DefaultDropDownValueNodeProps {
|
||||
selected: DropdownOption | DropdownOption[];
|
||||
|
|
@ -259,6 +262,10 @@ export const DropdownContainer = styled.div<{ width: string; height?: string }>`
|
|||
span.bp3-popover-target {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
span.bp3-popover-target div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
span.bp3-popover-wrapper {
|
||||
|
|
@ -333,6 +340,7 @@ const DropdownOptionsWrapper = styled.div<{
|
|||
|
||||
const OptionWrapper = styled.div<{
|
||||
selected: boolean;
|
||||
selectedHighlightBg?: string;
|
||||
}>`
|
||||
padding: ${(props) => props.theme.spaces[2] + 1}px
|
||||
${(props) => props.theme.spaces[5]}px;
|
||||
|
|
@ -340,7 +348,9 @@ const OptionWrapper = styled.div<{
|
|||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 36px;
|
||||
background-color: ${(props) => (props.selected ? Colors.GREEN_3 : null)};
|
||||
background-color: ${(props) =>
|
||||
props.selected ? props.selectedHighlightBg || Colors.GREEN_3 : null};
|
||||
|
||||
&&& svg {
|
||||
rect {
|
||||
fill: ${(props) => props.theme.colors.dropdownIconBg};
|
||||
|
|
@ -371,7 +381,7 @@ const OptionWrapper = styled.div<{
|
|||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${Colors.GREEN_3};
|
||||
background-color: ${(props) => props.selectedHighlightBg || Colors.GREEN_3};
|
||||
|
||||
&&& svg {
|
||||
rect {
|
||||
|
|
@ -668,9 +678,12 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) {
|
|||
};
|
||||
const theme = useTheme() as Theme;
|
||||
|
||||
if (!options.length) return null;
|
||||
|
||||
return (
|
||||
<DropdownWrapper
|
||||
className="ads-dropdown-options-wrapper"
|
||||
data-testid="dropdown-options-wrapper"
|
||||
isOpen={props.isOpen}
|
||||
width={optionWidth}
|
||||
>
|
||||
|
|
@ -724,6 +737,7 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) {
|
|||
}
|
||||
role="option"
|
||||
selected={isSelected}
|
||||
selectedHighlightBg={props.selectedHighlightBg}
|
||||
>
|
||||
{option.leftElement && (
|
||||
<LeftIconWrapper>{option.leftElement}</LeftIconWrapper>
|
||||
|
|
@ -747,12 +761,18 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) {
|
|||
) : null}
|
||||
{props.showLabelOnly ? (
|
||||
props.truncateOption ? (
|
||||
<TooltipWrappedText
|
||||
label={option.label || ""}
|
||||
type={TextType.P1}
|
||||
/>
|
||||
<>
|
||||
<TooltipWrappedText
|
||||
label={option.label || ""}
|
||||
type={TextType.P1}
|
||||
/>
|
||||
{option.hasCustomBadge && props.customBadge}
|
||||
</>
|
||||
) : (
|
||||
<Text type={TextType.P1}>{option.label}</Text>
|
||||
<>
|
||||
<Text type={TextType.P1}>{option.label}</Text>
|
||||
{option.hasCustomBadge && props.customBadge}
|
||||
</>
|
||||
)
|
||||
) : option.label && option.value ? (
|
||||
<LabelWrapper className="label-container">
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import React, { useState, useEffect } from "react";
|
|||
import styled from "styled-components";
|
||||
import * as log from "loglevel";
|
||||
|
||||
type OptionProps = {
|
||||
export type OptionProps = {
|
||||
label: string;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
|
|
@ -17,6 +17,9 @@ export type RadioProps = CommonComponentProps & {
|
|||
onSelect?: (value: string) => void;
|
||||
options: OptionProps[];
|
||||
backgroundColor?: string;
|
||||
// To prevent interference when there are multiple radio groups,
|
||||
// options corresponding to the same radio should have same name, which is different from others.
|
||||
name?: string;
|
||||
};
|
||||
|
||||
const RadioGroup = styled.div<{
|
||||
|
|
@ -149,7 +152,7 @@ export default function RadioComponent(props: RadioProps) {
|
|||
<input
|
||||
checked={selected === option.value}
|
||||
disabled={props.disabled || option.disabled}
|
||||
name="radio"
|
||||
name={props.name || "radio"}
|
||||
onChange={(e) => option.onSelect && option.onSelect(e.target.value)}
|
||||
type="radio"
|
||||
value={option.value}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { RefObject, useCallback, useState } from "react";
|
||||
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
|
||||
import "react-tabs/style/react-tabs.css";
|
||||
import styled from "styled-components";
|
||||
|
|
@ -6,6 +6,10 @@ import Icon, { IconName, IconSize } from "./Icon";
|
|||
import { Classes, CommonComponentProps } from "./common";
|
||||
import { useEffect } from "react";
|
||||
import { Indices } from "constants/Layers";
|
||||
import { theme } from "constants/DefaultTheme";
|
||||
import useResizeObserver from "utils/hooks/useResizeObserver";
|
||||
|
||||
export const TAB_MIN_HEIGHT = `36px`;
|
||||
|
||||
export type TabProp = {
|
||||
key: string;
|
||||
|
|
@ -28,7 +32,7 @@ const TabsWrapper = styled.div<{
|
|||
height: 100%;
|
||||
}
|
||||
.react-tabs__tab-panel {
|
||||
height: calc(100% - 36px);
|
||||
height: ${() => `calc(100% - ${TAB_MIN_HEIGHT})`};
|
||||
overflow: auto;
|
||||
}
|
||||
.react-tabs__tab-list {
|
||||
|
|
@ -251,6 +255,13 @@ const TabTitleWrapper = styled.div<{
|
|||
: ""}
|
||||
`;
|
||||
|
||||
const CollapseIconWrapper = styled.div`
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: ${() => theme.spaces[3] - 1}px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export type TabItemProps = {
|
||||
tab: TabProp;
|
||||
selected: boolean;
|
||||
|
|
@ -288,18 +299,88 @@ export type TabbedViewComponentType = CommonComponentProps & {
|
|||
vertical?: boolean;
|
||||
tabItemComponent?: (props: TabItemProps) => JSX.Element;
|
||||
responseViewer?: boolean;
|
||||
canCollapse?: boolean;
|
||||
// Reference to container for collapsing or expanding content
|
||||
containerRef?: RefObject<HTMLElement>;
|
||||
// height of container when expanded
|
||||
expandedHeight?: string;
|
||||
};
|
||||
|
||||
export function TabComponent(props: TabbedViewComponentType) {
|
||||
// Props required to support a collapsible (foldable) tab component
|
||||
export type CollapsibleTabProps = {
|
||||
// Reference to container for collapsing or expanding content
|
||||
containerRef: RefObject<HTMLDivElement>;
|
||||
// height of container when expanded( usually the default height of the tab component)
|
||||
expandedHeight: string;
|
||||
};
|
||||
|
||||
export type CollapsibleTabbedViewComponentType = TabbedViewComponentType &
|
||||
CollapsibleTabProps;
|
||||
|
||||
export const collapsibleTabRequiredPropKeys: Array<keyof CollapsibleTabProps> = [
|
||||
"containerRef",
|
||||
"expandedHeight",
|
||||
];
|
||||
|
||||
// Tab is considered collapsible only when all required collapsible props are present
|
||||
export const isCollapsibleTabComponent = (
|
||||
props: TabbedViewComponentType | CollapsibleTabbedViewComponentType,
|
||||
): props is CollapsibleTabbedViewComponentType =>
|
||||
collapsibleTabRequiredPropKeys.every((key) => key in props);
|
||||
|
||||
export function TabComponent(
|
||||
props: TabbedViewComponentType | CollapsibleTabbedViewComponentType,
|
||||
) {
|
||||
const TabItem = props.tabItemComponent || DefaultTabItem;
|
||||
// for setting selected state of an uncontrolled component
|
||||
const [selectedIndex, setSelectedIndex] = useState(props.selectedIndex || 0);
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof props.selectedIndex === "number")
|
||||
setSelectedIndex(props.selectedIndex);
|
||||
}, [props.selectedIndex]);
|
||||
|
||||
const handleContainerResize = () => {
|
||||
if (!isCollapsibleTabComponent(props)) return;
|
||||
const { containerRef, expandedHeight } = props;
|
||||
if (containerRef?.current && expandedHeight) {
|
||||
containerRef.current.style.height = isExpanded
|
||||
? TAB_MIN_HEIGHT
|
||||
: expandedHeight;
|
||||
}
|
||||
setIsExpanded((prev) => !prev);
|
||||
};
|
||||
|
||||
const resizeCallback = useCallback(
|
||||
(entries: ResizeObserverEntry[]) => {
|
||||
if (entries && entries.length) {
|
||||
const {
|
||||
contentRect: { height },
|
||||
} = entries[0];
|
||||
if (height > Number(TAB_MIN_HEIGHT.replace("px", "")) + 6) {
|
||||
!isExpanded && setIsExpanded(true);
|
||||
} else {
|
||||
isExpanded && setIsExpanded(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[isExpanded],
|
||||
);
|
||||
|
||||
useResizeObserver(
|
||||
isCollapsibleTabComponent(props) ? props.containerRef?.current : null,
|
||||
resizeCallback,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCollapsibleTabComponent(props)) return;
|
||||
const { containerRef } = props;
|
||||
if (!isExpanded && containerRef.current) {
|
||||
containerRef.current.style.height = TAB_MIN_HEIGHT;
|
||||
}
|
||||
}, [isExpanded]);
|
||||
|
||||
return (
|
||||
<TabsWrapper
|
||||
data-cy={props.cypressSelector}
|
||||
|
|
@ -307,6 +388,16 @@ export function TabComponent(props: TabbedViewComponentType) {
|
|||
shouldOverflow={props.overflow}
|
||||
vertical={props.vertical}
|
||||
>
|
||||
{isCollapsibleTabComponent(props) && (
|
||||
<CollapseIconWrapper>
|
||||
<Icon
|
||||
name={isExpanded ? "expand-more" : "expand-less"}
|
||||
onClick={handleContainerResize}
|
||||
size={IconSize.XXXXL}
|
||||
/>
|
||||
</CollapseIconWrapper>
|
||||
)}
|
||||
|
||||
<Tabs
|
||||
onSelect={(index: number) => {
|
||||
props.onSelect && props.onSelect(index);
|
||||
|
|
|
|||
77
app/client/src/components/ads/formFields/SelectField.tsx
Normal file
77
app/client/src/components/ads/formFields/SelectField.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Field,
|
||||
WrappedFieldMetaProps,
|
||||
WrappedFieldInputProps,
|
||||
} from "redux-form";
|
||||
import Dropdown from "components/ads/Dropdown";
|
||||
|
||||
type DropdownWrapperProps = {
|
||||
placeholder: string;
|
||||
input?: {
|
||||
value?: string;
|
||||
onChange?: (value?: string) => void;
|
||||
};
|
||||
options: Array<{ id: string; value: string; label?: string }>;
|
||||
fillOptions?: boolean;
|
||||
};
|
||||
|
||||
function DropdownWrapper(props: DropdownWrapperProps) {
|
||||
const [selectedOption, setSelectedOption] = useState({
|
||||
value: props.placeholder,
|
||||
});
|
||||
const onSelectHandler = (value?: string) => {
|
||||
props.input && props.input.onChange && props.input.onChange(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.input && props.input.value) {
|
||||
setSelectedOption({ value: props.input.value });
|
||||
} else if (props.placeholder) {
|
||||
setSelectedOption({ value: props.placeholder });
|
||||
}
|
||||
}, [props.input, props.placeholder]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
fillOptions={props.fillOptions}
|
||||
onSelect={onSelectHandler}
|
||||
options={props.options}
|
||||
selected={selectedOption}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const renderComponent = (
|
||||
componentProps: SelectFieldProps & {
|
||||
meta: Partial<WrappedFieldMetaProps>;
|
||||
input: Partial<WrappedFieldInputProps>;
|
||||
},
|
||||
) => {
|
||||
return <DropdownWrapper {...componentProps} />;
|
||||
};
|
||||
|
||||
type SelectFieldProps = {
|
||||
name: string;
|
||||
placeholder: string;
|
||||
options: Array<{ id: string; value: string; label?: string }>;
|
||||
size?: "large" | "small";
|
||||
outline?: boolean;
|
||||
fillOptions?: boolean;
|
||||
};
|
||||
|
||||
export function SelectField(props: SelectFieldProps) {
|
||||
return (
|
||||
<Field
|
||||
component={renderComponent}
|
||||
fillOptions={props.fillOptions}
|
||||
name={props.name}
|
||||
options={props.options}
|
||||
outline={props.outline}
|
||||
placeholder={props.placeholder}
|
||||
size={props.size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectField;
|
||||
|
|
@ -191,7 +191,7 @@ function getFieldFromValue(
|
|||
const errorArg = args[1] ? args[1][0] : "() => {}";
|
||||
const successArg = changeValue.endsWith(")")
|
||||
? `() => ${changeValue}`
|
||||
: `() => ${changeValue}()`;
|
||||
: `() => {}`;
|
||||
|
||||
return value.replace(
|
||||
ACTION_TRIGGER_REGEX,
|
||||
|
|
@ -217,7 +217,8 @@ function getFieldFromValue(
|
|||
const successArg = args[0] ? args[0][0] : "() => {}";
|
||||
const errorArg = changeValue.endsWith(")")
|
||||
? `() => ${changeValue}`
|
||||
: `() => ${changeValue}()`;
|
||||
: `() => {}`;
|
||||
|
||||
return value.replace(
|
||||
ACTION_TRIGGER_REGEX,
|
||||
`{{$1(${successArg}, ${errorArg})}}`,
|
||||
|
|
|
|||
|
|
@ -153,9 +153,9 @@ const StyledCallout = styled(Callout)`
|
|||
}
|
||||
`;
|
||||
|
||||
const InlineButton = styled(Button)`
|
||||
export const InlineButton = styled(Button)`
|
||||
display: inline-flex;
|
||||
margin: 0 4px;
|
||||
margin: 0 8px;
|
||||
`;
|
||||
|
||||
const HelpSection = styled.div`
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ export const WARNING_LINT_ERRORS = {
|
|||
|
||||
export const LINT_TOOLTIP_CLASS = "CodeMirror-lint-tooltip";
|
||||
|
||||
export const LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS =
|
||||
"CodeMirror-lint-tooltip-left";
|
||||
export const LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS = "CodeMirror-lint-tooltip-left";
|
||||
|
||||
export enum LintTooltipDirection {
|
||||
left = "left",
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ import { replayHighlightClass } from "globalStyles/portals";
|
|||
import {
|
||||
LintTooltipDirection,
|
||||
LINT_TOOLTIP_CLASS,
|
||||
LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS,
|
||||
LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS,
|
||||
} from "./constants";
|
||||
|
||||
interface ReduxStateProps {
|
||||
|
|
@ -135,6 +135,31 @@ export type EditorStyleProps = {
|
|||
popperPlacement?: Placement;
|
||||
popperZIndex?: Indices;
|
||||
};
|
||||
/**
|
||||
* line => Line to which the gutter is added
|
||||
*
|
||||
* element => HTML Element that gets added to line
|
||||
*
|
||||
* isFocusedAction => function called when focused
|
||||
*/
|
||||
export type GutterConfig = {
|
||||
line: number;
|
||||
element: HTMLElement;
|
||||
isFocusedAction: () => void;
|
||||
};
|
||||
|
||||
export type CodeEditorGutter = {
|
||||
getGutterConfig:
|
||||
| ((editorValue: string, cursorLineNumber: number) => GutterConfig | null)
|
||||
| null;
|
||||
gutterId: string;
|
||||
};
|
||||
|
||||
export type CustomKeyMap = {
|
||||
// combination of keys
|
||||
combination: string;
|
||||
onKeyDown: (cm: CodeMirror.Editor) => void;
|
||||
};
|
||||
|
||||
export type EditorProps = EditorStyleProps &
|
||||
EditorConfig & {
|
||||
|
|
@ -153,6 +178,8 @@ export type EditorProps = EditorStyleProps &
|
|||
handleMouseLeave?: () => void;
|
||||
isReadOnly?: boolean;
|
||||
isRawView?: boolean;
|
||||
// Custom gutter
|
||||
customGutter?: CodeEditorGutter;
|
||||
};
|
||||
|
||||
type Props = ReduxStateProps &
|
||||
|
|
@ -198,6 +225,7 @@ class CodeEditor extends Component<Props, State> {
|
|||
componentDidMount(): void {
|
||||
if (this.codeEditorTarget.current) {
|
||||
const options: EditorConfiguration = {
|
||||
autoRefresh: true,
|
||||
mode: this.props.mode,
|
||||
theme: EditorThemes[this.props.theme],
|
||||
viewportMargin: 10,
|
||||
|
|
@ -221,6 +249,8 @@ class CodeEditor extends Component<Props, State> {
|
|||
tabindex: -1,
|
||||
};
|
||||
|
||||
const gutters = new Set<string>();
|
||||
|
||||
if (!this.props.input.onChange || this.props.disabled) {
|
||||
options.readOnly = true;
|
||||
options.scrollbarStyle = "null";
|
||||
|
|
@ -230,9 +260,13 @@ class CodeEditor extends Component<Props, State> {
|
|||
if (this.props.tabBehaviour === TabBehaviour.INPUT) {
|
||||
options.extraKeys["Tab"] = false;
|
||||
}
|
||||
if (this.props.customGutter) {
|
||||
gutters.add(this.props.customGutter.gutterId);
|
||||
}
|
||||
if (this.props.folding) {
|
||||
options.foldGutter = true;
|
||||
options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"];
|
||||
gutters.add("CodeMirror-linenumbers");
|
||||
gutters.add("CodeMirror-foldgutter");
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
options.foldOptions = {
|
||||
|
|
@ -241,6 +275,7 @@ class CodeEditor extends Component<Props, State> {
|
|||
},
|
||||
};
|
||||
}
|
||||
options.gutters = Array.from(gutters);
|
||||
|
||||
// Set value of the editor
|
||||
const inputValue = getInputValue(this.props.input.value) || "";
|
||||
|
|
@ -262,7 +297,6 @@ class CodeEditor extends Component<Props, State> {
|
|||
// which means CodeMirror recalculates itself only one time, once all CodeMirror
|
||||
// changes here are completed
|
||||
//
|
||||
|
||||
editor.on("beforeChange", this.handleBeforeChange);
|
||||
editor.on("change", this.startChange);
|
||||
editor.on("keyup", this.handleAutocompleteKeyup);
|
||||
|
|
@ -270,6 +304,7 @@ class CodeEditor extends Component<Props, State> {
|
|||
editor.on("cursorActivity", this.handleCursorMovement);
|
||||
editor.on("blur", this.handleEditorBlur);
|
||||
editor.on("postPick", () => this.handleAutocompleteVisibility(editor));
|
||||
|
||||
if (this.props.height) {
|
||||
editor.setSize("100%", this.props.height);
|
||||
} else {
|
||||
|
|
@ -330,7 +365,8 @@ class CodeEditor extends Component<Props, State> {
|
|||
});
|
||||
}
|
||||
|
||||
handleMouseMove = () => {
|
||||
handleMouseMove = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
this.handleCustomGutter(this.editor.lineAtHeight(e.clientY, "window"));
|
||||
// this code only runs when we want custom tool tip for any highlighted text inside codemirror instance
|
||||
if (
|
||||
this.props.showCustomToolTipForHighlightedText &&
|
||||
|
|
@ -413,7 +449,29 @@ class CodeEditor extends Component<Props, State> {
|
|||
});
|
||||
}
|
||||
|
||||
handleCustomGutter = (lineNumber: number | null, isFocused = false) => {
|
||||
const { customGutter } = this.props;
|
||||
const editor = this.editor;
|
||||
if (!customGutter || !editor) return;
|
||||
editor.clearGutter(customGutter.gutterId);
|
||||
|
||||
if (lineNumber && customGutter.getGutterConfig) {
|
||||
const gutterConfig = customGutter.getGutterConfig(
|
||||
editor.getValue(),
|
||||
lineNumber,
|
||||
);
|
||||
if (!gutterConfig) return;
|
||||
editor.setGutterMarker(
|
||||
gutterConfig.line,
|
||||
customGutter.gutterId,
|
||||
gutterConfig.element,
|
||||
);
|
||||
isFocused && gutterConfig.isFocusedAction();
|
||||
}
|
||||
};
|
||||
|
||||
handleCursorMovement = (cm: CodeMirror.Editor) => {
|
||||
this.handleCustomGutter(cm.getCursor().line, true);
|
||||
// ignore if disabled
|
||||
if (!this.props.input.onChange || this.props.disabled) {
|
||||
return;
|
||||
|
|
@ -445,6 +503,7 @@ class CodeEditor extends Component<Props, State> {
|
|||
this.handleChange();
|
||||
this.setState({ isFocused: false });
|
||||
this.editor.setOption("matchBrackets", false);
|
||||
this.handleCustomGutter(null);
|
||||
};
|
||||
|
||||
handleBeforeChange = (
|
||||
|
|
@ -474,7 +533,7 @@ class CodeEditor extends Component<Props, State> {
|
|||
tooltip &&
|
||||
getLintTooltipDirection(tooltip) === LintTooltipDirection.left
|
||||
) {
|
||||
tooltip.classList.add(LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS);
|
||||
tooltip.classList.add(LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const ContainerWrapper = styled.div`
|
|||
height: 100%;
|
||||
`;
|
||||
|
||||
const ListWrapper = styled.div`
|
||||
export const ListWrapper = styled.div`
|
||||
overflow: auto;
|
||||
height: calc(100% - ${LIST_HEADER_HEIGHT});
|
||||
${thinScrollbar};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, RefObject } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setCurrentTab } from "actions/debuggerActions";
|
||||
import { TabComponent, TabProp } from "components/ads/Tabs";
|
||||
import {
|
||||
CollapsibleTabProps,
|
||||
collapsibleTabRequiredPropKeys,
|
||||
TabComponent,
|
||||
TabProp,
|
||||
} from "components/ads/Tabs";
|
||||
import { getCurrentDebuggerTab } from "selectors/debuggerSelectors";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers";
|
||||
|
|
@ -12,9 +17,26 @@ type EntityBottomTabsProps = {
|
|||
responseViewer?: boolean;
|
||||
onSelect?: (tab: any) => void;
|
||||
selectedTabIndex?: number; // this is used in the event you want to directly control the index changes.
|
||||
canCollapse?: boolean;
|
||||
// Reference to container for collapsing or expanding content
|
||||
containerRef?: RefObject<HTMLElement>;
|
||||
// height of container when expanded
|
||||
expandedHeight?: string;
|
||||
};
|
||||
|
||||
type CollapsibleEntityBottomTabsProps = EntityBottomTabsProps &
|
||||
CollapsibleTabProps;
|
||||
|
||||
// Tab is considered collapsible only when all required collapsible props are present
|
||||
export const isCollapsibleEntityBottomTab = (
|
||||
props: EntityBottomTabsProps | CollapsibleEntityBottomTabsProps,
|
||||
): props is CollapsibleEntityBottomTabsProps =>
|
||||
collapsibleTabRequiredPropKeys.every((key) => key in props);
|
||||
|
||||
// Using this if there are debugger related tabs
|
||||
function EntityBottomTabs(props: EntityBottomTabsProps) {
|
||||
function EntityBottomTabs(
|
||||
props: EntityBottomTabsProps | CollapsibleEntityBottomTabsProps,
|
||||
) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(props.defaultIndex);
|
||||
const currentTab = useSelector(getCurrentDebuggerTab);
|
||||
const dispatch = useDispatch();
|
||||
|
|
@ -51,6 +73,12 @@ function EntityBottomTabs(props: EntityBottomTabsProps) {
|
|||
props.selectedTabIndex ? props.selectedTabIndex : selectedIndex
|
||||
}
|
||||
tabs={props.tabs}
|
||||
{...(isCollapsibleEntityBottomTab(props)
|
||||
? {
|
||||
containerRef: props.containerRef,
|
||||
expandedHeight: props.expandedHeight,
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import React, { useState, useRef, RefObject, useCallback } from "react";
|
||||
import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useState,
|
||||
} from "react";
|
||||
import { connect, useDispatch } from "react-redux";
|
||||
import { withRouter, RouteComponentProps } from "react-router";
|
||||
import styled from "styled-components";
|
||||
|
|
@ -9,10 +15,10 @@ import {
|
|||
DEBUGGER_ERRORS,
|
||||
DEBUGGER_LOGS,
|
||||
EXECUTING_FUNCTION,
|
||||
EMPTY_JS_OBJECT,
|
||||
PARSING_ERROR,
|
||||
EMPTY_RESPONSE_FIRST_HALF,
|
||||
EMPTY_RESPONSE_LAST_HALF,
|
||||
EMPTY_JS_RESPONSE_LAST_HALF,
|
||||
NO_JS_FUNCTION_RETURN_VALUE,
|
||||
} from "@appsmith/constants/messages";
|
||||
import { EditorTheme } from "./CodeEditor/EditorConfig";
|
||||
import DebuggerLogs from "./Debugger/DebuggerLogs";
|
||||
|
|
@ -21,40 +27,35 @@ import Resizer, { ResizerCSS } from "./Debugger/Resizer";
|
|||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { JSCollection, JSAction } from "entities/JSCollection";
|
||||
import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor";
|
||||
import { startExecutingJSFunction } from "actions/jsPaneActions";
|
||||
import Text, { TextType } from "components/ads/Text";
|
||||
import { Classes } from "components/ads/common";
|
||||
import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen";
|
||||
import { sortBy } from "lodash";
|
||||
import { ReactComponent as JSFunction } from "assets/icons/menu/js-function.svg";
|
||||
import { ReactComponent as RunFunction } from "assets/icons/menu/run.svg";
|
||||
import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer";
|
||||
import Callout from "components/ads/Callout";
|
||||
import { Variant } from "components/ads/common";
|
||||
import { EvaluationError } from "utils/DynamicBindingUtils";
|
||||
import { Severity } from "entities/AppsmithConsole";
|
||||
import { getJSCollectionIdFromURL } from "pages/Editor/Explorer/helpers";
|
||||
import { DebugButton } from "./Debugger/DebugCTA";
|
||||
import { thinScrollbar } from "constants/DefaultTheme";
|
||||
import { setCurrentTab } from "actions/debuggerActions";
|
||||
import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers";
|
||||
import EntityBottomTabs from "./EntityBottomTabs";
|
||||
import Icon from "components/ads/Icon";
|
||||
import { ReactComponent as FunctionSettings } from "assets/icons/menu/settings.svg";
|
||||
import JSFunctionSettings from "pages/Editor/JSEditor/JSFunctionSettings";
|
||||
import FlagBadge from "components/utils/FlagBadge";
|
||||
import { TAB_MIN_HEIGHT } from "components/ads/Tabs";
|
||||
import { theme } from "constants/DefaultTheme";
|
||||
import { Button, Size } from "components/ads";
|
||||
import { CodeEditorWithGutterStyles } from "pages/Editor/JSEditor/constants";
|
||||
|
||||
const ResponseContainer = styled.div`
|
||||
${ResizerCSS}
|
||||
// Initial height of bottom tabs
|
||||
height: ${(props) => props.theme.actionsBottomTabInitialHeight};
|
||||
width: 100%;
|
||||
// Minimum height of bottom tabs as it can be resized
|
||||
min-height: 36px;
|
||||
min-height: ${TAB_MIN_HEIGHT};
|
||||
background-color: ${(props) => props.theme.colors.apiPane.responseBody.bg};
|
||||
height: ${({ theme }) => theme.actionsBottomTabInitialHeight};
|
||||
|
||||
.react-tabs__tab-panel {
|
||||
overflow: hidden;
|
||||
${CodeEditorWithGutterStyles}
|
||||
overflow-y: auto;
|
||||
height: calc(100% - ${TAB_MIN_HEIGHT});
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -71,79 +72,39 @@ const ResponseTabWrapper = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
const ResponseTabActionsList = styled.ul`
|
||||
height: 100%;
|
||||
width: 20%;
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
${thinScrollbar};
|
||||
scrollbar-width: thin;
|
||||
overflow: auto;
|
||||
padding-bottom: 40px;
|
||||
margin-top: 0;
|
||||
`;
|
||||
|
||||
const ResponseTabAction = styled.li`
|
||||
padding: 10px 0px 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.function-name {
|
||||
margin-left: 5px;
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
}
|
||||
.function-actions {
|
||||
margin-left: auto;
|
||||
order: 2;
|
||||
svg {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
.run-button {
|
||||
margin: 0 15px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
&.active {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
`;
|
||||
|
||||
const TabbedViewWrapper = styled.div`
|
||||
height: 100%;
|
||||
|
||||
&&& {
|
||||
ul.react-tabs__tab-list {
|
||||
padding: 0px ${(props) => props.theme.spaces[12]}px;
|
||||
height: ${TAB_MIN_HEIGHT};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ResponseViewer = styled.div`
|
||||
width: 80%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const NoResponseContainer = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
width: max-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
&.empty {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.${Classes.ICON} {
|
||||
margin-right: 0px;
|
||||
svg {
|
||||
width: 150px;
|
||||
width: auto;
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.${Classes.TEXT} {
|
||||
margin-top: ${(props) => props.theme.spaces[9]}px;
|
||||
color: #090707;
|
||||
|
|
@ -166,6 +127,23 @@ const StyledCallout = styled(Callout)`
|
|||
}
|
||||
`;
|
||||
|
||||
const NoReturnValueWrapper = styled.div`
|
||||
padding-left: ${(props) => props.theme.spaces[12]}px;
|
||||
padding-top: ${(props) => props.theme.spaces[6]}px;
|
||||
`;
|
||||
const InlineButton = styled(Button)`
|
||||
display: inline-flex;
|
||||
margin: 0 4px;
|
||||
`;
|
||||
|
||||
enum JSResponseState {
|
||||
IsExecuting = "IsExecuting",
|
||||
IsDirty = "IsDirty",
|
||||
NoResponse = "NoResponse",
|
||||
ShowResponse = "ShowResponse",
|
||||
NoReturnValue = "NoReturnValue",
|
||||
}
|
||||
|
||||
interface ReduxStateProps {
|
||||
responses: Record<string, any>;
|
||||
isExecuting: Record<string, boolean>;
|
||||
|
|
@ -173,26 +151,35 @@ interface ReduxStateProps {
|
|||
|
||||
type Props = ReduxStateProps &
|
||||
RouteComponentProps<JSEditorRouteParams> & {
|
||||
currentFunction: JSAction | null;
|
||||
theme?: EditorTheme;
|
||||
jsObject: JSCollection;
|
||||
errors: Array<EvaluationError>;
|
||||
disabled: boolean;
|
||||
isLoading: boolean;
|
||||
onButtonClick: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
};
|
||||
|
||||
function JSResponseView(props: Props) {
|
||||
const { errors, isExecuting, jsObject, responses } = props;
|
||||
const {
|
||||
currentFunction,
|
||||
disabled,
|
||||
errors,
|
||||
isExecuting,
|
||||
isLoading,
|
||||
jsObject,
|
||||
onButtonClick,
|
||||
responses,
|
||||
} = props;
|
||||
const [responseStatus, setResponseStatus] = useState<JSResponseState>(
|
||||
JSResponseState.NoResponse,
|
||||
);
|
||||
const panelRef: RefObject<HTMLDivElement> = useRef(null);
|
||||
const dispatch = useDispatch();
|
||||
const [selectActionId, setSelectActionId] = useState("");
|
||||
const actionList = jsObject?.actions;
|
||||
const sortedActionList = actionList && sortBy(actionList, "name");
|
||||
const response =
|
||||
selectActionId && selectActionId in responses
|
||||
? responses[selectActionId]
|
||||
currentFunction && currentFunction.id && currentFunction.id in responses
|
||||
? responses[currentFunction.id]
|
||||
: "";
|
||||
const isRunning = selectActionId && !!isExecuting[selectActionId];
|
||||
const errorsList = errors.filter((er) => {
|
||||
return er.severity === Severity.ERROR;
|
||||
});
|
||||
|
||||
const onDebugClick = useCallback(() => {
|
||||
AnalyticsUtil.logEvent("OPEN_DEBUGGER", {
|
||||
|
|
@ -200,18 +187,25 @@ function JSResponseView(props: Props) {
|
|||
});
|
||||
dispatch(setCurrentTab(DEBUGGER_TAB_KEYS.ERROR_TAB));
|
||||
}, []);
|
||||
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const [selectedFunction, setSelectedFunction] = useState<
|
||||
undefined | JSAction
|
||||
>(undefined);
|
||||
const isSelectedFunctionAsync = (id: string) => {
|
||||
const jsAction = jsObject.actions.find((action) => action.id === id);
|
||||
if (!!jsAction) {
|
||||
return jsAction?.actionConfiguration.isAsync;
|
||||
useEffect(() => {
|
||||
if (!currentFunction) {
|
||||
setResponseStatus(JSResponseState.NoResponse);
|
||||
} else if (isExecuting[currentFunction.id]) {
|
||||
setResponseStatus(JSResponseState.IsExecuting);
|
||||
} else if (
|
||||
!responses.hasOwnProperty(currentFunction.id) &&
|
||||
!isExecuting.hasOwnProperty(currentFunction.id)
|
||||
) {
|
||||
setResponseStatus(JSResponseState.NoResponse);
|
||||
} else if (
|
||||
responses.hasOwnProperty(currentFunction.id) &&
|
||||
responses[currentFunction.id] === undefined
|
||||
) {
|
||||
setResponseStatus(JSResponseState.NoReturnValue);
|
||||
} else if (responses.hasOwnProperty(currentFunction.id)) {
|
||||
setResponseStatus(JSResponseState.ShowResponse);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}, [responses, isExecuting, currentFunction]);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
|
|
@ -219,8 +213,8 @@ function JSResponseView(props: Props) {
|
|||
title: "Response",
|
||||
panelComponent: (
|
||||
<>
|
||||
<HelpSection>
|
||||
{errorsList.length > 0 ? (
|
||||
{errors.length > 0 && (
|
||||
<HelpSection>
|
||||
<StyledCallout
|
||||
fill
|
||||
label={
|
||||
|
|
@ -231,98 +225,55 @@ function JSResponseView(props: Props) {
|
|||
text={createMessage(PARSING_ERROR)}
|
||||
variant={Variant.danger}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</HelpSection>
|
||||
<ResponseTabWrapper className={errorsList.length ? "disable" : ""}>
|
||||
{sortedActionList && !sortedActionList?.length ? (
|
||||
<NoResponseContainer className="flex items-center">
|
||||
{createMessage(EMPTY_JS_OBJECT)}
|
||||
</NoResponseContainer>
|
||||
) : (
|
||||
</HelpSection>
|
||||
)}
|
||||
<ResponseTabWrapper className={errors.length ? "disable" : ""}>
|
||||
<ResponseViewer>
|
||||
<>
|
||||
<ResponseTabActionsList>
|
||||
{sortedActionList &&
|
||||
sortedActionList?.length > 0 &&
|
||||
sortedActionList.map((action) => {
|
||||
return (
|
||||
<ResponseTabAction
|
||||
className={
|
||||
action.id === selectActionId ? "active" : ""
|
||||
}
|
||||
key={action.id}
|
||||
onClick={() => {
|
||||
setSelectActionId(action.id);
|
||||
}}
|
||||
>
|
||||
<JSFunction />{" "}
|
||||
<div className="function-name">{action.name}</div>
|
||||
<div className="function-actions">
|
||||
{action.actionConfiguration.isAsync ? (
|
||||
<FlagBadge name={"ASYNC"} />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{isSelectedFunctionAsync(action.id) ? (
|
||||
<FunctionSettings
|
||||
onClick={() => {
|
||||
setSelectedFunction(action);
|
||||
setOpenSettings(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
|
||||
<RunFunction
|
||||
className="run-button"
|
||||
onClick={() => {
|
||||
runAction(action);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ResponseTabAction>
|
||||
);
|
||||
})}
|
||||
</ResponseTabActionsList>
|
||||
<ResponseViewer>
|
||||
{isRunning ? (
|
||||
<LoadingOverlayScreen theme={props.theme}>
|
||||
{createMessage(EXECUTING_FUNCTION)}
|
||||
</LoadingOverlayScreen>
|
||||
) : !responses.hasOwnProperty(selectActionId) ? (
|
||||
<NoResponseContainer className="empty">
|
||||
<Icon name="no-response" />
|
||||
<Text className="flex items-center" type={TextType.P1}>
|
||||
{EMPTY_RESPONSE_FIRST_HALF()}
|
||||
<RunFunction className="response-run" />
|
||||
{EMPTY_RESPONSE_LAST_HALF()}
|
||||
</Text>
|
||||
</NoResponseContainer>
|
||||
) : (
|
||||
<ReadOnlyEditor
|
||||
folding
|
||||
height={"100%"}
|
||||
input={{
|
||||
value: response,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ResponseViewer>
|
||||
{openSettings &&
|
||||
!!selectedFunction &&
|
||||
isSelectedFunctionAsync(selectedFunction.id) && (
|
||||
<JSFunctionSettings
|
||||
action={selectedFunction}
|
||||
openSettings={openSettings}
|
||||
toggleSettings={() => {
|
||||
setOpenSettings(!openSettings);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{responseStatus === JSResponseState.NoResponse && (
|
||||
<NoResponseContainer>
|
||||
<Icon name="no-response" />
|
||||
<Text type={TextType.P1}>
|
||||
{createMessage(EMPTY_RESPONSE_FIRST_HALF)}
|
||||
<InlineButton
|
||||
disabled={disabled}
|
||||
isLoading={isLoading}
|
||||
onClick={onButtonClick}
|
||||
size={Size.medium}
|
||||
tag="button"
|
||||
text="Run"
|
||||
type="button"
|
||||
/>
|
||||
{createMessage(EMPTY_JS_RESPONSE_LAST_HALF)}
|
||||
</Text>
|
||||
</NoResponseContainer>
|
||||
)}
|
||||
{responseStatus === JSResponseState.IsExecuting && (
|
||||
<LoadingOverlayScreen theme={props.theme}>
|
||||
{createMessage(EXECUTING_FUNCTION)}
|
||||
</LoadingOverlayScreen>
|
||||
)}
|
||||
{responseStatus === JSResponseState.NoReturnValue && (
|
||||
<NoReturnValueWrapper>
|
||||
<Text type={TextType.P1}>
|
||||
{createMessage(
|
||||
NO_JS_FUNCTION_RETURN_VALUE,
|
||||
currentFunction?.name,
|
||||
)}
|
||||
</Text>
|
||||
</NoReturnValueWrapper>
|
||||
)}
|
||||
{responseStatus === JSResponseState.ShowResponse && (
|
||||
<ReadOnlyEditor
|
||||
folding
|
||||
height={"100%"}
|
||||
input={{
|
||||
value: response,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ResponseViewer>
|
||||
</ResponseTabWrapper>
|
||||
</>
|
||||
),
|
||||
|
|
@ -339,23 +290,16 @@ function JSResponseView(props: Props) {
|
|||
},
|
||||
];
|
||||
|
||||
const runAction = (action: JSAction) => {
|
||||
setSelectActionId(action.id);
|
||||
const collectionId = getJSCollectionIdFromURL();
|
||||
dispatch(
|
||||
startExecutingJSFunction({
|
||||
collectionName: jsObject?.name || "",
|
||||
action: action,
|
||||
collectionId: collectionId || "",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponseContainer ref={panelRef}>
|
||||
<Resizer panelRef={panelRef} />
|
||||
<TabbedViewWrapper>
|
||||
<EntityBottomTabs defaultIndex={0} tabs={tabs} />
|
||||
<EntityBottomTabs
|
||||
containerRef={panelRef}
|
||||
defaultIndex={0}
|
||||
expandedHeight={theme.actionsBottomTabInitialHeight}
|
||||
tabs={tabs}
|
||||
/>
|
||||
</TabbedViewWrapper>
|
||||
</ResponseContainer>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -652,6 +652,7 @@ const lightShades = [
|
|||
"#F86A2B",
|
||||
"#FFDEDE",
|
||||
"#575757",
|
||||
"#191919",
|
||||
] as const;
|
||||
|
||||
type ShadeColor = typeof darkShades[number] | typeof lightShades[number];
|
||||
|
|
@ -1373,13 +1374,14 @@ const editorBottomBar = {
|
|||
const gitSyncModal = {
|
||||
menuBackgroundColor: Colors.ALABASTER_ALT,
|
||||
separator: Colors.ALTO2,
|
||||
closeIcon: "rgba(29, 28, 29, 0.7);",
|
||||
closeIcon: Colors.SCORPION,
|
||||
closeIconHover: Colors.COD_GRAY,
|
||||
};
|
||||
type GitSyncModalColors = typeof gitSyncModal;
|
||||
|
||||
const tabItemBackgroundFill = {
|
||||
highlightBackground: Colors.Gallery,
|
||||
highlightTextColor: Colors.CODE_GRAY,
|
||||
highlightTextColor: Colors.COD_GRAY,
|
||||
textColor: Colors.CHARCOAL,
|
||||
};
|
||||
|
||||
|
|
@ -2612,7 +2614,7 @@ export const light: ColorType = {
|
|||
},
|
||||
modal: {
|
||||
bg: lightShades[11],
|
||||
headerText: lightShades[10],
|
||||
headerText: lightShades[20],
|
||||
iconColor: lightShades[5],
|
||||
iconBg: lightShades[18],
|
||||
user: {
|
||||
|
|
|
|||
25
app/client/src/constants/ast.ts
Normal file
25
app/client/src/constants/ast.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export const ECMA_VERSION = 11;
|
||||
|
||||
/* Indicates the mode the code should be parsed in.
|
||||
This influences global strict mode and parsing of import and export declarations.
|
||||
*/
|
||||
export enum SourceType {
|
||||
script = "script",
|
||||
module = "module",
|
||||
}
|
||||
|
||||
// Each node has an attached type property which further defines
|
||||
// what all properties can the node have.
|
||||
// We will just define the ones we are working with
|
||||
export enum NodeTypes {
|
||||
MemberExpression = "MemberExpression",
|
||||
Identifier = "Identifier",
|
||||
VariableDeclarator = "VariableDeclarator",
|
||||
FunctionDeclaration = "FunctionDeclaration",
|
||||
FunctionExpression = "FunctionExpression",
|
||||
AssignmentPattern = "AssignmentPattern",
|
||||
Literal = "Literal",
|
||||
ExportDefaultDeclaration = "ExportDefaultDeclaration",
|
||||
Property = "Property",
|
||||
ArrowFunctionExpression = "ArrowFunctionExpression",
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { createGlobalStyle } from "styled-components";
|
||||
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import { getTypographyByKey, Theme } from "constants/DefaultTheme";
|
||||
import { LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS } from "components/editorComponents/CodeEditor/constants";
|
||||
import { LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS } from "components/editorComponents/CodeEditor/constants";
|
||||
|
||||
export const CodemirrorHintStyles = createGlobalStyle<{
|
||||
editorTheme: EditorTheme;
|
||||
|
|
@ -259,7 +259,7 @@ export const CodemirrorHintStyles = createGlobalStyle<{
|
|||
padding: 7px 12px;
|
||||
border-radius: 0;
|
||||
|
||||
&.${LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS}{
|
||||
&.${LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS}{
|
||||
transform: translate(-100%);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import React, { useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { JSCollection } from "entities/JSCollection";
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { JSAction, JSCollection } from "entities/JSCollection";
|
||||
import CloseEditor from "components/editorComponents/CloseEditor";
|
||||
import MoreJSCollectionsMenu from "../Explorer/JSActions/MoreJSActionsMenu";
|
||||
import { TabComponent } from "components/ads/Tabs";
|
||||
import FormLabel from "components/editorComponents/FormLabel";
|
||||
import CodeEditor from "components/editorComponents/CodeEditor";
|
||||
import {
|
||||
EditorModes,
|
||||
|
|
@ -14,184 +18,259 @@ import {
|
|||
} from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import FormRow from "components/editorComponents/FormRow";
|
||||
import JSObjectNameEditor from "./JSObjectNameEditor";
|
||||
import { updateJSCollectionBody } from "actions/jsPaneActions";
|
||||
import {
|
||||
setActiveJSAction,
|
||||
startExecutingJSFunction,
|
||||
updateJSCollectionBody,
|
||||
} from "actions/jsPaneActions";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useParams } from "react-router";
|
||||
import { ExplorerURLParams } from "../Explorer/helpers";
|
||||
import JSResponseView from "components/editorComponents/JSResponseView";
|
||||
import { EVAL_ERROR_PATH } from "utils/DynamicBindingUtils";
|
||||
import { get } from "lodash";
|
||||
import { getDataTree } from "selectors/dataTreeSelectors";
|
||||
import { EvaluationError } from "utils/DynamicBindingUtils";
|
||||
import { isEmpty, isEqual } from "lodash";
|
||||
import SearchSnippets from "components/ads/SnippetButton";
|
||||
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
|
||||
import { JSFunctionRun } from "./JSFunctionRun";
|
||||
import { AppState } from "reducers";
|
||||
import {
|
||||
getActiveJSActionId,
|
||||
getIsExecutingJSAction,
|
||||
getJSActions,
|
||||
getJSCollectionParseErrors,
|
||||
} from "selectors/entitiesSelector";
|
||||
import {
|
||||
convertJSActionsToDropdownOptions,
|
||||
convertJSActionToDropdownOption,
|
||||
getActionFromJsCollection,
|
||||
getJSActionOption,
|
||||
getJSFunctionLineGutter,
|
||||
JSActionDropdownOption,
|
||||
} from "./utils";
|
||||
import { DropdownOnSelect } from "components/ads";
|
||||
import JSFunctionSettingsView from "./JSFunctionSettings";
|
||||
import JSObjectHotKeys from "./JSObjectHotKeys";
|
||||
import {
|
||||
ActionButtons,
|
||||
Form,
|
||||
FormWrapper,
|
||||
MainConfiguration,
|
||||
NameWrapper,
|
||||
SecondaryWrapper,
|
||||
TabbedViewContainer,
|
||||
} from "./styledComponents";
|
||||
|
||||
const Form = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(
|
||||
100vh - ${(props) => props.theme.smallHeaderHeight} -
|
||||
${(props) => props.theme.backBanner}
|
||||
);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
${FormLabel} {
|
||||
padding: ${(props) => props.theme.spaces[3]}px;
|
||||
}
|
||||
${FormRow} {
|
||||
${FormLabel} {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const NameWrapper = styled.div`
|
||||
width: 49%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
input {
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
|
||||
const ActionButtons = styled.div`
|
||||
justify-self: flex-end;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
button:last-child {
|
||||
margin-left: ${(props) => props.theme.spaces[7]}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const SecondaryWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 50px);
|
||||
`;
|
||||
const MainConfiguration = styled.div`
|
||||
padding: ${(props) => props.theme.spaces[4]}px
|
||||
${(props) => props.theme.spaces[10]}px 0px
|
||||
${(props) => props.theme.spaces[10]}px;
|
||||
`;
|
||||
|
||||
export const TabbedViewContainer = styled.div`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
border-top: 2px solid ${(props) => props.theme.colors.apiPane.dividerBg};
|
||||
${FormRow} {
|
||||
min-height: auto;
|
||||
padding: ${(props) => props.theme.spaces[0]}px;
|
||||
& > * {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&&& {
|
||||
ul.react-tabs__tab-list {
|
||||
padding: 0px ${(props) => props.theme.spaces[12]}px;
|
||||
background-color: ${(props) =>
|
||||
props.theme.colors.apiPane.responseBody.bg};
|
||||
}
|
||||
.react-tabs__tab-panel {
|
||||
height: calc(100% - 36px);
|
||||
margin-top: 2px;
|
||||
background-color: ${(props) => props.theme.colors.apiPane.bg};
|
||||
}
|
||||
}
|
||||
`;
|
||||
interface JSFormProps {
|
||||
jsAction: JSCollection;
|
||||
settingsConfig: any;
|
||||
jsCollection: JSCollection;
|
||||
}
|
||||
|
||||
type Props = JSFormProps;
|
||||
|
||||
function JSEditorForm(props: Props) {
|
||||
function JSEditorForm({ jsCollection: currentJSCollection }: Props) {
|
||||
const theme = EditorTheme.LIGHT;
|
||||
const [mainTabIndex, setMainTabIndex] = useState(0);
|
||||
const dispatch = useDispatch();
|
||||
const currentJSAction = props.jsAction;
|
||||
const dataTree = useSelector(getDataTree);
|
||||
const handleOnChange = (event: string) => {
|
||||
if (currentJSAction) {
|
||||
dispatch(updateJSCollectionBody(event, currentJSAction.id));
|
||||
}
|
||||
};
|
||||
const { pageId } = useParams<ExplorerURLParams>();
|
||||
const getErrors = get(
|
||||
dataTree,
|
||||
`${currentJSAction.name}.${EVAL_ERROR_PATH}.body`,
|
||||
[],
|
||||
) as EvaluationError[];
|
||||
const [disableRunFunctionality, setDisableRunFunctionality] = useState(false);
|
||||
|
||||
// Currently active response (only changes upon execution)
|
||||
const [activeResponse, setActiveResponse] = useState<JSAction | null>(null);
|
||||
const parseErrors = useSelector(
|
||||
(state: AppState) =>
|
||||
getJSCollectionParseErrors(state, currentJSCollection.name),
|
||||
isEqual,
|
||||
);
|
||||
const jsActions = useSelector(
|
||||
(state: AppState) => getJSActions(state, currentJSCollection.id),
|
||||
isEqual,
|
||||
);
|
||||
const activeJSActionId = useSelector((state: AppState) =>
|
||||
getActiveJSActionId(state, currentJSCollection.id),
|
||||
);
|
||||
|
||||
const activeJSAction = getActionFromJsCollection(
|
||||
activeJSActionId,
|
||||
currentJSCollection,
|
||||
);
|
||||
|
||||
const [selectedJSActionOption, setSelectedJSActionOption] = useState<
|
||||
JSActionDropdownOption
|
||||
>(getJSActionOption(activeJSAction, jsActions));
|
||||
|
||||
const isExecutingCurrentJSAction = useSelector((state: AppState) =>
|
||||
getIsExecutingJSAction(
|
||||
state,
|
||||
currentJSCollection.id,
|
||||
selectedJSActionOption.data?.id || "",
|
||||
),
|
||||
);
|
||||
|
||||
// Triggered when there is a change in the code editor
|
||||
const handleEditorChange = (valueOrEvent: ChangeEvent<any> | string) => {
|
||||
const value: string =
|
||||
typeof valueOrEvent === "string"
|
||||
? valueOrEvent
|
||||
: valueOrEvent.target.value;
|
||||
|
||||
dispatch(updateJSCollectionBody(value, currentJSCollection.id));
|
||||
};
|
||||
|
||||
// Executes JS action
|
||||
const executeJSAction = (jsAction: JSAction) => {
|
||||
setActiveResponse(jsAction);
|
||||
if (jsAction.id !== selectedJSActionOption.data?.id)
|
||||
setSelectedJSActionOption(convertJSActionToDropdownOption(jsAction));
|
||||
dispatch(
|
||||
setActiveJSAction({
|
||||
jsCollectionId: currentJSCollection.id || "",
|
||||
jsActionId: jsAction.id || "",
|
||||
}),
|
||||
);
|
||||
dispatch(
|
||||
startExecutingJSFunction({
|
||||
collectionName: currentJSCollection.name || "",
|
||||
action: jsAction,
|
||||
collectionId: currentJSCollection.id || "",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleActiveActionChange = useCallback(
|
||||
(jsAction: JSAction) => {
|
||||
if (!jsAction) return;
|
||||
|
||||
// only update when there is a new active action
|
||||
if (jsAction.id !== selectedJSActionOption.data?.id) {
|
||||
setSelectedJSActionOption(convertJSActionToDropdownOption(jsAction));
|
||||
}
|
||||
},
|
||||
[selectedJSActionOption],
|
||||
);
|
||||
|
||||
const JSGutters = useMemo(
|
||||
() =>
|
||||
getJSFunctionLineGutter(
|
||||
jsActions,
|
||||
executeJSAction,
|
||||
!parseErrors.length,
|
||||
handleActiveActionChange,
|
||||
),
|
||||
[jsActions, parseErrors, handleActiveActionChange],
|
||||
);
|
||||
|
||||
const handleJSActionOptionSelection: DropdownOnSelect = (
|
||||
value,
|
||||
dropDownOption: JSActionDropdownOption,
|
||||
) => {
|
||||
dropDownOption.data &&
|
||||
setSelectedJSActionOption(
|
||||
convertJSActionToDropdownOption(dropDownOption.data),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRunAction = (
|
||||
event: React.MouseEvent<HTMLElement, MouseEvent> | KeyboardEvent,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
selectedJSActionOption.data && executeJSAction(selectedJSActionOption.data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (parseErrors.length || isEmpty(jsActions)) {
|
||||
setDisableRunFunctionality(true);
|
||||
} else {
|
||||
setDisableRunFunctionality(false);
|
||||
}
|
||||
setSelectedJSActionOption(getJSActionOption(activeJSAction, jsActions));
|
||||
}, [parseErrors, jsActions, activeJSActionId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CloseEditor />
|
||||
<Form>
|
||||
<MainConfiguration>
|
||||
<FormRow className="form-row-header">
|
||||
<NameWrapper className="t--nameOfJSObject">
|
||||
<JSObjectNameEditor page="JS_PANE" />
|
||||
</NameWrapper>
|
||||
<ActionButtons className="t--formActionButtons">
|
||||
<MoreJSCollectionsMenu
|
||||
className="t--more-action-menu"
|
||||
id={currentJSAction.id}
|
||||
name={currentJSAction.name}
|
||||
pageId={pageId}
|
||||
<FormWrapper>
|
||||
<JSObjectHotKeys runActiveJSFunction={handleRunAction}>
|
||||
<CloseEditor />
|
||||
<Form>
|
||||
<MainConfiguration>
|
||||
<FormRow className="form-row-header">
|
||||
<NameWrapper className="t--nameOfJSObject">
|
||||
<JSObjectNameEditor page="JS_PANE" />
|
||||
</NameWrapper>
|
||||
<ActionButtons className="t--formActionButtons">
|
||||
<MoreJSCollectionsMenu
|
||||
className="t--more-action-menu"
|
||||
id={currentJSCollection.id}
|
||||
name={currentJSCollection.name}
|
||||
pageId={pageId}
|
||||
/>
|
||||
<SearchSnippets
|
||||
entityId={currentJSCollection?.id}
|
||||
entityType={ENTITY_TYPE.JSACTION}
|
||||
/>
|
||||
<JSFunctionRun
|
||||
disabled={disableRunFunctionality}
|
||||
isLoading={isExecutingCurrentJSAction}
|
||||
jsCollection={currentJSCollection}
|
||||
onButtonClick={handleRunAction}
|
||||
onSelect={handleJSActionOptionSelection}
|
||||
options={convertJSActionsToDropdownOptions(jsActions)}
|
||||
selected={selectedJSActionOption}
|
||||
showTooltip={!selectedJSActionOption.data}
|
||||
/>
|
||||
</ActionButtons>
|
||||
</FormRow>
|
||||
</MainConfiguration>
|
||||
<SecondaryWrapper>
|
||||
<TabbedViewContainer isExecuting={isExecutingCurrentJSAction}>
|
||||
<TabComponent
|
||||
onSelect={setMainTabIndex}
|
||||
selectedIndex={mainTabIndex}
|
||||
tabs={[
|
||||
{
|
||||
key: "code",
|
||||
title: "Code",
|
||||
panelComponent: (
|
||||
<CodeEditor
|
||||
className={"js-editor"}
|
||||
customGutter={JSGutters}
|
||||
dataTreePath={`${currentJSCollection.name}.body`}
|
||||
folding
|
||||
height={"100%"}
|
||||
hideEvaluatedValue
|
||||
input={{
|
||||
value: currentJSCollection.body,
|
||||
onChange: handleEditorChange,
|
||||
}}
|
||||
mode={EditorModes.JAVASCRIPT}
|
||||
placeholder="Let's write some code!"
|
||||
showLightningMenu={false}
|
||||
showLineNumbers
|
||||
size={EditorSize.EXTENDED}
|
||||
tabBehaviour={TabBehaviour.INDENT}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
title: "Settings",
|
||||
panelComponent: (
|
||||
<JSFunctionSettingsView actions={jsActions} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<SearchSnippets
|
||||
entityId={currentJSAction?.id}
|
||||
entityType={ENTITY_TYPE.JSACTION}
|
||||
/>
|
||||
</ActionButtons>
|
||||
</FormRow>
|
||||
</MainConfiguration>
|
||||
<SecondaryWrapper>
|
||||
<TabbedViewContainer>
|
||||
<TabComponent
|
||||
onSelect={setMainTabIndex}
|
||||
selectedIndex={mainTabIndex}
|
||||
tabs={[
|
||||
{
|
||||
key: "code",
|
||||
title: "Code",
|
||||
panelComponent: (
|
||||
<CodeEditor
|
||||
className={"js-editor"}
|
||||
dataTreePath={`${currentJSAction.name}.body`}
|
||||
folding
|
||||
height={"100%"}
|
||||
hideEvaluatedValue
|
||||
input={{
|
||||
value: currentJSAction.body,
|
||||
onChange: (event: any) => handleOnChange(event),
|
||||
}}
|
||||
mode={EditorModes.JAVASCRIPT}
|
||||
placeholder="Let's write some code!"
|
||||
showLightningMenu={false}
|
||||
showLineNumbers
|
||||
size={EditorSize.EXTENDED}
|
||||
tabBehaviour={TabBehaviour.INDENT}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
</TabbedViewContainer>
|
||||
<JSResponseView
|
||||
currentFunction={activeResponse}
|
||||
disabled={disableRunFunctionality}
|
||||
errors={parseErrors}
|
||||
isLoading={isExecutingCurrentJSAction}
|
||||
jsObject={currentJSCollection}
|
||||
onButtonClick={handleRunAction}
|
||||
theme={theme}
|
||||
/>
|
||||
</TabbedViewContainer>
|
||||
<JSResponseView
|
||||
errors={getErrors}
|
||||
jsObject={currentJSAction}
|
||||
theme={theme}
|
||||
/>
|
||||
</SecondaryWrapper>
|
||||
</Form>
|
||||
</>
|
||||
</SecondaryWrapper>
|
||||
</Form>
|
||||
</JSObjectHotKeys>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
96
app/client/src/pages/Editor/JSEditor/JSFunctionRun.tsx
Normal file
96
app/client/src/pages/Editor/JSEditor/JSFunctionRun.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Dropdown, {
|
||||
DropdownOnSelect,
|
||||
DropdownContainer,
|
||||
} from "components/ads/Dropdown";
|
||||
import Button from "components/ads/Button";
|
||||
import FlagBadge from "components/utils/FlagBadge";
|
||||
import { JSCollection } from "entities/JSCollection";
|
||||
import Tooltip from "components/ads/Tooltip";
|
||||
import { createMessage, NO_JS_FUNCTION_TO_RUN } from "ce/constants/messages";
|
||||
import { StyledButton } from "components/ads/Button";
|
||||
import { JSActionDropdownOption } from "./utils";
|
||||
import { RUN_BUTTON_DEFAULTS, testLocators } from "./constants";
|
||||
|
||||
type Props = {
|
||||
disabled: boolean;
|
||||
isLoading: boolean;
|
||||
jsCollection: JSCollection;
|
||||
onButtonClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
onSelect: DropdownOnSelect;
|
||||
options: JSActionDropdownOption[];
|
||||
selected: JSActionDropdownOption;
|
||||
showTooltip: boolean;
|
||||
};
|
||||
|
||||
export type DropdownWithCTAWrapperProps = {
|
||||
isDisabled: boolean;
|
||||
};
|
||||
const disabledStyles = `
|
||||
opacity: 0.5;
|
||||
pointer-events:none;
|
||||
`;
|
||||
|
||||
const DropdownWithCTAWrapper = styled.div<DropdownWithCTAWrapperProps>`
|
||||
display: flex;
|
||||
|
||||
${StyledButton} {
|
||||
margin-left: ${RUN_BUTTON_DEFAULTS.GAP_SIZE};
|
||||
padding: 0px 20px;
|
||||
|
||||
${(props) =>
|
||||
props.isDisabled &&
|
||||
`
|
||||
${disabledStyles}
|
||||
`}
|
||||
}
|
||||
${DropdownContainer} {
|
||||
${(props) =>
|
||||
props.isDisabled &&
|
||||
`
|
||||
${disabledStyles}
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
export function JSFunctionRun({
|
||||
disabled,
|
||||
isLoading,
|
||||
jsCollection,
|
||||
onButtonClick,
|
||||
onSelect,
|
||||
options,
|
||||
selected,
|
||||
showTooltip,
|
||||
}: Props) {
|
||||
return (
|
||||
<DropdownWithCTAWrapper isDisabled={disabled}>
|
||||
<Dropdown
|
||||
customBadge={<FlagBadge name="Async" />}
|
||||
height={RUN_BUTTON_DEFAULTS.HEIGHT}
|
||||
onSelect={onSelect}
|
||||
options={options}
|
||||
selected={selected}
|
||||
selectedHighlightBg={RUN_BUTTON_DEFAULTS.DROPDOWN_HIGHLIGHT_BG}
|
||||
showLabelOnly
|
||||
truncateOption
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
content={createMessage(NO_JS_FUNCTION_TO_RUN, jsCollection.name)}
|
||||
disabled={!showTooltip}
|
||||
hoverOpenDelay={50}
|
||||
>
|
||||
<Button
|
||||
className={testLocators.runJSAction}
|
||||
height={RUN_BUTTON_DEFAULTS.HEIGHT}
|
||||
isLoading={isLoading}
|
||||
onClick={onButtonClick}
|
||||
tag="button"
|
||||
text={RUN_BUTTON_DEFAULTS.CTA_TEXT}
|
||||
/>
|
||||
</Tooltip>
|
||||
</DropdownWithCTAWrapper>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,79 +1,204 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Checkbox from "components/ads/Checkbox";
|
||||
import Dialog from "components/ads/DialogComponent";
|
||||
import { JSAction } from "entities/JSCollection";
|
||||
import { updateFunctionProperty } from "actions/jsPaneActions";
|
||||
import { useDispatch } from "react-redux";
|
||||
import {
|
||||
ASYNC_FUNCTION_SETTINGS_HEADING,
|
||||
createMessage,
|
||||
JS_SETTINGS_ONPAGELOAD,
|
||||
JS_SETTINGS_ONPAGELOAD_SUBTEXT,
|
||||
JS_SETTINGS_CONFIRM_EXECUTION,
|
||||
JS_SETTINGS_CONFIRM_EXECUTION_SUBTEXT,
|
||||
} from "@appsmith/constants/messages";
|
||||
NO_ASYNC_FUNCTIONS,
|
||||
} from "ce/constants/messages";
|
||||
import { AppIcon, Radio, RadioComponent } from "components/ads";
|
||||
import TooltipComponent from "components/ads/Tooltip";
|
||||
import { JSAction } from "entities/JSCollection";
|
||||
import React, { useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import { RADIO_OPTIONS, SETTINGS_HEADINGS } from "./constants";
|
||||
|
||||
const FormRow = styled.div`
|
||||
margin-bottom: ${(props) => props.theme.spaces[10] + 1}px;
|
||||
&.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.cs-text {
|
||||
margin-right: 30px;
|
||||
color: rgb(9, 7, 7);
|
||||
}
|
||||
type SettingsHeadingProps = {
|
||||
text: string;
|
||||
hasInfo?: boolean;
|
||||
info?: string;
|
||||
grow: boolean;
|
||||
};
|
||||
|
||||
type SettingsItemProps = {
|
||||
action: JSAction;
|
||||
};
|
||||
|
||||
type JSFunctionSettingsProps = {
|
||||
actions: JSAction[];
|
||||
};
|
||||
|
||||
const SettingRow = styled.div<{ isHeading?: boolean; noBorder?: boolean }>`
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
${(props) =>
|
||||
!props.noBorder &&
|
||||
`
|
||||
border-bottom: solid 1px ${props.theme.colors.table.border}};
|
||||
`}
|
||||
|
||||
${(props) =>
|
||||
props.isHeading &&
|
||||
`
|
||||
background: #f8f8f8;
|
||||
font-size: ${props.theme.typography.h5.fontSize}px;
|
||||
`};
|
||||
`;
|
||||
|
||||
const StyledIcon = styled(AppIcon)`
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
& > svg {
|
||||
width: 13px;
|
||||
height: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
interface JSFunctionSettingsProps {
|
||||
action: JSAction;
|
||||
openSettings: boolean;
|
||||
toggleSettings: () => void;
|
||||
const SettingColumn = styled.div<{ grow?: boolean; isHeading?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: ${(props) => (props.grow ? 1 : 0)};
|
||||
padding: 5px 12px;
|
||||
min-width: 250px;
|
||||
|
||||
${(props) =>
|
||||
props.isHeading &&
|
||||
`
|
||||
text-transform: uppercase;
|
||||
font-weight: ${props.theme.fontWeights[2]};
|
||||
font-size: ${props.theme.fontSizes[2]}px
|
||||
margin-right: 9px;
|
||||
`}
|
||||
|
||||
${StyledIcon} {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
${Radio} {
|
||||
margin-right: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const JSFunctionSettingsWrapper = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
border-bottom: 1px solid ${(props) => props.theme.colors.apiPane.dividerBg};
|
||||
border-top: 1px solid ${(props) => props.theme.colors.apiPane.dividerBg};
|
||||
`;
|
||||
|
||||
const SettingsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px ${(props) => props.theme.spaces[13] - 2}px;
|
||||
width: max-content;
|
||||
min-width: 700px;
|
||||
height: 100%;
|
||||
|
||||
& > h3 {
|
||||
margin: 20px 0;
|
||||
text-transform: capitalize;
|
||||
font-size: ${(props) => props.theme.fontSizes[5]}px;
|
||||
font-weight: ${(props) => props.theme.fontWeights[2]};
|
||||
}
|
||||
`;
|
||||
|
||||
function SettingsHeading({ grow, hasInfo, info, text }: SettingsHeadingProps) {
|
||||
return (
|
||||
<SettingColumn grow={grow} isHeading>
|
||||
<span>{text}</span>
|
||||
{hasInfo && info && (
|
||||
<TooltipComponent content={createMessage(() => info)}>
|
||||
<StyledIcon name="help" />
|
||||
</TooltipComponent>
|
||||
)}
|
||||
</SettingColumn>
|
||||
);
|
||||
}
|
||||
|
||||
function JSFunctionSettings(props: JSFunctionSettingsProps) {
|
||||
const { action } = props;
|
||||
function SettingsItem({ action }: SettingsItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
const [executeOnPageLoad, setExecuteOnPageLoad] = useState(
|
||||
String(!!action.executeOnLoad),
|
||||
);
|
||||
const [confirmBeforeExecute, setConfirmBeforeExecute] = useState(
|
||||
String(!!action.confirmBeforeExecute),
|
||||
);
|
||||
|
||||
const updateProperty = (value: boolean | number, propertyName: string) => {
|
||||
dispatch(
|
||||
updateFunctionProperty({
|
||||
action: props.action,
|
||||
action: action,
|
||||
propertyName: propertyName,
|
||||
value: value,
|
||||
}),
|
||||
);
|
||||
};
|
||||
const onChangeExecuteOnPageLoad = (value: string) => {
|
||||
setExecuteOnPageLoad(value);
|
||||
updateProperty(value === "true", "executeOnLoad");
|
||||
};
|
||||
const onChangeConfirmBeforeExecute = (value: string) => {
|
||||
setConfirmBeforeExecute(value);
|
||||
updateProperty(value === "true", "confirmBeforeExecute");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
canOutsideClickClose
|
||||
isOpen={props.openSettings}
|
||||
onClose={props.toggleSettings}
|
||||
title={`Function settings - ${props.action.name}`}
|
||||
>
|
||||
<FormRow>
|
||||
<Checkbox
|
||||
fill={false}
|
||||
info={createMessage(JS_SETTINGS_ONPAGELOAD_SUBTEXT)}
|
||||
isDefaultChecked={action.executeOnLoad}
|
||||
label={createMessage(JS_SETTINGS_ONPAGELOAD)}
|
||||
onCheckChange={(value: boolean) =>
|
||||
updateProperty(value, "executeOnLoad")
|
||||
}
|
||||
<SettingRow>
|
||||
<SettingColumn grow>
|
||||
<span>{action.name}</span>
|
||||
</SettingColumn>
|
||||
<SettingColumn className={`${action.name}-on-page-load-setting`}>
|
||||
<RadioComponent
|
||||
backgroundColor="#191919"
|
||||
defaultValue={executeOnPageLoad}
|
||||
name={`execute-on-page-load-${action.id}`}
|
||||
onSelect={onChangeExecuteOnPageLoad}
|
||||
options={RADIO_OPTIONS}
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
<Checkbox
|
||||
fill={false}
|
||||
info={createMessage(JS_SETTINGS_CONFIRM_EXECUTION_SUBTEXT)}
|
||||
isDefaultChecked={action.confirmBeforeExecute}
|
||||
label={createMessage(JS_SETTINGS_CONFIRM_EXECUTION)}
|
||||
onCheckChange={(value: boolean) =>
|
||||
updateProperty(value, "confirmBeforeExecute")
|
||||
}
|
||||
</SettingColumn>
|
||||
<SettingColumn className={`${action.name}-confirm-before-execute`}>
|
||||
<RadioComponent
|
||||
backgroundColor="#191919"
|
||||
defaultValue={confirmBeforeExecute}
|
||||
name={`confirm-before-execute-${action.id}`}
|
||||
onSelect={onChangeConfirmBeforeExecute}
|
||||
options={RADIO_OPTIONS}
|
||||
/>
|
||||
</FormRow>
|
||||
</Dialog>
|
||||
</SettingColumn>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
export default JSFunctionSettings;
|
||||
|
||||
function JSFunctionSettingsView({ actions }: JSFunctionSettingsProps) {
|
||||
const asyncActions = actions.filter(
|
||||
(action) => action.actionConfiguration.isAsync,
|
||||
);
|
||||
return (
|
||||
<JSFunctionSettingsWrapper>
|
||||
<SettingsContainer>
|
||||
<h3>{createMessage(ASYNC_FUNCTION_SETTINGS_HEADING)}</h3>
|
||||
<SettingRow isHeading>
|
||||
{SETTINGS_HEADINGS.map((setting, index) => (
|
||||
<SettingsHeading
|
||||
grow={index === 0}
|
||||
hasInfo={setting.hasInfo}
|
||||
info={setting.info}
|
||||
key={setting.key}
|
||||
text={setting.text}
|
||||
/>
|
||||
))}
|
||||
</SettingRow>
|
||||
{asyncActions && asyncActions.length ? (
|
||||
asyncActions.map((action) => (
|
||||
<SettingsItem action={action} key={action.id} />
|
||||
))
|
||||
) : (
|
||||
<SettingRow noBorder>
|
||||
<SettingColumn>{createMessage(NO_ASYNC_FUNCTIONS)}</SettingColumn>
|
||||
</SettingRow>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
</JSFunctionSettingsWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default JSFunctionSettingsView;
|
||||
|
|
|
|||
38
app/client/src/pages/Editor/JSEditor/JSObjectHotKeys.tsx
Normal file
38
app/client/src/pages/Editor/JSEditor/JSObjectHotKeys.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React from "react";
|
||||
import { Hotkey, Hotkeys } from "@blueprintjs/core";
|
||||
import { HotkeysTarget } from "@blueprintjs/core/lib/esnext/components/hotkeys/hotkeysTarget.js";
|
||||
import { JS_OBJECT_HOTKEYS_CLASSNAME } from "./constants";
|
||||
|
||||
type Props = {
|
||||
runActiveJSFunction: (e: KeyboardEvent) => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
@HotkeysTarget
|
||||
class JSObjectHotKeys extends React.Component<Props> {
|
||||
public renderHotkeys() {
|
||||
return (
|
||||
<Hotkeys>
|
||||
<Hotkey
|
||||
allowInInput
|
||||
combo="mod + enter"
|
||||
global
|
||||
label="Run Js Function"
|
||||
onKeyDown={this.props.runActiveJSFunction}
|
||||
/>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
/*
|
||||
Blueprint's v3 decorated component must return a single DOM element in its render() method, not a custom React component.
|
||||
This constraint allows HotkeysTarget to inject event handlers without creating an extra wrapper element.
|
||||
*/
|
||||
return (
|
||||
<div className={JS_OBJECT_HOTKEYS_CLASSNAME}>{this.props.children}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default JSObjectHotKeys;
|
||||
81
app/client/src/pages/Editor/JSEditor/constants.ts
Normal file
81
app/client/src/pages/Editor/JSEditor/constants.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { OptionProps } from "components/ads";
|
||||
import { css } from "styled-components";
|
||||
import { JSActionDropdownOption } from "./utils";
|
||||
|
||||
export const RUN_BUTTON_DEFAULTS = {
|
||||
HEIGHT: "30px",
|
||||
CTA_TEXT: "RUN",
|
||||
// space between button and dropdown
|
||||
GAP_SIZE: "10px",
|
||||
DROPDOWN_HIGHLIGHT_BG: "#E7E7E7",
|
||||
};
|
||||
export const NO_SELECTION_DROPDOWN_OPTION: JSActionDropdownOption = {
|
||||
label: "No function selected",
|
||||
value: "",
|
||||
data: null,
|
||||
};
|
||||
export const NO_FUNCTION_DROPDOWN_OPTION: JSActionDropdownOption = {
|
||||
label: "No function available",
|
||||
value: "",
|
||||
data: null,
|
||||
};
|
||||
export const SETTINGS_HEADINGS = [
|
||||
{
|
||||
text: "Function Name",
|
||||
hasInfo: false,
|
||||
key: "func_name",
|
||||
},
|
||||
{
|
||||
text: "Run on page load",
|
||||
hasInfo: true,
|
||||
info: "Allow function run when page loads",
|
||||
key: "run_on_pageload",
|
||||
},
|
||||
{
|
||||
text: "Confirm before calling ",
|
||||
hasInfo: true,
|
||||
info: "Ask for confirmation before executing function",
|
||||
key: "run_before_calling",
|
||||
},
|
||||
];
|
||||
export const RADIO_OPTIONS: OptionProps[] = [
|
||||
{
|
||||
label: "Yes",
|
||||
value: "true",
|
||||
},
|
||||
{
|
||||
label: "No",
|
||||
value: "false",
|
||||
},
|
||||
];
|
||||
export const RUN_GUTTER_ID = "run-gutter";
|
||||
export const RUN_GUTTER_CLASSNAME = "run-marker-gutter";
|
||||
export const JS_OBJECT_HOTKEYS_CLASSNAME = "js-object-hotkeys";
|
||||
export const ANIMATE_RUN_GUTTER = "animate-run-marker";
|
||||
|
||||
export const testLocators = {
|
||||
runJSAction: "run-js-action",
|
||||
};
|
||||
|
||||
export const CodeEditorWithGutterStyles = css`
|
||||
.${RUN_GUTTER_ID} {
|
||||
width: 0.5em;
|
||||
background: #f0f0f0;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.${RUN_GUTTER_CLASSNAME} {
|
||||
cursor: pointer;
|
||||
color: #f86a2b;
|
||||
}
|
||||
.CodeMirror-linenumbers {
|
||||
width: max-content;
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
text-align: right;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.cm-s-duotone-light.CodeMirror {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
|
@ -9,16 +9,13 @@ import { getJSCollectionById } from "selectors/editorSelectors";
|
|||
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
|
||||
import Spinner from "components/editorComponents/Spinner";
|
||||
import styled from "styled-components";
|
||||
import { getPluginSettingConfigs } from "selectors/entitiesSelector";
|
||||
import _ from "lodash";
|
||||
|
||||
const LoadingContainer = styled(CenteredWrapper)`
|
||||
height: 50%;
|
||||
`;
|
||||
interface ReduxStateProps {
|
||||
jsAction: JSCollection | undefined;
|
||||
jsCollection: JSCollection | undefined;
|
||||
isCreating: boolean;
|
||||
settingsConfig: any;
|
||||
}
|
||||
|
||||
type Props = ReduxStateProps &
|
||||
|
|
@ -26,7 +23,7 @@ type Props = ReduxStateProps &
|
|||
|
||||
class JSEditor extends React.Component<Props> {
|
||||
render() {
|
||||
const { isCreating, jsAction, settingsConfig } = this.props;
|
||||
const { isCreating, jsCollection } = this.props;
|
||||
if (isCreating) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
|
|
@ -34,24 +31,19 @@ class JSEditor extends React.Component<Props> {
|
|||
</LoadingContainer>
|
||||
);
|
||||
}
|
||||
if (!!jsAction) {
|
||||
return (
|
||||
<JsEditorForm jsAction={jsAction} settingsConfig={settingsConfig} />
|
||||
);
|
||||
if (!!jsCollection) {
|
||||
return <JsEditorForm jsCollection={jsCollection} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState, props: Props): ReduxStateProps => {
|
||||
const jsAction = getJSCollectionById(state, props);
|
||||
const jsCollection = getJSCollectionById(state, props);
|
||||
const { isCreating } = state.ui.jsPane;
|
||||
const pluginId = _.get(jsAction, "pluginId", "");
|
||||
const settingsConfig = getPluginSettingConfigs(state, pluginId);
|
||||
|
||||
return {
|
||||
jsAction,
|
||||
settingsConfig,
|
||||
jsCollection,
|
||||
isCreating: isCreating,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
9
app/client/src/pages/Editor/JSEditor/readme.md
Normal file
9
app/client/src/pages/Editor/JSEditor/readme.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
## Naming conventions
|
||||
|
||||
- jsCollection : refers to individual JS Object configs
|
||||
- jsAction : refers to actions derived from functions within a jsCollection
|
||||
|
||||
|
||||
## To do
|
||||
|
||||
- Optimize number of Ast calls
|
||||
132
app/client/src/pages/Editor/JSEditor/styledComponents.ts
Normal file
132
app/client/src/pages/Editor/JSEditor/styledComponents.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import styled, { css } from "styled-components";
|
||||
import FormRow from "components/editorComponents/FormRow";
|
||||
import FormLabel from "components/editorComponents/FormLabel";
|
||||
import {
|
||||
JS_OBJECT_HOTKEYS_CLASSNAME,
|
||||
RUN_GUTTER_CLASSNAME,
|
||||
RUN_GUTTER_ID,
|
||||
} from "./constants";
|
||||
|
||||
export const CodeEditorWithGutterStyles = css`
|
||||
.${RUN_GUTTER_ID} {
|
||||
width: 0.5em;
|
||||
background: #f0f0f0;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.${RUN_GUTTER_CLASSNAME} {
|
||||
cursor: pointer;
|
||||
color: #f86a2b;
|
||||
}
|
||||
.CodeMirror-linenumbers {
|
||||
width: max-content;
|
||||
}
|
||||
.CodeMirror-linenumber {
|
||||
text-align: right;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.cm-s-duotone-light.CodeMirror {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FormWrapper = styled.div`
|
||||
height: ${({ theme }) =>
|
||||
`calc(100vh - ${theme.smallHeaderHeight} - ${theme.backBanner})`};
|
||||
overflow: hidden;
|
||||
.${JS_OBJECT_HOTKEYS_CLASSNAME} {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Form = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: ${({ theme }) => `calc(100% - ${theme.backBanner})`};
|
||||
overflow: hidden;
|
||||
${FormLabel} {
|
||||
padding: ${(props) => props.theme.spaces[3]}px;
|
||||
}
|
||||
${FormRow} {
|
||||
${FormLabel} {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.t--no-binding-prompt {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const NameWrapper = styled.div`
|
||||
width: 49%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
input {
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ActionButtons = styled.div`
|
||||
justify-self: flex-end;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
button:last-child {
|
||||
margin: 0 ${(props) => props.theme.spaces[7]}px;
|
||||
height: 30px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SecondaryWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 50px);
|
||||
overflow: hidden;
|
||||
`;
|
||||
export const MainConfiguration = styled.div`
|
||||
padding: ${(props) => props.theme.spaces[4]}px
|
||||
${(props) => props.theme.spaces[10]}px 0px
|
||||
${(props) => props.theme.spaces[10]}px;
|
||||
`;
|
||||
|
||||
export const TabbedViewContainer = styled.div<{ isExecuting: boolean }>`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
border-top: 2px solid ${(props) => props.theme.colors.apiPane.dividerBg};
|
||||
${FormRow} {
|
||||
min-height: auto;
|
||||
padding: ${(props) => props.theme.spaces[0]}px;
|
||||
& > * {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
&&& {
|
||||
ul.react-tabs__tab-list {
|
||||
padding: 0px ${(props) => props.theme.spaces[12]}px;
|
||||
background-color: ${(props) =>
|
||||
props.theme.colors.apiPane.responseBody.bg};
|
||||
}
|
||||
.react-tabs__tab-panel {
|
||||
${CodeEditorWithGutterStyles}
|
||||
height: calc(100% - 36px);
|
||||
margin-top: 2px;
|
||||
background-color: ${(props) => props.theme.colors.apiPane.bg};
|
||||
.CodeEditorTarget {
|
||||
border-bottom: 1px solid
|
||||
${(props) => props.theme.colors.apiPane.dividerBg};
|
||||
outline: none;
|
||||
}
|
||||
${(props) =>
|
||||
props.isExecuting &&
|
||||
`
|
||||
.${RUN_GUTTER_CLASSNAME} {
|
||||
cursor: progress;
|
||||
}
|
||||
`}
|
||||
}
|
||||
}
|
||||
`;
|
||||
164
app/client/src/pages/Editor/JSEditor/utils.test.ts
Normal file
164
app/client/src/pages/Editor/JSEditor/utils.test.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import { JSAction } from "entities/JSCollection";
|
||||
import { uniqueId } from "lodash";
|
||||
import { NO_FUNCTION_DROPDOWN_OPTION } from "./constants";
|
||||
import {
|
||||
convertJSActionToDropdownOption,
|
||||
getJSActionOption,
|
||||
getJSFunctionStartLineFromCode,
|
||||
isCursorWithinNode,
|
||||
} from "./utils";
|
||||
|
||||
const BASE_JS_OBJECT_BODY = `export default {
|
||||
myVar1: [],
|
||||
myVar2: {},
|
||||
myFun1: () => {
|
||||
//write code here
|
||||
return FilePicker1
|
||||
},
|
||||
myFun2: async () => {
|
||||
//use async-await or promises
|
||||
await Api3.run()
|
||||
await Api3.run()
|
||||
return Api3.data
|
||||
}
|
||||
}`;
|
||||
|
||||
const BASE_JS_OBJECT_BODY_WITH_LITERALS = `export default {
|
||||
myVar1: [],
|
||||
myVar2: {},
|
||||
["myFun1"]: () => {
|
||||
//write code here
|
||||
return FilePicker1
|
||||
},
|
||||
["myFun2"]: async () => {
|
||||
//use async-await or promises
|
||||
await Api3.run()
|
||||
await Api3.run()
|
||||
return Api3.data
|
||||
}
|
||||
}`;
|
||||
|
||||
const BASE_JS_ACTION = (useLiterals = false) => {
|
||||
return {
|
||||
organizationId: "organization-id",
|
||||
pageId: "page-id",
|
||||
collectionId: "collection-id",
|
||||
pluginId: "plugin-id",
|
||||
executeOnLoad: false,
|
||||
dynamicBindingPathList: [],
|
||||
isValid: false,
|
||||
invalids: [],
|
||||
jsonPathKeys: [],
|
||||
cacheResponse: "",
|
||||
confirmBeforeExecute: false,
|
||||
messages: [],
|
||||
clientSideExecution: false,
|
||||
actionConfiguration: {
|
||||
body: useLiterals
|
||||
? BASE_JS_OBJECT_BODY_WITH_LITERALS
|
||||
: BASE_JS_OBJECT_BODY,
|
||||
isAsync: true,
|
||||
timeoutInMillisecond: 1000,
|
||||
jsArguments: [],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createJSAction = (name: string, useLiterals = false): JSAction => {
|
||||
return {
|
||||
...BASE_JS_ACTION(useLiterals),
|
||||
id: uniqueId(name),
|
||||
name,
|
||||
};
|
||||
};
|
||||
|
||||
describe("getJSFunctionStartLineFromCode", () => {
|
||||
it("returns null when cursor isn't within any function", () => {
|
||||
const actualResponse = getJSFunctionStartLineFromCode(
|
||||
BASE_JS_OBJECT_BODY,
|
||||
100,
|
||||
);
|
||||
|
||||
const expectedResponse = null;
|
||||
|
||||
expect(actualResponse).toStrictEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it("returns correct start line of function", () => {
|
||||
const actualResponse1 = getJSFunctionStartLineFromCode(
|
||||
BASE_JS_OBJECT_BODY,
|
||||
4,
|
||||
);
|
||||
const actualResponse2 = getJSFunctionStartLineFromCode(
|
||||
BASE_JS_OBJECT_BODY,
|
||||
9,
|
||||
);
|
||||
const expectedStartLine1 = 3; // startLine of myFun1
|
||||
const expectedStartLine2 = 7; // startLine of myFun2
|
||||
|
||||
expect(actualResponse1?.line).toStrictEqual(expectedStartLine1);
|
||||
expect(actualResponse2?.line).toStrictEqual(expectedStartLine2);
|
||||
});
|
||||
|
||||
it("returns correct start line of function when object keys are literals", () => {
|
||||
const actualResponse1 = getJSFunctionStartLineFromCode(
|
||||
BASE_JS_OBJECT_BODY_WITH_LITERALS,
|
||||
4,
|
||||
);
|
||||
const actualResponse2 = getJSFunctionStartLineFromCode(
|
||||
BASE_JS_OBJECT_BODY_WITH_LITERALS,
|
||||
9,
|
||||
);
|
||||
const expectedStartLine1 = 3; // startLine of myFun1
|
||||
const expectedStartLine2 = 7; // startLine of myFun2
|
||||
|
||||
expect(actualResponse1?.line).toStrictEqual(expectedStartLine1);
|
||||
expect(actualResponse2?.line).toStrictEqual(expectedStartLine2);
|
||||
});
|
||||
|
||||
it("isCursorWithinNode returns correct value", () => {
|
||||
const cursorLineNumber = 2;
|
||||
const testNodeLocation1 = {
|
||||
start: { line: 1, column: 1, offset: 0 },
|
||||
end: { line: 6, column: 1, offset: 0 },
|
||||
};
|
||||
const testNodeLocation2 = {
|
||||
start: { line: 4, column: 1, offset: 0 },
|
||||
end: { line: 6, column: 1, offset: 0 },
|
||||
};
|
||||
|
||||
const actualResponse1 = isCursorWithinNode(
|
||||
testNodeLocation1,
|
||||
cursorLineNumber,
|
||||
);
|
||||
const actualResponse2 = isCursorWithinNode(
|
||||
testNodeLocation2,
|
||||
cursorLineNumber,
|
||||
);
|
||||
|
||||
expect(actualResponse1).toBeTruthy();
|
||||
expect(actualResponse2).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("jsAction dropdown", () => {
|
||||
const jsActions = [
|
||||
createJSAction("myFun1"),
|
||||
createJSAction("myFun2"),
|
||||
createJSAction("myFun3"),
|
||||
];
|
||||
|
||||
it("getJSActionOption returns active JS Action on priority", () => {
|
||||
const activeJSAction = jsActions[0];
|
||||
const actualResponse = getJSActionOption(activeJSAction, jsActions);
|
||||
const expectedResponse = convertJSActionToDropdownOption(activeJSAction);
|
||||
expect(actualResponse).toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it("getJSActionOption returns default option when there is no jsAction present", () => {
|
||||
const activeJSAction = null;
|
||||
const actualResponse = getJSActionOption(activeJSAction, []);
|
||||
const expectedResponse = NO_FUNCTION_DROPDOWN_OPTION;
|
||||
expect(actualResponse).toEqual(expectedResponse);
|
||||
});
|
||||
});
|
||||
160
app/client/src/pages/Editor/JSEditor/utils.ts
Normal file
160
app/client/src/pages/Editor/JSEditor/utils.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import { parse, Node } from "acorn";
|
||||
import { ancestor } from "acorn-walk";
|
||||
import { CodeEditorGutter } from "components/editorComponents/CodeEditor";
|
||||
import { JSAction, JSCollection } from "entities/JSCollection";
|
||||
import {
|
||||
RUN_GUTTER_CLASSNAME,
|
||||
RUN_GUTTER_ID,
|
||||
NO_FUNCTION_DROPDOWN_OPTION,
|
||||
} from "./constants";
|
||||
import { DropdownOption } from "components/ads/Dropdown";
|
||||
import { find, memoize, sortBy } from "lodash";
|
||||
import { ECMA_VERSION, NodeTypes, SourceType } from "constants/ast";
|
||||
import { isLiteralNode, isPropertyNode, PropertyNode } from "workers/ast";
|
||||
|
||||
export interface JSActionDropdownOption extends DropdownOption {
|
||||
data: JSAction | null;
|
||||
}
|
||||
|
||||
export const getAST = memoize((code: string, sourceType: SourceType) =>
|
||||
parse(code, {
|
||||
ecmaVersion: ECMA_VERSION,
|
||||
sourceType: sourceType,
|
||||
locations: true, // Adds location data to each node
|
||||
}),
|
||||
);
|
||||
|
||||
export const isCursorWithinNode = (
|
||||
nodeLocation: acorn.SourceLocation,
|
||||
cursorLineNumber: number,
|
||||
) => {
|
||||
return (
|
||||
nodeLocation.start.line <= cursorLineNumber &&
|
||||
nodeLocation.end.line >= cursorLineNumber
|
||||
);
|
||||
};
|
||||
|
||||
const getNameFromPropertyNode = (node: PropertyNode): string =>
|
||||
isLiteralNode(node.key) ? String(node.key.value) : node.key.name;
|
||||
|
||||
// Function to get start line of js function from code, returns null if function not found
|
||||
export const getJSFunctionStartLineFromCode = (
|
||||
code: string,
|
||||
cursorLine: number,
|
||||
): { line: number; actionName: string } | null => {
|
||||
let ast: Node = { end: 0, start: 0, type: "" };
|
||||
let result: { line: number; actionName: string } | null = null;
|
||||
try {
|
||||
ast = getAST(code, SourceType.module);
|
||||
} catch (e) {
|
||||
return result;
|
||||
}
|
||||
|
||||
ancestor(ast, {
|
||||
Property(node, ancestors: Node[]) {
|
||||
// We are only interested in identifiers at this depth (exported object keys)
|
||||
const depth = ancestors.length - 3;
|
||||
if (
|
||||
isPropertyNode(node) &&
|
||||
(node.value.type === NodeTypes.ArrowFunctionExpression ||
|
||||
node.value.type === NodeTypes.FunctionExpression) &&
|
||||
node.loc &&
|
||||
isCursorWithinNode(node.loc, cursorLine + 1) &&
|
||||
ancestors[depth] &&
|
||||
ancestors[depth].type === NodeTypes.ExportDefaultDeclaration
|
||||
) {
|
||||
// 1 is subtracted because codeMirror's line is zero-indexed, this isn't
|
||||
result = {
|
||||
line: node.loc.start.line - 1,
|
||||
actionName: getNameFromPropertyNode(node),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export const createGutterMarker = (gutterOnclick: () => void) => {
|
||||
const marker = document.createElement("button");
|
||||
marker.innerHTML = "▶";
|
||||
marker.classList.add(RUN_GUTTER_CLASSNAME);
|
||||
marker.onmousedown = function(e) {
|
||||
e.preventDefault();
|
||||
gutterOnclick();
|
||||
};
|
||||
// Allows executing functions (via run gutter) when devtool is open
|
||||
marker.ontouchstart = function(e) {
|
||||
e.preventDefault();
|
||||
gutterOnclick();
|
||||
};
|
||||
return marker;
|
||||
};
|
||||
|
||||
export const getJSFunctionLineGutter = (
|
||||
jsActions: JSAction[],
|
||||
runFuction: (jsAction: JSAction) => void,
|
||||
showGutters: boolean,
|
||||
onFocusAction: (jsAction: JSAction) => void,
|
||||
): CodeEditorGutter => {
|
||||
const gutter: CodeEditorGutter = {
|
||||
getGutterConfig: null,
|
||||
gutterId: RUN_GUTTER_ID,
|
||||
};
|
||||
if (!showGutters || !jsActions.length) return gutter;
|
||||
|
||||
return {
|
||||
getGutterConfig: (code: string, lineNumber: number) => {
|
||||
const config = getJSFunctionStartLineFromCode(code, lineNumber);
|
||||
const action = find(jsActions, ["name", config?.actionName]);
|
||||
return config && action
|
||||
? {
|
||||
line: config.line,
|
||||
element: createGutterMarker(() => runFuction(action)),
|
||||
isFocusedAction: () => {
|
||||
onFocusAction(action);
|
||||
},
|
||||
}
|
||||
: null;
|
||||
},
|
||||
gutterId: RUN_GUTTER_ID,
|
||||
};
|
||||
};
|
||||
|
||||
export const convertJSActionsToDropdownOptions = (
|
||||
JSActions: JSAction[],
|
||||
): JSActionDropdownOption[] => {
|
||||
return sortBy(JSActions, ["name"]).map(convertJSActionToDropdownOption);
|
||||
};
|
||||
|
||||
export const convertJSActionToDropdownOption = (
|
||||
JSAction: JSAction,
|
||||
): JSActionDropdownOption => ({
|
||||
label: JSAction.name,
|
||||
value: JSAction.id,
|
||||
data: JSAction,
|
||||
hasCustomBadge: !!JSAction.actionConfiguration.isAsync,
|
||||
});
|
||||
|
||||
export const getActionFromJsCollection = (
|
||||
actionId: string | null,
|
||||
jsCollection: JSCollection,
|
||||
): JSAction | null => {
|
||||
if (!actionId) return null;
|
||||
return jsCollection.actions.find((action) => action.id === actionId) || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns dropdown option based on priority and availability
|
||||
*/
|
||||
export const getJSActionOption = (
|
||||
activeJSAction: JSAction | null,
|
||||
jsActions: JSAction[],
|
||||
): JSActionDropdownOption => {
|
||||
let jsActionOption = NO_FUNCTION_DROPDOWN_OPTION;
|
||||
if (activeJSAction) {
|
||||
jsActionOption = convertJSActionToDropdownOption(activeJSAction);
|
||||
} else if (jsActions.length) {
|
||||
jsActionOption = convertJSActionToDropdownOption(jsActions[0]);
|
||||
}
|
||||
return jsActionOption;
|
||||
};
|
||||
|
|
@ -79,7 +79,10 @@ import {
|
|||
ConditionalOutput,
|
||||
FormEvalOutput,
|
||||
} from "reducers/evaluationReducers/formEvaluationReducer";
|
||||
import { responseTabComponent } from "components/editorComponents/ApiResponseView";
|
||||
import {
|
||||
responseTabComponent,
|
||||
InlineButton,
|
||||
} from "components/editorComponents/ApiResponseView";
|
||||
|
||||
const QueryFormContainer = styled.form`
|
||||
flex: 1;
|
||||
|
|
@ -362,11 +365,6 @@ const TabContainerView = styled.div`
|
|||
position: relative;
|
||||
`;
|
||||
|
||||
const InlineButton = styled(Button)`
|
||||
display: inline-flex;
|
||||
margin: 0 4px;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
@ -895,6 +893,11 @@ export function EditorJSONtoForm(props: Props) {
|
|||
name={currentActionConfig ? currentActionConfig.name : ""}
|
||||
pageId={pageId}
|
||||
/>
|
||||
<SearchSnippets
|
||||
className="search-snippets"
|
||||
entityId={currentActionConfig?.id}
|
||||
entityType={ENTITY_TYPE.ACTION}
|
||||
/>
|
||||
<DropdownSelect>
|
||||
<DropdownField
|
||||
className={"t--switch-datasource"}
|
||||
|
|
@ -906,11 +909,6 @@ export function EditorJSONtoForm(props: Props) {
|
|||
width={232}
|
||||
/>
|
||||
</DropdownSelect>
|
||||
<SearchSnippets
|
||||
className="search-snippets"
|
||||
entityId={currentActionConfig?.id}
|
||||
entityType={ENTITY_TYPE.ACTION}
|
||||
/>
|
||||
<Button
|
||||
className="t--run-query"
|
||||
data-guided-tour-iid="run-query"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { useParams } from "react-router";
|
|||
import classNames from "classnames";
|
||||
import { forceOpenWidgetPanel } from "actions/widgetSidebarActions";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { getCurrentThemeDetails } from "selectors/themeSelectors";
|
||||
|
||||
const Container = styled.section`
|
||||
width: 100%;
|
||||
|
|
@ -38,6 +39,7 @@ function CanvasContainer() {
|
|||
const isFetchingPage = useSelector(getIsFetchingPage);
|
||||
const widgets = useSelector(getCanvasWidgetDsl);
|
||||
const pages = useSelector(getViewModePageList);
|
||||
const theme = useSelector(getCurrentThemeDetails);
|
||||
const isPreviewMode = useSelector(previewModeSelector);
|
||||
const params = useParams<{ applicationId: string; pageId: string }>();
|
||||
const shouldHaveTopMargin = !isPreviewMode || pages.length > 1;
|
||||
|
|
@ -63,7 +65,9 @@ function CanvasContainer() {
|
|||
if (!isFetchingPage && widgets) {
|
||||
node = <Canvas dsl={widgets} pageId={params.pageId} />;
|
||||
}
|
||||
|
||||
// calculating exact height to not allow scroll at this component,
|
||||
// calculating total height minus margin on top, top bar and bottom bar
|
||||
const heightWithTopMargin = `calc(100vh - 2.25rem - ${theme.smallHeaderHeight} - ${theme.bottomBarHeight})`;
|
||||
return (
|
||||
<Container
|
||||
className={classNames({
|
||||
|
|
@ -73,7 +77,7 @@ function CanvasContainer() {
|
|||
})}
|
||||
key={currentPageId}
|
||||
style={{
|
||||
height: `calc(100vh - ${shouldHaveTopMargin ? "2rem" : "0px"})`,
|
||||
height: shouldHaveTopMargin ? heightWithTopMargin : "100vh",
|
||||
}}
|
||||
>
|
||||
{node}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ const Container = styled.div`
|
|||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow-y: hidden;
|
||||
padding: 0px 8px 0px 8px;
|
||||
`;
|
||||
|
||||
const BodyContainer = styled.div`
|
||||
|
|
@ -49,10 +48,17 @@ const MenuContainer = styled.div`
|
|||
|
||||
const CloseBtnContainer = styled.div`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
right: -5px;
|
||||
top: 0;
|
||||
padding: ${(props) => props.theme.spaces[1]}px;
|
||||
padding: ${(props) => props.theme.spaces[1]}px 0;
|
||||
border-radius: ${(props) => props.theme.radii[1]}px;
|
||||
|
||||
&:hover {
|
||||
svg,
|
||||
svg path {
|
||||
fill: ${({ theme }) => get(theme, "colors.gitSyncModal.closeIconHover")};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ComponentsByTab = {
|
||||
|
|
|
|||
117
app/client/src/pages/Editor/gitSync/ImportedAppSuccessModal.tsx
Normal file
117
app/client/src/pages/Editor/gitSync/ImportedAppSuccessModal.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import React, { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import Dialog from "components/ads/DialogComponent";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import Text, { TextType } from "components/ads/Text";
|
||||
import { Colors } from "constants/Colors";
|
||||
import {
|
||||
createMessage,
|
||||
APPLICATION_IMPORT_SUCCESS,
|
||||
APPLICATION_IMPORT_SUCCESS_DESCRIPTION,
|
||||
} from "@appsmith/constants/messages";
|
||||
import Icon from "components/ads/Icon";
|
||||
import { Theme } from "constants/DefaultTheme";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
import { Button, Category, Size } from "components/ads";
|
||||
|
||||
const Container = styled.div`
|
||||
height: 461px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow-y: hidden;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
`;
|
||||
|
||||
const BodyContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.cs-icon {
|
||||
margin: auto;
|
||||
svg {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
}
|
||||
|
||||
.cs-text {
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const ActionButtonWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 30px 0px 0px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0px;
|
||||
`;
|
||||
|
||||
const ActionButton = styled(Button)`
|
||||
margin-right: 16px;
|
||||
`;
|
||||
|
||||
function ImportedApplicationSuccessModal() {
|
||||
const importedAppSuccess = localStorage.getItem("importApplicationSuccess");
|
||||
// const isOpen = importedAppSuccess === "true";
|
||||
const [isOpen, setIsOpen] = useState(importedAppSuccess === "true");
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
|
||||
const onClose = () => {
|
||||
setIsOpen(false);
|
||||
localStorage.setItem("importApplicationSuccess", "false");
|
||||
};
|
||||
const theme = useTheme() as Theme;
|
||||
return (
|
||||
<Dialog
|
||||
canEscapeKeyClose
|
||||
canOutsideClickClose
|
||||
className="t--import-app-success-modal"
|
||||
isOpen={isOpen}
|
||||
maxWidth={"900px"}
|
||||
noModalBodyMarginTop
|
||||
onClose={onClose}
|
||||
width={"600px"}
|
||||
>
|
||||
<Container>
|
||||
<BodyContainer>
|
||||
<Icon fillColor={Colors.GREEN_1} name="oval-check-fill" />
|
||||
<Text
|
||||
color={Colors.BLACK}
|
||||
style={{ marginTop: 64 }}
|
||||
type={TextType.DANGER_HEADING}
|
||||
weight="bold"
|
||||
>
|
||||
{createMessage(
|
||||
APPLICATION_IMPORT_SUCCESS,
|
||||
currentUser?.name || currentUser?.username,
|
||||
)}
|
||||
</Text>
|
||||
<Text
|
||||
color={Colors.GRAY_700}
|
||||
style={{ marginTop: theme.spaces[3] }}
|
||||
type={TextType.P1}
|
||||
>
|
||||
{createMessage(APPLICATION_IMPORT_SUCCESS_DESCRIPTION)}
|
||||
</Text>
|
||||
<ActionButtonWrapper>
|
||||
<ActionButton
|
||||
category={Category.primary}
|
||||
className="t--import-success-modal-got-it"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
size={Size.medium}
|
||||
text="GOT IT"
|
||||
/>
|
||||
</ActionButtonWrapper>
|
||||
</BodyContainer>
|
||||
</Container>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportedApplicationSuccessModal;
|
||||
|
|
@ -59,6 +59,7 @@ import { Toaster, Variant } from "components/ads";
|
|||
import { getOAuthAccessToken } from "actions/datasourceActions";
|
||||
import { builderURL } from "RouteBuilder";
|
||||
import { PLACEHOLDER_APP_SLUG } from "constants/routes";
|
||||
import localStorage from "utils/localStorage";
|
||||
|
||||
const Container = styled.div`
|
||||
height: 765px;
|
||||
|
|
@ -276,13 +277,22 @@ function ReconnectDatasourceModal() {
|
|||
const isDatasourceTesting = useSelector(getIsDatasourceTesting);
|
||||
const isDatasourceUpdating = useSelector(getDatasourceLoading);
|
||||
|
||||
// checking refresh modal
|
||||
const pendingApp = JSON.parse(
|
||||
localStorage.getItem("importedAppPendingInfo") || "null",
|
||||
);
|
||||
// getting query from redirection url
|
||||
const userOrgs = useSelector(getUserApplicationsOrgsList);
|
||||
const queryParams = useQuery();
|
||||
const queryAppId = queryParams.get("appId");
|
||||
const queryPageId = queryParams.get("pageId");
|
||||
const queryDatasourceId = queryParams.get("datasourceId");
|
||||
const queryIsImport = JSON.parse(queryParams.get("importForGit") ?? "false");
|
||||
const queryAppId =
|
||||
queryParams.get("appId") || (pendingApp ? pendingApp.appId : null);
|
||||
const queryPageId =
|
||||
queryParams.get("pageId") || (pendingApp ? pendingApp.pageId : null);
|
||||
const queryDatasourceId =
|
||||
queryParams.get("datasourceId") ||
|
||||
(pendingApp ? pendingApp.datasourceId : null);
|
||||
const queryIsImport =
|
||||
queryParams.get("importForGit") === "true" || !!pendingApp;
|
||||
|
||||
const [selectedDatasourceId, setSelectedDatasourceId] = useState<
|
||||
string | null
|
||||
|
|
@ -471,7 +481,17 @@ function ReconnectDatasourceModal() {
|
|||
next = next || pending[0];
|
||||
setSelectedDatasourceId(next.id);
|
||||
setDatasource(next);
|
||||
// when refresh, it should be opened.
|
||||
const appInfo = {
|
||||
appId: appId,
|
||||
pageId: pageId,
|
||||
datasourceId: next.id,
|
||||
};
|
||||
localStorage.setItem("importedAppPendingInfo", JSON.stringify(appInfo));
|
||||
} else if (appURL) {
|
||||
// open application import successfule
|
||||
localStorage.setItem("importApplicationSuccess", "true");
|
||||
localStorage.setItem("importedAppPendingInfo", "null");
|
||||
window.open(appURL, "_self");
|
||||
}
|
||||
}
|
||||
|
|
@ -557,6 +577,7 @@ function ReconnectDatasourceModal() {
|
|||
AnalyticsUtil.logEvent(
|
||||
"RECONNECTING_SKIP_TO_APPLICATION_BUTTON_CLICK",
|
||||
);
|
||||
localStorage.setItem("importedAppPendingInfo", "null");
|
||||
}}
|
||||
size={Size.medium}
|
||||
text={createMessage(SKIP_TO_APPLICATION)}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ const KeyText = styled.span`
|
|||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: ${Colors.CODE_GRAY};
|
||||
color: ${Colors.COD_GRAY};
|
||||
`;
|
||||
|
||||
const MoreMenuWrapper = styled.div`
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ export const Title = styled.p`
|
|||
${(props) => getTypographyByKey(props, "h1")};
|
||||
margin: ${(props) =>
|
||||
`${props.theme.spaces[7]}px 0px ${props.theme.spaces[3]}px 0px`};
|
||||
color: ${Colors.COD_GRAY};
|
||||
`;
|
||||
|
||||
export const Subtitle = styled.span`
|
||||
${(props) => getTypographyByKey(props, "p1")};
|
||||
color: ${Colors.BLACK};
|
||||
color: ${Colors.COD_GRAY};
|
||||
`;
|
||||
|
||||
export const Caption = styled.span`
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import { loading } from "selectors/onboardingSelectors";
|
|||
import GuidedTourModal from "./GuidedTour/DeviationModal";
|
||||
import { getPageLevelSocketRoomId } from "sagas/WebsocketSagas/utils";
|
||||
import RepoLimitExceededErrorModal from "./gitSync/RepoLimitExceededErrorModal";
|
||||
import ImportedApplicationSuccessModal from "./gitSync/ImportedAppSuccessModal";
|
||||
|
||||
type EditorProps = {
|
||||
currentApplicationId?: string;
|
||||
|
|
@ -229,6 +230,7 @@ class Editor extends Component<Props> {
|
|||
<ConcurrentPageEditorToast />
|
||||
<GuidedTourModal />
|
||||
<RepoLimitExceededErrorModal />
|
||||
<ImportedApplicationSuccessModal />
|
||||
</GlobalHotKeys>
|
||||
</div>
|
||||
<RequestConfirmationModal />
|
||||
|
|
|
|||
|
|
@ -54,6 +54,14 @@ export function DisconnectService(props: {
|
|||
warning: string;
|
||||
}) {
|
||||
const [warnDisconnectAuth, setWarnDisconnectAuth] = useState(false);
|
||||
const [disconnectCalled, setDisconnectCalled] = useState(false);
|
||||
|
||||
const callDisconnect = () => {
|
||||
if (!disconnectCalled) {
|
||||
setDisconnectCalled(true);
|
||||
props.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
|
@ -63,7 +71,7 @@ export function DisconnectService(props: {
|
|||
<DisconnectButton
|
||||
data-testid="disconnect-service-button"
|
||||
onClick={() =>
|
||||
warnDisconnectAuth ? props.disconnect() : setWarnDisconnectAuth(true)
|
||||
warnDisconnectAuth ? callDisconnect() : setWarnDisconnectAuth(true)
|
||||
}
|
||||
text={
|
||||
warnDisconnectAuth
|
||||
|
|
|
|||
|
|
@ -20,16 +20,15 @@ const StyledIcon = styled(Icon)`
|
|||
export const StyledFormGroup = styled.div`
|
||||
width: 40rem;
|
||||
margin-bottom: ${(props) => props.theme.spaces[7]}px;
|
||||
& span.bp3-popover-target {
|
||||
display: inline-block;
|
||||
background: ${(props) => props.theme.colors.menuItem.normalIcon};
|
||||
border-radius: ${(props) => props.theme.radii[2]}px;
|
||||
width: 14px;
|
||||
padding: 3px 3px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
left: 6px;
|
||||
cursor: default;
|
||||
&.t--admin-settings-dropdown {
|
||||
div {
|
||||
width: 100%;
|
||||
&:hover {
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.colors.textInput.hover.bg};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
& svg:hover {
|
||||
cursor: default;
|
||||
|
|
|
|||
28
app/client/src/pages/Settings/FormGroup/Dropdown.tsx
Normal file
28
app/client/src/pages/Settings/FormGroup/Dropdown.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import React from "react";
|
||||
import { FormGroup, SettingComponentProps } from "./Common";
|
||||
import SelectField from "components/ads/formFields/SelectField";
|
||||
|
||||
export default function DropDown(
|
||||
props: {
|
||||
dropdownOptions: Array<{ id: string; value: string; label?: string }>;
|
||||
} & SettingComponentProps,
|
||||
) {
|
||||
const { dropdownOptions, setting } = props;
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
className={`t--admin-settings-dropdown t--admin-settings-${setting.name ||
|
||||
setting.id}`}
|
||||
setting={setting}
|
||||
>
|
||||
<SelectField
|
||||
fillOptions
|
||||
name={setting.id}
|
||||
options={dropdownOptions}
|
||||
outline={false}
|
||||
placeholder="Select an option"
|
||||
size="large"
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import { Callout } from "components/ads/CalloutV2";
|
|||
import { CopyUrlReduxForm } from "components/ads/formFields/CopyUrlForm";
|
||||
import Accordion from "./Accordion";
|
||||
import TagInputField from "./TagInputField";
|
||||
import Dropdown from "./Dropdown";
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
import { Colors } from "constants/Colors";
|
||||
|
||||
|
|
@ -228,6 +229,21 @@ export default function Group({
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
case SettingTypes.DROPDOWN:
|
||||
return (
|
||||
<div
|
||||
className={setting.isHidden ? "hide" : ""}
|
||||
data-testid="admin-settings-dropdown"
|
||||
key={setting.name || setting.id}
|
||||
>
|
||||
{setting.dropdownOptions && (
|
||||
<Dropdown
|
||||
dropdownOptions={setting.dropdownOptions}
|
||||
setting={setting}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</GroupBody>
|
||||
|
|
|
|||
|
|
@ -25,14 +25,17 @@ import {
|
|||
import { DisconnectService } from "./DisconnectService";
|
||||
import {
|
||||
createMessage,
|
||||
DISCONNECT_AUTH_ERROR,
|
||||
DISCONNECT_SERVICE_SUBHEADER,
|
||||
DISCONNECT_SERVICE_WARNING,
|
||||
MANDATORY_FIELDS_ERROR,
|
||||
} from "@appsmith/constants/messages";
|
||||
import { Toaster, Variant } from "components/ads";
|
||||
import {
|
||||
connectedMethods,
|
||||
saveAllowed,
|
||||
} from "@appsmith/utils/adminSettingsHelpers";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
flex-basis: calc(100% - ${(props) => props.theme.homePage.leftPane.width}px);
|
||||
|
|
@ -103,13 +106,19 @@ export function SettingsForm(
|
|||
const onSave = () => {
|
||||
if (checkMandatoryFileds()) {
|
||||
if (saveAllowed(props.settings)) {
|
||||
AnalyticsUtil.logEvent("ADMIN_SETTINGS_SAVE", {
|
||||
method: pageTitle,
|
||||
});
|
||||
dispatch(saveSettings(props.settings));
|
||||
} else {
|
||||
saveBlocked();
|
||||
}
|
||||
} else {
|
||||
AnalyticsUtil.logEvent("ADMIN_SETTINGS_ERROR", {
|
||||
error: createMessage(MANDATORY_FIELDS_ERROR),
|
||||
});
|
||||
Toaster.show({
|
||||
text: "Mandatory fields cannot be empty",
|
||||
text: createMessage(MANDATORY_FIELDS_ERROR),
|
||||
variant: Variant.danger,
|
||||
});
|
||||
}
|
||||
|
|
@ -138,7 +147,12 @@ export function SettingsForm(
|
|||
return !(requiredFields.length > 0);
|
||||
};
|
||||
|
||||
const onClear = () => {
|
||||
const onClear = (event?: React.FocusEvent<any, any>) => {
|
||||
if (event?.type === "click") {
|
||||
AnalyticsUtil.logEvent("ADMIN_SETTINGS_RESET", {
|
||||
method: pageTitle,
|
||||
});
|
||||
}
|
||||
_.forEach(props.settingsConfig, (value, settingName) => {
|
||||
const setting = AdminConfig.settingsMap[settingName];
|
||||
if (setting && setting.controlType == SettingTypes.TOGGLE) {
|
||||
|
|
@ -159,8 +173,11 @@ export function SettingsForm(
|
|||
}, []);
|
||||
|
||||
const saveBlocked = () => {
|
||||
AnalyticsUtil.logEvent("ADMIN_SETTINGS_ERROR", {
|
||||
error: createMessage(DISCONNECT_AUTH_ERROR),
|
||||
});
|
||||
Toaster.show({
|
||||
text: "Cannot disconnect the only connected authentication method.",
|
||||
text: createMessage(DISCONNECT_AUTH_ERROR),
|
||||
variant: Variant.danger,
|
||||
});
|
||||
};
|
||||
|
|
@ -174,6 +191,9 @@ export function SettingsForm(
|
|||
}
|
||||
});
|
||||
dispatch(saveSettings(updatedSettings));
|
||||
AnalyticsUtil.logEvent("ADMIN_SETTINGS_DISCONNECT_AUTH_METHOD", {
|
||||
method: pageTitle,
|
||||
});
|
||||
} else {
|
||||
saveBlocked();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,25 +17,25 @@ import { ANONYMOUS_USERNAME } from "constants/userConstants";
|
|||
import { Colors } from "constants/Colors";
|
||||
import { viewerURL } from "RouteBuilder";
|
||||
|
||||
const StyledCopyToClipBoard = styled(CopyToClipBoard)`
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const CommonTitleTextStyle = css`
|
||||
color: ${Colors.CHARCOAL};
|
||||
font-weight: normal;
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
padding: 0 0 10px 0;
|
||||
padding: 0 0 8px 0;
|
||||
& > span[type="h5"] {
|
||||
${CommonTitleTextStyle}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledCopyToClipBoard = styled(CopyToClipBoard)`
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const ShareWithPublicOption = styled.div`
|
||||
display: flex;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Classes } from "components/ads/common";
|
|||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const StyledManageUsers = styled("a")`
|
||||
margin-top: 20px;
|
||||
margin-top: 12px;
|
||||
display: inline-flex;
|
||||
&&&& {
|
||||
text-decoration: none;
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ const UserRole = styled.div`
|
|||
flex-basis: 25%;
|
||||
flex-shrink: 0;
|
||||
.${Classes.TEXT} {
|
||||
color: ${(props) => props.theme.colors.modal.headerText};
|
||||
color: ${Colors.COD_GRAY};
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -139,6 +139,14 @@ const UserName = styled.div`
|
|||
&:nth-child(1) {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
&[type="h5"] {
|
||||
color: ${Colors.COD_GRAY};
|
||||
}
|
||||
|
||||
&[type="p2"] {
|
||||
color: ${Colors.GRAY};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -155,8 +163,8 @@ const Loading = styled(Spinner)`
|
|||
const MailConfigContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${(props) => props.theme.spaces[9]}px
|
||||
${(props) => props.theme.spaces[2]}px;
|
||||
padding: 24px 4px;
|
||||
padding-bottom: 0;
|
||||
align-items: center;
|
||||
&& > span {
|
||||
color: ${(props) => props.theme.colors.modal.email.message};
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import {
|
|||
ReduxActionTypes,
|
||||
ReduxAction,
|
||||
ReduxActionErrorTypes,
|
||||
} from "@appsmith/constants/ReduxActionConstants";
|
||||
import { set, keyBy } from "lodash";
|
||||
} from "ce/constants/ReduxActionConstants";
|
||||
import { set, keyBy, findIndex, unset } from "lodash";
|
||||
import produce from "immer";
|
||||
|
||||
const initialState: JSCollectionDataState = [];
|
||||
|
|
@ -14,6 +14,7 @@ export interface JSCollectionData {
|
|||
config: JSCollection;
|
||||
data?: Record<string, unknown>;
|
||||
isExecuting?: Record<string, boolean>;
|
||||
activeJSActionId?: string;
|
||||
}
|
||||
export type JSCollectionDataState = JSCollectionData[];
|
||||
export interface PartialActionData {
|
||||
|
|
@ -61,13 +62,25 @@ const jsActionsReducer = createReducer(initialState, {
|
|||
),
|
||||
[ReduxActionTypes.UPDATE_JS_ACTION_SUCCESS]: (
|
||||
state: JSCollectionDataState,
|
||||
action: ReduxAction<{ data: JSCollection }>,
|
||||
action: ReduxAction<{
|
||||
data: JSCollection;
|
||||
}>,
|
||||
): JSCollectionDataState =>
|
||||
state.map((a) => {
|
||||
if (a.config.id === action.payload.data.id) {
|
||||
return { ...a, isLoading: false, config: action.payload.data };
|
||||
state.map((jsCollection) => {
|
||||
if (jsCollection.config.id === action.payload.data.id) {
|
||||
return {
|
||||
...jsCollection,
|
||||
isLoading: false,
|
||||
config: action.payload.data,
|
||||
activeJSActionId:
|
||||
findIndex(jsCollection.config.actions, {
|
||||
id: jsCollection.activeJSActionId,
|
||||
}) === -1
|
||||
? undefined
|
||||
: jsCollection.activeJSActionId,
|
||||
};
|
||||
}
|
||||
return a;
|
||||
return jsCollection;
|
||||
}),
|
||||
[ReduxActionTypes.UPDATE_JS_ACTION_BODY_SUCCESS]: (
|
||||
state: JSCollectionDataState,
|
||||
|
|
@ -254,12 +267,17 @@ const jsActionsReducer = createReducer(initialState, {
|
|||
): JSCollectionDataState =>
|
||||
state.map((a) => {
|
||||
if (a.config.id === action.payload.collectionId) {
|
||||
const newData = { ...a.data };
|
||||
unset(newData, action.payload.action.id);
|
||||
return {
|
||||
...a,
|
||||
isExecuting: {
|
||||
...a.isExecuting,
|
||||
[action.payload.action.id]: true,
|
||||
},
|
||||
data: {
|
||||
...newData,
|
||||
},
|
||||
};
|
||||
}
|
||||
return a;
|
||||
|
|
@ -353,6 +371,22 @@ const jsActionsReducer = createReducer(initialState, {
|
|||
});
|
||||
});
|
||||
},
|
||||
[ReduxActionTypes.SET_ACTIVE_JS_ACTION]: (
|
||||
state: JSCollectionDataState,
|
||||
action: ReduxAction<{
|
||||
jsCollectionId: string;
|
||||
jsActionId: string;
|
||||
}>,
|
||||
): JSCollectionDataState =>
|
||||
state.map((jsCollection) => {
|
||||
if (jsCollection.config.id === action.payload.jsCollectionId) {
|
||||
return {
|
||||
...jsCollection,
|
||||
activeJSActionId: action.payload.jsActionId,
|
||||
};
|
||||
}
|
||||
return jsCollection;
|
||||
}),
|
||||
});
|
||||
|
||||
export default jsActionsReducer;
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ import { submitCurlImportForm } from "actions/importActions";
|
|||
import { getBasePath } from "pages/Editor/Explorer/helpers";
|
||||
import { isTrueObject } from "workers/evaluationUtils";
|
||||
import { handleExecuteJSFunctionSaga } from "sagas/JSPaneSagas";
|
||||
import { Plugin } from "api/PluginApi";
|
||||
|
||||
enum ActionResponseDataTypes {
|
||||
BINARY = "BINARY",
|
||||
}
|
||||
|
|
@ -835,17 +837,24 @@ function* executePluginActionSaga(
|
|||
response: payload,
|
||||
}),
|
||||
);
|
||||
let plugin;
|
||||
let plugin: Plugin | undefined;
|
||||
if (!!pluginAction.pluginId) {
|
||||
plugin = yield select(getPlugin, pluginAction.pluginId);
|
||||
}
|
||||
yield put(
|
||||
setActionResponseDisplayFormat({
|
||||
id: actionId,
|
||||
field: "responseDisplayFormat",
|
||||
value: plugin && plugin.responseType ? plugin.responseType : "JSON",
|
||||
}),
|
||||
);
|
||||
|
||||
if (!!plugin) {
|
||||
const responseType = payload?.dataTypes.find(
|
||||
(type) =>
|
||||
plugin?.responseType && type.dataType === plugin?.responseType,
|
||||
);
|
||||
yield put(
|
||||
setActionResponseDisplayFormat({
|
||||
id: actionId,
|
||||
field: "responseDisplayFormat",
|
||||
value: responseType ? responseType?.dataType : "JSON",
|
||||
}),
|
||||
);
|
||||
}
|
||||
return {
|
||||
payload,
|
||||
isError: isErrorResponse(response),
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import {
|
|||
JS_EXECUTION_SUCCESS,
|
||||
JS_EXECUTION_FAILURE,
|
||||
JS_EXECUTION_FAILURE_TOASTER,
|
||||
JS_EXECUTION_SUCCESS_TOASTER,
|
||||
JS_FUNCTION_CREATE_SUCCESS,
|
||||
JS_FUNCTION_DELETE_SUCCESS,
|
||||
JS_FUNCTION_UPDATE_SUCCESS,
|
||||
|
|
@ -257,7 +258,12 @@ function* updateJSCollection(data: {
|
|||
createMessage(JS_FUNCTION_DELETE_SUCCESS),
|
||||
);
|
||||
}
|
||||
yield put(updateJSCollectionSuccess({ data: response?.data }));
|
||||
|
||||
yield put(
|
||||
updateJSCollectionSuccess({
|
||||
data: response?.data,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -330,6 +336,10 @@ export function* handleExecuteJSFunctionSaga(data: {
|
|||
},
|
||||
state: { response: result },
|
||||
});
|
||||
Toaster.show({
|
||||
text: createMessage(JS_EXECUTION_SUCCESS_TOASTER, action.name),
|
||||
variant: Variant.success,
|
||||
});
|
||||
} catch (e) {
|
||||
AppsmithConsole.addError({
|
||||
id: actionId,
|
||||
|
|
|
|||
|
|
@ -976,6 +976,7 @@ export function* generateTemplatePageSaga(
|
|||
responseMeta: response.responseMeta,
|
||||
},
|
||||
pageId,
|
||||
isFirstLoad: true,
|
||||
});
|
||||
|
||||
// TODO : Add this to onSuccess (Redux Action)
|
||||
|
|
|
|||
|
|
@ -12,18 +12,20 @@ import {
|
|||
isEmbeddedRestDatasource,
|
||||
} from "entities/Datasource";
|
||||
import { Action, PluginType } from "entities/Action";
|
||||
import { find, sortBy } from "lodash";
|
||||
import { find, get, sortBy } from "lodash";
|
||||
import ImageAlt from "assets/images/placeholder-image.svg";
|
||||
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
|
||||
import { AppStoreState } from "reducers/entityReducers/appReducer";
|
||||
import { JSCollectionDataState } from "reducers/entityReducers/jsActionsReducer";
|
||||
import { JSCollection } from "entities/JSCollection";
|
||||
import { DefaultPlugin, GenerateCRUDEnabledPluginMap } from "api/PluginApi";
|
||||
import { JSAction, JSCollection } from "entities/JSCollection";
|
||||
import { APP_MODE } from "entities/App";
|
||||
import { ExplorerFileEntity } from "pages/Editor/Explorer/helpers";
|
||||
import { ActionValidationConfigMap } from "constants/PropertyControlConstants";
|
||||
import { selectFeatureFlags } from "./usersSelectors";
|
||||
import { EvaluationError, EVAL_ERROR_PATH } from "utils/DynamicBindingUtils";
|
||||
import { Severity } from "entities/AppsmithConsole";
|
||||
|
||||
export const getEntities = (state: AppState): AppState["entities"] =>
|
||||
state.entities;
|
||||
|
|
@ -782,3 +784,52 @@ function getActionValidationConfigFromPlugin(
|
|||
}
|
||||
return newValidationConfig;
|
||||
}
|
||||
export const getJSActions = (
|
||||
state: AppState,
|
||||
JSCollectionId: string,
|
||||
): JSAction[] => {
|
||||
const jsCollection = state.entities.jsActions.find(
|
||||
(jsCollectionData) => jsCollectionData.config.id === JSCollectionId,
|
||||
);
|
||||
|
||||
return jsCollection?.config.actions ?? [];
|
||||
};
|
||||
|
||||
export const getActiveJSActionId = (
|
||||
state: AppState,
|
||||
jsCollectionId: string,
|
||||
): string | null => {
|
||||
const jsCollection = state.entities.jsActions.find(
|
||||
(jsCollectionData) => jsCollectionData.config.id === jsCollectionId,
|
||||
);
|
||||
return jsCollection?.activeJSActionId ?? null;
|
||||
};
|
||||
|
||||
export const getIsExecutingJSAction = (
|
||||
state: AppState,
|
||||
jsCollectionId: string,
|
||||
actionId: string,
|
||||
): boolean => {
|
||||
const jsCollection = state.entities.jsActions.find(
|
||||
(jsCollectionData) => jsCollectionData.config.id === jsCollectionId,
|
||||
);
|
||||
if (jsCollection?.isExecuting && jsCollection.isExecuting[actionId]) {
|
||||
return jsCollection.isExecuting[actionId];
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getJSCollectionParseErrors = (
|
||||
state: AppState,
|
||||
jsCollectionName: string,
|
||||
) => {
|
||||
const dataTree = state.evaluations.tree;
|
||||
const allErrors = get(
|
||||
dataTree,
|
||||
`${jsCollectionName}.${EVAL_ERROR_PATH}.body`,
|
||||
[],
|
||||
) as EvaluationError[];
|
||||
return allErrors.filter((error) => {
|
||||
return error.severity === Severity.ERROR;
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -199,6 +199,13 @@ export type EventName =
|
|||
| "GS_REGENERATE_SSH_KEY_CONFIRM_CLICK"
|
||||
| "GS_REGENERATE_SSH_KEY_MORE_CLICK"
|
||||
| "GS_SWITCH_BRANCH"
|
||||
| "ADMIN_SETTINGS_RESET"
|
||||
| "ADMIN_SETTINGS_SAVE"
|
||||
| "ADMIN_SETTINGS_ERROR"
|
||||
| "ADMIN_SETTINGS_DISCONNECT_AUTH_METHOD"
|
||||
| "ADMIN_SETTINGS_UPGRADE_AUTH_METHOD"
|
||||
| "ADMIN_SETTINGS_EDIT_AUTH_METHOD"
|
||||
| "ADMIN_SETTINGS_ENABLE_AUTH_METHOD"
|
||||
| "REFLOW_BETA_FLAG"
|
||||
| "CONTAINER_JUMP"
|
||||
| "CONNECT_GIT_CLICK"
|
||||
|
|
@ -216,7 +223,8 @@ export type EventName =
|
|||
| "RECONNECTING_SKIP_TO_APPLICATION_BUTTON_CLICK"
|
||||
| "TEMPLATE_FILTER_SELECTED"
|
||||
| "MANUAL_UPGRADE_CLICK"
|
||||
| "PAGE_NOT_FOUND";
|
||||
| "PAGE_NOT_FOUND"
|
||||
| "RUN_JS_FUNCTION";
|
||||
|
||||
function getApplicationId(location: Location) {
|
||||
const pathSplit = location.pathname.split("/");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect } from "react";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
|
||||
const useResizeObserver = (
|
||||
ref: HTMLDivElement | null,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import equal from "fast-deep-equal/es6";
|
||||
import { difference, isEmpty } from "lodash";
|
||||
import log from "loglevel";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
|
||||
import { isDynamicValue } from "utils/DynamicBindingUtils";
|
||||
import { MetaInternalFieldState } from ".";
|
||||
|
|
@ -264,6 +265,17 @@ export const computeSchema = ({
|
|||
|
||||
const count = countFields(currSourceData);
|
||||
if (count > MAX_ALLOWED_FIELDS) {
|
||||
AnalyticsUtil.logEvent("WIDGET_PROPERTY_UPDATE", {
|
||||
widgetType: "JSON_FORM_WIDGET",
|
||||
widgetName,
|
||||
propertyName: "sourceData",
|
||||
updatedValue: currSourceData,
|
||||
metaInfo: {
|
||||
limitExceeded: true,
|
||||
currentLimit: MAX_ALLOWED_FIELDS,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
status: ComputedSchemaStatus.LIMIT_EXCEEDED,
|
||||
schema: prevSchema,
|
||||
|
|
|
|||
|
|
@ -165,6 +165,11 @@ function TableHeader(props: TableHeaderProps) {
|
|||
|
||||
{props.isVisiblePagination && props.serverSidePaginationEnabled && (
|
||||
<PaginationWrapper>
|
||||
{props.totalRecordsCount ? (
|
||||
<RowWrapper className="show-page-items">
|
||||
{props.totalRecordsCount} Records
|
||||
</RowWrapper>
|
||||
) : null}
|
||||
<PaginationItemWrapper
|
||||
className="t--table-widget-prev-page"
|
||||
disabled={props.pageNo === 0}
|
||||
|
|
@ -174,9 +179,21 @@ function TableHeader(props: TableHeaderProps) {
|
|||
>
|
||||
<Icon color={Colors.HIT_GRAY} icon="chevron-left" iconSize={16} />
|
||||
</PaginationItemWrapper>
|
||||
<PaginationItemWrapper className="page-item" selected>
|
||||
{props.pageNo + 1}
|
||||
</PaginationItemWrapper>
|
||||
{props.totalRecordsCount ? (
|
||||
<RowWrapper>
|
||||
Page
|
||||
<PaginationItemWrapper className="page-item" selected>
|
||||
{props.pageNo + 1}
|
||||
</PaginationItemWrapper>
|
||||
|
||||
<span>{`of ${props.pageCount}`}</span>
|
||||
</RowWrapper>
|
||||
) : (
|
||||
<PaginationItemWrapper className="page-item" selected>
|
||||
{props.pageNo + 1}
|
||||
</PaginationItemWrapper>
|
||||
)}
|
||||
|
||||
<PaginationItemWrapper
|
||||
className="t--table-widget-next-page"
|
||||
disabled={
|
||||
|
|
@ -207,14 +224,14 @@ function TableHeader(props: TableHeaderProps) {
|
|||
<Icon color={Colors.GRAY} icon="chevron-left" iconSize={16} />
|
||||
</PaginationItemWrapper>
|
||||
<RowWrapper>
|
||||
Page{" "}
|
||||
Page
|
||||
<PageNumberInput
|
||||
disabled={props.pageCount === 1}
|
||||
pageCount={props.pageCount}
|
||||
pageNo={props.pageNo + 1}
|
||||
updatePageNo={props.updatePageNo}
|
||||
/>{" "}
|
||||
of {props.pageCount}
|
||||
/>
|
||||
of {props.pageCount}
|
||||
</RowWrapper>
|
||||
<PaginationItemWrapper
|
||||
className="t--table-widget-next-page"
|
||||
|
|
|
|||
|
|
@ -435,8 +435,12 @@ export const CellWrapper = styled.div<{
|
|||
`;
|
||||
|
||||
export const CellCheckboxWrapper = styled(CellWrapper)<{ isChecked?: boolean }>`
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
&&& {
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
padding: 0px;
|
||||
align-items: center;
|
||||
}
|
||||
& > div {
|
||||
${(props) =>
|
||||
props.isChecked
|
||||
|
|
|
|||
|
|
@ -484,7 +484,6 @@ export const renderCheckBoxHeaderCell = (
|
|||
isChecked={!!checkState}
|
||||
onClick={onClick}
|
||||
role="columnheader"
|
||||
style={{ padding: "0px", justifyContent: "center" }}
|
||||
>
|
||||
<CellCheckbox>
|
||||
{checkState === 1 && <CheckBoxCheckIcon className="th-svg" />}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { parse, Node } from "acorn";
|
||||
import { ancestor } from "acorn-walk";
|
||||
import { ECMA_VERSION, NodeTypes } from "constants/ast";
|
||||
import _ from "lodash";
|
||||
import { ECMA_VERSION } from "workers/constants";
|
||||
import { sanitizeScript } from "./evaluate";
|
||||
|
||||
/*
|
||||
|
|
@ -23,19 +23,6 @@ import { sanitizeScript } from "./evaluate";
|
|||
*
|
||||
*/
|
||||
|
||||
// Each node has an attached type property which further defines
|
||||
// what all properties can the node have.
|
||||
// We will just define the ones we are working with
|
||||
enum NodeTypes {
|
||||
MemberExpression = "MemberExpression",
|
||||
Identifier = "Identifier",
|
||||
VariableDeclarator = "VariableDeclarator",
|
||||
FunctionDeclaration = "FunctionDeclaration",
|
||||
FunctionExpression = "FunctionExpression",
|
||||
AssignmentPattern = "AssignmentPattern",
|
||||
Literal = "Literal",
|
||||
}
|
||||
|
||||
type Pattern = IdentifierNode | AssignmentPatternNode;
|
||||
|
||||
// doc: https://github.com/estree/estree/blob/master/es5.md#memberexpression
|
||||
|
|
@ -88,8 +75,16 @@ interface LiteralNode extends Node {
|
|||
value: string | boolean | null | number | RegExp;
|
||||
}
|
||||
|
||||
// https://github.com/estree/estree/blob/master/es5.md#property
|
||||
export interface PropertyNode extends Node {
|
||||
type: NodeTypes.Property;
|
||||
key: LiteralNode | IdentifierNode;
|
||||
value: Node;
|
||||
kind: "init" | "get" | "set";
|
||||
}
|
||||
|
||||
/* We need these functions to typescript casts the nodes with the correct types */
|
||||
const isIdentifierNode = (node: Node): node is IdentifierNode => {
|
||||
export const isIdentifierNode = (node: Node): node is IdentifierNode => {
|
||||
return node.type === NodeTypes.Identifier;
|
||||
};
|
||||
|
||||
|
|
@ -113,10 +108,14 @@ const isAssignmentPatternNode = (node: Node): node is AssignmentPatternNode => {
|
|||
return node.type === NodeTypes.AssignmentPattern;
|
||||
};
|
||||
|
||||
const isLiteralNode = (node: Node): node is LiteralNode => {
|
||||
export const isLiteralNode = (node: Node): node is LiteralNode => {
|
||||
return node.type === NodeTypes.Literal;
|
||||
};
|
||||
|
||||
export const isPropertyNode = (node: Node): node is PropertyNode => {
|
||||
return node.type === NodeTypes.Property;
|
||||
};
|
||||
|
||||
const isArrayAccessorNode = (node: Node): node is MemberExpressionNode => {
|
||||
return (
|
||||
isMemberExpressionNode(node) &&
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const ECMA_VERSION = 11;
|
||||
|
|
@ -11,8 +11,8 @@ import {
|
|||
EvaluationScriptType,
|
||||
ScriptTemplate,
|
||||
} from "workers/evaluate";
|
||||
import { ECMA_VERSION } from "workers/constants";
|
||||
import { getLintSeverity } from "components/editorComponents/CodeEditor/lintHelpers";
|
||||
import { ECMA_VERSION } from "constants/ast";
|
||||
|
||||
export const getPositionInEvaluationScript = (
|
||||
type: EvaluationScriptType,
|
||||
|
|
|
|||
|
|
@ -73,4 +73,10 @@ public enum ConditionalOperator {
|
|||
return "or";
|
||||
}
|
||||
},
|
||||
CONTAINS {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "like";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -310,27 +310,25 @@ public class DataTypeStringUtils {
|
|||
|
||||
private static boolean isDisplayTypeTable(Object data) {
|
||||
if (data instanceof List) {
|
||||
// Check if the data is a list of simple json objects i.e. all values in the key value pairs are simple
|
||||
// objects or their wrappers.
|
||||
return ((List)data).stream()
|
||||
.allMatch(item -> item instanceof Map
|
||||
&& ((Map)item).entrySet().stream()
|
||||
.allMatch(e -> ((Map.Entry)e).getValue() == null ||
|
||||
isPrimitiveOrWrapper(((Map.Entry)e).getValue().getClass())));
|
||||
// Check if the data is a list of json objects
|
||||
return ((List) data).stream()
|
||||
.allMatch(item -> item instanceof Map);
|
||||
}
|
||||
else if (data instanceof JsonNode) {
|
||||
// Check if the data is an array of simple json objects
|
||||
// Check if the data is an array of json objects
|
||||
try {
|
||||
objectMapper.convertValue(data, new TypeReference<List<Map<String, String>>>() {});
|
||||
objectMapper.convertValue(data, new TypeReference<List<Map<String, Object>>>() {
|
||||
});
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (data instanceof String) {
|
||||
// Check if the data is an array of simple json objects
|
||||
// Check if the data is an array of json objects
|
||||
try {
|
||||
objectMapper.readValue((String)data, new TypeReference<List<Map<String, String>>>() {});
|
||||
objectMapper.readValue((String) data, new TypeReference<List<Map<String, Object>>>() {
|
||||
});
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
ConditionalOperator.NOT_EQ, "<>",
|
||||
ConditionalOperator.GT, ">",
|
||||
ConditionalOperator.GTE, ">=",
|
||||
ConditionalOperator.CONTAINS, "LIKE",
|
||||
ConditionalOperator.IN, "IN",
|
||||
ConditionalOperator.NOT_IN, "NOT IN"
|
||||
);
|
||||
|
|
@ -99,6 +100,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
* Overloaded Method to handle plugin-based DataType conversion.
|
||||
* Plugins implementing this passes parameter 'dataTypeConversionMap' to instruct how their DataTypes to be processed.
|
||||
* Example: GoogleSheet plugin handled it in a way that Integer, Long and Float DataTypes to be treated as Double.
|
||||
*
|
||||
* @param items
|
||||
* @param conditionList
|
||||
* @param dataTypeConversionMap - A Map to provide custom Datatype(value) against the actual Datatype(key) found.
|
||||
|
|
@ -137,7 +139,8 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
|
||||
/**
|
||||
* This filter method is using the new UQI format.
|
||||
* @param items - data
|
||||
*
|
||||
* @param items - data
|
||||
* @param uqiDataFilterParams - filter conditions to apply on data
|
||||
* @return filtered data
|
||||
*/
|
||||
|
|
@ -292,9 +295,9 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
* E.g. if the projectionColumns is a list that contains ["ID, Name"], then this method will add the following
|
||||
* SQL line: `SELECT ID, Name from tableName`, otherwise it will add: `SELECT * FROM tableName`
|
||||
*
|
||||
* @param sb - SQL query builder
|
||||
* @param sb - SQL query builder
|
||||
* @param projectionColumns - list of columns that need to be displayed
|
||||
* @param tableName - table name in database
|
||||
* @param tableName - table name in database
|
||||
*/
|
||||
private void addProjectionCondition(StringBuilder sb, List<String> projectionColumns, String tableName) {
|
||||
if (!CollectionUtils.isEmpty(projectionColumns)) {
|
||||
|
|
@ -304,8 +307,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
|
||||
sb.setLength(sb.length() - 1);
|
||||
sb.append(" FROM " + tableName);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
sb.append("SELECT * FROM " + tableName);
|
||||
}
|
||||
}
|
||||
|
|
@ -313,12 +315,12 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
/**
|
||||
* This method adds `ORDER BY` clause to the SQL query. E.g. if the sortBy list is
|
||||
* [
|
||||
* {"columnName": "ID", "type": "ASCENDING"},
|
||||
* {"columnName": "Name", "type": "DESCENDING"}
|
||||
* {"columnName": "ID", "type": "ASCENDING"},
|
||||
* {"columnName": "Name", "type": "DESCENDING"}
|
||||
* ]
|
||||
* then this method will add the following line to the SQL query: `ORDER BY ID ASC, Name DESC`
|
||||
*
|
||||
* @param sb - SQL query builder
|
||||
* @param sb - SQL query builder
|
||||
* @param sortBy - list of columns to sort by and sort type (ascending / descending)
|
||||
* @throws AppsmithPluginException
|
||||
*/
|
||||
|
|
@ -354,8 +356,8 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
|
||||
/**
|
||||
* Checks if:
|
||||
* o `sortBy` condition list is null or empty
|
||||
* o all column names in the sortBy list are empty
|
||||
* o `sortBy` condition list is null or empty
|
||||
* o all column names in the sortBy list are empty
|
||||
*/
|
||||
private boolean isSortConditionEmpty(List<Map<String, String>> sortBy) {
|
||||
if (CollectionUtils.isEmpty(sortBy)) {
|
||||
|
|
@ -368,9 +370,10 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
|
||||
/**
|
||||
* Filter Query before UQI implementation
|
||||
* @param tableName - table name in database
|
||||
* @param conditions - Where Conditions
|
||||
* @param schema - The Schema
|
||||
*
|
||||
* @param tableName - table name in database
|
||||
* @param conditions - Where Conditions
|
||||
* @param schema - The Schema
|
||||
* @param dataTypeConversionMap - A Map to provide custom Datatype against the actual Datatype found.
|
||||
* @return
|
||||
*/
|
||||
|
|
@ -459,9 +462,20 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
sb.append(" ");
|
||||
|
||||
if (value == null || value.equals(StringUtils.EMPTY)) {
|
||||
if (operator == ConditionalOperator.EQ || operator == ConditionalOperator.IN) {
|
||||
if (Set.of(
|
||||
ConditionalOperator.EQ,
|
||||
ConditionalOperator.IN,
|
||||
ConditionalOperator.CONTAINS,
|
||||
ConditionalOperator.LTE,
|
||||
ConditionalOperator.LT
|
||||
).contains(operator)) {
|
||||
sb.append("IS NULL");
|
||||
} else if (operator == ConditionalOperator.NOT_IN) {
|
||||
} else if (Set.of(
|
||||
ConditionalOperator.NOT_IN,
|
||||
ConditionalOperator.NOT_EQ,
|
||||
ConditionalOperator.GTE,
|
||||
ConditionalOperator.GT
|
||||
).contains(operator)) {
|
||||
sb.append("IS NOT NULL");
|
||||
}
|
||||
isEmptyConditionValue = true;
|
||||
|
|
@ -470,50 +484,62 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
}
|
||||
sb.append(" ");
|
||||
|
||||
// These are array operations. Convert value into appropriate format and then append
|
||||
if (!(value == null || StringUtils.EMPTY.equals(value)) && //value should not be EMPTY or null
|
||||
(operator == ConditionalOperator.IN || operator == ConditionalOperator.NOT_IN)) {
|
||||
// value should not be EMPTY or null
|
||||
if (!isEmptyConditionValue) {
|
||||
// These are array operations. Convert value into appropriate format and then append
|
||||
if (operator == ConditionalOperator.IN || operator == ConditionalOperator.NOT_IN) {
|
||||
|
||||
StringBuilder valueBuilder = new StringBuilder("(");
|
||||
StringBuilder valueBuilder = new StringBuilder("(");
|
||||
|
||||
try {
|
||||
List<Object> arrayValues = objectMapper.readValue(value, List.class);
|
||||
List<String> updatedStringValues = arrayValues
|
||||
.stream()
|
||||
.map(fieldValue -> {
|
||||
values.add(new PreparedStatementValueDTO(String.valueOf(fieldValue), schema.get(path)));
|
||||
return "?";
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
String finalValues = String.join(",", updatedStringValues);
|
||||
valueBuilder.append(finalValues);
|
||||
} catch (IOException e) {
|
||||
throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
value + " could not be parsed into an array");
|
||||
try {
|
||||
List<Object> arrayValues = objectMapper.readValue(value, List.class);
|
||||
List<String> updatedStringValues = arrayValues
|
||||
.stream()
|
||||
.map(fieldValue -> {
|
||||
values.add(new PreparedStatementValueDTO(String.valueOf(fieldValue), schema.get(path)));
|
||||
return "?";
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
String finalValues = String.join(",", updatedStringValues);
|
||||
valueBuilder.append(finalValues);
|
||||
} catch (IOException e) {
|
||||
throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
value + " could not be parsed into an array");
|
||||
}
|
||||
|
||||
valueBuilder.append(")");
|
||||
value = valueBuilder.toString();
|
||||
sb.append(value);
|
||||
|
||||
} else if (operator == ConditionalOperator.CONTAINS) {
|
||||
String escapedLikeValue = value
|
||||
.replace("!", "!!")
|
||||
.replace("%", "!%")
|
||||
.replace("_", "!_")
|
||||
.replace("[", "![");
|
||||
sb.append("? ESCAPE '!'");
|
||||
escapedLikeValue = "%" + escapedLikeValue + "%";
|
||||
values.add(new PreparedStatementValueDTO(escapedLikeValue, schema.get(path)));
|
||||
} else {
|
||||
// Not an array. Simply add a placeholder
|
||||
sb.append("?");
|
||||
values.add(new PreparedStatementValueDTO(value, schema.get(path)));
|
||||
}
|
||||
|
||||
valueBuilder.append(")");
|
||||
value = valueBuilder.toString();
|
||||
sb.append(value);
|
||||
|
||||
} else if (!isEmptyConditionValue) {
|
||||
// Not an array. Simply add a placeholder
|
||||
sb.append("?");
|
||||
values.add(new PreparedStatementValueDTO(value, schema.get(path)));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public void insertAllData(String tableName, ArrayNode items, Map<String, DataType> schema) {
|
||||
insertAllData( tableName, items, schema, null);
|
||||
insertAllData(tableName, items, schema, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloaded Method to handle plugin-based DataType conversion.
|
||||
* @param tableName - table name in database
|
||||
* @param items - Data
|
||||
* @param schema - The Schema
|
||||
*
|
||||
* @param tableName - table name in database
|
||||
* @param items - Data
|
||||
* @param schema - The Schema
|
||||
* @param dataTypeConversionMap - A Map to provide custom Datatype against the actual Datatype found.
|
||||
*/
|
||||
public void insertAllData(String tableName, ArrayNode items, Map<String, DataType> schema, Map<DataType, DataType> dataTypeConversionMap) {
|
||||
|
|
@ -714,6 +740,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
|
||||
/**
|
||||
* Overloaded Method to handle plugin-based DataType conversion.
|
||||
*
|
||||
* @param items
|
||||
* @param dataTypeConversionMap - A Map to provide custom Datatype against the actual Datatype found.
|
||||
* @return
|
||||
|
|
@ -761,7 +788,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
} else {
|
||||
DataType foundDataType = stringToKnownDataTypeConverter(value);
|
||||
DataType convertedDataType = foundDataType;
|
||||
if(name != "rowIndex" && dataTypeConversionMap != null) {
|
||||
if (name != "rowIndex" && dataTypeConversionMap != null) {
|
||||
convertedDataType = dataTypeConversionMap.getOrDefault(foundDataType, foundDataType);
|
||||
}
|
||||
return convertedDataType;
|
||||
|
|
@ -786,7 +813,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
if (!StringUtils.isEmpty(value)) {
|
||||
DataType foundDataType = stringToKnownDataTypeConverter(value);
|
||||
DataType dataType = foundDataType;
|
||||
if(dataTypeConversionMap != null) {
|
||||
if (dataTypeConversionMap != null) {
|
||||
dataType = dataTypeConversionMap.getOrDefault(foundDataType, foundDataType);
|
||||
}
|
||||
schema.put(columnName, dataType);
|
||||
|
|
@ -810,6 +837,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
|
||||
/**
|
||||
* Overloaded Method to handle plugin-based DataType conversion.
|
||||
*
|
||||
* @param preparedStatement
|
||||
* @param index
|
||||
* @param value
|
||||
|
|
@ -825,7 +853,7 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
dataType = dataTypeConversionMap.getOrDefault(topRowDataType, topRowDataType);
|
||||
}
|
||||
|
||||
String strNumericValue = value.trim().replaceAll(",","");
|
||||
String strNumericValue = value.trim().replaceAll(",", "");
|
||||
|
||||
// Override datatype to null for empty values
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
|
|
@ -985,6 +1013,14 @@ public class FilterDataServiceCE implements IFilterDataServiceCE {
|
|||
value = valueBuilder.toString();
|
||||
sb.append(value);
|
||||
|
||||
} else if (operator == ConditionalOperator.CONTAINS) {
|
||||
final String escapedLikeValue = value
|
||||
.replace("!", "!!")
|
||||
.replace("%", "!%")
|
||||
.replace("_", "!_")
|
||||
.replace("[", "![");
|
||||
sb.append("? ESCAPE '!'");
|
||||
values.add(new PreparedStatementValueDTO("%" + escapedLikeValue + "%", schema.get(path)));
|
||||
} else {
|
||||
// Not an array. Simply add a placeholder
|
||||
sb.append("?");
|
||||
|
|
|
|||
|
|
@ -1,8 +1,19 @@
|
|||
package com.appsmith.external.helpers;
|
||||
|
||||
import com.appsmith.external.constants.DataType;
|
||||
import com.appsmith.external.constants.DisplayDataType;
|
||||
import com.appsmith.external.models.ParsedDataType;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.appsmith.external.helpers.DataTypeStringUtils.getDisplayDataTypes;
|
||||
import static com.appsmith.external.helpers.DataTypeStringUtils.stringToKnownDataTypeConverter;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
|
@ -176,4 +187,49 @@ public class DataTypeStringUtilsTest {
|
|||
assertThat(DataType.JSON_OBJECT).isEqualByComparingTo(stringToKnownDataTypeConverter("{\"a\": \"\"}"));
|
||||
assertThat(DataType.JSON_OBJECT).isEqualByComparingTo(stringToKnownDataTypeConverter("{\"a\": []}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDisplayDataTypes_withNestedObjectsInList_returnsWithTable() {
|
||||
|
||||
final List<Object> data = new ArrayList<>();
|
||||
final Map<String, Object> objectMap = new HashMap<>();
|
||||
final Map<String, Object> nestedObjectMap = new HashMap<>();
|
||||
nestedObjectMap.put("k2", "v2");
|
||||
objectMap.put("k", nestedObjectMap);
|
||||
|
||||
data.add(objectMap);
|
||||
final List<ParsedDataType> displayDataTypes = getDisplayDataTypes(data);
|
||||
|
||||
assertThat(displayDataTypes).anyMatch(parsedDataType -> parsedDataType.getDataType().equals(DisplayDataType.TABLE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDisplayDataTypes_withNestedObjectsInArrayNode_returnsWithTable() {
|
||||
final ObjectMapper objectMapper = new ObjectMapper();
|
||||
final ArrayNode data = objectMapper.createArrayNode();
|
||||
final ObjectNode objectNode = objectMapper.createObjectNode();
|
||||
final ObjectNode nestedObjectNode = objectMapper.createObjectNode();
|
||||
nestedObjectNode.put("k2", "v2");
|
||||
objectNode.set("k", nestedObjectNode);
|
||||
|
||||
data.add(objectNode);
|
||||
final List<ParsedDataType> displayDataTypes = getDisplayDataTypes(data);
|
||||
|
||||
assertThat(displayDataTypes).anyMatch(parsedDataType -> parsedDataType.getDataType().equals(DisplayDataType.TABLE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDisplayDataTypes_withNestedObjectsInString_returnsWithTable() {
|
||||
final ObjectMapper objectMapper = new ObjectMapper();
|
||||
final ArrayNode data = objectMapper.createArrayNode();
|
||||
final ObjectNode objectNode = objectMapper.createObjectNode();
|
||||
final ObjectNode nestedObjectNode = objectMapper.createObjectNode();
|
||||
nestedObjectNode.put("k2", "v2");
|
||||
objectNode.set("k", nestedObjectNode);
|
||||
|
||||
data.add(objectNode);
|
||||
final List<ParsedDataType> displayDataTypes = getDisplayDataTypes(data.toString());
|
||||
|
||||
assertThat(displayDataTypes).anyMatch(parsedDataType -> parsedDataType.getDataType().equals(DisplayDataType.TABLE));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,10 +139,15 @@ public class FilterDataServiceTest {
|
|||
Condition condition1 = new Condition("anotherKey", "GT", "15");
|
||||
conditionList.add(condition1);
|
||||
|
||||
|
||||
Condition condition2 = new Condition("orderStatus", "EQ", "READY");
|
||||
conditionList.add(condition2);
|
||||
|
||||
Condition condition3 = new Condition("productName", "CONTAINS", "Chicken");
|
||||
conditionList.add(condition3);
|
||||
|
||||
Condition condition4 = new Condition("productName", "NOT_EQ", "Chicken Sub");
|
||||
conditionList.add(condition4);
|
||||
|
||||
ArrayNode filteredData = filterDataService.filterData(items, conditionList);
|
||||
|
||||
assertEquals(filteredData.size(), 1);
|
||||
|
|
|
|||
|
|
@ -402,6 +402,10 @@
|
|||
"label": "==",
|
||||
"value": "EQ"
|
||||
},
|
||||
{
|
||||
"label": "!=",
|
||||
"value": "NOT_EQ"
|
||||
},
|
||||
{
|
||||
"label": ">=",
|
||||
"value": "GTE"
|
||||
|
|
@ -410,6 +414,10 @@
|
|||
"label": ">",
|
||||
"value": "GT"
|
||||
},
|
||||
{
|
||||
"label": "contains",
|
||||
"value": "CONTAINS"
|
||||
},
|
||||
{
|
||||
"label": "in",
|
||||
"value": "IN"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ public class ApplicationJson {
|
|||
|
||||
List<NewPage> pageList;
|
||||
|
||||
List<String> pageOrder;
|
||||
|
||||
List<String> publishedPageOrder;
|
||||
|
||||
String publishedDefaultPageName;
|
||||
|
||||
String unpublishedDefaultPageName;
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNestedNonNullProperties;
|
||||
import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS;
|
||||
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
|
||||
import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES;
|
||||
|
|
@ -557,7 +558,9 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE {
|
|||
unpublishedCollection.setPageId(newPageId);
|
||||
actionCollection.setApplicationId(clonedPage.getApplicationId());
|
||||
|
||||
actionCollection.setDefaultResources(clonedPageDefaultResources);
|
||||
DefaultResources defaultResources = new DefaultResources();
|
||||
copyNestedNonNullProperties(clonedPageDefaultResources, defaultResources);
|
||||
actionCollection.setDefaultResources(defaultResources);
|
||||
|
||||
DefaultResources defaultResourcesForDTO = new DefaultResources();
|
||||
defaultResourcesForDTO.setPageId(clonedPageDefaultResources.getPageId());
|
||||
|
|
@ -576,14 +579,27 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE {
|
|||
if (StringUtils.isEmpty(clonedPageDefaultResources.getBranchName())) {
|
||||
unpublishedCollection
|
||||
.getDefaultToBranchedActionIdsMap()
|
||||
.forEach((defaultId, oldActionId) ->
|
||||
updatedDefaultToBranchedActionId.put(actionIdsMap.get(oldActionId), actionIdsMap.get(oldActionId)));
|
||||
|
||||
.forEach((defaultId, oldActionId) -> {
|
||||
// Filter out the actionIds for which the reference is not
|
||||
// present in cloned actions, this happens when we have
|
||||
// deleted action in unpublished mode
|
||||
if (StringUtils.hasLength(oldActionId) && StringUtils.hasLength(actionIdsMap.get(oldActionId))) {
|
||||
updatedDefaultToBranchedActionId
|
||||
.put(actionIdsMap.get(oldActionId), actionIdsMap.get(oldActionId));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
unpublishedCollection
|
||||
.getDefaultToBranchedActionIdsMap()
|
||||
.forEach((defaultId, oldActionId) ->
|
||||
updatedDefaultToBranchedActionId.put(defaultId, actionIdsMap.get(oldActionId)));
|
||||
.forEach((defaultId, oldActionId) -> {
|
||||
// Filter out the actionIds for which the reference is not
|
||||
// present in cloned actions, this happens when we have
|
||||
// deleted action in unpublished mode
|
||||
if (StringUtils.hasLength(defaultId) && StringUtils.hasLength(actionIdsMap.get(oldActionId))) {
|
||||
updatedDefaultToBranchedActionId
|
||||
.put(defaultId, actionIdsMap.get(oldActionId));
|
||||
}
|
||||
});
|
||||
}
|
||||
unpublishedCollection.setDefaultToBranchedActionIdsMap(updatedDefaultToBranchedActionId);
|
||||
|
||||
|
|
|
|||
|
|
@ -99,4 +99,5 @@ public interface NewActionServiceCE extends CrudService<NewAction, String> {
|
|||
|
||||
Mono<String> findBranchedIdByBranchNameAndDefaultActionId(String branchName, String defaultActionId, AclPermission permission);
|
||||
|
||||
public Mono<NewAction> sanitizeAction(NewAction action);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ public class NewActionServiceCEImpl extends BaseService<NewActionRepository, New
|
|||
public static final String NATIVE_QUERY_PATH = "formToNativeQuery";
|
||||
public static final String NATIVE_QUERY_PATH_DATA = NATIVE_QUERY_PATH + "." + DATA;
|
||||
public static final String NATIVE_QUERY_PATH_STATUS = NATIVE_QUERY_PATH + "." + STATUS;
|
||||
public static final PluginType JS_PLUGIN_TYPE = PluginType.JS;
|
||||
public static final String JS_PLUGIN_PACKAGE_NAME = "js-plugin";
|
||||
|
||||
private final NewActionRepository repository;
|
||||
private final DatasourceService datasourceService;
|
||||
|
|
@ -230,6 +232,8 @@ public class NewActionServiceCEImpl extends BaseService<NewActionRepository, New
|
|||
} else {
|
||||
if (newAction.getUnpublishedAction() != null) {
|
||||
action = newAction.getUnpublishedAction();
|
||||
} else {
|
||||
return Mono.error(new AppsmithException(AppsmithError.INVALID_ACTION, newAction.getId(), "No unpublished action found for edit mode"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -248,6 +252,9 @@ public class NewActionServiceCEImpl extends BaseService<NewActionRepository, New
|
|||
|
||||
@Override
|
||||
public void generateAndSetActionPolicies(NewPage page, NewAction action) {
|
||||
if (page == null) {
|
||||
throw new AppsmithException(AppsmithError.INTERNAL_SERVER_ERROR, "No page found to copy policies from.");
|
||||
}
|
||||
Set<Policy> documentPolicies = policyGenerator.getAllChildPolicies(page.getPolicies(), Page.class, Action.class);
|
||||
action.setPolicies(documentPolicies);
|
||||
}
|
||||
|
|
@ -371,10 +378,10 @@ public class NewActionServiceCEImpl extends BaseService<NewActionRepository, New
|
|||
action.setInvalids(invalids);
|
||||
action.setPluginName(plugin.getName());
|
||||
newAction.setUnpublishedAction(action);
|
||||
newAction.setPluginType(plugin.getType());
|
||||
newAction.setPluginId(plugin.getId());
|
||||
return newAction;
|
||||
}).map(this::extractAndSetJsonPathKeys)
|
||||
})
|
||||
.flatMap(this::sanitizeAction)
|
||||
.map(this::extractAndSetJsonPathKeys)
|
||||
.map(updatedAction -> {
|
||||
// In case of external datasource (not embedded) instead of storing the entire datasource
|
||||
// again inside the action, instead replace it with just the datasource ID. This is so that
|
||||
|
|
@ -1100,7 +1107,8 @@ public class NewActionServiceCEImpl extends BaseService<NewActionRepository, New
|
|||
@Override
|
||||
public Flux<NewAction> findUnpublishedOnLoadActionsExplicitSetByUserInPage(String pageId) {
|
||||
return repository
|
||||
.findUnpublishedActionsByPageIdAndExecuteOnLoadSetByUserTrue(pageId, MANAGE_ACTIONS);
|
||||
.findUnpublishedActionsByPageIdAndExecuteOnLoadSetByUserTrue(pageId, MANAGE_ACTIONS)
|
||||
.flatMap(this::sanitizeAction);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1113,27 +1121,32 @@ public class NewActionServiceCEImpl extends BaseService<NewActionRepository, New
|
|||
@Override
|
||||
public Flux<NewAction> findUnpublishedActionsInPageByNames(Set<String> names, String pageId) {
|
||||
return repository
|
||||
.findUnpublishedActionsByNameInAndPageId(names, pageId, MANAGE_ACTIONS);
|
||||
.findUnpublishedActionsByNameInAndPageId(names, pageId, MANAGE_ACTIONS)
|
||||
.flatMap(this::sanitizeAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<NewAction> findById(String id) {
|
||||
return repository.findById(id);
|
||||
return repository.findById(id)
|
||||
.flatMap(this::sanitizeAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<NewAction> findById(String id, AclPermission aclPermission) {
|
||||
return repository.findById(id, aclPermission);
|
||||
return repository.findById(id, aclPermission)
|
||||
.flatMap(this::sanitizeAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<NewAction> findByPageId(String pageId, AclPermission permission) {
|
||||
return repository.findByPageId(pageId, permission);
|
||||
return repository.findByPageId(pageId, permission)
|
||||
.flatMap(this::sanitizeAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<NewAction> findByPageIdAndViewMode(String pageId, Boolean viewMode, AclPermission permission) {
|
||||
return repository.findByPageIdAndViewMode(pageId, viewMode, permission);
|
||||
return repository.findByPageIdAndViewMode(pageId, viewMode, permission)
|
||||
.flatMap(this::sanitizeAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1151,7 +1164,8 @@ public class NewActionServiceCEImpl extends BaseService<NewActionRepository, New
|
|||
// every created action starts from an unpublishedAction state.
|
||||
|
||||
return Mono.just(action);
|
||||
});
|
||||
})
|
||||
.flatMap(this::sanitizeAction);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1320,9 +1334,11 @@ public class NewActionServiceCEImpl extends BaseService<NewActionRepository, New
|
|||
return applicationService
|
||||
.findById(params.getFirst(FieldName.APPLICATION_ID), READ_APPLICATIONS)
|
||||
.flatMapMany(application -> repository.findByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS))
|
||||
.flatMap(this::sanitizeAction)
|
||||
.flatMap(this::setTransientFieldsInUnpublishedAction);
|
||||
}
|
||||
return repository.findAllActionsByNameAndPageIdsAndViewMode(name, pageIds, false, READ_ACTIONS, sort)
|
||||
.flatMap(this::sanitizeAction)
|
||||
.flatMap(this::setTransientFieldsInUnpublishedAction);
|
||||
}
|
||||
|
||||
|
|
@ -1365,6 +1381,75 @@ public class NewActionServiceCEImpl extends BaseService<NewActionRepository, New
|
|||
.filter(actionDTO -> !PluginType.JS.equals(actionDTO.getPluginType()));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is meant to be used to check for any missing or bad values in NewAction object and attempt to fix it.
|
||||
*
|
||||
* This method is added in response to certain cases where it was found that pluginId and pluginType keys
|
||||
* were missing from the NewAction object in the database.Since it is currently not know what exactly causes
|
||||
* these values to go missing, this check will serve as a workaround by fetching and setting pluginId and
|
||||
* pluginType using the datasource object contained in the ActionDTO object.
|
||||
* Ref: https://github.com/appsmithorg/appsmith/issues/11927
|
||||
*
|
||||
*/
|
||||
public Mono<NewAction> sanitizeAction(NewAction action) {
|
||||
Mono<NewAction> actionMono = Mono.just(action);
|
||||
if (isPluginTypeOrPluginIdMissing(action)) {
|
||||
actionMono = providePluginTypeAndIdToNewActionObjectUsingJSTypeOrDatasource(action);
|
||||
}
|
||||
|
||||
return actionMono;
|
||||
}
|
||||
|
||||
private boolean isPluginTypeOrPluginIdMissing(NewAction action) {
|
||||
return action.getPluginId() == null || action.getPluginType() == null;
|
||||
}
|
||||
|
||||
private Mono<NewAction> providePluginTypeAndIdToNewActionObjectUsingJSTypeOrDatasource(NewAction action) {
|
||||
ActionDTO actionDTO = action.getUnpublishedAction();
|
||||
if (actionDTO == null) {
|
||||
return Mono.just(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* if path:
|
||||
* In case an action object is related to a JS Object then it must have a non-null collectionId.
|
||||
*
|
||||
* else path:
|
||||
* Otherwise, check if the datasource object has the pluginId. If so, use this pluginId to fetch the correct
|
||||
* pluginType.
|
||||
*/
|
||||
Datasource datasource = actionDTO.getDatasource();
|
||||
if (actionDTO.getCollectionId() != null) {
|
||||
return setPluginIdAndTypeForJSAction(action);
|
||||
}
|
||||
else if (datasource != null && datasource.getPluginId() != null) {
|
||||
String pluginId = datasource.getPluginId();
|
||||
action.setPluginId(pluginId);
|
||||
|
||||
return setPluginTypeFromId(action, pluginId);
|
||||
}
|
||||
|
||||
return Mono.just(action);
|
||||
}
|
||||
|
||||
private Mono<NewAction> setPluginTypeFromId(NewAction action, String pluginId) {
|
||||
return pluginService.findById(pluginId)
|
||||
.flatMap(plugin -> {
|
||||
action.setPluginType(plugin.getType());
|
||||
return Mono.just(action);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<NewAction> setPluginIdAndTypeForJSAction(NewAction action) {
|
||||
action.setPluginType(JS_PLUGIN_TYPE);
|
||||
|
||||
return pluginService.findByPackageName(JS_PLUGIN_PACKAGE_NAME)
|
||||
.flatMap(plugin -> {
|
||||
action.setPluginId(plugin.getId());
|
||||
return Mono.just(action);
|
||||
});
|
||||
}
|
||||
|
||||
// We can afford to make this call all the time since we already have all the info we need in context
|
||||
private Mono<DatasourceContext> getRemoteDatasourceContext(Plugin plugin, Datasource datasource) {
|
||||
final DatasourceContext datasourceContext = new DatasourceContext();
|
||||
|
|
@ -1388,7 +1473,9 @@ public class NewActionServiceCEImpl extends BaseService<NewActionRepository, New
|
|||
if (action.getGitSyncId() == null) {
|
||||
action.setGitSyncId(action.getApplicationId() + "_" + Instant.now().toString());
|
||||
}
|
||||
return repository.save(action);
|
||||
|
||||
return sanitizeAction(action)
|
||||
.flatMap(sanitizedAction -> repository.save(sanitizedAction));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1396,12 +1483,17 @@ public class NewActionServiceCEImpl extends BaseService<NewActionRepository, New
|
|||
actions.stream()
|
||||
.filter(action -> action.getGitSyncId() == null)
|
||||
.forEach(action -> action.setGitSyncId(action.getApplicationId() + "_" + Instant.now().toString()));
|
||||
return repository.saveAll(actions);
|
||||
|
||||
return Flux.fromIterable(actions)
|
||||
.flatMap(this::sanitizeAction)
|
||||
.collectList()
|
||||
.flatMapMany(actionList -> repository.saveAll(actionList));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<NewAction> findByPageId(String pageId) {
|
||||
return repository.findByPageId(pageId);
|
||||
return repository.findByPageId(pageId)
|
||||
.flatMap(this::sanitizeAction);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1642,7 +1734,8 @@ public class NewActionServiceCEImpl extends BaseService<NewActionRepository, New
|
|||
return repository.findByBranchNameAndDefaultActionId(branchName, defaultActionId, permission)
|
||||
.switchIfEmpty(Mono.error(
|
||||
new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, defaultActionId + "," + branchName))
|
||||
);
|
||||
)
|
||||
.flatMap(this::sanitizeAction);
|
||||
}
|
||||
|
||||
public Mono<String> findBranchedIdByBranchNameAndDefaultActionId(String branchName, String defaultActionId, AclPermission permission) {
|
||||
|
|
|
|||
|
|
@ -112,9 +112,15 @@ public class ApplicationFetcherCEImpl implements ApplicationFetcherCE {
|
|||
// Collect all the applications as a map with organization id as a key
|
||||
Flux<Application> applicationFlux = applicationRepository
|
||||
.findByMultipleOrganizationIds(orgIds, READ_APPLICATIONS)
|
||||
// Git connected apps will have gitApplicationMetadat
|
||||
.filter(application -> application.getGitApplicationMetadata() == null
|
||||
|| (!StringUtils.isEmpty(application.getGitApplicationMetadata().getDefaultBranchName())
|
||||
&& application.getGitApplicationMetadata().getBranchName().equals(application.getGitApplicationMetadata().getDefaultBranchName()))
|
||||
// 1. When the ssh key is generated by user and then the connect app fails
|
||||
|| (StringUtils.isEmpty(application.getGitApplicationMetadata().getDefaultBranchName())
|
||||
&& StringUtils.isEmpty(application.getGitApplicationMetadata().getBranchName()))
|
||||
// 2. When the DefaultBranchName is missing due to branch creation flow failures or corrupted scenarios
|
||||
|| (!StringUtils.isEmpty(application.getGitApplicationMetadata().getBranchName())
|
||||
&& application.getGitApplicationMetadata().getBranchName().equals(application.getGitApplicationMetadata().getDefaultBranchName())
|
||||
)
|
||||
)
|
||||
.map(responseUtils::updateApplicationWithDefaultResources);
|
||||
|
||||
|
|
|
|||
|
|
@ -314,11 +314,13 @@ public class ExamplesOrganizationClonerCEImpl implements ExamplesOrganizationClo
|
|||
unpublishedCollection
|
||||
.getDefaultToBranchedActionIdsMap()
|
||||
.forEach((defaultActionId, oldActionId) -> {
|
||||
if (!StringUtils.isEmpty(actionIdsMap.get(oldActionId))) {
|
||||
if (StringUtils.hasLength(oldActionId)
|
||||
&& StringUtils.hasLength(actionIdsMap.get(oldActionId))) {
|
||||
|
||||
// As this is a new application and not connected
|
||||
// through git branch, the default and newly
|
||||
// created actionId will be same
|
||||
newActionIds
|
||||
// As this is a new application and not connected
|
||||
// through git branch, the default and newly
|
||||
// created actionId will be same
|
||||
.put(actionIdsMap.get(oldActionId), actionIdsMap.get(oldActionId));
|
||||
} else {
|
||||
log.debug("Unable to find action {} while forking inside ID map: {}", oldActionId, actionIdsMap);
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ import java.util.HashMap;
|
|||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -233,6 +234,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
// Refactor application to remove the ids
|
||||
final String organizationId = application.getOrganizationId();
|
||||
List<String> pageOrderList = application.getPages().stream().map(applicationPage -> applicationPage.getId()).collect(Collectors.toList());
|
||||
List<String> publishedPageOrderList = application.getPublishedPages().stream().map(applicationPage -> applicationPage.getId()).collect(Collectors.toList());
|
||||
removeUnwantedFieldsFromApplicationDuringExport(application);
|
||||
examplesOrganizationCloner.makePristine(application);
|
||||
applicationJson.setExportedApplication(application);
|
||||
|
|
@ -244,9 +246,24 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
|
||||
return pageFlux
|
||||
.collectList()
|
||||
// Maintain the page order while exporting the application
|
||||
// Save the page order to json while exporting the application
|
||||
.flatMap(newPages -> {
|
||||
Collections.sort(newPages, Comparator.comparing(newPage -> pageOrderList.indexOf(newPage.getId())));
|
||||
List<String> pageOrder = new ArrayList<>();
|
||||
for (NewPage page: newPages) {
|
||||
pageOrder.add(page.getUnpublishedPage().getName());
|
||||
}
|
||||
applicationJson.setPageOrder(pageOrder);
|
||||
|
||||
List<NewPage> newPageList = newPages;
|
||||
// When there is difference in number of pages between edit and view mode
|
||||
newPageList = newPageList.stream().filter(newPage -> !Optional.ofNullable(newPage.getPublishedPage()).isEmpty()).collect(Collectors.toList());
|
||||
Collections.sort(newPageList, Comparator.comparing(newPage -> publishedPageOrderList.indexOf(newPage.getId())));
|
||||
pageOrder = new ArrayList<>();
|
||||
for (NewPage page: newPageList) {
|
||||
pageOrder.add(page.getPublishedPage().getName());
|
||||
}
|
||||
applicationJson.setPublishedPageOrder(pageOrder);
|
||||
return Mono.just(newPages);
|
||||
})
|
||||
.flatMap(newPageList -> {
|
||||
|
|
@ -306,7 +323,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
|
||||
Flux<Datasource> datasourceFlux = Boolean.TRUE.equals(application.getExportWithConfiguration())
|
||||
? datasourceRepository.findAllByOrganizationId(organizationId, AclPermission.READ_DATASOURCES)
|
||||
: datasourceRepository.findAllByOrganizationId(organizationId, AclPermission.MANAGE_DATASOURCES);
|
||||
: datasourceRepository.findAllByOrganizationId(organizationId, MANAGE_DATASOURCES);
|
||||
|
||||
return datasourceFlux.collectList();
|
||||
})
|
||||
|
|
@ -854,15 +871,17 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
existingPagesMono
|
||||
).collectList()
|
||||
.map(newPageList -> {
|
||||
// Check if the pages order match with json file
|
||||
List<String> pageOrderList = importedNewPageList.stream().map(newPage -> newPage.getUnpublishedPage().getName()).collect(Collectors.toList());
|
||||
// Sort the unPublished pages based on the json file order only
|
||||
List<String> pageOrderList;
|
||||
if(Optional.ofNullable(applicationJson.getPageOrder()).isEmpty()) {
|
||||
pageOrderList = importedNewPageList.stream().map(newPage -> newPage.getUnpublishedPage().getName()).collect(Collectors.toList());
|
||||
} else {
|
||||
pageOrderList = applicationJson.getPageOrder();
|
||||
}
|
||||
Collections.sort(newPageList,
|
||||
Comparator.comparing(newPage -> pageOrderList.indexOf(newPage.getUnpublishedPage().getName())));
|
||||
for (NewPage newPage : newPageList) {
|
||||
|
||||
ApplicationPage unpublishedAppPage = new ApplicationPage();
|
||||
ApplicationPage publishedAppPage = new ApplicationPage();
|
||||
|
||||
if (newPage.getUnpublishedPage() != null && newPage.getUnpublishedPage().getName() != null) {
|
||||
unpublishedAppPage.setIsDefault(
|
||||
StringUtils.equals(
|
||||
|
|
@ -875,6 +894,27 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
}
|
||||
pageNameMap.put(newPage.getUnpublishedPage().getName(), newPage);
|
||||
}
|
||||
if (unpublishedAppPage.getId() != null && newPage.getUnpublishedPage().getDeletedAt() == null) {
|
||||
applicationPages.get(PublishType.UNPUBLISHED).add(unpublishedAppPage);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the published pages based on the json file order only
|
||||
List<String> publishedPageOrderList;
|
||||
if(Optional.ofNullable(applicationJson.getPublishedPageOrder()).isEmpty()) {
|
||||
publishedPageOrderList = importedNewPageList.stream()
|
||||
.filter(newPage -> !Optional.ofNullable(newPage.getPublishedPage()).isEmpty())
|
||||
.map(newPage -> newPage.getPublishedPage().getName()).collect(Collectors.toList());
|
||||
} else {
|
||||
publishedPageOrderList = applicationJson.getPublishedPageOrder();
|
||||
}
|
||||
|
||||
// When there is difference in number of pages between edit and view mode
|
||||
newPageList = newPageList.stream().filter(newPage -> !Optional.ofNullable(newPage.getPublishedPage()).isEmpty()).collect(Collectors.toList());
|
||||
Collections.sort(newPageList,
|
||||
Comparator.comparing(newPage -> publishedPageOrderList.indexOf(newPage.getPublishedPage().getName())));
|
||||
for (NewPage newPage : newPageList) {
|
||||
ApplicationPage publishedAppPage = new ApplicationPage();
|
||||
|
||||
if (newPage.getPublishedPage() != null && newPage.getPublishedPage().getName() != null) {
|
||||
publishedAppPage.setIsDefault(
|
||||
|
|
@ -888,9 +928,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
}
|
||||
pageNameMap.put(newPage.getPublishedPage().getName(), newPage);
|
||||
}
|
||||
if (unpublishedAppPage.getId() != null && newPage.getUnpublishedPage().getDeletedAt() == null) {
|
||||
applicationPages.get(PublishType.UNPUBLISHED).add(unpublishedAppPage);
|
||||
}
|
||||
|
||||
if (publishedAppPage.getId() != null && newPage.getPublishedPage().getDeletedAt() == null) {
|
||||
applicationPages.get(PublishType.PUBLISHED).add(publishedAppPage);
|
||||
}
|
||||
|
|
@ -929,6 +967,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
});
|
||||
})
|
||||
.flatMap(applicationPageMap -> {
|
||||
// set it based on the order for published and unublished pages
|
||||
importedApplication.setPages(applicationPageMap.get(PublishType.UNPUBLISHED));
|
||||
importedApplication.setPublishedPages(applicationPageMap.get(PublishType.PUBLISHED));
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"displayName": "Classic",
|
||||
"config": {
|
||||
"colors": {
|
||||
"primaryColor": "#50AF6C",
|
||||
"primaryColor": "#16a34a",
|
||||
"backgroundColor": "#F6F6F6"
|
||||
},
|
||||
"borderRadius": {
|
||||
|
|
@ -50,7 +50,6 @@
|
|||
},
|
||||
"BUTTON_GROUP_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"boxShadow": "none",
|
||||
"childStylesheet": {
|
||||
"button": {
|
||||
|
|
@ -100,7 +99,6 @@
|
|||
"FILE_PICKER_WIDGET_V2": {
|
||||
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"FORM_WIDGET": {
|
||||
|
|
@ -119,24 +117,22 @@
|
|||
},
|
||||
"IFRAME_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
|
||||
},
|
||||
"IMAGE_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"INPUT_WIDGET": {
|
||||
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"INPUT_WIDGET_V2": {
|
||||
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"JSON_FORM_WIDGET": {
|
||||
|
|
@ -338,6 +334,7 @@
|
|||
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
|
||||
},
|
||||
"TEXT_WIDGET": {
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
|
||||
},
|
||||
"VIDEO_WIDGET": {
|
||||
|
|
@ -352,7 +349,7 @@
|
|||
},
|
||||
"properties": {
|
||||
"colors": {
|
||||
"primaryColor": "#50AF6C",
|
||||
"primaryColor": "#16a34a",
|
||||
"backgroundColor": "#F6F6F6"
|
||||
},
|
||||
"borderRadius": {
|
||||
|
|
@ -417,7 +414,7 @@
|
|||
},
|
||||
"BUTTON_GROUP_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none",
|
||||
"childStylesheet": {
|
||||
"button": {
|
||||
|
|
@ -467,7 +464,7 @@
|
|||
"FILE_PICKER_WIDGET_V2": {
|
||||
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"FORM_WIDGET": {
|
||||
|
|
@ -486,24 +483,24 @@
|
|||
},
|
||||
"IFRAME_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
|
||||
},
|
||||
"IMAGE_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"INPUT_WIDGET": {
|
||||
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"INPUT_WIDGET_V2": {
|
||||
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"JSON_FORM_WIDGET": {
|
||||
|
|
@ -705,6 +702,7 @@
|
|||
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
|
||||
},
|
||||
"TEXT_WIDGET": {
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
|
||||
},
|
||||
"VIDEO_WIDGET": {
|
||||
|
|
@ -784,7 +782,7 @@
|
|||
},
|
||||
"BUTTON_GROUP_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none",
|
||||
"childStylesheet": {
|
||||
"button": {
|
||||
|
|
@ -834,7 +832,7 @@
|
|||
"FILE_PICKER_WIDGET_V2": {
|
||||
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"FORM_WIDGET": {
|
||||
|
|
@ -853,24 +851,24 @@
|
|||
},
|
||||
"IFRAME_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
|
||||
},
|
||||
"IMAGE_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"INPUT_WIDGET": {
|
||||
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"INPUT_WIDGET_V2": {
|
||||
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"JSON_FORM_WIDGET": {
|
||||
|
|
@ -1072,7 +1070,8 @@
|
|||
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
|
||||
},
|
||||
"TEXT_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}"
|
||||
},
|
||||
"VIDEO_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
|
|
@ -1151,7 +1150,7 @@
|
|||
},
|
||||
"BUTTON_GROUP_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
|
||||
"boxShadow": "none",
|
||||
"childStylesheet": {
|
||||
"button": {
|
||||
|
|
@ -1201,7 +1200,6 @@
|
|||
"FILE_PICKER_WIDGET_V2": {
|
||||
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"FORM_WIDGET": {
|
||||
|
|
@ -1220,24 +1218,20 @@
|
|||
},
|
||||
"IFRAME_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
|
||||
},
|
||||
"IMAGE_WIDGET": {
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"INPUT_WIDGET": {
|
||||
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"INPUT_WIDGET_V2": {
|
||||
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"boxShadow": "none"
|
||||
},
|
||||
"JSON_FORM_WIDGET": {
|
||||
|
|
@ -1439,6 +1433,7 @@
|
|||
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
|
||||
},
|
||||
"TEXT_WIDGET": {
|
||||
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
|
||||
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
|
||||
},
|
||||
"VIDEO_WIDGET": {
|
||||
|
|
|
|||
|
|
@ -1276,7 +1276,7 @@ public class ApplicationServiceTest {
|
|||
temp = new JSONArray();
|
||||
temp.addAll(List.of(new JSONObject(Map.of("key", "testField1"))));
|
||||
secondWidget.put("dynamicBindingPathList", temp);
|
||||
secondWidget.put("testField1", "{{ testCollection1.cloneActionCollection1.data }}");
|
||||
secondWidget.put("testField1", "{{ testCollection1.getData.data }}");
|
||||
children.add(secondWidget);
|
||||
|
||||
Layout layout = testPage.getLayouts().get(0);
|
||||
|
|
@ -1295,16 +1295,29 @@ public class ApplicationServiceTest {
|
|||
"\t\tconst data = await cloneActionTest.run();\n" +
|
||||
"\t\treturn data;\n" +
|
||||
"\t}\n" +
|
||||
"\tanotherMethod: async () => {\n" +
|
||||
"\t\tconst data = await cloneActionTest.run();\n" +
|
||||
"\t\treturn data;\n" +
|
||||
"\t}\n" +
|
||||
"}");
|
||||
ActionDTO action1 = new ActionDTO();
|
||||
action1.setName("cloneActionCollection1");
|
||||
action1.setName("getData");
|
||||
action1.setActionConfiguration(new ActionConfiguration());
|
||||
action1.getActionConfiguration().setBody(
|
||||
"async () => {\n" +
|
||||
"\t\tconst data = await cloneActionTest.run();\n" +
|
||||
"\t\treturn data;\n" +
|
||||
"\t}");
|
||||
actionCollectionDTO.setActions(List.of(action1));
|
||||
|
||||
ActionDTO action2 = new ActionDTO();
|
||||
action2.setName("anotherMethod");
|
||||
action2.setActionConfiguration(new ActionConfiguration());
|
||||
action2.getActionConfiguration().setBody(
|
||||
"async () => {\n" +
|
||||
"\t\tconst data = await cloneActionTest.run();\n" +
|
||||
"\t\treturn data;\n" +
|
||||
"\t}");
|
||||
actionCollectionDTO.setActions(List.of(action1, action2));
|
||||
actionCollectionDTO.setPluginType(PluginType.JS);
|
||||
|
||||
return Mono.zip(
|
||||
|
|
@ -1399,7 +1412,7 @@ public class ApplicationServiceTest {
|
|||
});
|
||||
});
|
||||
|
||||
assertThat(actionList).hasSize(2);
|
||||
assertThat(actionList).hasSize(3);
|
||||
actionList.forEach(newAction -> {
|
||||
assertThat(newAction.getDefaultResources()).isNotNull();
|
||||
assertThat(newAction.getDefaultResources().getActionId()).isEqualTo(newAction.getId());
|
||||
|
|
@ -1422,7 +1435,7 @@ public class ApplicationServiceTest {
|
|||
ActionCollectionDTO unpublishedCollection = actionCollection.getUnpublishedCollection();
|
||||
|
||||
assertThat(unpublishedCollection.getDefaultToBranchedActionIdsMap())
|
||||
.hasSize(1);
|
||||
.hasSize(2);
|
||||
unpublishedCollection.getDefaultToBranchedActionIdsMap().keySet()
|
||||
.forEach(key ->
|
||||
assertThat(key).isEqualTo(unpublishedCollection.getDefaultToBranchedActionIdsMap().get(key))
|
||||
|
|
@ -1512,6 +1525,241 @@ public class ApplicationServiceTest {
|
|||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUserDetails(value = "api_user")
|
||||
public void cloneApplication_withDeletedActionInActionCollection_deletedActionIsNotCloned() {
|
||||
Application testApplication = new Application();
|
||||
testApplication.setName("ApplicationServiceTest-clone-application-deleted-action-within-collection");
|
||||
|
||||
Mono<Application> originalApplicationMono = applicationPageService.createApplication(testApplication, orgId)
|
||||
.cache();
|
||||
|
||||
Map<String, List<String>> originalResourceIds = new HashMap<>();
|
||||
Mono<Application> resultMono = originalApplicationMono
|
||||
.zipWhen(application -> newPageService.findPageById(application.getPages().get(0).getId(), READ_PAGES, false))
|
||||
.flatMap(tuple -> {
|
||||
Application application = tuple.getT1();
|
||||
PageDTO testPage = tuple.getT2();
|
||||
|
||||
ActionDTO action = new ActionDTO();
|
||||
action.setName("cloneActionTest");
|
||||
action.setPageId(application.getPages().get(0).getId());
|
||||
action.setExecuteOnLoad(true);
|
||||
ActionConfiguration actionConfiguration = new ActionConfiguration();
|
||||
actionConfiguration.setHttpMethod(HttpMethod.GET);
|
||||
action.setActionConfiguration(actionConfiguration);
|
||||
action.setDatasource(testDatasource);
|
||||
|
||||
|
||||
// Save actionCollection
|
||||
ActionCollectionDTO actionCollectionDTO = new ActionCollectionDTO();
|
||||
actionCollectionDTO.setName("testCollection1");
|
||||
actionCollectionDTO.setPageId(application.getPages().get(0).getId());
|
||||
actionCollectionDTO.setApplicationId(application.getId());
|
||||
actionCollectionDTO.setOrganizationId(application.getOrganizationId());
|
||||
actionCollectionDTO.setPluginId(testPlugin.getId());
|
||||
actionCollectionDTO.setVariables(List.of(new JSValue("test", "String", "test", true)));
|
||||
actionCollectionDTO.setBody("export default {\n" +
|
||||
"\tgetData: async () => {\n" +
|
||||
"\t\tconst data = await cloneActionTest.run();\n" +
|
||||
"\t\treturn data;\n" +
|
||||
"\t},\n" +
|
||||
"\tanotherMethod: async () => {\n" +
|
||||
"\t\tconst data = await cloneActionTest.run();\n" +
|
||||
"\t\treturn data;\n" +
|
||||
"\t}\n" +
|
||||
"}");
|
||||
ActionDTO action1 = new ActionDTO();
|
||||
action1.setName("getData");
|
||||
action1.setActionConfiguration(new ActionConfiguration());
|
||||
action1.getActionConfiguration().setBody(
|
||||
"async () => {\n" +
|
||||
"\t\tconst data = await cloneActionTest.run();\n" +
|
||||
"\t\treturn data;\n" +
|
||||
"\t}");
|
||||
ActionDTO action2 = new ActionDTO();
|
||||
action2.setName("anotherMethod");
|
||||
action2.setActionConfiguration(new ActionConfiguration());
|
||||
action2.getActionConfiguration().setBody(
|
||||
"async () => {\n" +
|
||||
"\t\tconst data = await cloneActionTest.run();\n" +
|
||||
"\t\treturn data;\n" +
|
||||
"\t}");
|
||||
actionCollectionDTO.setActions(List.of(action1, action2));
|
||||
actionCollectionDTO.setPluginType(PluginType.JS);
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
JSONObject parentDsl = null;
|
||||
try {
|
||||
parentDsl = new JSONObject(objectMapper.readValue(DEFAULT_PAGE_LAYOUT, new TypeReference<HashMap<String, Object>>() {
|
||||
}));
|
||||
} catch (JsonProcessingException e) {
|
||||
log.debug("Error while creating JSONObj from defaultPageLayout: ", e);
|
||||
}
|
||||
|
||||
ArrayList children = (ArrayList) parentDsl.get("children");
|
||||
JSONObject firstWidget = new JSONObject();
|
||||
firstWidget.put("widgetName", "firstWidget");
|
||||
JSONArray temp = new JSONArray();
|
||||
temp.addAll(List.of(new JSONObject(Map.of("key", "testField"))));
|
||||
firstWidget.put("dynamicBindingPathList", temp);
|
||||
firstWidget.put("testField", "{{ cloneActionTest.data }}");
|
||||
children.add(firstWidget);
|
||||
|
||||
JSONObject secondWidget = new JSONObject();
|
||||
secondWidget.put("widgetName", "secondWidget");
|
||||
temp = new JSONArray();
|
||||
temp.addAll(List.of(new JSONObject(Map.of("key", "testField1"))));
|
||||
secondWidget.put("dynamicBindingPathList", temp);
|
||||
secondWidget.put("testField1", "{{ testCollection1.getData.data }}");
|
||||
children.add(secondWidget);
|
||||
|
||||
JSONObject thirdWidget = new JSONObject();
|
||||
thirdWidget.put("widgetName", "thirdWidget");
|
||||
temp = new JSONArray();
|
||||
temp.addAll(List.of(new JSONObject(Map.of("key", "testField1"))));
|
||||
thirdWidget.put("dynamicBindingPathList", temp);
|
||||
thirdWidget.put("testField1", "{{ testCollection1.anotherMethod.data }}");
|
||||
children.add(thirdWidget);
|
||||
|
||||
Layout layout = testPage.getLayouts().get(0);
|
||||
layout.setDsl(parentDsl);
|
||||
|
||||
return Mono.zip(
|
||||
layoutCollectionService.createCollection(actionCollectionDTO),
|
||||
layoutActionService.createSingleAction(action),
|
||||
layoutActionService.updateLayout(testPage.getId(), layout.getId(), layout),
|
||||
Mono.just(application)
|
||||
);
|
||||
})
|
||||
.flatMap(tuple -> {
|
||||
List<String> pageIds = new ArrayList<>(), collectionIds = new ArrayList<>();
|
||||
ActionCollectionDTO collectionDTO = tuple.getT1();
|
||||
collectionIds.add(collectionDTO.getId());
|
||||
tuple.getT4().getPages().forEach(page -> pageIds.add(page.getId()));
|
||||
|
||||
originalResourceIds.put("pageIds", pageIds);
|
||||
originalResourceIds.put("collectionIds", collectionIds);
|
||||
|
||||
String deletedActionIdWithinActionCollection = String
|
||||
.valueOf(collectionDTO.getDefaultToBranchedActionIdsMap().values().stream().findAny().orElse(null));
|
||||
|
||||
return newActionService.deleteUnpublishedAction(deletedActionIdWithinActionCollection)
|
||||
.thenMany(newActionService.findAllByApplicationIdAndViewMode(tuple.getT4().getId(), false, READ_ACTIONS, null))
|
||||
.collectList()
|
||||
.flatMap(actionList -> {
|
||||
List<String> actionIds = actionList.stream().map(BaseDomain::getId).collect(Collectors.toList());
|
||||
originalResourceIds.put("actionIds", actionIds);
|
||||
return applicationPageService.cloneApplication(tuple.getT4().getId(), null);
|
||||
});
|
||||
})
|
||||
.cache();
|
||||
|
||||
Policy manageAppPolicy = Policy.builder().permission(MANAGE_APPLICATIONS.getValue())
|
||||
.users(Set.of("api_user"))
|
||||
.build();
|
||||
Policy readAppPolicy = Policy.builder().permission(READ_APPLICATIONS.getValue())
|
||||
.users(Set.of("api_user"))
|
||||
.build();
|
||||
|
||||
Policy managePagePolicy = Policy.builder().permission(MANAGE_PAGES.getValue())
|
||||
.users(Set.of("api_user"))
|
||||
.build();
|
||||
Policy readPagePolicy = Policy.builder().permission(READ_PAGES.getValue())
|
||||
.users(Set.of("api_user"))
|
||||
.build();
|
||||
|
||||
StepVerifier.create(resultMono
|
||||
.zipWhen(application -> Mono.zip(
|
||||
newActionService.findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null).collectList(),
|
||||
actionCollectionService.findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null).collectList(),
|
||||
newPageService.findNewPagesByApplicationId(application.getId(), READ_PAGES).collectList()
|
||||
)))
|
||||
.assertNext(tuple -> {
|
||||
Application application = tuple.getT1(); // cloned application
|
||||
List<NewAction> actionList = tuple.getT2().getT1();
|
||||
List<ActionCollection> actionCollectionList = tuple.getT2().getT2();
|
||||
List<NewPage> pageList = tuple.getT2().getT3();
|
||||
|
||||
assertThat(application).isNotNull();
|
||||
assertThat(application.isAppIsExample()).isFalse();
|
||||
assertThat(application.getId()).isNotNull();
|
||||
assertThat(application.getName().equals("ApplicationServiceTest Clone Source TestApp Copy"));
|
||||
assertThat(application.getPolicies()).containsAll(Set.of(manageAppPolicy, readAppPolicy));
|
||||
assertThat(application.getOrganizationId().equals(orgId));
|
||||
assertThat(application.getModifiedBy()).isEqualTo("api_user");
|
||||
assertThat(application.getUpdatedAt()).isNotNull();
|
||||
List<ApplicationPage> pages = application.getPages();
|
||||
Set<String> pageIdsFromApplication = pages.stream().map(page -> page.getId()).collect(Collectors.toSet());
|
||||
Set<String> pageIdsFromDb = pageList.stream().map(page -> page.getId()).collect(Collectors.toSet());
|
||||
|
||||
assertThat(pageIdsFromApplication.containsAll(pageIdsFromDb));
|
||||
|
||||
assertThat(pageList).isNotEmpty();
|
||||
for (NewPage page : pageList) {
|
||||
assertThat(page.getPolicies()).containsAll(Set.of(managePagePolicy, readPagePolicy));
|
||||
assertThat(page.getApplicationId()).isEqualTo(application.getId());
|
||||
}
|
||||
|
||||
assertThat(pageList).isNotEmpty();
|
||||
pageList.forEach(newPage -> {
|
||||
assertThat(newPage.getDefaultResources()).isNotNull();
|
||||
assertThat(newPage.getDefaultResources().getPageId()).isEqualTo(newPage.getId());
|
||||
assertThat(newPage.getDefaultResources().getApplicationId()).isEqualTo(application.getId());
|
||||
|
||||
newPage.getUnpublishedPage()
|
||||
.getLayouts()
|
||||
.forEach(layout -> {
|
||||
assertThat(layout.getLayoutOnLoadActions()).hasSize(1);
|
||||
layout.getLayoutOnLoadActions().forEach(dslActionDTOS -> {
|
||||
assertThat(dslActionDTOS).hasSize(2);
|
||||
dslActionDTOS.forEach(actionDTO -> {
|
||||
assertThat(actionDTO.getId()).isEqualTo(actionDTO.getDefaultActionId());
|
||||
if (StringUtils.hasLength(actionDTO.getCollectionId())) {
|
||||
assertThat(actionDTO.getDefaultCollectionId()).isEqualTo(actionDTO.getCollectionId());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
assertThat(actionList).hasSize(2);
|
||||
actionList.forEach(newAction -> {
|
||||
assertThat(newAction.getDefaultResources()).isNotNull();
|
||||
assertThat(newAction.getDefaultResources().getActionId()).isEqualTo(newAction.getId());
|
||||
assertThat(newAction.getDefaultResources().getApplicationId()).isEqualTo(application.getId());
|
||||
|
||||
ActionDTO action = newAction.getUnpublishedAction();
|
||||
assertThat(action.getDefaultResources()).isNotNull();
|
||||
assertThat(action.getDefaultResources().getPageId()).isEqualTo(application.getPages().get(0).getId());
|
||||
if (!StringUtils.isEmpty(action.getDefaultResources().getCollectionId())) {
|
||||
assertThat(action.getDefaultResources().getCollectionId()).isEqualTo(action.getCollectionId());
|
||||
}
|
||||
});
|
||||
|
||||
assertThat(actionCollectionList).hasSize(1);
|
||||
actionCollectionList.forEach(actionCollection -> {
|
||||
assertThat(actionCollection.getDefaultResources()).isNotNull();
|
||||
assertThat(actionCollection.getDefaultResources().getCollectionId()).isEqualTo(actionCollection.getId());
|
||||
assertThat(actionCollection.getDefaultResources().getApplicationId()).isEqualTo(application.getId());
|
||||
|
||||
ActionCollectionDTO unpublishedCollection = actionCollection.getUnpublishedCollection();
|
||||
|
||||
// We should have single entry as other action is deleted from the parent application
|
||||
assertThat(unpublishedCollection.getDefaultToBranchedActionIdsMap()).hasSize(1);
|
||||
unpublishedCollection.getDefaultToBranchedActionIdsMap().keySet()
|
||||
.forEach(key ->
|
||||
assertThat(key).isEqualTo(unpublishedCollection.getDefaultToBranchedActionIdsMap().get(key))
|
||||
);
|
||||
|
||||
assertThat(unpublishedCollection.getDefaultResources()).isNotNull();
|
||||
assertThat(unpublishedCollection.getDefaultResources().getPageId())
|
||||
.isEqualTo(application.getPages().get(0).getId());
|
||||
});
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUserDetails(value = "api_user")
|
||||
public void basicPublishApplicationTest() {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user