|
|
@ -120,6 +120,7 @@
|
|||
"TableInput": [
|
||||
{
|
||||
"id": 2381224,
|
||||
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==",
|
||||
"email": "michael.lawson@reqres.in",
|
||||
"userName": "Michael Lawson",
|
||||
"productName": "Chicken Sandwich",
|
||||
|
|
@ -127,6 +128,7 @@
|
|||
},
|
||||
{
|
||||
"id": 2736212,
|
||||
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==",
|
||||
"email": "lindsay.ferguson@reqres.in",
|
||||
"userName": "Lindsay Ferguson",
|
||||
"productName": "Tuna Salad",
|
||||
|
|
@ -134,6 +136,7 @@
|
|||
},
|
||||
{
|
||||
"id": 6788734,
|
||||
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==",
|
||||
"email": "tobias.funke@reqres.in",
|
||||
"userName": "Tobias Funke",
|
||||
"productName": "Beef steak",
|
||||
|
|
@ -141,6 +144,7 @@
|
|||
},
|
||||
{
|
||||
"id": 7434532,
|
||||
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==",
|
||||
"email": "byron.fields@reqres.in",
|
||||
"userName": "Byron Fields",
|
||||
"productName": "Chicken Sandwich",
|
||||
|
|
@ -148,6 +152,7 @@
|
|||
},
|
||||
{
|
||||
"id": 7434532,
|
||||
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==",
|
||||
"email": "ryan.holmes@reqres.in",
|
||||
"userName": "Ryan Holmes",
|
||||
"productName": "Avocado Panini",
|
||||
|
|
|
|||
|
|
@ -39,18 +39,33 @@ describe("Table Widget Functionality", function() {
|
|||
cy.get(commonlocators.editPropCrossButton).click();
|
||||
cy.PublishtheApp();
|
||||
});
|
||||
|
||||
it("Table Widget Functionality To Verify The Data", function() {
|
||||
cy.isSelectRow(1);
|
||||
cy.readTabledataPublish("1", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("1", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
cy.log("the value is" + tabValue);
|
||||
});
|
||||
});
|
||||
|
||||
it("Table Widget Functionality To Show a Base64 Image", function() {
|
||||
cy.get(publish.backToEditor).click();
|
||||
cy.openPropertyPane("tablewidget");
|
||||
cy.editColumn("image");
|
||||
cy.changeColumnType("Image");
|
||||
cy.isSelectRow(1);
|
||||
|
||||
const index = 1;
|
||||
const imageVal = this.data.TableInput[index].image;
|
||||
cy.readTableLinkPublish(index, "1").then((hrefVal) => {
|
||||
expect(hrefVal).to.be.equal(imageVal);
|
||||
});
|
||||
});
|
||||
|
||||
it("Table Widget Functionality To Search The Data", function() {
|
||||
cy.isSelectRow(1);
|
||||
cy.readTabledataPublish("1", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("1", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
cy.log("the value is" + tabValue);
|
||||
|
|
@ -58,7 +73,7 @@ describe("Table Widget Functionality", function() {
|
|||
.first()
|
||||
.type(tabData);
|
||||
cy.wait(500);
|
||||
cy.readTabledataPublish("0", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("1", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
});
|
||||
|
|
@ -69,7 +84,7 @@ describe("Table Widget Functionality", function() {
|
|||
.clear()
|
||||
.type("7434532");
|
||||
cy.wait(1000);
|
||||
cy.readTabledataPublish("0", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("3", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Byron Fields");
|
||||
});
|
||||
|
|
@ -82,7 +97,7 @@ describe("Table Widget Functionality", function() {
|
|||
.clear();
|
||||
cy.wait(1000);
|
||||
cy.isSelectRow(1);
|
||||
cy.readTabledataPublish("1", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("1", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
cy.log("the value is" + tabValue);
|
||||
|
|
@ -100,14 +115,14 @@ describe("Table Widget Functionality", function() {
|
|||
cy.get(publish.canvas)
|
||||
.first()
|
||||
.click();
|
||||
cy.readTabledataPublish("0", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("0", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
});
|
||||
cy.get(publish.filterBtn).click();
|
||||
cy.get(publish.removeFilter).click();
|
||||
cy.wait(500);
|
||||
cy.readTabledataPublish("0", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("0", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Michael Lawson");
|
||||
});
|
||||
|
|
@ -119,7 +134,7 @@ describe("Table Widget Functionality", function() {
|
|||
|
||||
it("Table Widget Functionality To Filter The Data using contains", function() {
|
||||
cy.isSelectRow(1);
|
||||
cy.readTabledataPublish("1", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("1", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
cy.log("the value is" + tabValue);
|
||||
|
|
@ -137,14 +152,14 @@ describe("Table Widget Functionality", function() {
|
|||
cy.get(publish.canvas)
|
||||
.first()
|
||||
.click();
|
||||
cy.readTabledataPublish("0", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("0", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
});
|
||||
cy.get(publish.filterBtn).click();
|
||||
cy.get(publish.removeFilter).click();
|
||||
cy.wait(500);
|
||||
cy.readTabledataPublish("0", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("0", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Michael Lawson");
|
||||
});
|
||||
|
|
@ -156,7 +171,7 @@ describe("Table Widget Functionality", function() {
|
|||
|
||||
it("Table Widget Functionality To Filter The Data using starts with ", function() {
|
||||
cy.isSelectRow(1);
|
||||
cy.readTabledataPublish("1", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("1", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
cy.log("the value is" + tabValue);
|
||||
|
|
@ -174,14 +189,14 @@ describe("Table Widget Functionality", function() {
|
|||
cy.get(publish.canvas)
|
||||
.first()
|
||||
.click();
|
||||
cy.readTabledataPublish("0", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("0", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
});
|
||||
cy.get(publish.filterBtn).click();
|
||||
cy.get(publish.removeFilter).click();
|
||||
cy.wait(500);
|
||||
cy.readTabledataPublish("0", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("0", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Michael Lawson");
|
||||
});
|
||||
|
|
@ -193,7 +208,7 @@ describe("Table Widget Functionality", function() {
|
|||
|
||||
it("Table Widget Functionality To Filter The Data using ends with ", function() {
|
||||
cy.isSelectRow(1);
|
||||
cy.readTabledataPublish("1", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("1", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
cy.log("the value is" + tabValue);
|
||||
|
|
@ -211,14 +226,14 @@ describe("Table Widget Functionality", function() {
|
|||
cy.get(publish.canvas)
|
||||
.first()
|
||||
.click();
|
||||
cy.readTabledataPublish("0", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("0", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
});
|
||||
cy.get(publish.filterBtn).click();
|
||||
cy.get(publish.removeFilter).click();
|
||||
cy.wait(500);
|
||||
cy.readTabledataPublish("0", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("0", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Michael Lawson");
|
||||
});
|
||||
|
|
@ -230,7 +245,7 @@ describe("Table Widget Functionality", function() {
|
|||
|
||||
it("Table Widget Functionality To Check Compact Mode", function() {
|
||||
cy.isSelectRow(1);
|
||||
cy.readTabledataPublish("1", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("1", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
cy.log("the value is" + tabValue);
|
||||
|
|
@ -238,7 +253,7 @@ describe("Table Widget Functionality", function() {
|
|||
cy.get(publish.compactOpt)
|
||||
.contains("Tall")
|
||||
.click();
|
||||
cy.scrollTabledataPublish("3", "2").then((tabData) => {
|
||||
cy.scrollTabledataPublish("3", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Byron Fields");
|
||||
});
|
||||
|
|
@ -246,7 +261,7 @@ describe("Table Widget Functionality", function() {
|
|||
cy.get(publish.compactOpt)
|
||||
.contains("Short")
|
||||
.click();
|
||||
cy.readTabledataPublish("4", "2").then((tabData) => {
|
||||
cy.readTabledataPublish("4", "3").then((tabData) => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Ryan Holmes");
|
||||
});
|
||||
|
|
@ -261,7 +276,7 @@ describe("Table Widget Functionality", function() {
|
|||
.first()
|
||||
.click();
|
||||
cy.isSelectRow(1);
|
||||
cy.readTabledataPublish("1", "2").then(tabData => {
|
||||
cy.readTabledataPublish("1", "3").then(tabData => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
cy.log("the value is" + tabValue);
|
||||
|
|
@ -270,7 +285,7 @@ describe("Table Widget Functionality", function() {
|
|||
.contains("userName")
|
||||
.click();
|
||||
cy.get(publish.containerWidget).click();
|
||||
cy.readTabledataPublish("1", "2").then(tabData => {
|
||||
cy.readTabledataPublish("1", "3").then(tabData => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.not.equal("Lindsay Ferguson");
|
||||
});
|
||||
|
|
@ -279,7 +294,7 @@ describe("Table Widget Functionality", function() {
|
|||
.contains("userName")
|
||||
.click();
|
||||
cy.get(publish.containerWidget).click();
|
||||
cy.readTabledataPublish("1", "2").then(tabData => {
|
||||
cy.readTabledataPublish("1", "3").then(tabData => {
|
||||
const tabValue = tabData;
|
||||
expect(tabValue).to.be.equal("Lindsay Ferguson");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
const commonlocators = require("../../../../locators/commonlocators.json");
|
||||
const queryLocators = require("../../../../locators/QueryEditor.json");
|
||||
const dsl = require("../../../../fixtures/MultipleWidgetDsl.json");
|
||||
|
||||
describe("GlobalSearch", function() {
|
||||
before(() => {
|
||||
cy.addDsl(dsl);
|
||||
});
|
||||
|
||||
it("showsAndHidesUsingKeyboardShortcuts", () => {
|
||||
const isMac = Cypress.platform === "darwin";
|
||||
if (isMac) {
|
||||
cy.wait(2000);
|
||||
cy.get("body").type("{cmd}{k}");
|
||||
cy.get(commonlocators.globalSearchModal);
|
||||
cy.get("body").type("{esc}");
|
||||
cy.get(commonlocators.globalSearchModal).should("not.exist");
|
||||
} else {
|
||||
cy.wait(2000);
|
||||
cy.get("body").type("{ctrl}{k}");
|
||||
cy.get(commonlocators.globalSearchModal);
|
||||
cy.get("body").type("{esc}");
|
||||
cy.get(commonlocators.globalSearchModal).should("not.exist");
|
||||
}
|
||||
});
|
||||
|
||||
it("selectsWidget", () => {
|
||||
const table = dsl.dsl.children[2];
|
||||
cy.get(commonlocators.globalSearchTrigger).click({ force: true });
|
||||
cy.wait(1000);
|
||||
cy.get(commonlocators.globalSearchInput).type(table.widgetName);
|
||||
cy.get("body").type("{enter}");
|
||||
cy.window()
|
||||
.its("store")
|
||||
.invoke("getState")
|
||||
.then((state) => {
|
||||
const { selectedWidget } = state.ui.widgetDragResize;
|
||||
expect(selectedWidget).to.be.equal(table.widgetId);
|
||||
});
|
||||
});
|
||||
|
||||
it("navigatesToApi", () => {
|
||||
cy.NavigateToAPI_Panel();
|
||||
cy.CreateAPI("SomeApi");
|
||||
|
||||
cy.get(commonlocators.globalSearchTrigger).click({ force: true });
|
||||
cy.wait(1000);
|
||||
cy.get(commonlocators.globalSearchClearInput).click({ force: true });
|
||||
cy.get(commonlocators.globalSearchInput).type("Page1");
|
||||
cy.get("body").type("{enter}");
|
||||
|
||||
cy.get(commonlocators.globalSearchTrigger).click({ force: true });
|
||||
cy.wait(1000);
|
||||
cy.get(commonlocators.globalSearchClearInput).click({ force: true });
|
||||
cy.get(commonlocators.globalSearchInput).type("SomeApi");
|
||||
cy.get("body").type("{enter}");
|
||||
cy.window()
|
||||
.its("store")
|
||||
.invoke("getState")
|
||||
.then((state) => {
|
||||
const { actions } = state.entities;
|
||||
const expectedAction = actions.find(
|
||||
(actions) => actions.config.name === "SomeApi",
|
||||
);
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.pathname).includes(expectedAction.config.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("navigatesToDatasourceHavingAQuery", () => {
|
||||
cy.window()
|
||||
.its("store")
|
||||
.invoke("getState")
|
||||
.then((state) => {
|
||||
cy.createPostgresDatasource();
|
||||
cy.NavigateToQueryEditor();
|
||||
|
||||
const { datasources } = state.entities;
|
||||
const expectedDatasource =
|
||||
datasources.list[datasources.list.length - 1];
|
||||
|
||||
cy.contains(".t--datasource-name", expectedDatasource.name)
|
||||
.find(queryLocators.createQuery)
|
||||
.click();
|
||||
|
||||
cy.get(commonlocators.globalSearchTrigger).click({ force: true });
|
||||
cy.wait(1000);
|
||||
cy.get(commonlocators.globalSearchClearInput).click({ force: true });
|
||||
cy.get(commonlocators.globalSearchInput).type("Page1");
|
||||
cy.get("body").type("{enter}");
|
||||
|
||||
cy.get(commonlocators.globalSearchTrigger).click({ force: true });
|
||||
cy.wait(1000);
|
||||
cy.get(commonlocators.globalSearchClearInput).click({ force: true });
|
||||
cy.get(commonlocators.globalSearchInput).type(expectedDatasource.name);
|
||||
cy.get("body").type("{enter}");
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.pathname).includes(expectedDatasource.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("navigatesToPage", () => {
|
||||
cy.Createpage("NewPage");
|
||||
cy.get(commonlocators.globalSearchTrigger).click({ force: true });
|
||||
cy.wait(1000);
|
||||
cy.get(commonlocators.globalSearchClearInput).click({ force: true });
|
||||
cy.get(commonlocators.globalSearchInput).type("Page1");
|
||||
cy.get("body").type("{enter}");
|
||||
cy.window()
|
||||
.its("store")
|
||||
.invoke("getState")
|
||||
.then((state) => {
|
||||
const { pages } = state.entities.pageList;
|
||||
const expectedPage = pages.find((page) => page.pageName === "Page1");
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.pathname).includes(expectedPage.pageId);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -100,5 +100,9 @@
|
|||
"filePickerUploadButton": ".uppy-StatusBar-actionBtn--upload",
|
||||
"filePickerOnFilesSelected": ".t--property-control-onfilesselected",
|
||||
"dataType": ".t--property-control-datatype",
|
||||
"evaluateMsg": ".t--CodeEditor-evaluatedValue p"
|
||||
}
|
||||
"evaluateMsg": ".t--CodeEditor-evaluatedValue p",
|
||||
"globalSearchModal": ".t--global-search-modal",
|
||||
"globalSearchInput": ".t--global-search-input",
|
||||
"globalSearchTrigger": ".t--global-search-modal-trigger",
|
||||
"globalSearchClearInput": ".t--global-clear-input"
|
||||
}
|
||||
50
app/client/cypress/manual_TestSuite/Modal_Spec.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
const dsl = require("../../../fixtures/ModalWidgetDsl.json");
|
||||
|
||||
describe("Modal Functionality ", function() {
|
||||
|
||||
it("Collapse the tabs of Property pane", function()
|
||||
{
|
||||
// Add a modal widget from teh entity explorer
|
||||
// Click on the property Pane
|
||||
// Select Form Type as Modal Type
|
||||
// Add any widget on the Modal
|
||||
// Add a table
|
||||
// Click on the property pane
|
||||
// Add a custom column
|
||||
// Click on control pane of the column
|
||||
// Select Column type as button
|
||||
// Add action to "on click"
|
||||
// Add Modal
|
||||
// Close the modal
|
||||
// Click on the Table Action button
|
||||
// Ensure the modal pop up
|
||||
}
|
||||
)
|
||||
|
||||
it("Rename a modal", function()
|
||||
{
|
||||
// Click on the entity explore
|
||||
// Ensure modal is dispalyed to user
|
||||
// Rename the modal
|
||||
// Ensure the modal name is replaced in the table
|
||||
// Click on the action button
|
||||
// Ensure the modal pop up
|
||||
}
|
||||
)
|
||||
it("Convert Modal to ", function()
|
||||
{
|
||||
// Click on the entity explore
|
||||
// Ensure modal is dispalyed to user
|
||||
// Add a button widget
|
||||
// Add an "On click" action with modal
|
||||
// Click on the button
|
||||
// Ensure the Alert modal is dispalyed to user
|
||||
// Now click on the Modal in entity explorer
|
||||
// Convert the Modal from "Alert" to "Form"
|
||||
// Click on the button
|
||||
// Ensure a form modal is dispalyed to user
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
)
|
||||
53
app/client/cypress/manual_TestSuite/new_Table_Spec.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
const dsl = require("../../../fixtures/tableWidgetDsl.json");
|
||||
|
||||
describe("Table functionality ", function() {
|
||||
it("Adding background Colour to table", function()
|
||||
{
|
||||
// Add a table
|
||||
// Click on the property pane
|
||||
// Scroll Styles
|
||||
// Add background colour
|
||||
// Add Text Colour
|
||||
// Navigate to one of the column
|
||||
// Click on the setting/ Control pane of the column
|
||||
// Navigate to add background colour and Text colour
|
||||
// Ensure the row colour gets overlapped on table colour
|
||||
}
|
||||
)
|
||||
it("Collapse the tabs of Property pane", function()
|
||||
{
|
||||
// Add a table
|
||||
// Click on the property pane
|
||||
// Collapse the General ,Action and Tab option
|
||||
}
|
||||
)
|
||||
it("Bind the column with same name", function()
|
||||
{
|
||||
// Add a table
|
||||
// Click on the property pane
|
||||
// Click on the Add new column
|
||||
// Ensure to add two new column
|
||||
// Name two column with same name
|
||||
// Add an input widget
|
||||
// Bind the column with new column name
|
||||
// Select the row from the binded table
|
||||
}
|
||||
)
|
||||
|
||||
it("Hide and created custom column ", function()
|
||||
{
|
||||
// Add a table
|
||||
// Click on the property pane
|
||||
// Click on the Add new column
|
||||
// Click on Setting of column
|
||||
// Select Column Type "Date"
|
||||
// Now navigate to exsiting column
|
||||
// Click on the hide icon
|
||||
// and observe on edit mode the table column is dispalyed
|
||||
// Click on deploy
|
||||
// Ensure the hidden column is not displayed and custom column is disaplyed to user
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
)
|
||||
|
|
@ -2098,6 +2098,12 @@ Cypress.Commands.add("scrollTabledataPublish", (rowNum, colNum) => {
|
|||
return tabVal;
|
||||
});
|
||||
|
||||
Cypress.Commands.add("readTableLinkPublish", (rowNum, colNum) => {
|
||||
const selector = `.t--widget-tablewidget .tbody .td[data-rowindex=${rowNum}][data-colindex=${colNum}] a`;
|
||||
const hrefVal = cy.get(selector).invoke("attr", "href");
|
||||
return hrefVal;
|
||||
});
|
||||
|
||||
Cypress.Commands.add("assertEvaluatedValuePopup", (expectedType) => {
|
||||
cy.get(dynamicInputLocators.evaluatedValue)
|
||||
.should("be.visible")
|
||||
|
|
|
|||
|
|
@ -73,11 +73,13 @@
|
|||
"lodash": "^4.17.19",
|
||||
"loglevel": "^1.6.7",
|
||||
"lottie-web": "^5.7.4",
|
||||
"marked": "^2.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.27",
|
||||
"nanoid": "^2.0.4",
|
||||
"node-sass": "^4.11.0",
|
||||
"normalizr": "^3.3.0",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"popper.js": "^1.15.0",
|
||||
"prettier": "^1.18.2",
|
||||
"prismjs": "^1.23.0",
|
||||
|
|
@ -112,6 +114,7 @@
|
|||
"redux-form": "^8.2.6",
|
||||
"redux-saga": "^1.1.3",
|
||||
"reselect": "^4.0.0",
|
||||
"scroll-into-view-if-needed": "^2.2.26",
|
||||
"shallowequal": "^1.1.0",
|
||||
"smartlook-client": "^4.5.1",
|
||||
"styled-components": "^5.2.0",
|
||||
|
|
@ -172,6 +175,7 @@
|
|||
"@types/deep-diff": "^1.0.0",
|
||||
"@types/downloadjs": "^1.4.2",
|
||||
"@types/jest": "^24.0.22",
|
||||
"@types/marked": "^1.2.2",
|
||||
"@types/react-beautiful-dnd": "^11.0.4",
|
||||
"@types/react-select": "^3.0.5",
|
||||
"@types/react-tabs": "^2.3.1",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
|
||||
import { RenderMode } from "constants/WidgetConstants";
|
||||
import { BatchAction, batchAction } from "actions/batchActions";
|
||||
import { DynamicPath } from "utils/DynamicBindingUtils";
|
||||
|
||||
export const updateWidgetPropertyRequest = (
|
||||
|
|
@ -20,24 +19,6 @@ export const updateWidgetPropertyRequest = (
|
|||
};
|
||||
};
|
||||
|
||||
export const updateWidgetProperty = (
|
||||
widgetId: string,
|
||||
updates: Record<string, unknown>,
|
||||
dynamicUpdates?: {
|
||||
dynamicBindingPathList: DynamicPath[];
|
||||
dynamicTriggerPathList: DynamicPath[];
|
||||
},
|
||||
): BatchAction<UpdateWidgetPropertyPayload> => {
|
||||
return batchAction({
|
||||
type: ReduxActionTypes.UPDATE_WIDGET_PROPERTY,
|
||||
payload: {
|
||||
widgetId,
|
||||
updates,
|
||||
dynamicUpdates,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export interface BatchPropertyUpdatePayload {
|
||||
modify?: Record<string, unknown>; //Key value pairs of paths and values to update
|
||||
remove?: string[]; //Array of paths to delete
|
||||
|
|
|
|||
34
app/client/src/actions/globalSearchActions.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
import { RecentEntity } from "components/editorComponents/GlobalSearch/utils";
|
||||
|
||||
export const setGlobalSearchQuery = (query: string) => ({
|
||||
type: ReduxActionTypes.SET_GLOBAL_SEARCH_QUERY,
|
||||
payload: query,
|
||||
});
|
||||
|
||||
export const toggleShowGlobalSearchModal = () => ({
|
||||
type: ReduxActionTypes.TOGGLE_SHOW_GLOBAL_SEARCH_MODAL,
|
||||
});
|
||||
|
||||
export const updateRecentEntity = (payload: RecentEntity) => ({
|
||||
type: ReduxActionTypes.UPDATE_RECENT_ENTITY,
|
||||
payload,
|
||||
});
|
||||
|
||||
export const restoreRecentEntitiesRequest = (payload: string) => ({
|
||||
type: ReduxActionTypes.RESTORE_RECENT_ENTITIES_REQUEST,
|
||||
payload,
|
||||
});
|
||||
|
||||
export const restoreRecentEntitiesSuccess = () => ({
|
||||
type: ReduxActionTypes.RESTORE_RECENT_ENTITIES_SUCCESS,
|
||||
});
|
||||
|
||||
export const resetRecentEntities = () => ({
|
||||
type: ReduxActionTypes.RESET_RECENT_ENTITIES,
|
||||
});
|
||||
|
||||
export const setRecentEntities = (payload: Array<RecentEntity>) => ({
|
||||
type: ReduxActionTypes.SET_RECENT_ENTITIES,
|
||||
payload,
|
||||
});
|
||||
|
|
@ -14,3 +14,11 @@ export const initEditor = (
|
|||
pageId,
|
||||
},
|
||||
});
|
||||
|
||||
export const resetEditorRequest = () => ({
|
||||
type: ReduxActionTypes.RESET_EDITOR_REQUEST,
|
||||
});
|
||||
|
||||
export const resetEditorSuccess = () => ({
|
||||
type: ReduxActionTypes.RESET_EDITOR_SUCCESS,
|
||||
});
|
||||
|
|
|
|||
6
app/client/src/actions/recentEntityActions.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
|
||||
export const handlePathUpdated = (pathName: string) => ({
|
||||
type: ReduxActionTypes.HANDLE_PATH_UPDATED,
|
||||
payload: { pathName },
|
||||
});
|
||||
|
|
@ -66,6 +66,13 @@ export const focusWidget = (
|
|||
payload: { widgetId },
|
||||
});
|
||||
|
||||
export const selectWidget = (
|
||||
widgetId?: string,
|
||||
): ReduxAction<{ widgetId?: string }> => ({
|
||||
type: ReduxActionTypes.SELECT_WIDGET,
|
||||
payload: { widgetId },
|
||||
});
|
||||
|
||||
export const showModal = (id: string) => {
|
||||
return {
|
||||
type: ReduxActionTypes.SHOW_MODAL,
|
||||
|
|
|
|||
|
|
@ -65,6 +65,11 @@ export interface DuplicateApplicationRequest {
|
|||
applicationId: string;
|
||||
}
|
||||
|
||||
export interface ForkApplicationRequest {
|
||||
applicationId: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export interface GetAllApplicationResponse extends ApiResponse {
|
||||
data: Array<ApplicationResponsePayload & { pages: ApplicationPagePayload[] }>;
|
||||
}
|
||||
|
|
@ -196,6 +201,17 @@ class ApplicationApi extends Api {
|
|||
): AxiosPromise<ApiResponse> {
|
||||
return Api.post(ApplicationApi.baseURL + "clone/" + request.applicationId);
|
||||
}
|
||||
|
||||
static forkApplication(
|
||||
request: ForkApplicationRequest,
|
||||
): AxiosPromise<ApiResponse> {
|
||||
return Api.post(
|
||||
"v1/applications/" +
|
||||
request.applicationId +
|
||||
"/fork/" +
|
||||
request.organizationId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationApi;
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 870 KiB After Width: | Height: | Size: 818 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 20 KiB |
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="14" fill="none" viewBox="0 0 16 14"><path fill="#4B4848" d="M4.83105 12.4443C6.06152 12.4443 6.86816 12.9092 7.2373 13.1621C7.36719 13.2305 7.74316 13.4561 7.8252 13.4629V2.34082C7.35352 1.43848 5.93848 0.727539 4.48242 0.727539C2.65039 0.727539 1.08496 1.78027 0.743164 2.49805V12.8818C0.743164 13.3672 1.02344 13.5586 1.37207 13.5586C1.65234 13.5586 1.83008 13.4629 2.00781 13.3125C2.45898 12.9297 3.4707 12.4443 4.83105 12.4443ZM11.6943 12.4443C13.0547 12.4443 14.0596 12.9297 14.5107 13.3125C14.6885 13.4561 14.8662 13.5586 15.1465 13.5586C15.4883 13.5586 15.7754 13.3672 15.7754 12.8818V2.49805C15.4336 1.78027 13.875 0.727539 12.043 0.727539C10.5869 0.727539 9.17188 1.43848 8.7002 2.34082V13.4766C8.78223 13.4697 9.1582 13.2373 9.29492 13.1621C9.65723 12.9092 10.4639 12.4443 11.6943 12.4443Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="14" fill="none" viewBox="0 0 16 14"><path fill="#4B4848" d="M4.83105 12.4443C6.06152 12.4443 6.86816 12.9092 7.2373 13.1621C7.36719 13.2305 7.74316 13.4561 7.8252 13.4629V2.34082C7.35352 1.43848 5.93848 0.727539 4.48242 0.727539C2.65039 0.727539 1.08496 1.78027 0.743164 2.49805V12.8818C0.743164 13.3672 1.02344 13.5586 1.37207 13.5586C1.65234 13.5586 1.83008 13.4629 2.00781 13.3125C2.45898 12.9297 3.4707 12.4443 4.83105 12.4443ZM11.6943 12.4443C13.0547 12.4443 14.0596 12.9297 14.5107 13.3125C14.6885 13.4561 14.8662 13.5586 15.1465 13.5586C15.4883 13.5586 15.7754 13.3672 15.7754 12.8818V2.49805C15.4336 1.78027 13.875 0.727539 12.043 0.727539C10.5869 0.727539 9.17188 1.43848 8.7002 2.34082V13.4766C8.78223 13.4697 9.1582 13.2373 9.29492 13.1621C9.65723 12.9092 10.4639 12.4443 11.6943 12.4443Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 868 B After Width: | Height: | Size: 867 B |
|
|
@ -1,3 +1 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.8281 0.976562H1.17188C0.525703 0.976562 0 1.50227 0 2.14844V13.7891C0 14.4352 0.525703 14.9609 1.17188 14.9609H6.91406V17.8516H3.75C3.42641 17.8516 3.16406 18.1139 3.16406 18.4375C3.16406 18.7611 3.42641 19.0234 3.75 19.0234H16.25C16.5736 19.0234 16.8359 18.7611 16.8359 18.4375C16.8359 18.1139 16.5736 17.8516 16.25 17.8516H13.0859V14.9609H18.8281C19.4743 14.9609 20 14.4352 20 13.7891V2.14844C20 1.50227 19.4743 0.976562 18.8281 0.976562ZM11.9141 17.8516H8.08594V14.9609H11.9141V17.8516ZM18.8281 13.7891C18.3142 13.7891 1.58375 13.7891 1.17188 13.7891V2.14844H18.8281C18.8289 14.0419 18.832 13.7891 18.8281 13.7891Z" fill="#716E6E"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"><path fill="#716E6E" d="M18.8281 0.976562H1.17188C0.525703 0.976562 0 1.50227 0 2.14844V13.7891C0 14.4352 0.525703 14.9609 1.17188 14.9609H6.91406V17.8516H3.75C3.42641 17.8516 3.16406 18.1139 3.16406 18.4375C3.16406 18.7611 3.42641 19.0234 3.75 19.0234H16.25C16.5736 19.0234 16.8359 18.7611 16.8359 18.4375C16.8359 18.1139 16.5736 17.8516 16.25 17.8516H13.0859V14.9609H18.8281C19.4743 14.9609 20 14.4352 20 13.7891V2.14844C20 1.50227 19.4743 0.976562 18.8281 0.976562ZM11.9141 17.8516H8.08594V14.9609H11.9141V17.8516ZM18.8281 13.7891C18.3142 13.7891 1.58375 13.7891 1.17188 13.7891V2.14844H18.8281C18.8289 14.0419 18.832 13.7891 18.8281 13.7891Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 752 B After Width: | Height: | Size: 749 B |
1
app/client/src/assets/icons/ads/docs.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 14 14"><path fill="#4086F4" d="M12.332 3.69141L9.46094 2.87109L8.64062 0H2.89844C2.21886 0 1.66797 0.550894 1.66797 1.23047V12.7695C1.66797 13.4491 2.21886 14 2.89844 14H11.1016C11.7811 14 12.332 13.4491 12.332 12.7695V3.69141Z"/><path fill="#4175DF" d="M12.332 3.69141V12.7695C12.332 13.4491 11.7811 14 11.1016 14H7V0H8.64062L9.46094 2.87109L12.332 3.69141Z"/><path fill="#80AEF8" d="M12.332 3.69141H9.46094C9.00977 3.69141 8.64062 3.32227 8.64062 2.87109V0C8.74727 0 8.85391 0.0410156 8.92771 0.123074L12.209 3.40432C12.291 3.47813 12.332 3.58477 12.332 3.69141Z"/><path fill="#FFF5F5" d="M9.46094 6.58984H4.53906C4.31236 6.58984 4.12891 6.40639 4.12891 6.17969C4.12891 5.95298 4.31236 5.76953 4.53906 5.76953H9.46094C9.68764 5.76953 9.87109 5.95298 9.87109 6.17969C9.87109 6.40639 9.68764 6.58984 9.46094 6.58984Z"/><path fill="#FFF5F5" d="M9.46094 8.23047H4.53906C4.31236 8.23047 4.12891 8.04702 4.12891 7.82031C4.12891 7.59361 4.31236 7.41016 4.53906 7.41016H9.46094C9.68764 7.41016 9.87109 7.59361 9.87109 7.82031C9.87109 8.04702 9.68764 8.23047 9.46094 8.23047Z"/><path fill="#FFF5F5" d="M9.46094 9.87109H4.53906C4.31236 9.87109 4.12891 9.68764 4.12891 9.46094C4.12891 9.23423 4.31236 9.05078 4.53906 9.05078H9.46094C9.68764 9.05078 9.87109 9.23423 9.87109 9.46094C9.87109 9.68764 9.68764 9.87109 9.46094 9.87109Z"/><path fill="#FFF5F5" d="M7.82031 11.5117H4.53906C4.31236 11.5117 4.12891 11.3283 4.12891 11.1016C4.12891 10.8749 4.31236 10.6914 4.53906 10.6914H7.82031C8.04702 10.6914 8.23047 10.8749 8.23047 11.1016C8.23047 11.3283 8.04702 11.5117 7.82031 11.5117Z"/><path fill="#E3E7EA" d="M7 11.5117H7.82031C8.04702 11.5117 8.23047 11.3283 8.23047 11.1016C8.23047 10.8749 8.04702 10.6914 7.82031 10.6914H7V11.5117Z"/><path fill="#E3E7EA" d="M7 9.87109H9.46094C9.68764 9.87109 9.87109 9.68764 9.87109 9.46094C9.87109 9.23423 9.68764 9.05078 9.46094 9.05078H7V9.87109Z"/><path fill="#E3E7EA" d="M7 8.23047H9.46094C9.68764 8.23047 9.87109 8.04702 9.87109 7.82031C9.87109 7.59361 9.68764 7.41016 9.46094 7.41016H7V8.23047Z"/><path fill="#E3E7EA" d="M7 6.58984H9.46094C9.68764 6.58984 9.87109 6.40639 9.87109 6.17969C9.87109 5.95298 9.68764 5.76953 9.46094 5.76953H7V6.58984Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
1
app/client/src/assets/icons/ads/entities.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12"><path fill="#5BB749" fill-rule="evenodd" d="M6 6V0H12V6H6ZM0 6V1H5V6H0ZM1 11V7H5V11H1ZM6 7V12H11V7H6Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 225 B |
|
|
@ -1,8 +1 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.8926 9.80273H18.3471" stroke="#716E6E" stroke-width="1.2"/>
|
||||
<path d="M15.438 6.36523L18.876 9.80325L15.438 13.2413" stroke="#716E6E" stroke-width="1.2"/>
|
||||
<path d="M7.80176 9.80273H2.34721" stroke="#716E6E" stroke-width="1.2"/>
|
||||
<path d="M5.25635 6.36523L1.81833 9.80325L5.25635 13.2413" stroke="#716E6E" stroke-width="1.2"/>
|
||||
<path d="M9.2561 2.72852V17.274" stroke="#716E6E" stroke-width="1.2"/>
|
||||
<path d="M11.438 2.72852V17.274" stroke="#716E6E" stroke-width="1.2"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"><path stroke="#716E6E" stroke-width="1.2" d="M12.8926 9.80273H18.3471"/><path stroke="#716E6E" stroke-width="1.2" d="M15.438 6.36523L18.876 9.80325L15.438 13.2413"/><path stroke="#716E6E" stroke-width="1.2" d="M7.80176 9.80273H2.34721"/><path stroke="#716E6E" stroke-width="1.2" d="M5.25635 6.36523L1.81833 9.80325L5.25635 13.2413"/><path stroke="#716E6E" stroke-width="1.2" d="M9.2561 2.72852V17.274"/><path stroke="#716E6E" stroke-width="1.2" d="M11.438 2.72852V17.274"/></svg>
|
||||
|
Before Width: | Height: | Size: 582 B After Width: | Height: | Size: 574 B |
1
app/client/src/assets/icons/ads/link.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16"><rect width="11" height="11" x="2.5" y="2.5" stroke="#fff" stroke-opacity=".6" rx="1.5"/><path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-opacity=".6" d="M6 10L10 6M10 6H6.92308M10 6V9.07692"/></svg>
|
||||
|
After Width: | Height: | Size: 319 B |
|
|
@ -1,3 +1 @@
|
|||
<svg width="12" height="20" viewBox="0 0 12 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.4583 0H1.54178C0.798918 0 0.190918 0.607429 0.190918 1.35086V18.6486C0.190918 19.392 0.798918 20 1.54178 20H10.4583C11.2012 20 11.8092 19.3931 11.8092 18.6491V1.35086C11.8092 0.607429 11.2012 0 10.4583 0ZM4.57378 0.973714H7.42635C7.51663 0.973714 7.58978 1.04686 7.58978 1.13771C7.58978 1.228 7.51663 1.30114 7.42635 1.30114H4.57378C4.48349 1.30114 4.41035 1.228 4.41035 1.13771C4.41035 1.04686 4.48349 0.973714 4.57378 0.973714ZM6.00006 19.3246C5.62692 19.3246 5.32463 19.0223 5.32463 18.6486C5.32463 18.2749 5.62692 17.9731 6.00006 17.9731C6.3732 17.9731 6.67549 18.2749 6.67549 18.6486C6.67549 19.0223 6.3732 19.3246 6.00006 19.3246ZM10.8692 17.5H1.13092V2.14229H10.8692V17.5Z" fill="#716E6E"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="20" fill="none" viewBox="0 0 12 20"><path fill="#716E6E" d="M10.4583 0H1.54178C0.798918 0 0.190918 0.607429 0.190918 1.35086V18.6486C0.190918 19.392 0.798918 20 1.54178 20H10.4583C11.2012 20 11.8092 19.3931 11.8092 18.6491V1.35086C11.8092 0.607429 11.2012 0 10.4583 0ZM4.57378 0.973714H7.42635C7.51663 0.973714 7.58978 1.04686 7.58978 1.13771C7.58978 1.228 7.51663 1.30114 7.42635 1.30114H4.57378C4.48349 1.30114 4.41035 1.228 4.41035 1.13771C4.41035 1.04686 4.48349 0.973714 4.57378 0.973714ZM6.00006 19.3246C5.62692 19.3246 5.32463 19.0223 5.32463 18.6486C5.32463 18.2749 5.62692 17.9731 6.00006 17.9731C6.3732 17.9731 6.67549 18.2749 6.67549 18.6486C6.67549 19.0223 6.3732 19.3246 6.00006 19.3246ZM10.8692 17.5H1.13092V2.14229H10.8692V17.5Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 814 B After Width: | Height: | Size: 811 B |
1
app/client/src/assets/icons/ads/recent.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="12" fill="none" viewBox="0 0 14 12"><path fill="#FCD43E" d="M7.33325 3.33337V6.66669L10.1866 8.36004L10.6666 7.55004L8.33325 6.16669V3.33337H7.33325Z"/><path fill="#FCD43E" d="M7.99666 0C4.68 0 2 2.68666 2 6H0L2.59666 8.59665L2.64331 8.69331L5.33334 6H3.33334C3.33334 3.42334 5.42334 1.33334 8 1.33334C10.5767 1.33334 12.6667 3.42334 12.6667 6C12.6667 8.57666 10.5767 10.6667 8 10.6667C6.71 10.6667 5.54666 10.14 4.70334 9.29666L3.76 10.24C4.84334 11.3267 6.34 12 7.99666 12C11.3133 12 14 9.31334 14 6C14 2.68666 11.3133 0 7.99666 0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 601 B |
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 14 14"><path fill="#4B4848" d="M7.05273 13.7158C7.30566 13.7158 7.51074 13.5312 7.55176 13.2578C8.13965 8.70508 8.78223 8.05566 13.2324 7.56348C13.5127 7.53613 13.7109 7.33105 13.7109 7.06445C13.7109 6.79785 13.5127 6.59277 13.2324 6.55859C8.78223 6.07324 8.13965 5.42383 7.55176 0.864258C7.51074 0.59082 7.30566 0.413086 7.05273 0.413086C6.7998 0.413086 6.59473 0.59082 6.55371 0.864258C5.96582 5.42383 5.32324 6.07324 0.873047 6.55859C0.592773 6.59277 0.394531 6.79785 0.394531 7.06445C0.394531 7.33105 0.592773 7.53613 0.873047 7.56348C5.32324 8.15137 5.9248 8.71191 6.55371 13.2578C6.59473 13.5312 6.7998 13.7158 7.05273 13.7158Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 14 14"><path fill="#4B4848" d="M7.05273 13.7158C7.30566 13.7158 7.51074 13.5312 7.55176 13.2578C8.13965 8.70508 8.78223 8.05566 13.2324 7.56348C13.5127 7.53613 13.7109 7.33105 13.7109 7.06445C13.7109 6.79785 13.5127 6.59277 13.2324 6.55859C8.78223 6.07324 8.13965 5.42383 7.55176 0.864258C7.51074 0.59082 7.30566 0.413086 7.05273 0.413086C6.7998 0.413086 6.59473 0.59082 6.55371 0.864258C5.96582 5.42383 5.32324 6.07324 0.873047 6.55859C0.592773 6.59277 0.394531 6.79785 0.394531 7.06445C0.394531 7.33105 0.592773 7.53613 0.873047 7.56348C5.32324 8.15137 5.9248 8.71191 6.55371 13.2578C6.59473 13.5312 6.7998 13.7158 7.05273 13.7158Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 731 B After Width: | Height: | Size: 730 B |
|
|
@ -1,3 +1 @@
|
|||
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.9588 0H2.0402C1.28889 0 0.684082 0.607754 0.684082 1.35848V18.6421C0.684082 19.3905 1.28889 20 2.0402 20H13.9588C14.7077 20 15.3155 19.3911 15.3155 18.6421V1.35848C15.3167 0.607754 14.7083 0 13.9588 0ZM8.00008 19.4911C7.55412 19.4911 7.19347 19.1304 7.19347 18.6851C7.19347 18.2391 7.55412 17.8779 8.00008 17.8779C8.44486 17.8779 8.80669 18.2391 8.80669 18.6851C8.80669 19.1304 8.44486 19.4911 8.00008 19.4911ZM13.9429 17.1713H2.05727V1.63205H13.9429V17.1713Z" fill="#444444"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="20" fill="none" viewBox="0 0 16 20"><path fill="#444" d="M13.9588 0H2.0402C1.28889 0 0.684082 0.607754 0.684082 1.35848V18.6421C0.684082 19.3905 1.28889 20 2.0402 20H13.9588C14.7077 20 15.3155 19.3911 15.3155 18.6421V1.35848C15.3167 0.607754 14.7083 0 13.9588 0ZM8.00008 19.4911C7.55412 19.4911 7.19347 19.1304 7.19347 18.6851C7.19347 18.2391 7.55412 17.8779 8.00008 17.8779C8.44486 17.8779 8.80669 18.2391 8.80669 18.6851C8.80669 19.1304 8.44486 19.4911 8.00008 19.4911ZM13.9429 17.1713H2.05727V1.63205H13.9429V17.1713Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 594 B After Width: | Height: | Size: 588 B |
BIN
app/client/src/assets/images/no_search_data.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 80 KiB |
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" fill="none" viewBox="0 0 11 11"><path fill="#fff" d="M4.31445 10.9434C4.56641 10.9434 4.76562 10.832 4.90625 10.6152L10.4434 1.89648C10.5488 1.72656 10.5898 1.59766 10.5898 1.46289C10.5898 1.14062 10.3789 0.929688 10.0566 0.929688C9.82227 0.929688 9.69336 1.00586 9.55273 1.22852L4.29102 9.61328L1.56055 6.03906C1.41406 5.83398 1.26758 5.75195 1.05664 5.75195C0.722656 5.75195 0.494141 5.98047 0.494141 6.30273C0.494141 6.4375 0.552734 6.58984 0.664062 6.73047L3.70508 10.6035C3.88086 10.832 4.0625 10.9434 4.31445 10.9434Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" fill="none" viewBox="0 0 11 11"><path fill="#fff" d="M4.31445 10.9434C4.56641 10.9434 4.76562 10.832 4.90625 10.6152L10.4434 1.89648C10.5488 1.72656 10.5898 1.59766 10.5898 1.46289C10.5898 1.14062 10.3789 0.929688 10.0566 0.929688C9.82227 0.929688 9.69336 1.00586 9.55273 1.22852L4.29102 9.61328L1.56055 6.03906C1.41406 5.83398 1.26758 5.75195 1.05664 5.75195C0.722656 5.75195 0.494141 5.98047 0.494141 6.30273C0.494141 6.4375 0.552734 6.58984 0.664062 6.73047L3.70508 10.6035C3.88086 10.832 4.0625 10.9434 4.31445 10.9434Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 596 B After Width: | Height: | Size: 595 B |
|
|
@ -90,6 +90,7 @@ type DialogComponentProps = {
|
|||
showHeaderUnderline?: boolean;
|
||||
getHeader?: () => ReactNode;
|
||||
canEscapeKeyClose?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const DialogComponent = (props: DialogComponentProps) => {
|
||||
|
|
@ -108,6 +109,7 @@ export const DialogComponent = (props: DialogComponentProps) => {
|
|||
return (
|
||||
<React.Fragment>
|
||||
<TriggerWrapper
|
||||
className="ads-dialog-trigger"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
|
|
@ -126,6 +128,7 @@ export const DialogComponent = (props: DialogComponentProps) => {
|
|||
maxHeight={props.maxHeight}
|
||||
onOpening={props.onOpening}
|
||||
showHeaderUnderline={props.showHeaderUnderline}
|
||||
className={props.className}
|
||||
>
|
||||
{getHeader && getHeader()}
|
||||
<div className={Classes.DIALOG_BODY}>{props.children}</div>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import { ReactComponent as ArrowLeft } from "assets/icons/ads/arrow-left.svg";
|
|||
import { ReactComponent as Fork } from "assets/icons/ads/fork.svg";
|
||||
import { ReactComponent as ChevronLeft } from "assets/icons/ads/chevron_left.svg";
|
||||
import { ReactComponent as ChevronRight } from "assets/icons/ads/chevron_right.svg";
|
||||
import { ReactComponent as LinkIcon } from "assets/icons/ads/link.svg";
|
||||
import { ReactComponent as HelpIcon } from "assets/icons/help/help.svg";
|
||||
import { ReactComponent as CloseModalIcon } from "assets/icons/ads/close-modal.svg";
|
||||
import { ReactComponent as NoResponseIcon } from "assets/icons/ads/no-response.svg";
|
||||
import { ReactComponent as LightningIcon } from "assets/icons/ads/lightning.svg";
|
||||
|
|
@ -120,6 +122,8 @@ export const IconCollection = [
|
|||
"fork",
|
||||
"chevron-left",
|
||||
"chevron-right",
|
||||
"link",
|
||||
"help",
|
||||
"close-modal",
|
||||
"no-response",
|
||||
"lightning",
|
||||
|
|
@ -265,6 +269,12 @@ const Icon = forwardRef(
|
|||
case "chevron-right":
|
||||
returnIcon = <ChevronRight />;
|
||||
break;
|
||||
case "link":
|
||||
returnIcon = <LinkIcon />;
|
||||
break;
|
||||
case "help":
|
||||
returnIcon = <HelpIcon />;
|
||||
break;
|
||||
case "close-modal":
|
||||
returnIcon = <CloseModalIcon />;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ const ToastComponent = (props: ToastProps & { undoAction?: () => void }) => {
|
|||
return (
|
||||
<ToastBody
|
||||
variant={props.variant || Variant.info}
|
||||
isUndo={props.onUndo ? true : false}
|
||||
isUndo={!!props.onUndo}
|
||||
dispatchableAction={props.dispatchableAction}
|
||||
className="t--toast-action"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -63,14 +63,16 @@ export const renderCell = (
|
|||
</CellWrapper>
|
||||
);
|
||||
}
|
||||
const imageRegex = /(http(s?):)([/|.|\w|\s|-])*\.(?:jpeg|jpg|gif|png)??(?:&?[^=&]*=[^=&]*)*/;
|
||||
const imageSplitRegex = /[^(base64)],/;
|
||||
const imageUrlRegex = /(http(s?):)([/|.|\w|\s|-])*\.(?:jpeg|jpg|gif|png)??(?:&?[^=&]*=[^=&]*)*/;
|
||||
const base64ImageRegex = /^data:image\/.*;base64/;
|
||||
return (
|
||||
<CellWrapper cellProperties={cellProperties} isHidden={isHidden}>
|
||||
{value
|
||||
.toString()
|
||||
.split(",")
|
||||
.split(imageSplitRegex)
|
||||
.map((item: string, index: number) => {
|
||||
if (imageRegex.test(item)) {
|
||||
if (imageUrlRegex.test(item) || base64ImageRegex.test(item)) {
|
||||
return (
|
||||
<a
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import {
|
|||
} from "actions/helpActions";
|
||||
import { Icon } from "@blueprintjs/core";
|
||||
import moment from "moment";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
import { User } from "constants/userConstants";
|
||||
|
||||
const {
|
||||
algolia,
|
||||
|
|
@ -290,12 +292,26 @@ const HelpFooter = styled.div`
|
|||
font-size: 6pt;
|
||||
`;
|
||||
|
||||
const HelpBody = styled.div`
|
||||
padding-top: 68px;
|
||||
const HelpBody = styled.div<{ hideSearch?: boolean }>`
|
||||
${(props) =>
|
||||
props.hideSearch
|
||||
? `
|
||||
padding: ${props.theme.spaces[2]}px;
|
||||
`
|
||||
: `
|
||||
padding-top: 68px;
|
||||
`}
|
||||
flex: 5;
|
||||
`;
|
||||
|
||||
type Props = { hitsPerPage: number; defaultRefinement: string; dispatch: any };
|
||||
type Props = {
|
||||
hitsPerPage: number;
|
||||
defaultRefinement: string;
|
||||
dispatch: any;
|
||||
hideSearch?: boolean;
|
||||
hideMinimizeBtn?: boolean;
|
||||
user?: User;
|
||||
};
|
||||
type State = { showResults: boolean };
|
||||
|
||||
type HelpItem = {
|
||||
|
|
@ -343,6 +359,17 @@ class DocumentationSearch extends React.Component<Props, State> {
|
|||
showResults: props.defaultRefinement.length > 0,
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
const { user } = this.props;
|
||||
if (cloudHosting && intercomAppID && window.Intercom) {
|
||||
window.Intercom("boot", {
|
||||
app_id: intercomAppID,
|
||||
user_id: user?.username,
|
||||
name: user?.name,
|
||||
email: user?.email,
|
||||
});
|
||||
}
|
||||
}
|
||||
onSearchValueChange = (event: SyntheticEvent<HTMLInputElement, Event>) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: No types available
|
||||
|
|
@ -365,34 +392,38 @@ class DocumentationSearch extends React.Component<Props, State> {
|
|||
if (!algolia.enabled) return null;
|
||||
return (
|
||||
<SearchContainer className="ais-InstantSearch t--docSearchModal">
|
||||
<Icon
|
||||
className="t--docsMinimize"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 6,
|
||||
right: 10,
|
||||
cursor: "pointer",
|
||||
zIndex: 1,
|
||||
}}
|
||||
icon="minus"
|
||||
color="white"
|
||||
iconSize={14}
|
||||
onClick={this.handleClose}
|
||||
/>
|
||||
{!this.props.hideMinimizeBtn && (
|
||||
<Icon
|
||||
className="t--docsMinimize"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 6,
|
||||
right: 10,
|
||||
cursor: "pointer",
|
||||
zIndex: 1,
|
||||
}}
|
||||
icon="minus"
|
||||
color="white"
|
||||
iconSize={14}
|
||||
onClick={this.handleClose}
|
||||
/>
|
||||
)}
|
||||
<InstantSearch
|
||||
indexName={algolia.indexName}
|
||||
searchClient={searchClient}
|
||||
>
|
||||
<Configure hitsPerPage={this.props.hitsPerPage} />
|
||||
<HelpContainer>
|
||||
<Header>
|
||||
<StyledPoweredBy />
|
||||
<SearchBox
|
||||
onChange={this.onSearchValueChange}
|
||||
defaultRefinement={this.props.defaultRefinement}
|
||||
/>
|
||||
</Header>
|
||||
<HelpBody>
|
||||
{!this.props.hideSearch && (
|
||||
<Header>
|
||||
<StyledPoweredBy />
|
||||
<SearchBox
|
||||
onChange={this.onSearchValueChange}
|
||||
defaultRefinement={this.props.defaultRefinement}
|
||||
/>
|
||||
</Header>
|
||||
)}
|
||||
<HelpBody hideSearch={this.props.hideSearch}>
|
||||
{this.state.showResults ? (
|
||||
<Hits hitComponent={Hit as any} />
|
||||
) : (
|
||||
|
|
@ -422,6 +453,7 @@ class DocumentationSearch extends React.Component<Props, State> {
|
|||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
defaultRefinement: getDefaultRefinement(state),
|
||||
user: getCurrentUser(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(DocumentationSearch);
|
||||
|
|
|
|||
|
|
@ -13,11 +13,10 @@ import { getAppsmithConfigs } from "configs";
|
|||
import { LayersContext } from "constants/Layers";
|
||||
import { connect } from "react-redux";
|
||||
import { AppState } from "reducers";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
import { User } from "constants/userConstants";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { HELP_MODAL_HEIGHT, HELP_MODAL_WIDTH } from "constants/HelpConstants";
|
||||
|
||||
const { algolia, cloudHosting, intercomAppID } = getAppsmithConfigs();
|
||||
const { algolia } = getAppsmithConfigs();
|
||||
const HelpButton = styled.button<{
|
||||
highlight: boolean;
|
||||
layer: number;
|
||||
|
|
@ -47,8 +46,8 @@ const HelpButton = styled.button<{
|
|||
}
|
||||
`;
|
||||
|
||||
const MODAL_WIDTH = 240;
|
||||
const MODAL_HEIGHT = 206;
|
||||
const MODAL_WIDTH = HELP_MODAL_WIDTH;
|
||||
const MODAL_HEIGHT = HELP_MODAL_HEIGHT;
|
||||
const MODAL_BOTTOM_DISTANCE = 100;
|
||||
const MODAL_RIGHT_DISTANCE = 27;
|
||||
|
||||
|
|
@ -58,25 +57,12 @@ const CloseIcon = HelpIcons.CLOSE_ICON;
|
|||
type Props = {
|
||||
isHelpModalOpen: boolean;
|
||||
dispatch: any;
|
||||
user?: User;
|
||||
page: string;
|
||||
};
|
||||
|
||||
class HelpModal extends React.Component<Props> {
|
||||
static contextType = LayersContext;
|
||||
|
||||
componentDidMount() {
|
||||
const { user } = this.props;
|
||||
if (cloudHosting && intercomAppID && window.Intercom) {
|
||||
window.Intercom("boot", {
|
||||
app_id: intercomAppID,
|
||||
user_id: user?.username,
|
||||
name: user?.name,
|
||||
email: user?.email,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* closes help modal
|
||||
*
|
||||
|
|
@ -151,7 +137,6 @@ class HelpModal extends React.Component<Props> {
|
|||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
isHelpModalOpen: getHelpModalOpen(state),
|
||||
user: getCurrentUser(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(HelpModal);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const Container = styled.div<{
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
& .${Classes.OVERLAY_CONTENT} {
|
||||
max-width: 95%;
|
||||
width: ${(props) => props.width}px;
|
||||
min-height: ${(props) => props.height}px;
|
||||
background: white;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import React from "react";
|
||||
import Icon, { IconSize } from "components/ads/Icon";
|
||||
import { Theme } from "constants/DefaultTheme";
|
||||
import { useContext } from "react";
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import SearchContext from "./GlobalSearchContext";
|
||||
import { SearchItem } from "./utils";
|
||||
|
||||
export const StyledActionLink = styled.span<{ isActiveItem?: boolean }>`
|
||||
visibility: ${(props) => (props.isActiveItem ? "visible" : "hidden")};
|
||||
display: inline-flex;
|
||||
`;
|
||||
|
||||
export const ActionLink = withTheme(
|
||||
({
|
||||
item,
|
||||
theme,
|
||||
isActiveItem,
|
||||
}: {
|
||||
item: SearchItem;
|
||||
theme: Theme;
|
||||
isActiveItem?: boolean;
|
||||
}) => {
|
||||
const searchContext = useContext(SearchContext);
|
||||
return (
|
||||
<StyledActionLink isActiveItem={isActiveItem}>
|
||||
<Icon
|
||||
name="link"
|
||||
size={IconSize.LARGE}
|
||||
fillColor={theme.colors.globalSearch.searchItemText}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // to prevent toggleModal getting called twice
|
||||
searchContext?.handleItemLinkClick(item, "SEARCH_ITEM_ICON_CLICK");
|
||||
}}
|
||||
/>
|
||||
</StyledActionLink>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default ActionLink;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import algoliasearch from "algoliasearch/lite";
|
||||
import { InstantSearch } from "react-instantsearch-dom";
|
||||
import { getAppsmithConfigs } from "configs";
|
||||
import { debounce } from "lodash";
|
||||
|
||||
const { algolia } = getAppsmithConfigs();
|
||||
const searchClient = algoliasearch(algolia.apiId, algolia.apiKey);
|
||||
|
||||
type SearchProps = {
|
||||
query: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const Search = ({ query, children }: SearchProps) => {
|
||||
const [queryInState, setQueryInState] = useState(query);
|
||||
const debouncedSetQueryInState = useCallback(
|
||||
debounce(setQueryInState, 100),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
debouncedSetQueryInState(query);
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<InstantSearch
|
||||
searchState={{ query: queryInState }}
|
||||
indexName={algolia.indexName}
|
||||
searchClient={searchClient}
|
||||
>
|
||||
{children}
|
||||
</InstantSearch>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import React, { useCallback, useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import ActionLink from "./ActionLink";
|
||||
import Highlight from "./Highlight";
|
||||
import { getItemTitle, SEARCH_ITEM_TYPES } from "./utils";
|
||||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
import { SearchItem } from "./utils";
|
||||
import parseDocumentationContent from "./parseDocumentationContent";
|
||||
|
||||
type Props = {
|
||||
activeItem: SearchItem;
|
||||
activeItemType?: SEARCH_ITEM_TYPES;
|
||||
query: string;
|
||||
scrollPositionRef: React.MutableRefObject<number>;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${(props) =>
|
||||
`${props.theme.spaces[5]}px ${props.theme.spaces[7]}px 0`};
|
||||
color: ${(props) => props.theme.colors.globalSearch.searchItemText};
|
||||
overflow: auto;
|
||||
|
||||
${(props) => getTypographyByKey(props, "spacedOutP1")};
|
||||
[class^="ais-"] {
|
||||
${(props) => getTypographyByKey(props, "spacedOutP1")};
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
${(props) => getTypographyByKey(props, "largeH1")};
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
strong {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.documentation-cta {
|
||||
${(props) => getTypographyByKey(props, "p3")}
|
||||
white-space: nowrap;
|
||||
background: ${(props) =>
|
||||
props.theme.colors.globalSearch.documentationCtaBackground};
|
||||
color: ${(props) => props.theme.colors.globalSearch.documentationCtaText};
|
||||
padding: ${(props) => props.theme.spaces[2]}px;
|
||||
margin: 0 ${(props) => props.theme.spaces[2]}px;
|
||||
position: relative;
|
||||
bottom: 3px;
|
||||
}
|
||||
|
||||
& a {
|
||||
color: ${(props) => props.theme.colors.globalSearch.documentLink};
|
||||
}
|
||||
|
||||
code {
|
||||
word-break: break-word;
|
||||
background: ${(props) => props.theme.colors.globalSearch.codeBackground};
|
||||
padding: ${(props) => props.theme.spaces[2]}px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: ${(props) => props.theme.colors.globalSearch.codeBackground};
|
||||
white-space: pre-wrap;
|
||||
overflow: hidden;
|
||||
padding: ${(props) => props.theme.spaces[6]}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const DocumentationDescription = ({ item }: { item: SearchItem }) => {
|
||||
try {
|
||||
const {
|
||||
_highlightResult: {
|
||||
document: { value: rawDocument },
|
||||
title: { value: rawTitle },
|
||||
},
|
||||
} = item;
|
||||
const content = parseDocumentationContent({
|
||||
rawDocument: rawDocument,
|
||||
rawTitle: rawTitle,
|
||||
path: item.path,
|
||||
});
|
||||
|
||||
return content ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: content }} />
|
||||
) : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const StyledHitEnterMessageContainer = styled.div`
|
||||
background: ${(props) =>
|
||||
props.theme.colors.globalSearch.navigateUsingEnterSection};
|
||||
padding: ${(props) =>
|
||||
`${props.theme.spaces[6]}px ${props.theme.spaces[3]}px`};
|
||||
${(props) => getTypographyByKey(props, "p3")}
|
||||
`;
|
||||
|
||||
const StyledKey = styled.span`
|
||||
margin: 0 ${(props) => props.theme.spaces[1]}px;
|
||||
color: ${(props) => props.theme.colors.globalSearch.navigateToEntityEnterkey};
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const StyledHighlightWrapper = styled.span`
|
||||
margin: 0 ${(props) => props.theme.spaces[1]}px;
|
||||
`;
|
||||
|
||||
const HitEnterMessage = ({
|
||||
item,
|
||||
query,
|
||||
}: {
|
||||
item: SearchItem;
|
||||
query: string;
|
||||
}) => {
|
||||
const title = getItemTitle(item);
|
||||
|
||||
return (
|
||||
<StyledHitEnterMessageContainer
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
✨ Press <StyledKey>↵</StyledKey> to navigate to
|
||||
<StyledHighlightWrapper>
|
||||
<Highlight match={query} text={title} />
|
||||
</StyledHighlightWrapper>
|
||||
<ActionLink item={item} isActiveItem={true} />
|
||||
</StyledHitEnterMessageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const descriptionByType = {
|
||||
[SEARCH_ITEM_TYPES.document]: DocumentationDescription,
|
||||
[SEARCH_ITEM_TYPES.action]: HitEnterMessage,
|
||||
[SEARCH_ITEM_TYPES.widget]: HitEnterMessage,
|
||||
[SEARCH_ITEM_TYPES.datasource]: HitEnterMessage,
|
||||
[SEARCH_ITEM_TYPES.page]: HitEnterMessage,
|
||||
[SEARCH_ITEM_TYPES.sectionTitle]: () => null,
|
||||
};
|
||||
|
||||
const Description = (props: Props) => {
|
||||
const { activeItem, activeItemType } = props;
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const onScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
||||
if (
|
||||
props.scrollPositionRef?.current ||
|
||||
props.scrollPositionRef?.current === 0
|
||||
) {
|
||||
props.scrollPositionRef.current = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.scrollTop = props.scrollPositionRef?.current;
|
||||
}
|
||||
}, [containerRef.current, activeItem]);
|
||||
|
||||
if (!activeItemType || !activeItem) return null;
|
||||
const Component = descriptionByType[activeItemType];
|
||||
|
||||
return (
|
||||
<Container onScroll={onScroll} ref={containerRef}>
|
||||
<Component item={activeItem} query={props.query} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Description;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import React from "react";
|
||||
import { SearchItem } from "./utils";
|
||||
|
||||
type SearchContextType = {
|
||||
handleItemLinkClick: (item?: SearchItem, source?: string) => void;
|
||||
setActiveItemIndex: (index: number) => void;
|
||||
activeItemIndex: number;
|
||||
};
|
||||
|
||||
const SearchContext = React.createContext<SearchContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export default SearchContext;
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import React from "react";
|
||||
import { HotkeysTarget } from "@blueprintjs/core/lib/esnext/components/hotkeys/hotkeysTarget.js";
|
||||
import { Hotkey, Hotkeys } from "@blueprintjs/core";
|
||||
import { SearchItem } from "./utils";
|
||||
|
||||
type Props = {
|
||||
modalOpen: boolean;
|
||||
toggleShow: () => void;
|
||||
handleUpKey: () => void;
|
||||
handleDownKey: () => void;
|
||||
handleItemLinkClick: (item?: SearchItem, source?: string) => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
@HotkeysTarget
|
||||
class GlobalSearchHotKeys extends React.Component<Props> {
|
||||
get hotKeysConfig() {
|
||||
return [
|
||||
{
|
||||
combo: "up",
|
||||
onKeyDown: this.props.handleUpKey,
|
||||
hideWhenModalClosed: true,
|
||||
allowInInput: true,
|
||||
group: "Omnibar",
|
||||
label: "Move up the list",
|
||||
},
|
||||
{
|
||||
combo: "down",
|
||||
onKeyDown: this.props.handleDownKey,
|
||||
hideWhenModalClosed: true,
|
||||
allowInInput: true,
|
||||
group: "Omnibar",
|
||||
label: "Move down the list",
|
||||
},
|
||||
{
|
||||
combo: "return",
|
||||
onKeyDown: () => {
|
||||
const activeElement = document.activeElement as any;
|
||||
activeElement?.blur(); // scroll into view doesn't work with the search input focused
|
||||
this.props.handleItemLinkClick(null, "ENTER_KEY");
|
||||
},
|
||||
hideWhenModalClosed: true,
|
||||
allowInInput: true,
|
||||
group: "Omnibar",
|
||||
label: "Navigate",
|
||||
},
|
||||
].filter(
|
||||
({ hideWhenModalClosed }) =>
|
||||
!hideWhenModalClosed || (hideWhenModalClosed && this.props.modalOpen),
|
||||
);
|
||||
}
|
||||
|
||||
renderHotkeys() {
|
||||
return (
|
||||
<Hotkeys>
|
||||
{this.hotKeysConfig.map(
|
||||
({ combo, onKeyDown, allowInInput, label, group }, index) => (
|
||||
<Hotkey
|
||||
key={index}
|
||||
global={false}
|
||||
combo={combo}
|
||||
onKeyDown={onKeyDown}
|
||||
label={label}
|
||||
allowInInput={allowInInput}
|
||||
group={group}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>{this.props.children}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default GlobalSearchHotKeys;
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
import Text, { TextType } from "components/ads/Text";
|
||||
import { toggleShowGlobalSearchModal } from "actions/globalSearchActions";
|
||||
import { HELPBAR_PLACEHOLDER } from "constants/messages";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { isMac } from "utils/helpers";
|
||||
|
||||
const StyledHelpBar = styled.div`
|
||||
padding: 0 ${(props) => props.theme.spaces[4]}px;
|
||||
.placeholder-text {
|
||||
${(props) => getTypographyByKey(props, "p2")}
|
||||
}
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: ${(props) => props.theme.colors.globalSearch.helpBarText};
|
||||
background: ${(props) => props.theme.colors.globalSearch.helpBarBackground};
|
||||
height: 28px;
|
||||
flex: 1;
|
||||
max-width: 350px;
|
||||
`;
|
||||
|
||||
const modText = () => (isMac() ? <span>⌘</span> : "ctrl");
|
||||
const comboText = <>{modText()} + K</>;
|
||||
|
||||
type Props = {
|
||||
toggleShowModal: () => void;
|
||||
};
|
||||
|
||||
const HelpBar = ({ toggleShowModal }: Props) => {
|
||||
return (
|
||||
<StyledHelpBar
|
||||
onClick={toggleShowModal}
|
||||
className="t--global-search-modal-trigger"
|
||||
>
|
||||
<Text type={TextType.P2}>{HELPBAR_PLACEHOLDER}</Text>
|
||||
<Text type={TextType.P3} italic>
|
||||
{comboText}
|
||||
</Text>
|
||||
</StyledHelpBar>
|
||||
);
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => ({
|
||||
toggleShowModal: () => {
|
||||
AnalyticsUtil.logEvent("OPEN_OMNIBAR", { source: "NAVBAR_CLICK" });
|
||||
dispatch(toggleShowGlobalSearchModal());
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(HelpBar);
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import React from "react";
|
||||
|
||||
const Highlight = ({ match, text }: { match: string; text: string }) => {
|
||||
if (!match) return <span>{text}</span>;
|
||||
|
||||
const regEx = new RegExp(match, "ig");
|
||||
const parts = text?.split(regEx);
|
||||
if (parts?.length === 1) return <span>{text}</span>;
|
||||
let lastIndex = 0;
|
||||
|
||||
return (
|
||||
<span>
|
||||
{parts?.map((part, index) => {
|
||||
lastIndex += Math.max(part.length, 0);
|
||||
const result = (
|
||||
<React.Fragment key={index}>
|
||||
{part}
|
||||
{index !== parts.length - 1 && (
|
||||
<span className="search-highlighted">
|
||||
{text.slice(lastIndex, lastIndex + match.length)}
|
||||
</span>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
lastIndex += match.length;
|
||||
return result;
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Highlight;
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import NoSearchDataImage from "assets/images/no_search_data.png";
|
||||
import { NO_SEARCH_DATA_TEXT } from "constants/messages";
|
||||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
${(props) => getTypographyByKey(props, "spacedOutP1")}
|
||||
color: ${(props) => props.theme.colors.globalSearch.emptyStateText};
|
||||
|
||||
.no-data-title {
|
||||
margin-top: ${(props) => props.theme.spaces[3]}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ResultsNotFound = () => (
|
||||
<Container>
|
||||
<img alt="No data" src={NoSearchDataImage} />
|
||||
<div className="no-data-title">{NO_SEARCH_DATA_TEXT}</div>
|
||||
</Container>
|
||||
);
|
||||
|
||||
export default ResultsNotFound;
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import { connectSearchBox } from "react-instantsearch-dom";
|
||||
import { SearchBoxProvided } from "react-instantsearch-core";
|
||||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
import Icon from "components/ads/Icon";
|
||||
import { AppState } from "reducers";
|
||||
import { OMNIBAR_PLACEHOLDER } from "constants/messages";
|
||||
|
||||
const Container = styled.div`
|
||||
padding: ${(props) => `0 ${props.theme.spaces[11]}px`};
|
||||
& input {
|
||||
${(props) => getTypographyByKey(props, "cardSubheader")}
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.colors.globalSearch.searchInputText};
|
||||
border: none;
|
||||
padding: ${(props) => `${props.theme.spaces[7]}px 0`};
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const InputContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.keyCode === 38 || e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
type SearchBoxProps = SearchBoxProvided & {
|
||||
query: string;
|
||||
setQuery: (query: string) => void;
|
||||
};
|
||||
|
||||
const useListenToChange = (modalOpen: boolean) => {
|
||||
const [listenToChange, setListenToChange] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setListenToChange(false);
|
||||
let timer: number;
|
||||
if (modalOpen) {
|
||||
timer = setTimeout(() => setListenToChange(true), 100);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [modalOpen]);
|
||||
|
||||
return listenToChange;
|
||||
};
|
||||
|
||||
const SearchBox = ({ query, setQuery }: SearchBoxProps) => {
|
||||
const { modalOpen } = useSelector((state: AppState) => state.ui.globalSearch);
|
||||
const listenToChange = useListenToChange(modalOpen);
|
||||
|
||||
const updateSearchQuery = useCallback(
|
||||
(query) => {
|
||||
// to prevent key combo to open modal from trigging query update
|
||||
if (!listenToChange) return;
|
||||
setQuery(query);
|
||||
},
|
||||
[listenToChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<InputContainer>
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => updateSearchQuery(e.currentTarget.value)}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={OMNIBAR_PLACEHOLDER}
|
||||
className="t--global-search-input"
|
||||
/>
|
||||
{query && (
|
||||
<Icon
|
||||
name="close"
|
||||
className="t--global-clear-input"
|
||||
onClick={() => updateSearchQuery("")}
|
||||
/>
|
||||
)}
|
||||
</InputContainer>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default connectSearchBox<SearchBoxProps>(SearchBox);
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Overlay, Classes } from "@blueprintjs/core";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
|
||||
const StyledDocsSearchModal = styled.div`
|
||||
& {
|
||||
.${Classes.OVERLAY} {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.${Classes.OVERLAY_CONTENT} {
|
||||
overflow: hidden;
|
||||
top: 10vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
modalOpen: boolean;
|
||||
toggleShow: () => void;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const DocsSearchModal = ({ modalOpen, toggleShow, children }: Props) => (
|
||||
<StyledDocsSearchModal>
|
||||
<Overlay
|
||||
isOpen={modalOpen}
|
||||
onClose={toggleShow}
|
||||
hasBackdrop={true}
|
||||
usePortal={false}
|
||||
onClosing={() => {
|
||||
AnalyticsUtil.logEvent("CLOSE_OMNIBAR");
|
||||
}}
|
||||
transitionDuration={25}
|
||||
>
|
||||
<div className={`${Classes.OVERLAY_CONTENT} t--global-search-modal`}>
|
||||
{children}
|
||||
</div>
|
||||
</Overlay>
|
||||
</StyledDocsSearchModal>
|
||||
);
|
||||
|
||||
export default DocsSearchModal;
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
import React, { useEffect, useRef, useContext, useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Highlight as AlgoliaHighlight } from "react-instantsearch-dom";
|
||||
import { Hit as IHit } from "react-instantsearch-core";
|
||||
import styled from "styled-components";
|
||||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
import Highlight from "./Highlight";
|
||||
import ActionLink, { StyledActionLink } from "./ActionLink";
|
||||
import scrollIntoView from "scroll-into-view-if-needed";
|
||||
import {
|
||||
getItemType,
|
||||
getItemTitle,
|
||||
SEARCH_ITEM_TYPES,
|
||||
SearchItem,
|
||||
} from "./utils";
|
||||
import SearchContext from "./GlobalSearchContext";
|
||||
import {
|
||||
getWidgetIcon,
|
||||
getPluginIcon,
|
||||
homePageIcon,
|
||||
pageIcon,
|
||||
} from "pages/Editor/Explorer/ExplorerIcons";
|
||||
import { HelpIcons } from "icons/HelpIcons";
|
||||
import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers";
|
||||
import { AppState } from "reducers";
|
||||
import { keyBy, noop } from "lodash";
|
||||
import { getPageList } from "selectors/editorSelectors";
|
||||
|
||||
const DocumentIcon = HelpIcons.DOCUMENT;
|
||||
|
||||
export const SearchItemContainer = styled.div<{
|
||||
isActiveItem: boolean;
|
||||
itemType: SEARCH_ITEM_TYPES;
|
||||
}>`
|
||||
cursor: ${(props) =>
|
||||
props.itemType !== SEARCH_ITEM_TYPES.sectionTitle ? "pointer" : "default"};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: ${(props) =>
|
||||
`${props.theme.spaces[4]}px ${props.theme.spaces[4]}px`};
|
||||
color: ${(props) => props.theme.colors.globalSearch.searchItemText};
|
||||
margin: ${(props) => props.theme.spaces[1]}px 0;
|
||||
background-color: ${(props) =>
|
||||
props.isActiveItem && props.itemType !== SEARCH_ITEM_TYPES.sectionTitle
|
||||
? props.theme.colors.globalSearch.activeSearchItemBackground
|
||||
: "unset"};
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) =>
|
||||
props.itemType !== SEARCH_ITEM_TYPES.sectionTitle
|
||||
? props.theme.colors.globalSearch.activeSearchItemBackground
|
||||
: "unset"};
|
||||
${StyledActionLink} {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
${(props) => getTypographyByKey(props, "p3")};
|
||||
[class^="ais-"] {
|
||||
${(props) => getTypographyByKey(props, "p3")};
|
||||
}
|
||||
`;
|
||||
|
||||
const ItemTitle = styled.div`
|
||||
margin-left: ${(props) => props.theme.spaces[5]}px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
${(props) => getTypographyByKey(props, "p3")};
|
||||
font-w [class^="ais-"] {
|
||||
${(props) => getTypographyByKey(props, "p3")};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledDocumentIcon = styled(DocumentIcon)`
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
path {
|
||||
fill: transparent;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const DocumentationItem = (props: {
|
||||
item: SearchItem;
|
||||
isActiveItem: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<StyledDocumentIcon />
|
||||
<ItemTitle>
|
||||
<span>
|
||||
<AlgoliaHighlight attribute="title" hit={props.item} />
|
||||
</span>
|
||||
<ActionLink item={props.item} isActiveItem={props.isActiveItem} />
|
||||
</ItemTitle>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const WidgetIconWrapper = styled.span`
|
||||
svg {
|
||||
height: 14px;
|
||||
}
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const usePageName = (pageId: string) => {
|
||||
const pages = useSelector(getPageList);
|
||||
const page = pages.find((page) => page.pageId === pageId);
|
||||
return page?.pageName;
|
||||
};
|
||||
|
||||
const WidgetItem = (props: {
|
||||
query: string;
|
||||
item: SearchItem;
|
||||
isActiveItem: boolean;
|
||||
}) => {
|
||||
const { query, item } = props;
|
||||
const { type } = item || {};
|
||||
let title = getItemTitle(item);
|
||||
const pageName = usePageName(item.pageId);
|
||||
title = `${pageName} / ${title}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<WidgetIconWrapper>{getWidgetIcon(type)}</WidgetIconWrapper>
|
||||
<ItemTitle>
|
||||
<Highlight match={query} text={title} />
|
||||
<ActionLink item={props.item} isActiveItem={props.isActiveItem} />
|
||||
</ItemTitle>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ActionIconWrapper = styled.div`
|
||||
& > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const ActionItem = (props: {
|
||||
query: string;
|
||||
item: SearchItem;
|
||||
isActiveItem: boolean;
|
||||
}) => {
|
||||
const { item, query } = props;
|
||||
const { config } = item || {};
|
||||
const { pluginType } = config;
|
||||
const plugins = useSelector((state: AppState) => {
|
||||
return state.entities.plugins.list;
|
||||
});
|
||||
const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]);
|
||||
const icon = getActionConfig(pluginType)?.getIcon(
|
||||
item.config,
|
||||
pluginGroups[item.config.datasource.pluginId],
|
||||
);
|
||||
|
||||
let title = getItemTitle(item);
|
||||
const pageName = usePageName(config.pageId);
|
||||
title = `${pageName} / ${title}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionIconWrapper>{icon}</ActionIconWrapper>
|
||||
<ItemTitle>
|
||||
<Highlight match={query} text={title} />
|
||||
<ActionLink item={props.item} isActiveItem={props.isActiveItem} />
|
||||
</ItemTitle>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DatasourceItem = (props: {
|
||||
query: string;
|
||||
item: SearchItem;
|
||||
isActiveItem: boolean;
|
||||
}) => {
|
||||
const { item, query } = props;
|
||||
const plugins = useSelector((state: AppState) => {
|
||||
return state.entities.plugins.list;
|
||||
});
|
||||
const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]);
|
||||
const icon = getPluginIcon(pluginGroups[item.pluginId]);
|
||||
const title = getItemTitle(item);
|
||||
return (
|
||||
<>
|
||||
{icon}
|
||||
<ItemTitle>
|
||||
<Highlight match={query} text={title} />
|
||||
<ActionLink item={props.item} isActiveItem={props.isActiveItem} />
|
||||
</ItemTitle>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PageItem = (props: {
|
||||
query: string;
|
||||
item: SearchItem;
|
||||
isActiveItem: boolean;
|
||||
}) => {
|
||||
const { query, item } = props;
|
||||
const title = getItemTitle(item);
|
||||
const icon = item.isDefault ? homePageIcon : pageIcon;
|
||||
|
||||
return (
|
||||
<>
|
||||
{icon}
|
||||
<ItemTitle>
|
||||
<Highlight match={query} text={title} />
|
||||
<ActionLink item={props.item} isActiveItem={props.isActiveItem} />
|
||||
</ItemTitle>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledSectionTitleContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& .section-title__icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: ${(props) => props.theme.spaces[5]}px;
|
||||
}
|
||||
& .section-title__text {
|
||||
color: ${(props) => props.theme.colors.globalSearch.sectionTitle};
|
||||
}
|
||||
margin-left: -${(props) => props.theme.spaces[3]}px;
|
||||
`;
|
||||
|
||||
const SectionTitle = ({ item }: { item: SearchItem }) => (
|
||||
<StyledSectionTitleContainer>
|
||||
<img className="section-title__icon" src={item.icon} />
|
||||
<span className="section-title__text">{item.title}</span>
|
||||
</StyledSectionTitleContainer>
|
||||
);
|
||||
|
||||
const SearchItemByType = {
|
||||
[SEARCH_ITEM_TYPES.document]: DocumentationItem,
|
||||
[SEARCH_ITEM_TYPES.widget]: WidgetItem,
|
||||
[SEARCH_ITEM_TYPES.action]: ActionItem,
|
||||
[SEARCH_ITEM_TYPES.datasource]: DatasourceItem,
|
||||
[SEARCH_ITEM_TYPES.page]: PageItem,
|
||||
[SEARCH_ITEM_TYPES.sectionTitle]: SectionTitle,
|
||||
};
|
||||
|
||||
type ItemProps = {
|
||||
item: IHit | SearchItem;
|
||||
index: number;
|
||||
query: string;
|
||||
};
|
||||
|
||||
const SearchItemComponent = (props: ItemProps) => {
|
||||
const { item, index, query } = props;
|
||||
const itemRef = useRef<HTMLDivElement>(null);
|
||||
const searchContext = useContext(SearchContext);
|
||||
const activeItemIndex = searchContext?.activeItemIndex;
|
||||
const setActiveItemIndex = searchContext?.setActiveItemIndex || noop;
|
||||
|
||||
const isActiveItem = activeItemIndex === index;
|
||||
|
||||
useEffect(() => {
|
||||
if (isActiveItem && itemRef.current) {
|
||||
scrollIntoView(itemRef.current, { scrollMode: "if-needed" });
|
||||
}
|
||||
}, [isActiveItem]);
|
||||
|
||||
const itemType = getItemType(item);
|
||||
const Item = SearchItemByType[itemType];
|
||||
|
||||
return (
|
||||
<SearchItemContainer
|
||||
ref={itemRef}
|
||||
onClick={() => {
|
||||
if (itemType !== SEARCH_ITEM_TYPES.sectionTitle) {
|
||||
setActiveItemIndex(index);
|
||||
if (itemType !== SEARCH_ITEM_TYPES.document) {
|
||||
searchContext?.handleItemLinkClick(item, "SEARCH_ITEM");
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="t--docHit"
|
||||
isActiveItem={isActiveItem}
|
||||
itemType={itemType}
|
||||
>
|
||||
<Item item={item} query={query} isActiveItem={isActiveItem} />
|
||||
</SearchItemContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const SearchResultsContainer = styled.div`
|
||||
padding: 0 ${(props) => props.theme.spaces[6]}px;
|
||||
overflow: auto;
|
||||
width: 250px;
|
||||
`;
|
||||
|
||||
const SearchResults = ({
|
||||
searchResults,
|
||||
query,
|
||||
}: {
|
||||
searchResults: SearchItem[];
|
||||
query: string;
|
||||
}) => {
|
||||
return (
|
||||
<SearchResultsContainer>
|
||||
{searchResults.map((item: SearchItem, index: number) => (
|
||||
<SearchItemComponent
|
||||
key={index}
|
||||
index={index}
|
||||
item={item}
|
||||
query={query}
|
||||
/>
|
||||
))}
|
||||
</SearchResultsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchResults;
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { useEffect, useCallback } from "react";
|
||||
import { connectHits } from "react-instantsearch-dom";
|
||||
import { Hit as IHit } from "react-instantsearch-core";
|
||||
import { debounce } from "lodash";
|
||||
import { DocSearchItem, SearchItem, SEARCH_ITEM_TYPES } from "./utils";
|
||||
|
||||
type Props = {
|
||||
setDocumentationSearchResults: (item: DocSearchItem) => void;
|
||||
hits: IHit[];
|
||||
};
|
||||
|
||||
const SearchResults = ({ hits, setDocumentationSearchResults }: Props) => {
|
||||
const debounsedSetter = useCallback(
|
||||
debounce(setDocumentationSearchResults, 100),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredHits = hits.filter(
|
||||
(doc: SearchItem) => doc.kind === SEARCH_ITEM_TYPES.document,
|
||||
);
|
||||
debounsedSetter(filteredHits as any);
|
||||
}, [hits]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default connectHits<Props, IHit>(SearchResults);
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
import React, {
|
||||
useState,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import { useParams } from "react-router";
|
||||
import history from "utils/history";
|
||||
import { AppState } from "reducers";
|
||||
import SearchModal from "./SearchModal";
|
||||
import AlgoliaSearchWrapper from "./AlgoliaSearchWrapper";
|
||||
import SearchBox from "./SearchBox";
|
||||
import SearchResults from "./SearchResults";
|
||||
import SetSearchResults from "./SetSearchResults";
|
||||
import GlobalSearchHotKeys from "./GlobalSearchHotKeys";
|
||||
import SearchContext from "./GlobalSearchContext";
|
||||
import Description from "./Description";
|
||||
import ResultsNotFound from "./ResultsNotFound";
|
||||
import { getActions, getAllPageWidgets } from "selectors/entitiesSelector";
|
||||
import { useNavigateToWidget } from "pages/Editor/Explorer/Widgets/WidgetEntity";
|
||||
import {
|
||||
toggleShowGlobalSearchModal,
|
||||
setGlobalSearchQuery,
|
||||
} from "actions/globalSearchActions";
|
||||
import {
|
||||
getItemType,
|
||||
SEARCH_ITEM_TYPES,
|
||||
useDefaultDocumentationResults,
|
||||
DocSearchItem,
|
||||
SearchItem,
|
||||
algoliaHighlightTag,
|
||||
attachKind,
|
||||
} from "./utils";
|
||||
import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers";
|
||||
import { HelpBaseURL } from "constants/HelpConstants";
|
||||
import { ExplorerURLParams } from "pages/Editor/Explorer/helpers";
|
||||
import { BUILDER_PAGE_URL, DATA_SOURCES_EDITOR_ID_URL } from "constants/routes";
|
||||
import { getSelectedWidget } from "selectors/ui";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { getPageList } from "selectors/editorSelectors";
|
||||
import useRecentEntities from "./useRecentEntities";
|
||||
import { keyBy, noop } from "lodash";
|
||||
import EntitiesIcon from "assets/icons/ads/entities.svg";
|
||||
import DocsIcon from "assets/icons/ads/docs.svg";
|
||||
import RecentIcon from "assets/icons/ads/recent.svg";
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 750px;
|
||||
height: 45vh;
|
||||
background: ${(props) => props.theme.colors.globalSearch.containerBackground};
|
||||
box-shadow: ${(props) => props.theme.colors.globalSearch.containerShadow};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
& .main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background-color: #383838;
|
||||
}
|
||||
${algoliaHighlightTag},
|
||||
& .ais-Highlight-highlighted,
|
||||
& .search-highlighted {
|
||||
background: unset;
|
||||
color: ${(props) => props.theme.colors.globalSearch.searchItemHighlight};
|
||||
font-style: normal;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: ${(props) =>
|
||||
props.theme.colors.globalSearch.highlightedTextUnderline};
|
||||
}
|
||||
`;
|
||||
|
||||
const Separator = styled.div`
|
||||
margin: ${(props) => props.theme.spaces[3]}px 0;
|
||||
width: 1px;
|
||||
background-color: ${(props) => props.theme.colors.globalSearch.separator};
|
||||
`;
|
||||
|
||||
const isModalOpenSelector = (state: AppState) =>
|
||||
state.ui.globalSearch.modalOpen;
|
||||
|
||||
const searchQuerySelector = (state: AppState) => state.ui.globalSearch.query;
|
||||
|
||||
const isMatching = (text = "", query = "") =>
|
||||
text?.toLowerCase().indexOf(query?.toLowerCase()) > -1;
|
||||
|
||||
const getSectionTitle = (title: string, icon: any) => ({
|
||||
kind: SEARCH_ITEM_TYPES.sectionTitle,
|
||||
title,
|
||||
icon,
|
||||
});
|
||||
|
||||
const GlobalSearch = () => {
|
||||
const defaultDocs = useDefaultDocumentationResults();
|
||||
const params = useParams<ExplorerURLParams>();
|
||||
const dispatch = useDispatch();
|
||||
const toggleShow = () => dispatch(toggleShowGlobalSearchModal());
|
||||
const [query, setQueryInState] = useState("");
|
||||
const setQuery = useCallback((query: string) => {
|
||||
setQueryInState(query);
|
||||
}, []);
|
||||
const scrollPositionRef = useRef(0);
|
||||
|
||||
const [
|
||||
documentationSearchResults,
|
||||
setDocumentationSearchResultsInState,
|
||||
] = useState<Array<DocSearchItem>>([]);
|
||||
|
||||
const setDocumentationSearchResults = useCallback((res) => {
|
||||
setDocumentationSearchResultsInState(res);
|
||||
}, []);
|
||||
|
||||
const [activeItemIndex, setActiveItemIndexInState] = useState(1);
|
||||
const setActiveItemIndex = useCallback((index) => {
|
||||
scrollPositionRef.current = 0;
|
||||
setActiveItemIndexInState(index);
|
||||
}, []);
|
||||
|
||||
const allWidgets = useSelector(getAllPageWidgets);
|
||||
|
||||
const searchableWidgets = useMemo(
|
||||
() =>
|
||||
allWidgets.filter(
|
||||
(widget: any) =>
|
||||
["CANVAS_WIDGET", "ICON_WIDGET"].indexOf(widget.type) === -1,
|
||||
),
|
||||
[allWidgets],
|
||||
);
|
||||
const actions = useSelector(getActions);
|
||||
const modalOpen = useSelector(isModalOpenSelector);
|
||||
const pages = useSelector(getPageList) || [];
|
||||
const pageMap = keyBy(pages, "pageId");
|
||||
|
||||
const reducerDatasources = useSelector((state: AppState) => {
|
||||
return state.entities.datasources.list;
|
||||
});
|
||||
const datasourcesList = useMemo(() => {
|
||||
return reducerDatasources.map((datasource) => ({
|
||||
...datasource,
|
||||
pageId: params?.pageId,
|
||||
}));
|
||||
}, [reducerDatasources]);
|
||||
|
||||
const filteredDatasources = useMemo(() => {
|
||||
if (!query) return datasourcesList;
|
||||
return datasourcesList.filter((datasource) =>
|
||||
isMatching(datasource.name, query),
|
||||
);
|
||||
}, [reducerDatasources, query]);
|
||||
const recentEntities = useRecentEntities();
|
||||
|
||||
const resetSearchQuery = useSelector(searchQuerySelector);
|
||||
const selectedWidgetId = useSelector(getSelectedWidget);
|
||||
|
||||
// keeping query in component state until we can figure out fixed for the perf issues
|
||||
// this is used to update query from outside the component, for ex. using the help button within prop. pane
|
||||
useEffect(() => {
|
||||
if (modalOpen && resetSearchQuery) {
|
||||
setQuery(resetSearchQuery);
|
||||
} else {
|
||||
dispatch(setGlobalSearchQuery(""));
|
||||
if (!query) setActiveItemIndex(1);
|
||||
}
|
||||
}, [modalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveItemIndex(1);
|
||||
}, [query]);
|
||||
|
||||
const filteredWidgets = useMemo(() => {
|
||||
if (!query) return searchableWidgets;
|
||||
|
||||
return searchableWidgets.filter((widget: any) => {
|
||||
const page = pageMap[widget.pageId];
|
||||
const isPageNameMatching = isMatching(page?.pageName, query);
|
||||
const isWidgetNameMatching = isMatching(widget?.widgetName, query);
|
||||
|
||||
return isWidgetNameMatching || isPageNameMatching;
|
||||
});
|
||||
}, [allWidgets, query]);
|
||||
const filteredActions = useMemo(() => {
|
||||
if (!query) return actions;
|
||||
|
||||
return actions.filter((action: any) => {
|
||||
const page = pageMap[action?.config?.pageId];
|
||||
const isPageNameMatching = isMatching(page?.pageName, query);
|
||||
const isActionNameMatching = isMatching(action?.config?.name, query);
|
||||
|
||||
return isActionNameMatching || isPageNameMatching;
|
||||
});
|
||||
}, [actions, query]);
|
||||
const filteredPages = useMemo(() => {
|
||||
if (!query) return attachKind(pages, SEARCH_ITEM_TYPES.page);
|
||||
|
||||
return attachKind(
|
||||
pages.filter(
|
||||
(page: any) =>
|
||||
page.pageName.toLowerCase().indexOf(query?.toLowerCase()) > -1,
|
||||
),
|
||||
SEARCH_ITEM_TYPES.page,
|
||||
);
|
||||
}, [pages, query]);
|
||||
|
||||
const recentsSectionTitle = getSectionTitle("Recents", RecentIcon);
|
||||
const docsSectionTitle = getSectionTitle("Documentation Links", DocsIcon);
|
||||
const entitiesSectionTitle = getSectionTitle("Entities", EntitiesIcon);
|
||||
|
||||
const searchResults = useMemo(() => {
|
||||
if (!query) {
|
||||
return [
|
||||
recentsSectionTitle,
|
||||
...recentEntities,
|
||||
docsSectionTitle,
|
||||
...defaultDocs,
|
||||
];
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
const entities = [
|
||||
entitiesSectionTitle,
|
||||
...filteredPages,
|
||||
...filteredWidgets,
|
||||
...filteredActions,
|
||||
...filteredDatasources,
|
||||
];
|
||||
|
||||
if (entities.length > 1) {
|
||||
results.push(...entities);
|
||||
}
|
||||
|
||||
if (documentationSearchResults.length > 0) {
|
||||
results.push(docsSectionTitle, ...documentationSearchResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
}, [
|
||||
filteredWidgets,
|
||||
filteredActions,
|
||||
documentationSearchResults,
|
||||
filteredDatasources,
|
||||
query,
|
||||
recentEntities,
|
||||
]);
|
||||
|
||||
const activeItem = useMemo(() => {
|
||||
return searchResults[activeItemIndex] || {};
|
||||
}, [searchResults, activeItemIndex]);
|
||||
|
||||
const getNextActiveItem = (nextIndex: number) => {
|
||||
const max = Math.max(searchResults.length - 1, 0);
|
||||
if (nextIndex < 0) return max;
|
||||
else if (nextIndex > max) return 0;
|
||||
else return nextIndex;
|
||||
};
|
||||
|
||||
const handleUpKey = () => {
|
||||
let nextIndex = getNextActiveItem(activeItemIndex - 1);
|
||||
const activeItem = searchResults[nextIndex];
|
||||
if (activeItem && activeItem?.kind === SEARCH_ITEM_TYPES.sectionTitle) {
|
||||
nextIndex = getNextActiveItem(nextIndex - 1);
|
||||
}
|
||||
setActiveItemIndex(nextIndex);
|
||||
};
|
||||
|
||||
const handleDownKey = () => {
|
||||
let nextIndex = getNextActiveItem(activeItemIndex + 1);
|
||||
const activeItem = searchResults[nextIndex];
|
||||
if (activeItem && activeItem?.kind === SEARCH_ITEM_TYPES.sectionTitle) {
|
||||
nextIndex = getNextActiveItem(nextIndex + 1);
|
||||
}
|
||||
setActiveItemIndex(nextIndex);
|
||||
};
|
||||
|
||||
const { navigateToWidget } = useNavigateToWidget();
|
||||
|
||||
const handleDocumentationItemClick = (item: SearchItem) => {
|
||||
window.open(item.path.replace("master", HelpBaseURL), "_blank");
|
||||
};
|
||||
|
||||
const handleWidgetClick = (activeItem: SearchItem) => {
|
||||
toggleShow();
|
||||
navigateToWidget(
|
||||
activeItem.widgetId,
|
||||
activeItem.type,
|
||||
activeItem.pageId,
|
||||
selectedWidgetId === activeItem.widgetId,
|
||||
activeItem.parentModalId,
|
||||
);
|
||||
};
|
||||
|
||||
const handleActionClick = (item: SearchItem) => {
|
||||
const { config } = item;
|
||||
const { pageId, pluginType, id } = config;
|
||||
const actionConfig = getActionConfig(pluginType);
|
||||
const url = actionConfig?.getURL(params.applicationId, pageId, id);
|
||||
toggleShow();
|
||||
url && history.push(url);
|
||||
};
|
||||
|
||||
const handleDatasourceClick = (item: SearchItem) => {
|
||||
toggleShow();
|
||||
history.push(
|
||||
DATA_SOURCES_EDITOR_ID_URL(params.applicationId, item.pageId, item.id),
|
||||
);
|
||||
};
|
||||
|
||||
const handlePageClick = (item: SearchItem) => {
|
||||
toggleShow();
|
||||
history.push(BUILDER_PAGE_URL(params.applicationId, item.pageId));
|
||||
};
|
||||
|
||||
const itemClickHandlerByType = {
|
||||
[SEARCH_ITEM_TYPES.document]: handleDocumentationItemClick,
|
||||
[SEARCH_ITEM_TYPES.widget]: handleWidgetClick,
|
||||
[SEARCH_ITEM_TYPES.action]: handleActionClick,
|
||||
[SEARCH_ITEM_TYPES.datasource]: handleDatasourceClick,
|
||||
[SEARCH_ITEM_TYPES.page]: handlePageClick,
|
||||
[SEARCH_ITEM_TYPES.sectionTitle]: noop,
|
||||
};
|
||||
|
||||
const handleItemLinkClick = (itemArg?: SearchItem, source?: string) => {
|
||||
const item = itemArg || activeItem;
|
||||
const type = getItemType(item) as SEARCH_ITEM_TYPES;
|
||||
|
||||
AnalyticsUtil.logEvent("NAVIGATE_TO_ENTITY_FROM_OMNIBAR", {
|
||||
type,
|
||||
source,
|
||||
});
|
||||
|
||||
itemClickHandlerByType[type](item);
|
||||
};
|
||||
|
||||
const searchContext = {
|
||||
handleItemLinkClick,
|
||||
setActiveItemIndex,
|
||||
activeItemIndex,
|
||||
};
|
||||
|
||||
const hotKeyProps = {
|
||||
modalOpen,
|
||||
toggleShow,
|
||||
handleUpKey,
|
||||
handleDownKey,
|
||||
handleItemLinkClick,
|
||||
};
|
||||
|
||||
const activeItemType = useMemo(() => {
|
||||
return activeItem ? getItemType(activeItem) : undefined;
|
||||
}, [activeItem]);
|
||||
|
||||
return (
|
||||
<SearchContext.Provider value={searchContext}>
|
||||
<GlobalSearchHotKeys {...hotKeyProps}>
|
||||
<SearchModal toggleShow={toggleShow} modalOpen={modalOpen}>
|
||||
<AlgoliaSearchWrapper query={query}>
|
||||
<StyledContainer>
|
||||
<SearchBox query={query} setQuery={setQuery} />
|
||||
<div className="main">
|
||||
<SetSearchResults
|
||||
setDocumentationSearchResults={setDocumentationSearchResults}
|
||||
/>
|
||||
{searchResults.length > 0 ? (
|
||||
<>
|
||||
<SearchResults
|
||||
searchResults={searchResults}
|
||||
query={query}
|
||||
/>
|
||||
<Separator />
|
||||
<Description
|
||||
activeItem={activeItem}
|
||||
activeItemType={activeItemType}
|
||||
query={query}
|
||||
scrollPositionRef={scrollPositionRef}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ResultsNotFound />
|
||||
)}
|
||||
</div>
|
||||
</StyledContainer>
|
||||
</AlgoliaSearchWrapper>
|
||||
</SearchModal>
|
||||
</GlobalSearchHotKeys>
|
||||
</SearchContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearch;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// eslint-disable-next-line
|
||||
import parseDocumentationContent from "./parseDocumentationContent";
|
||||
|
||||
const expectedResult = `<h1><ais-highlight-0000000000>Security</ais-highlight-0000000000> <a class="documentation-cta" href="https://docs.appsmith.com/security" target="_blank">Open Documentation</a></h1><h2>Does Appsmith store my data?</h2>
|
||||
<p>No, Appsmith does not store any data returned from your API endpoints or DB queries. Appsmith only acts as a proxy layer. When you query your database/API endpoint, the Appsmith server only appends sensitive credentials before forwarding the request to your backend. The Appsmith server doesn't expose sensitive credentials to the browser because that can lead to <ais-highlight-0000000000>security</ais-highlight-0000000000> breaches. Such a routing ensures <ais-highlight-0000000000>security</ais-highlight-0000000000> of your systems and data.</p>
|
||||
<h2><ais-highlight-0000000000>Security</ais-highlight-0000000000> measures within Appsmith</h2>
|
||||
<p>Appsmith applications are secure-by-default. The <ais-highlight-0000000000>security</ais-highlight-0000000000> measures implemented for Appsmith installations are:</p>
|
||||
<ul>
|
||||
<li>On Appsmith Cloud, all connections are encrypted with TLS. For self-hosted instances, we offer the capability to setup SSL certificates via LetsEncrypt during the installation process.</li>
|
||||
<li>Encrypt all sensitive credentials such as database credentials with AES-256 encryption. Each self-hosted Appsmith instance is configured with unique salt and password values ensuring data-at-rest <ais-highlight-0000000000>security</ais-highlight-0000000000>.</li>
|
||||
<li>Appsmith Cloud will only connect to your databases/API endpoints through whitelisted IPs: 18.223.74.85 & 3.131.104.27. This ensures that you only have to expose database access to specific IPs when using our cloud offering.</li>
|
||||
<li>Appsmith Cloud is hosted in AWS data centers on servers that are SOC 1 and SOC 2 compliant. We also maintain data redundancy on our cloud instances via regular backups.</li>
|
||||
<li>Internal access to Appsmith Cloud is controlled through 2-factor authentication system along with audit logs.</li>
|
||||
<li>Maintain an open channel of communication with <ais-highlight-0000000000>security</ais-highlight-0000000000> researchers to allow them to report <ais-highlight-0000000000>security</ais-highlight-0000000000> vulnerabilities responsibly. If you notice a <ais-highlight-0000000000>security</ais-highlight-0000000000> vulnerability, please email <a href="mailto:security@appsmith.com" target="_blank"><ais-highlight-0000000000>security</ais-highlight-0000000000>@appsmith.com</a> and we'll resolve them ASAP.</li>
|
||||
</ul>`;
|
||||
|
||||
const sampleTitleResponse = `<ais-highlight-0000000000>Security</ais-highlight-0000000000>`;
|
||||
|
||||
const sampleDocumentResponse = `# Does Appsmith store my data?
|
||||
|
||||
No, Appsmith does not store any data returned from your API endpoints or DB queries. Appsmith only acts as a proxy layer. When you query your database/API endpoint, the Appsmith server only appends sensitive credentials before forwarding the request to your backend. The Appsmith server doesn't expose sensitive credentials to the browser because that can lead to <ais-highlight-0000000000>security</ais-highlight-0000000000> breaches. Such a routing ensures <ais-highlight-0000000000>security</ais-highlight-0000000000> of your systems and data.
|
||||
|
||||
# <ais-highlight-0000000000>Security</ais-highlight-0000000000> measures within Appsmith
|
||||
|
||||
Appsmith applications are secure-by-default. The <ais-highlight-0000000000>security</ais-highlight-0000000000> measures implemented for Appsmith installations are:
|
||||
|
||||
* On Appsmith Cloud, all connections are encrypted with TLS. For self-hosted instances, we offer the capability to setup SSL certificates via LetsEncrypt during the installation process.
|
||||
* Encrypt all sensitive credentials such as database credentials with AES-256 encryption. Each self-hosted Appsmith instance is configured with unique salt and password values ensuring data-at-rest <ais-highlight-0000000000>security</ais-highlight-0000000000>.
|
||||
* Appsmith Cloud will only connect to your databases/API endpoints through whitelisted IPs: 18.223.74.85 & 3.131.104.27. This ensures that you only have to expose database access to specific IPs when using our cloud offering.
|
||||
* Appsmith Cloud is hosted in AWS data centers on servers that are SOC 1 and SOC 2 compliant. We also maintain data redundancy on our cloud instances via regular backups.
|
||||
* Internal access to Appsmith Cloud is controlled through 2-factor authentication system along with audit logs.
|
||||
* Maintain an open channel of communication with <ais-highlight-0000000000>security</ais-highlight-0000000000> researchers to allow them to report <ais-highlight-0000000000>security</ais-highlight-0000000000> vulnerabilities responsibly. If you notice a <ais-highlight-0000000000>security</ais-highlight-0000000000> vulnerability, please email [<ais-highlight-0000000000>security</ais-highlight-0000000000>@appsmith.com](mailto:<ais-highlight-0000000000>security</ais-highlight-0000000000>@appsmith.com) and we'll resolve them ASAP.`;
|
||||
|
||||
describe("parseDocumentationContent", () => {
|
||||
it("works as expected", () => {
|
||||
const sampleItem = {
|
||||
rawTitle: sampleTitleResponse,
|
||||
rawDocument: sampleDocumentResponse,
|
||||
path: "master/security",
|
||||
};
|
||||
const result = parseDocumentationContent(sampleItem);
|
||||
expect(result).toStrictEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import marked from "marked";
|
||||
import { HelpBaseURL } from "constants/HelpConstants";
|
||||
import { algoliaHighlightTag } from "./utils";
|
||||
|
||||
/**
|
||||
* @param {String} HTML representing a single element
|
||||
* @return {Element}
|
||||
*/
|
||||
export const htmlToElement = (html: string) => {
|
||||
const template = document.createElement("template");
|
||||
html = html.trim(); // Never return a text node of whitespace as the result
|
||||
template.innerHTML = html;
|
||||
return template.content.firstChild;
|
||||
};
|
||||
|
||||
/**
|
||||
* strip:
|
||||
* gitbook plugin tags
|
||||
*/
|
||||
const strip = (text: string) => text.replace(/{% .*?%}/gm, "");
|
||||
|
||||
/**
|
||||
* strip: description tag from the top
|
||||
*/
|
||||
const stripMarkdown = (text: string) =>
|
||||
text.replace(/---\n[description]([\S\s]*?)---/gm, "");
|
||||
|
||||
const getDocumentationCTA = (path: any) => {
|
||||
const href = path.replace("master", HelpBaseURL);
|
||||
const htmlString = `<a class="documentation-cta" href="${href}" target="_blank">Open Documentation</a>`;
|
||||
return htmlToElement(htmlString);
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace all H1s with H2s
|
||||
* Check first child of body
|
||||
* if exact match as title -> replace with h1
|
||||
* else prepend h1
|
||||
* Append open documentation button to title
|
||||
*/
|
||||
const updateDocumentDescriptionTitle = (documentObj: any, item: any) => {
|
||||
const { rawTitle, path } = item;
|
||||
|
||||
Array.from(documentObj.querySelectorAll("h1")).forEach((match: any) => {
|
||||
match.outerHTML = `<h2>${match.innerHTML}</h2>`;
|
||||
});
|
||||
|
||||
let firstChild = documentObj.querySelector("body")
|
||||
?.firstChild as HTMLElement | null;
|
||||
|
||||
const matchesExactly = rawTitle === firstChild?.innerHTML;
|
||||
|
||||
// additional space for word-break
|
||||
if (matchesExactly && firstChild) {
|
||||
firstChild.outerHTML = `<h1>${firstChild?.innerHTML} </h1>`;
|
||||
} else {
|
||||
const h = document.createElement("h1");
|
||||
h.innerHTML = `${rawTitle} `;
|
||||
firstChild?.parentNode?.insertBefore(h, firstChild);
|
||||
}
|
||||
|
||||
firstChild = documentObj.querySelector("body")
|
||||
?.firstChild as HTMLElement | null;
|
||||
|
||||
if (firstChild) {
|
||||
// append documentation button after title:
|
||||
const ctaElement = getDocumentationCTA(path) as Node;
|
||||
firstChild.appendChild(ctaElement);
|
||||
}
|
||||
};
|
||||
|
||||
const replaceHintTagsWithCode = (text: string) => {
|
||||
let result = text.replace(/{% hint .*?%}/, "```");
|
||||
result = result.replace(/{% endhint .*?%}/, "```");
|
||||
result = marked(result);
|
||||
return result;
|
||||
};
|
||||
|
||||
const parseDocumentationContent = (item: any): string | undefined => {
|
||||
try {
|
||||
const { rawDocument } = item;
|
||||
let value = rawDocument;
|
||||
if (!value) return;
|
||||
|
||||
value = stripMarkdown(value);
|
||||
value = replaceHintTagsWithCode(value);
|
||||
|
||||
const parsedDocument = marked(value);
|
||||
|
||||
const domparser = new DOMParser();
|
||||
const documentObj = domparser.parseFromString(parsedDocument, "text/html");
|
||||
|
||||
// remove algolia highlight within code sections
|
||||
const aisTag = new RegExp(
|
||||
`<${algoliaHighlightTag}>|</${algoliaHighlightTag}>`,
|
||||
"g",
|
||||
);
|
||||
Array.from(documentObj.querySelectorAll("code")).forEach((match) => {
|
||||
match.innerHTML = match.innerHTML.replace(aisTag, "");
|
||||
});
|
||||
|
||||
// update link hrefs and target
|
||||
const aisTagEncoded = new RegExp(
|
||||
`%3C${algoliaHighlightTag}%3E|%3C/${algoliaHighlightTag}%3E`,
|
||||
"g",
|
||||
);
|
||||
|
||||
Array.from(documentObj.querySelectorAll("a")).forEach((match) => {
|
||||
match.target = "_blank";
|
||||
try {
|
||||
const hrefURL = new URL(match.href);
|
||||
const isRelativeURL = hrefURL.hostname === window.location.hostname;
|
||||
match.href = !isRelativeURL
|
||||
? match.href
|
||||
: `${HelpBaseURL}/${match.getAttribute("href")}`;
|
||||
match.href = match.href.replace(aisTagEncoded, "");
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
// update description title
|
||||
updateDocumentDescriptionTitle(documentObj, item);
|
||||
|
||||
const content = strip(documentObj.body.innerHTML).trim();
|
||||
return content;
|
||||
} catch (e) {
|
||||
console.log(e, "err");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export default parseDocumentationContent;
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "reducers";
|
||||
import { getPageList } from "selectors/editorSelectors";
|
||||
import { getActions, getAllWidgetsMap } from "selectors/entitiesSelector";
|
||||
import { SEARCH_ITEM_TYPES } from "./utils";
|
||||
import { get } from "lodash";
|
||||
|
||||
const recentEntitiesSelector = (state: AppState) =>
|
||||
state.ui.globalSearch.recentEntities;
|
||||
|
||||
const useResentEntities = () => {
|
||||
const widgetsMap = useSelector(getAllWidgetsMap);
|
||||
const recentEntities = useSelector(recentEntitiesSelector);
|
||||
const actions = useSelector(getActions);
|
||||
const reducerDatasources = useSelector((state: AppState) => {
|
||||
return state.entities.datasources.list;
|
||||
});
|
||||
|
||||
const pages = useSelector(getPageList) || [];
|
||||
|
||||
const populatedRecentEntities = recentEntities
|
||||
.map((entity) => {
|
||||
const { type, id, params } = entity;
|
||||
if (type === "page") {
|
||||
const result = pages.find((page) => page.pageId === id);
|
||||
if (result) {
|
||||
return {
|
||||
...result,
|
||||
kind: SEARCH_ITEM_TYPES.page,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (type === "datasource") {
|
||||
const datasource = reducerDatasources.find(
|
||||
(reducerDatasource) => reducerDatasource.id === id,
|
||||
);
|
||||
return (
|
||||
datasource && {
|
||||
...datasource,
|
||||
pageId: params?.pageId,
|
||||
}
|
||||
);
|
||||
} else if (type === "action")
|
||||
return actions.find((action) => action?.config?.id === id);
|
||||
else if (type === "widget") {
|
||||
return get(widgetsMap, id, null);
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return populatedRecentEntities;
|
||||
};
|
||||
|
||||
export default useResentEntities;
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import { Datasource } from "entities/Datasource";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export type RecentEntity = {
|
||||
type: string;
|
||||
id: string;
|
||||
params?: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
export enum SEARCH_ITEM_TYPES {
|
||||
document = "document",
|
||||
action = "action",
|
||||
widget = "widget",
|
||||
datasource = "datasource",
|
||||
page = "page",
|
||||
sectionTitle = "sectionTitle",
|
||||
}
|
||||
|
||||
export type DocSearchItem = {
|
||||
document?: string;
|
||||
title: string;
|
||||
_highlightResult: {
|
||||
document: { value: string };
|
||||
title: { value: string };
|
||||
};
|
||||
kind: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type SearchItem = DocSearchItem | Datasource | any;
|
||||
|
||||
// todo better checks here?
|
||||
export const getItemType = (item: SearchItem): SEARCH_ITEM_TYPES => {
|
||||
let type: SEARCH_ITEM_TYPES;
|
||||
if (item.widgetName) type = SEARCH_ITEM_TYPES.widget;
|
||||
else if (
|
||||
item.kind === SEARCH_ITEM_TYPES.document ||
|
||||
item.kind === SEARCH_ITEM_TYPES.page ||
|
||||
item.kind === SEARCH_ITEM_TYPES.sectionTitle
|
||||
)
|
||||
type = item.kind;
|
||||
else if (item.kind === SEARCH_ITEM_TYPES.page) type = SEARCH_ITEM_TYPES.page;
|
||||
else if (item.config?.name) type = SEARCH_ITEM_TYPES.action;
|
||||
else type = SEARCH_ITEM_TYPES.datasource;
|
||||
|
||||
return type;
|
||||
};
|
||||
|
||||
export const getItemTitle = (item: SearchItem): string => {
|
||||
const type = getItemType(item);
|
||||
|
||||
switch (type) {
|
||||
case SEARCH_ITEM_TYPES.action:
|
||||
return item?.config?.name;
|
||||
case SEARCH_ITEM_TYPES.widget:
|
||||
return item?.widgetName;
|
||||
case SEARCH_ITEM_TYPES.datasource:
|
||||
return item?.name;
|
||||
case SEARCH_ITEM_TYPES.page:
|
||||
return item?.pageName;
|
||||
case SEARCH_ITEM_TYPES.sectionTitle:
|
||||
return item?.title;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const defaultDocsConfig = [
|
||||
{
|
||||
link:
|
||||
"https://raw.githubusercontent.com/appsmithorg/appsmith-docs/v1.2.1/tutorial-1/README.md",
|
||||
title: "Tutorial",
|
||||
path: "master/tutorial-1",
|
||||
kind: "document",
|
||||
},
|
||||
{
|
||||
link:
|
||||
"https://raw.githubusercontent.com/appsmithorg/appsmith-docs/v1.2.1/core-concepts/connecting-to-data-sources/README.md",
|
||||
title: "Connecting to Data Sources",
|
||||
path: "master/core-concepts/connecting-to-data-sources",
|
||||
kind: "document",
|
||||
},
|
||||
{
|
||||
link:
|
||||
"https://raw.githubusercontent.com/appsmithorg/appsmith-docs/v1.2.1/core-concepts/displaying-data-read/README.md",
|
||||
title: "Displaying Data (Read)",
|
||||
path: "master/core-concepts/displaying-data-read",
|
||||
kind: "document",
|
||||
},
|
||||
{
|
||||
link:
|
||||
"https://raw.githubusercontent.com/appsmithorg/appsmith-docs/v1.2.1/core-concepts/writing-code/README.md",
|
||||
title: "Writing Code",
|
||||
path: "master/core-concepts/writing-code",
|
||||
kind: "document",
|
||||
},
|
||||
];
|
||||
|
||||
const githubDocsAssetsPath =
|
||||
"https://raw.githubusercontent.com/appsmithorg/appsmith-docs/v1.2.1/.gitbook";
|
||||
|
||||
export const useDefaultDocumentationResults = () => {
|
||||
const [defaultDocs, setDefaultDocs] = useState<DocSearchItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await Promise.all(
|
||||
defaultDocsConfig.map(async (doc: any) => {
|
||||
const response = await fetch(doc.link);
|
||||
let document = await response.text();
|
||||
const assetRegex = new RegExp("[../]*?/.gitbook", "g");
|
||||
document = document.replaceAll(assetRegex, githubDocsAssetsPath);
|
||||
return {
|
||||
_highlightResult: {
|
||||
document: {
|
||||
value: document,
|
||||
},
|
||||
title: {
|
||||
value: doc.title,
|
||||
},
|
||||
},
|
||||
...doc,
|
||||
} as DocSearchItem;
|
||||
}),
|
||||
);
|
||||
setDefaultDocs(data);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return defaultDocs;
|
||||
};
|
||||
|
||||
export const algoliaHighlightTag = "ais-highlight-0000000000";
|
||||
|
||||
export const attachKind = (source: any[], kind: string) => {
|
||||
return source.map((s) => ({
|
||||
...s,
|
||||
kind,
|
||||
}));
|
||||
};
|
||||
|
|
@ -10,7 +10,6 @@ import {
|
|||
} from "utils/hooks/dragResizeHooks";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { WidgetType } from "constants/WidgetConstants";
|
||||
import HelpControl from "./HelpControl";
|
||||
import PerformanceTracker, {
|
||||
PerformanceTransactionName,
|
||||
} from "utils/PerformanceTracker";
|
||||
|
|
@ -110,10 +109,6 @@ export const WidgetNameComponent = (props: WidgetNameComponentProps) => {
|
|||
return showWidgetName ? (
|
||||
<PositionStyle>
|
||||
<ControlGroup>
|
||||
<HelpControl
|
||||
type={props.type}
|
||||
show={selectedWidget === props.widgetId}
|
||||
/>
|
||||
<SettingsControl
|
||||
toggleSettings={togglePropertyEditor}
|
||||
activity={currentActivity}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export interface ControlData {
|
|||
dataType?: InputType;
|
||||
isRequired?: boolean;
|
||||
hidden?: HiddenType;
|
||||
placeholderText?: string;
|
||||
}
|
||||
|
||||
export interface ControlFunctions {
|
||||
|
|
|
|||
|
|
@ -104,6 +104,9 @@ const KeyValueRow = (props: KeyValueArrayProps & WrappedFieldArrayProps) => {
|
|||
name={`${field}.${keyName[1]}`}
|
||||
showError
|
||||
validate={keyFieldValidate}
|
||||
placeholder={
|
||||
(extraData && extraData[0].placeholderText) || ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!props.actionConfig && (
|
||||
|
|
@ -116,6 +119,9 @@ const KeyValueRow = (props: KeyValueArrayProps & WrappedFieldArrayProps) => {
|
|||
<StyledTextField
|
||||
name={`${field}.${valueName[1]}`}
|
||||
type={valueDataType}
|
||||
placeholder={
|
||||
(extraData && extraData[1].placeholderText) || ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{index === props.fields.length - 1 ? (
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
EditorTheme,
|
||||
TabBehaviour,
|
||||
} from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
const StyledOptionControlWrapper = styled(ControlWrapper)`
|
||||
display: flex;
|
||||
|
|
@ -174,32 +173,6 @@ class ChartDataControl extends BaseControl<ControlProps> {
|
|||
return [];
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.migrateChartData(this.props.propertyValue);
|
||||
}
|
||||
|
||||
migrateChartData(chartData: Array<{ seriesName: string; data: string }>) {
|
||||
// Added a migration script for older chart data that was strings
|
||||
// deprecate after enough charts have moved to the new format
|
||||
if (_.isString(chartData)) {
|
||||
try {
|
||||
const parsedData: Array<{
|
||||
seriesName: string;
|
||||
data: string;
|
||||
}> = JSON.parse(chartData);
|
||||
this.updateProperty(this.props.propertyName, parsedData);
|
||||
return parsedData;
|
||||
} catch (error) {
|
||||
Sentry.captureException({
|
||||
message: "Chart Migration Failed",
|
||||
oldData: this.props.propertyValue,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return this.props.propertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const chartData: Array<{ seriesName: string; data: string }> = _.isString(
|
||||
this.props.propertyValue,
|
||||
|
|
|
|||
|
|
@ -881,6 +881,27 @@ type ColorType = {
|
|||
activeTabBorderBottom: string;
|
||||
activeTabText: string;
|
||||
};
|
||||
globalSearch: {
|
||||
containerBackground: string;
|
||||
activeSearchItemBackground: string;
|
||||
searchInputText: string;
|
||||
containerShadow: string;
|
||||
separator: string;
|
||||
searchItemHighlight: string;
|
||||
searchItemText: string;
|
||||
highlightedTextUnderline: string;
|
||||
documentationCtaBackground: string;
|
||||
documentationCtaText: string;
|
||||
emptyStateText: string;
|
||||
navigateUsingEnterSection: string;
|
||||
codeBackground: string;
|
||||
documentLink: string;
|
||||
helpBarBackground: string;
|
||||
helpButtonBackground: string;
|
||||
helpBarBorder: string;
|
||||
sectionTitle: string;
|
||||
navigateToEntityEnterkey: string;
|
||||
};
|
||||
gif: {
|
||||
overlay: string;
|
||||
text: string;
|
||||
|
|
@ -916,7 +937,33 @@ const formMessage = {
|
|||
},
|
||||
};
|
||||
|
||||
const globalSearch = {
|
||||
containerBackground:
|
||||
"linear-gradient(0deg, rgba(43, 43, 43, 0.9), rgba(43, 43, 43, 0.9)), linear-gradient(119.61deg, rgba(35, 35, 35, 0.01) 0.43%, rgba(49, 49, 49, 0.01) 100.67%);",
|
||||
activeSearchItemBackground: "rgba(0, 0, 0, 0.24)",
|
||||
searchInputText: "#fff",
|
||||
containerShadow: "0px 0px 32px 8px rgba(0, 0, 0, 0.25)",
|
||||
separator: "#424242",
|
||||
searchItemHighlight: "#fff",
|
||||
searchItemText: "rgba(255, 255, 255, 0.6)",
|
||||
highlightedTextUnderline: "#03B365",
|
||||
helpBarText: "#C2C2C2",
|
||||
documentationCtaBackground: "rgba(3, 179, 101, 0.1)",
|
||||
documentationCtaText: "#03B365",
|
||||
emptyStateText: "#ABABAB",
|
||||
navigateUsingEnterSection: "#154E6B",
|
||||
codeBackground: "#494949",
|
||||
documentLink: "#54a9fb",
|
||||
helpBarBackground: "#000",
|
||||
helpButtonBackground: "#333333",
|
||||
helpBarBorder: "#404040",
|
||||
helpButtonBorder: "#404040",
|
||||
sectionTitle: "#D4D4D4",
|
||||
navigateToEntityEnterkey: "#3DA5D9",
|
||||
};
|
||||
|
||||
export const dark: ColorType = {
|
||||
globalSearch,
|
||||
header: {
|
||||
separator: darkShades[4],
|
||||
appName: darkShades[7],
|
||||
|
|
@ -1304,6 +1351,7 @@ export const dark: ColorType = {
|
|||
};
|
||||
|
||||
export const light: ColorType = {
|
||||
globalSearch,
|
||||
header: {
|
||||
separator: "#E0DEDE",
|
||||
appName: lightShades[8],
|
||||
|
|
@ -1780,18 +1828,30 @@ export const theme: Theme = {
|
|||
letterSpacing: -0.24,
|
||||
fontWeight: "normal",
|
||||
},
|
||||
authCardHeader: {
|
||||
cardHeader: {
|
||||
fontStyle: "normal",
|
||||
fontWeight: 600,
|
||||
fontSize: 25,
|
||||
lineHeight: 20,
|
||||
},
|
||||
authCardSubheader: {
|
||||
cardSubheader: {
|
||||
fontStyle: "normal",
|
||||
fontWeight: "normal",
|
||||
fontSize: 15,
|
||||
lineHeight: 20,
|
||||
},
|
||||
largeH1: {
|
||||
fontStyle: "normal",
|
||||
fontWeight: "bold",
|
||||
fontSize: 28,
|
||||
lineHeight: 36,
|
||||
},
|
||||
spacedOutP1: {
|
||||
fontStyle: "normal",
|
||||
fontWeight: "normal",
|
||||
fontSize: 14,
|
||||
lineHeight: 24,
|
||||
},
|
||||
},
|
||||
iconSizes: {
|
||||
XXS: 8,
|
||||
|
|
@ -2026,7 +2086,6 @@ export const theme: Theme = {
|
|||
|
||||
export const scrollbarLight = css<{ backgroundColor?: Color }>`
|
||||
scrollbar-color: ${(props) => props.theme.colors.paneText};
|
||||
|
||||
scrollbar-width: thin;
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
|
|
|
|||
|
|
@ -110,3 +110,6 @@ export const HelpMap = {
|
|||
};
|
||||
|
||||
export const HelpBaseURL = "https://docs.appsmith.com";
|
||||
|
||||
export const HELP_MODAL_WIDTH = 240;
|
||||
export const HELP_MODAL_HEIGHT = 206;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import { ERROR_CODES } from "constants/ApiConstants";
|
|||
import { AppLayoutConfig } from "reducers/entityReducers/pageListReducer";
|
||||
|
||||
export const ReduxActionTypes: { [key: string]: string } = {
|
||||
HANDLE_PATH_UPDATED: "HANDLE_PATH_UPDATED",
|
||||
RESET_EDITOR_REQUEST: "RESET_EDITOR_REQUEST",
|
||||
RESET_EDITOR_SUCCESS: "RESET_EDITOR_SUCCESS",
|
||||
INITIALIZE_EDITOR: "INITIALIZE_EDITOR",
|
||||
INITIALIZE_EDITOR_SUCCESS: "INITIALIZE_EDITOR_SUCCESS",
|
||||
REPORT_ERROR: "REPORT_ERROR",
|
||||
|
|
@ -337,11 +340,20 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
|||
START_EVALUATION: "START_EVALUATION",
|
||||
CURRENT_APPLICATION_NAME_UPDATE: "CURRENT_APPLICATION_NAME_UPDATE",
|
||||
CURRENT_APPLICATION_LAYOUT_UPDATE: "CURRENT_APPLICATION_LAYOUT_UPDATE",
|
||||
FORK_APPLICATION_INIT: "FORK_APPLICATION_INIT",
|
||||
FORK_APPLICATION_SUCCESS: "FORK_APPLICATION_SUCCESS",
|
||||
SET_WIDGET_LOADING: "SET_WIDGET_LOADING",
|
||||
SET_GLOBAL_SEARCH_QUERY: "SET_GLOBAL_SEARCH_QUERY",
|
||||
TOGGLE_SHOW_GLOBAL_SEARCH_MODAL: "TOGGLE_SHOW_GLOBAL_SEARCH_MODAL",
|
||||
FETCH_RELEASES_SUCCESS: "FETCH_RELEASES_SUCCESS",
|
||||
RESET_UNREAD_RELEASES_COUNT: "RESET_UNREAD_RELEASES_COUNT",
|
||||
SET_LOADING_ENTITIES: "SET_LOADING_ENTITIES",
|
||||
RESET_CURRENT_APPLICATION: "RESET_CURRENT_APPLICATION",
|
||||
UPDATE_RECENT_ENTITY: "UPDATE_RECENT_ENTITY",
|
||||
RESTORE_RECENT_ENTITIES_REQUEST: "RESTORE_RECENT_ENTITIES_REQUEST",
|
||||
RESTORE_RECENT_ENTITIES_SUCCESS: "RESTORE_RECENT_ENTITIES_SUCCESS",
|
||||
SET_RECENT_ENTITIES: "SET_RECENT_ENTITIES",
|
||||
RESET_RECENT_ENTITIES: "RESET_RECENT_ENTITIES",
|
||||
UPDATE_API_ACTION_BODY_CONTENT_TYPE: "UPDATE_API_ACTION_BODY_CONTENT_TYPE",
|
||||
};
|
||||
|
||||
|
|
@ -435,6 +447,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
|
|||
"FETCH_PROVIDER_DETAILS_BY_PROVIDER_ID_ERROR",
|
||||
SAVE_ACTION_NAME_ERROR: "SAVE_ACTION_NAME_ERROR",
|
||||
FETCH_USER_APPLICATIONS_ORGS_ERROR: "FETCH_USER_APPLICATIONS_ORGS_ERROR",
|
||||
FORK_APPLICATION_ERROR: "FORK_APPLICATION_ERROR",
|
||||
FETCH_ALL_USERS_ERROR: "FETCH_ALL_USERS_ERROR",
|
||||
FETCH_ALL_ROLES_ERROR: "FETCH_ALL_ROLES_ERROR",
|
||||
UPDATE_USER_DETAILS_ERROR: "UPDATE_USER_DETAILS_ERROR",
|
||||
|
|
@ -535,6 +548,7 @@ export type ApplicationPayload = {
|
|||
isPublic?: boolean;
|
||||
userPermissions?: string[];
|
||||
appIsExample: boolean;
|
||||
forkingEnabled?: boolean;
|
||||
appLayout?: AppLayoutConfig;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -190,3 +190,7 @@ export const LOCAL_STORAGE_QUOTA_EXCEEDED_MESSAGE =
|
|||
"Error saving a key in localStorage. You have exceeded the allowed storage size limit";
|
||||
export const LOCAL_STORAGE_NO_SPACE_LEFT_ON_DEVICE_MESSAGE =
|
||||
"Error saving a key in localStorage. You have run out of disk space";
|
||||
|
||||
export const OMNIBAR_PLACEHOLDER = "Search Widgets, Queries, Documentation";
|
||||
export const HELPBAR_PLACEHOLDER = "Quick search & navigation";
|
||||
export const NO_SEARCH_DATA_TEXT = "Search you must meaningful but";
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
const { match } = require("path-to-regexp");
|
||||
|
||||
export const BASE_URL = "/";
|
||||
export const ORG_URL = "/org";
|
||||
export const PAGE_NOT_FOUND_URL = "/404";
|
||||
|
|
@ -176,3 +178,8 @@ export const AUTH_LOGIN_URL = `${USER_AUTH_URL}/login`;
|
|||
|
||||
export const ORG_INVITE_USERS_PAGE_URL = `${ORG_URL}/invite`;
|
||||
export const ORG_SETTINGS_PAGE_URL = `${ORG_URL}/settings`;
|
||||
|
||||
export const matchApiPath = match(API_EDITOR_ID_URL());
|
||||
export const matchDatasourcePath = match(DATA_SOURCES_EDITOR_ID_URL());
|
||||
export const matchQueryPath = match(QUERIES_EDITOR_ID_URL());
|
||||
export const matchBuilderPath = match(BUILDER_URL);
|
||||
|
|
|
|||
|
|
@ -84,26 +84,28 @@ export const getAllPathsFromPropertyConfig = (
|
|||
if (controlConfig.children) {
|
||||
// Property in array structure
|
||||
const basePropertyPath = controlConfig.propertyName;
|
||||
const widgetPropertyValue = get(widget, basePropertyPath);
|
||||
widgetPropertyValue.forEach(
|
||||
(arrayPropertyValue: any, index: number) => {
|
||||
const arrayIndexPropertyPath = `${basePropertyPath}[${index}]`;
|
||||
controlConfig.children.forEach((childPropertyConfig: any) => {
|
||||
const childArrayPropertyPath = `${arrayIndexPropertyPath}.${childPropertyConfig.propertyName}`;
|
||||
if (
|
||||
childPropertyConfig.isBindProperty &&
|
||||
!childPropertyConfig.isTriggerProperty
|
||||
) {
|
||||
bindingPaths[childArrayPropertyPath] = true;
|
||||
} else if (
|
||||
childPropertyConfig.isBindProperty &&
|
||||
childPropertyConfig.isTriggerProperty
|
||||
) {
|
||||
triggerPaths[childArrayPropertyPath] = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
const widgetPropertyValue = get(widget, basePropertyPath, []);
|
||||
if (Array.isArray(widgetPropertyValue)) {
|
||||
widgetPropertyValue.forEach(
|
||||
(arrayPropertyValue: any, index: number) => {
|
||||
const arrayIndexPropertyPath = `${basePropertyPath}[${index}]`;
|
||||
controlConfig.children.forEach((childPropertyConfig: any) => {
|
||||
const childArrayPropertyPath = `${arrayIndexPropertyPath}.${childPropertyConfig.propertyName}`;
|
||||
if (
|
||||
childPropertyConfig.isBindProperty &&
|
||||
!childPropertyConfig.isTriggerProperty
|
||||
) {
|
||||
bindingPaths[childArrayPropertyPath] = true;
|
||||
} else if (
|
||||
childPropertyConfig.isBindProperty &&
|
||||
childPropertyConfig.isTriggerProperty
|
||||
) {
|
||||
triggerPaths[childArrayPropertyPath] = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { WidgetCardProps } from "widgets/BaseWidget";
|
||||
import { generateReactKey } from "utils/generators";
|
||||
import { keyBy } from "lodash";
|
||||
/* eslint-disable no-useless-computed-key */
|
||||
|
||||
const WidgetSidebarResponse: WidgetCardProps[] = [
|
||||
|
|
@ -101,3 +102,5 @@ const WidgetSidebarResponse: WidgetCardProps[] = [
|
|||
];
|
||||
|
||||
export default WidgetSidebarResponse;
|
||||
|
||||
export const widgetSidebarConfig = keyBy(WidgetSidebarResponse, "type");
|
||||
|
|
|
|||
|
|
@ -13,11 +13,7 @@ import {
|
|||
ApplicationPayload,
|
||||
PageListPayload,
|
||||
} from "constants/ReduxActionConstants";
|
||||
import {
|
||||
APPLICATIONS_URL,
|
||||
AUTH_LOGIN_URL,
|
||||
SIGN_UP_URL,
|
||||
} from "constants/routes";
|
||||
import { APPLICATIONS_URL, AUTH_LOGIN_URL } from "constants/routes";
|
||||
import { connect } from "react-redux";
|
||||
import { AppState } from "reducers";
|
||||
import { getEditorURL } from "selectors/appViewSelectors";
|
||||
|
|
@ -37,6 +33,7 @@ import ProfileDropdown from "pages/common/ProfileDropdown";
|
|||
import { Profile } from "pages/common/ProfileImage";
|
||||
import PageTabsContainer from "./PageTabsContainer";
|
||||
import { getThemeDetails, ThemeMode } from "selectors/themeSelectors";
|
||||
import ForkApplicationModal from "pages/Applications/ForkApplicationModal";
|
||||
|
||||
const HeaderWrapper = styled(StyledHeader)<{ hasPages: boolean }>`
|
||||
box-shadow: unset;
|
||||
|
|
@ -69,6 +66,14 @@ const HeaderWrapper = styled(StyledHeader)<{ hasPages: boolean }>`
|
|||
}
|
||||
}
|
||||
|
||||
.header__application-fork-btn-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header__application-fork-btn-wrapper .ads-dialog-trigger {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
& ${Profile} {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
|
@ -111,8 +116,8 @@ const ForkButton = styled(Cta)`
|
|||
svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
height: ${(props) => `calc(${props.theme.smallHeaderHeight})`};
|
||||
`;
|
||||
|
||||
const HeaderRightItemContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -136,7 +141,6 @@ type AppViewerHeaderProps = {
|
|||
|
||||
export const AppViewerHeader = (props: AppViewerHeaderProps) => {
|
||||
const { currentApplicationDetails, currentOrgId, currentUser, pages } = props;
|
||||
const isExampleApp = currentApplicationDetails?.appIsExample;
|
||||
const userPermissions = currentApplicationDetails?.userPermissions ?? [];
|
||||
const permissionRequired = PERMISSION_TYPE.MANAGE_APPLICATION;
|
||||
const canEdit = isPermitted(userPermissions, permissionRequired);
|
||||
|
|
@ -155,8 +159,7 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
|
|||
};
|
||||
if (hideHeader) return <HtmlTitle />;
|
||||
|
||||
const forkAppUrl = `${window.location.origin}${SIGN_UP_URL}?appId=${currentApplicationDetails?.id}`;
|
||||
const loginAppUrl = `${window.location.origin}${AUTH_LOGIN_URL}?appId=${currentApplicationDetails?.id}`;
|
||||
const redirectUrl = `${AUTH_LOGIN_URL}?redirectUrl=${window.location.href}`;
|
||||
|
||||
let CTA = null;
|
||||
|
||||
|
|
@ -169,11 +172,15 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
|
|||
text={EDIT_APP}
|
||||
/>
|
||||
);
|
||||
} else if (isExampleApp) {
|
||||
} else if (
|
||||
currentApplicationDetails?.forkingEnabled &&
|
||||
currentApplicationDetails?.isPublic &&
|
||||
currentUser?.username === ANONYMOUS_USERNAME
|
||||
) {
|
||||
CTA = (
|
||||
<ForkButton
|
||||
className="t--fork-app"
|
||||
href={forkAppUrl}
|
||||
href={redirectUrl}
|
||||
text={FORK_APP}
|
||||
icon="fork"
|
||||
/>
|
||||
|
|
@ -182,7 +189,7 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
|
|||
currentApplicationDetails?.isPublic &&
|
||||
currentUser?.username === ANONYMOUS_USERNAME
|
||||
) {
|
||||
CTA = <Cta className="t--fork-app" href={loginAppUrl} text={SIGN_IN} />;
|
||||
CTA = <Cta className="t--sign-in" href={redirectUrl} text={SIGN_IN} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -218,6 +225,15 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
|
|||
title={currentApplicationDetails.name}
|
||||
canOutsideClickClose={true}
|
||||
/>
|
||||
{currentUser &&
|
||||
currentUser.username !== ANONYMOUS_USERNAME &&
|
||||
currentApplicationDetails?.forkingEnabled && (
|
||||
<div className="header__application-fork-btn-wrapper">
|
||||
<ForkApplicationModal
|
||||
applicationId={currentApplicationDetails.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{CTA && (
|
||||
<HeaderRightItemContainer>{CTA}</HeaderRightItemContainer>
|
||||
)}
|
||||
|
|
|
|||
126
app/client/src/pages/Applications/ForkApplicationModal.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import Dialog from "components/ads/DialogComponent";
|
||||
import Button, { Size } from "components/ads/Button";
|
||||
import styled from "styled-components";
|
||||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
import Divider from "components/editorComponents/Divider";
|
||||
import { FORK_APP } from "constants/messages";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { getAllApplications } from "actions/applicationActions";
|
||||
import { useSelector } from "store";
|
||||
import { getUserApplicationsOrgs } from "selectors/applicationSelectors";
|
||||
import { isPermitted, PERMISSION_TYPE } from "./permissionHelpers";
|
||||
import RadioComponent from "components/ads/Radio";
|
||||
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
|
||||
const TriggerButton = styled(Button)`
|
||||
${(props) => getTypographyByKey(props, "btnLarge")}
|
||||
height: 100%;
|
||||
svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
margin-right: ${(props) => props.theme.spaces[7]}px;
|
||||
`;
|
||||
|
||||
const StyledDialog = styled(Dialog)`
|
||||
&& .${Classes.DIALOG_BODY} {
|
||||
padding-top: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRadioComponent = styled(RadioComponent)`
|
||||
label {
|
||||
font-size: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ForkButton = styled(Button)`
|
||||
height: 38px;
|
||||
width: 203px;
|
||||
`;
|
||||
|
||||
const OrganizationList = styled.div`
|
||||
overflow: auto;
|
||||
max-height: 250px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 20px;
|
||||
`;
|
||||
|
||||
const ForkApplicationModal = (props: any) => {
|
||||
const [organizationId, selectOrganizationId] = useState("");
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(getAllApplications());
|
||||
}, [dispatch, getAllApplications]);
|
||||
const userOrgs = useSelector(getUserApplicationsOrgs);
|
||||
|
||||
const forkApplication = () => {
|
||||
dispatch({
|
||||
type: ReduxActionTypes.FORK_APPLICATION_INIT,
|
||||
payload: {
|
||||
applicationId: props.applicationId,
|
||||
organizationId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const organizationList = useMemo(() => {
|
||||
const filteredUserOrgs = userOrgs.filter((item) => {
|
||||
const permitted = isPermitted(
|
||||
item.organization.userPermissions ?? [],
|
||||
PERMISSION_TYPE.CREATE_APPLICATION,
|
||||
);
|
||||
return permitted;
|
||||
});
|
||||
|
||||
if (filteredUserOrgs.length) {
|
||||
selectOrganizationId(filteredUserOrgs[0].organization.id);
|
||||
}
|
||||
|
||||
return filteredUserOrgs.map((org) => {
|
||||
return {
|
||||
label: org.organization.name,
|
||||
value: org.organization.id,
|
||||
};
|
||||
});
|
||||
}, [userOrgs]);
|
||||
|
||||
return (
|
||||
<StyledDialog
|
||||
title={"Select the organisation to fork"}
|
||||
maxHeight={"540px"}
|
||||
className={"fork-modal"}
|
||||
trigger={
|
||||
<TriggerButton
|
||||
text={FORK_APP}
|
||||
icon="fork"
|
||||
size={Size.small}
|
||||
className="t--fork-app"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Divider />
|
||||
{organizationList.length && (
|
||||
<OrganizationList>
|
||||
<StyledRadioComponent
|
||||
className={"radio-group"}
|
||||
columns={1}
|
||||
defaultValue={organizationList[0].value}
|
||||
options={organizationList}
|
||||
onSelect={(value) => selectOrganizationId(value)}
|
||||
/>
|
||||
</OrganizationList>
|
||||
)}
|
||||
<ForkButton
|
||||
disabled={!organizationId}
|
||||
text={"FORK"}
|
||||
onClick={forkApplication}
|
||||
size={Size.large}
|
||||
/>
|
||||
</StyledDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForkApplicationModal;
|
||||
|
|
@ -11,7 +11,6 @@ import {
|
|||
import AppInviteUsersForm from "pages/organization/AppInviteUsersForm";
|
||||
import StyledHeader from "components/designSystems/appsmith/StyledHeader";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import HelpModal from "components/designSystems/appsmith/help/HelpModal";
|
||||
import { FormDialogComponent } from "components/editorComponents/form/FormDialogComponent";
|
||||
import AppsmithLogo from "assets/images/appsmith_logo_square.png";
|
||||
import { Link } from "react-router-dom";
|
||||
|
|
@ -38,6 +37,7 @@ import EditableAppName from "./EditableAppName";
|
|||
import Boxed from "components/editorComponents/Onboarding/Boxed";
|
||||
import OnboardingHelper from "components/editorComponents/Onboarding/Helper";
|
||||
import { OnboardingStep } from "constants/OnboardingConstants";
|
||||
import GlobalSearch from "components/editorComponents/GlobalSearch";
|
||||
import EndOnboardingTour from "components/editorComponents/Onboarding/EndTour";
|
||||
import ProfileDropdown from "pages/common/ProfileDropdown";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
|
|
@ -46,6 +46,8 @@ import Button, { Size } from "components/ads/Button";
|
|||
import { IconWrapper } from "components/ads/Icon";
|
||||
import { Profile } from "pages/common/ProfileImage";
|
||||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
import HelpBar from "components/editorComponents/GlobalSearch/HelpBar";
|
||||
import HelpButton from "./HelpButton";
|
||||
import OnboardingIndicator from "components/editorComponents/Onboarding/Indicator";
|
||||
import { getThemeDetails, ThemeMode } from "selectors/themeSelectors";
|
||||
|
||||
|
|
@ -81,7 +83,10 @@ const HeaderWrapper = styled(StyledHeader)`
|
|||
}
|
||||
`;
|
||||
|
||||
// looks offset by 1px even though, checking bounding rect values
|
||||
const HeaderSection = styled.div`
|
||||
position: relative;
|
||||
top: -1px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
|
@ -90,6 +95,9 @@ const HeaderSection = styled.div`
|
|||
justify-content: flex-start;
|
||||
}
|
||||
:nth-child(2) {
|
||||
justify-content: center;
|
||||
}
|
||||
:nth-child(3) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
|
|
@ -231,6 +239,10 @@ export const EditorHeader = (props: EditorHeaderProps) => {
|
|||
)}
|
||||
</Boxed>
|
||||
</HeaderSection>
|
||||
<HeaderSection>
|
||||
<HelpBar />
|
||||
<HelpButton />
|
||||
</HeaderSection>
|
||||
<HeaderSection>
|
||||
<Boxed step={OnboardingStep.FINISH}>
|
||||
<SaveStatusContainer className={"t--save-status-container"}>
|
||||
|
|
@ -294,8 +306,8 @@ export const EditorHeader = (props: EditorHeaderProps) => {
|
|||
</ProfileDropdownContainer>
|
||||
)}
|
||||
</HeaderSection>
|
||||
<HelpModal page={"Editor"} />
|
||||
<OnboardingHelper />
|
||||
<GlobalSearch />
|
||||
</HeaderWrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { useDispatch, useSelector } from "react-redux";
|
|||
import TreeDropdown from "components/editorComponents/actioncreator/TreeDropdown";
|
||||
|
||||
import { AppState } from "reducers";
|
||||
import { getNextEntityName } from "utils/AppsmithUtils";
|
||||
import ContextMenuTrigger from "../ContextMenuTrigger";
|
||||
|
||||
import {
|
||||
|
|
@ -15,18 +14,7 @@ import {
|
|||
import { initExplorerEntityNameEdit } from "actions/explorerActions";
|
||||
import { ContextMenuPopoverModifiers } from "../helpers";
|
||||
import { noop } from "lodash";
|
||||
|
||||
const useNewAPIName = () => {
|
||||
// This takes into consideration only the current page widgets
|
||||
// If we're moving to a different page, there could be a widget
|
||||
// with the same name as the generated API name
|
||||
// TODO: Figure out how to handle this scenario
|
||||
const apiNames = useSelector((state: AppState) =>
|
||||
state.entities.actions.map((action) => action.config.name),
|
||||
);
|
||||
return (name: string) =>
|
||||
apiNames.indexOf(name) > -1 ? getNextEntityName(name, apiNames) : name;
|
||||
};
|
||||
import { useNewActionName } from "./helpers";
|
||||
|
||||
type EntityContextMenuProps = {
|
||||
id: string;
|
||||
|
|
@ -35,7 +23,7 @@ type EntityContextMenuProps = {
|
|||
pageId: string;
|
||||
};
|
||||
export const ActionEntityContextMenu = (props: EntityContextMenuProps) => {
|
||||
const nextEntityName = useNewAPIName();
|
||||
const nextEntityName = useNewActionName();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const copyActionToPage = useCallback(
|
||||
|
|
@ -44,7 +32,7 @@ export const ActionEntityContextMenu = (props: EntityContextMenuProps) => {
|
|||
copyActionRequest({
|
||||
id: actionId,
|
||||
destinationPageId: pageId,
|
||||
name: nextEntityName(`${actionName}Copy`),
|
||||
name: nextEntityName(`${actionName}Copy`, pageId),
|
||||
}),
|
||||
),
|
||||
[dispatch, nextEntityName],
|
||||
|
|
@ -56,7 +44,7 @@ export const ActionEntityContextMenu = (props: EntityContextMenuProps) => {
|
|||
id: actionId,
|
||||
destinationPageId,
|
||||
originalPageId: props.pageId,
|
||||
name: nextEntityName(actionName),
|
||||
name: nextEntityName(actionName, destinationPageId),
|
||||
}),
|
||||
),
|
||||
[dispatch, nextEntityName, props.pageId],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, { useCallback } from "react";
|
|||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { AppState } from "reducers";
|
||||
import { getNextEntityName } from "utils/AppsmithUtils";
|
||||
|
||||
import {
|
||||
moveActionRequest,
|
||||
|
|
@ -13,18 +12,7 @@ import {
|
|||
import { ContextMenuPopoverModifiers } from "../helpers";
|
||||
import { noop } from "lodash";
|
||||
import TreeDropdown from "components/ads/TreeDropdown";
|
||||
|
||||
const useNewAPIName = () => {
|
||||
// This takes into consideration only the current page widgets
|
||||
// If we're moving to a different page, there could be a widget
|
||||
// with the same name as the generated API name
|
||||
// TODO: Figure out how to handle this scenario
|
||||
const apiNames = useSelector((state: AppState) =>
|
||||
state.entities.actions.map((action) => action.config.name),
|
||||
);
|
||||
return (name: string) =>
|
||||
apiNames.indexOf(name) > -1 ? getNextEntityName(name, apiNames) : name;
|
||||
};
|
||||
import { useNewActionName } from "./helpers";
|
||||
|
||||
type EntityContextMenuProps = {
|
||||
id: string;
|
||||
|
|
@ -33,7 +21,7 @@ type EntityContextMenuProps = {
|
|||
pageId: string;
|
||||
};
|
||||
export const MoreActionsMenu = (props: EntityContextMenuProps) => {
|
||||
const nextEntityName = useNewAPIName();
|
||||
const nextEntityName = useNewActionName();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const copyActionToPage = useCallback(
|
||||
|
|
@ -42,7 +30,7 @@ export const MoreActionsMenu = (props: EntityContextMenuProps) => {
|
|||
copyActionRequest({
|
||||
id: actionId,
|
||||
destinationPageId: pageId,
|
||||
name: nextEntityName(`${actionName}Copy`),
|
||||
name: nextEntityName(`${actionName}Copy`, pageId),
|
||||
}),
|
||||
),
|
||||
[dispatch, nextEntityName],
|
||||
|
|
@ -54,7 +42,7 @@ export const MoreActionsMenu = (props: EntityContextMenuProps) => {
|
|||
id: actionId,
|
||||
destinationPageId,
|
||||
originalPageId: props.pageId,
|
||||
name: nextEntityName(actionName),
|
||||
name: nextEntityName(actionName, destinationPageId),
|
||||
}),
|
||||
),
|
||||
[dispatch, nextEntityName, props.pageId],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { ReactNode } from "react";
|
||||
import React, { ReactNode, useMemo } from "react";
|
||||
import { apiIcon, dbQueryIcon, MethodTag, QueryIcon } from "../ExplorerIcons";
|
||||
import { PluginType } from "entities/Action";
|
||||
import { generateReactKey } from "utils/generators";
|
||||
|
|
@ -15,6 +15,11 @@ import { ExplorerURLParams } from "../helpers";
|
|||
import { Datasource } from "entities/Datasource";
|
||||
import { Plugin } from "api/PluginApi";
|
||||
import PluginGroup from "../PluginGroup/PluginGroup";
|
||||
import { useSelector } from "react-redux";
|
||||
import { AppState } from "reducers";
|
||||
import { groupBy } from "lodash";
|
||||
import { ActionData } from "reducers/entityReducers/actionsReducer";
|
||||
import { getNextEntityName } from "utils/AppsmithUtils";
|
||||
|
||||
export type ActionGroupConfig = {
|
||||
groupName: string;
|
||||
|
|
@ -90,6 +95,12 @@ export const ACTION_PLUGIN_MAP: Array<
|
|||
}
|
||||
});
|
||||
|
||||
export const getActionConfig = (type: PluginType) =>
|
||||
ACTION_PLUGIN_MAP.find(
|
||||
(configByType: ActionGroupConfig | undefined) =>
|
||||
configByType?.type === type,
|
||||
);
|
||||
|
||||
export const getPluginGroups = (
|
||||
page: Page,
|
||||
step: number,
|
||||
|
|
@ -135,3 +146,25 @@ export const getPluginGroups = (
|
|||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const useNewActionName = () => {
|
||||
// This takes into consideration only the current page widgets
|
||||
// If we're moving to a different page, there could be a widget
|
||||
// with the same name as the generated API name
|
||||
// TODO: Figure out how to handle this scenario
|
||||
const actions = useSelector((state: AppState) => state.entities.actions);
|
||||
const groupedActions = useMemo(() => {
|
||||
return groupBy(actions, "config.pageId");
|
||||
}, [actions]);
|
||||
return (name: string, destinationPageId: string) => {
|
||||
const pageActions = groupedActions[destinationPageId];
|
||||
// Get action names of the destination page only
|
||||
const actionNames = pageActions
|
||||
? pageActions.map((action: ActionData) => action.config.name)
|
||||
: [];
|
||||
|
||||
return actionNames.indexOf(name) > -1
|
||||
? getNextEntityName(name, actionNames)
|
||||
: name;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { hiddenPageIcon, homePageIcon, pageIcon } from "../ExplorerIcons";
|
|||
import { getPluginGroups } from "../Actions/helpers";
|
||||
import ExplorerWidgetGroup from "../Widgets/WidgetGroup";
|
||||
import { resolveAsSpaceChar } from "utils/helpers";
|
||||
import { CanvasStructure } from "reducers/uiReducers/pageCanvasStructure";
|
||||
import { CanvasStructure } from "reducers/uiReducers/pageCanvasStructureReducer";
|
||||
import { Datasource } from "entities/Datasource";
|
||||
import { Plugin } from "api/PluginApi";
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { ExplorerURLParams } from "../helpers";
|
|||
import { Page } from "constants/ReduxActionConstants";
|
||||
import ExplorerPageEntity from "./PageEntity";
|
||||
import { AppState } from "reducers";
|
||||
import { CanvasStructure } from "reducers/uiReducers/pageCanvasStructure";
|
||||
import { CanvasStructure } from "reducers/uiReducers/pageCanvasStructureReducer";
|
||||
import { Datasource } from "entities/Datasource";
|
||||
import { Plugin } from "api/PluginApi";
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import WidgetContextMenu from "./WidgetContextMenu";
|
|||
import { updateWidgetName } from "actions/propertyPaneActions";
|
||||
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
|
||||
import EntityProperties from "../Entity/EntityProperties";
|
||||
import { CanvasStructure } from "reducers/uiReducers/pageCanvasStructure";
|
||||
import { CanvasStructure } from "reducers/uiReducers/pageCanvasStructureReducer";
|
||||
import CurrentPageEntityProperties from "../Entity/CurrentPageEntityProperties";
|
||||
|
||||
export type WidgetTree = WidgetProps & { children?: WidgetTree[] };
|
||||
|
|
@ -43,15 +43,42 @@ export const navigateToCanvas = (
|
|||
}
|
||||
};
|
||||
|
||||
export const useNavigateToWidget = () => {
|
||||
const params = useParams<ExplorerURLParams>();
|
||||
const dispatch = useDispatch();
|
||||
const { selectWidget } = useWidgetSelection();
|
||||
|
||||
const navigateToWidget = useCallback(
|
||||
(
|
||||
widgetId: string,
|
||||
widgetType: WidgetType,
|
||||
pageId: string,
|
||||
isWidgetSelected?: boolean,
|
||||
parentModalId?: string,
|
||||
) => {
|
||||
if (widgetType === WidgetTypes.MODAL_WIDGET) {
|
||||
dispatch(showModal(widgetId));
|
||||
return;
|
||||
}
|
||||
if (parentModalId) dispatch(showModal(parentModalId));
|
||||
else dispatch(closeAllModals());
|
||||
navigateToCanvas(params, window.location.pathname, pageId, widgetId);
|
||||
flashElementById(widgetId);
|
||||
if (!isWidgetSelected) selectWidget(widgetId);
|
||||
dispatch(forceOpenPropertyPane(widgetId));
|
||||
},
|
||||
[dispatch, params, selectWidget],
|
||||
);
|
||||
|
||||
return { navigateToWidget };
|
||||
};
|
||||
|
||||
const useWidget = (
|
||||
widgetId: string,
|
||||
widgetType: WidgetType,
|
||||
pageId: string,
|
||||
parentModalId?: string,
|
||||
) => {
|
||||
const params = useParams<ExplorerURLParams>();
|
||||
const dispatch = useDispatch();
|
||||
const { selectWidget } = useWidgetSelection();
|
||||
const selectedWidget = useSelector(
|
||||
(state: AppState) => state.ui.widgetDragResize.selectedWidget,
|
||||
);
|
||||
|
|
@ -60,29 +87,21 @@ const useWidget = (
|
|||
widgetId,
|
||||
]);
|
||||
|
||||
const navigateToWidget = useCallback(() => {
|
||||
if (widgetType === WidgetTypes.MODAL_WIDGET) {
|
||||
dispatch(showModal(widgetId));
|
||||
return;
|
||||
}
|
||||
if (parentModalId) dispatch(showModal(parentModalId));
|
||||
else dispatch(closeAllModals());
|
||||
navigateToCanvas(params, window.location.pathname, pageId, widgetId);
|
||||
flashElementById(widgetId);
|
||||
if (!isWidgetSelected) selectWidget(widgetId);
|
||||
dispatch(forceOpenPropertyPane(widgetId));
|
||||
}, [
|
||||
dispatch,
|
||||
params,
|
||||
selectWidget,
|
||||
widgetType,
|
||||
widgetId,
|
||||
parentModalId,
|
||||
pageId,
|
||||
isWidgetSelected,
|
||||
]);
|
||||
const { navigateToWidget } = useNavigateToWidget();
|
||||
|
||||
return { navigateToWidget, isWidgetSelected };
|
||||
const boundNavigateToWidget = useCallback(
|
||||
() =>
|
||||
navigateToWidget(
|
||||
widgetId,
|
||||
widgetType,
|
||||
pageId,
|
||||
isWidgetSelected,
|
||||
parentModalId,
|
||||
),
|
||||
[widgetId, widgetType, pageId, isWidgetSelected, parentModalId],
|
||||
);
|
||||
|
||||
return { navigateToWidget: boundNavigateToWidget, isWidgetSelected };
|
||||
};
|
||||
|
||||
export type WidgetEntityProps = {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { BUILDER_PAGE_URL } from "constants/routes";
|
|||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { AppState } from "reducers";
|
||||
import { CanvasStructure } from "reducers/uiReducers/pageCanvasStructure";
|
||||
import { CanvasStructure } from "reducers/uiReducers/pageCanvasStructureReducer";
|
||||
|
||||
type ExplorerWidgetGroupProps = {
|
||||
pageId: string;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { debounce } from "lodash";
|
|||
import { WidgetProps } from "widgets/BaseWidget";
|
||||
import log from "loglevel";
|
||||
import produce from "immer";
|
||||
import { CanvasStructure } from "reducers/uiReducers/pageCanvasStructure";
|
||||
import { CanvasStructure } from "reducers/uiReducers/pageCanvasStructureReducer";
|
||||
|
||||
const findWidgets = (widgets: CanvasStructure, keyword: string) => {
|
||||
if (!widgets || !widgets.widgetName) return widgets;
|
||||
|
|
|
|||
156
app/client/src/pages/Editor/GlobalHotKeys.tsx
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { AppState } from "reducers";
|
||||
import { Hotkey, Hotkeys } from "@blueprintjs/core";
|
||||
import { HotkeysTarget } from "@blueprintjs/core/lib/esnext/components/hotkeys/hotkeysTarget.js";
|
||||
import {
|
||||
copyWidget,
|
||||
cutWidget,
|
||||
deleteSelectedWidget,
|
||||
pasteWidget,
|
||||
} from "actions/widgetActions";
|
||||
import { toggleShowGlobalSearchModal } from "actions/globalSearchActions";
|
||||
import { isMac } from "utils/helpers";
|
||||
import { getSelectedWidget } from "selectors/ui";
|
||||
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
|
||||
import { getSelectedText } from "utils/helpers";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import {
|
||||
ENTITY_EXPLORER_SEARCH_ID,
|
||||
WIDGETS_SEARCH_ID,
|
||||
} from "constants/Explorer";
|
||||
|
||||
type Props = {
|
||||
copySelectedWidget: () => void;
|
||||
pasteCopiedWidget: () => void;
|
||||
deleteSelectedWidget: () => void;
|
||||
cutSelectedWidget: () => void;
|
||||
toggleShowGlobalSearchModal: () => void;
|
||||
selectedWidget?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
@HotkeysTarget
|
||||
class GlobalHotKeys extends React.Component<Props> {
|
||||
public stopPropagationIfWidgetSelected(e: KeyboardEvent): boolean {
|
||||
if (
|
||||
this.props.selectedWidget &&
|
||||
this.props.selectedWidget != MAIN_CONTAINER_WIDGET_ID &&
|
||||
!getSelectedText()
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public renderHotkeys() {
|
||||
return (
|
||||
<Hotkeys>
|
||||
<Hotkey
|
||||
global={true}
|
||||
combo="mod + f"
|
||||
label="Search entities"
|
||||
onKeyDown={(e: any) => {
|
||||
const entitySearchInput = document.getElementById(
|
||||
ENTITY_EXPLORER_SEARCH_ID,
|
||||
);
|
||||
const widgetSearchInput = document.getElementById(
|
||||
WIDGETS_SEARCH_ID,
|
||||
);
|
||||
if (entitySearchInput) entitySearchInput.focus();
|
||||
if (widgetSearchInput) widgetSearchInput.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
<Hotkey
|
||||
combo="mod + k"
|
||||
onKeyDown={(e: KeyboardEvent) => {
|
||||
console.log("toggleShowGlobalSearchModal");
|
||||
e.preventDefault();
|
||||
this.props.toggleShowGlobalSearchModal();
|
||||
AnalyticsUtil.logEvent("OPEN_OMNIBAR", { source: "HOTKEY_COMBO" });
|
||||
}}
|
||||
allowInInput={false}
|
||||
label="Show omnibar"
|
||||
global={true}
|
||||
/>
|
||||
<Hotkey
|
||||
global={true}
|
||||
combo="mod + c"
|
||||
label="Copy Widget"
|
||||
group="Canvas"
|
||||
onKeyDown={(e: any) => {
|
||||
if (this.stopPropagationIfWidgetSelected(e)) {
|
||||
this.props.copySelectedWidget();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Hotkey
|
||||
global={true}
|
||||
combo="mod + v"
|
||||
label="Paste Widget"
|
||||
group="Canvas"
|
||||
onKeyDown={() => {
|
||||
this.props.pasteCopiedWidget();
|
||||
}}
|
||||
/>
|
||||
<Hotkey
|
||||
global={true}
|
||||
combo="backspace"
|
||||
label="Delete Widget"
|
||||
group="Canvas"
|
||||
onKeyDown={(e: any) => {
|
||||
if (this.stopPropagationIfWidgetSelected(e) && isMac()) {
|
||||
this.props.deleteSelectedWidget();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Hotkey
|
||||
global={true}
|
||||
combo="del"
|
||||
label="Delete Widget"
|
||||
group="Canvas"
|
||||
onKeyDown={(e: any) => {
|
||||
if (this.stopPropagationIfWidgetSelected(e)) {
|
||||
this.props.deleteSelectedWidget();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Hotkey
|
||||
global={true}
|
||||
combo="mod + x"
|
||||
label="Cut Widget"
|
||||
group="Canvas"
|
||||
onKeyDown={(e: any) => {
|
||||
if (this.stopPropagationIfWidgetSelected(e)) {
|
||||
this.props.cutSelectedWidget();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>{this.props.children}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
selectedWidget: getSelectedWidget(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
copySelectedWidget: () => dispatch(copyWidget(true)),
|
||||
pasteCopiedWidget: () => dispatch(pasteWidget()),
|
||||
deleteSelectedWidget: () => dispatch(deleteSelectedWidget(true)),
|
||||
cutSelectedWidget: () => dispatch(cutWidget()),
|
||||
toggleShowGlobalSearchModal: () => dispatch(toggleShowGlobalSearchModal()),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GlobalHotKeys);
|
||||
64
app/client/src/pages/Editor/HelpButton.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import React from "react";
|
||||
import styled, { createGlobalStyle } from "styled-components";
|
||||
import { Popover, Position } from "@blueprintjs/core";
|
||||
|
||||
import DocumentationSearch from "components/designSystems/appsmith/help/DocumentationSearch";
|
||||
import Icon, { IconSize } from "components/ads/Icon";
|
||||
|
||||
import { HELP_MODAL_WIDTH } from "constants/HelpConstants";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
|
||||
const HelpPopoverStyle = createGlobalStyle`
|
||||
.bp3-popover.bp3-minimal.navbar-help-popover {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTrigger = styled.div`
|
||||
cursor: pointer;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 ${(props) => props.theme.spaces[2]}px;
|
||||
background: ${(props) =>
|
||||
props.theme.colors.globalSearch.helpButtonBackground};
|
||||
`;
|
||||
|
||||
const Trigger = () => (
|
||||
<StyledTrigger>
|
||||
<Icon name="help" size={IconSize.XS} />
|
||||
</StyledTrigger>
|
||||
);
|
||||
|
||||
const onOpened = () => {
|
||||
AnalyticsUtil.logEvent("OPEN_HELP", { page: "Editor" });
|
||||
};
|
||||
const HelpButton = () => {
|
||||
return (
|
||||
<Popover
|
||||
modifiers={{
|
||||
offset: {
|
||||
enabled: true,
|
||||
offset: "0, 6",
|
||||
},
|
||||
}}
|
||||
minimal
|
||||
position={Position.BOTTOM_RIGHT}
|
||||
onOpened={onOpened}
|
||||
popoverClassName="navbar-help-popover"
|
||||
>
|
||||
<>
|
||||
<HelpPopoverStyle />
|
||||
<Trigger />
|
||||
</>
|
||||
<div style={{ width: HELP_MODAL_WIDTH }}>
|
||||
<DocumentationSearch hitsPerPage={4} hideSearch hideMinimizeBtn />
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpButton;
|
||||
39
app/client/src/pages/Editor/PropertyPaneHelpButton.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { withTheme } from "styled-components";
|
||||
import { Icon } from "@blueprintjs/core";
|
||||
|
||||
import {
|
||||
setGlobalSearchQuery,
|
||||
toggleShowGlobalSearchModal,
|
||||
} from "actions/globalSearchActions";
|
||||
import { getSelectedWidget } from "sagas/selectors";
|
||||
import { Theme } from "constants/DefaultTheme";
|
||||
import { widgetSidebarConfig } from "mockResponses/WidgetSidebarResponse";
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
const PropertyPaneHelpButton = withTheme(({ theme }: Props) => {
|
||||
const selectedWidget = useSelector(getSelectedWidget);
|
||||
const selectedWidgetType = selectedWidget?.type;
|
||||
const dispatch = useDispatch();
|
||||
const config = selectedWidgetType && widgetSidebarConfig[selectedWidgetType];
|
||||
|
||||
const openHelpModal = useCallback(() => {
|
||||
dispatch(setGlobalSearchQuery(config?.widgetCardName || ""));
|
||||
dispatch(toggleShowGlobalSearchModal());
|
||||
}, [selectedWidgetType]);
|
||||
|
||||
return (
|
||||
<Icon
|
||||
onClick={openHelpModal}
|
||||
color={theme.colors.paneSectionLabel}
|
||||
icon="help"
|
||||
iconSize={16}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default PropertyPaneHelpButton;
|
||||
|
|
@ -11,7 +11,7 @@ import { getExistingWidgetNames } from "sagas/selectors";
|
|||
import { removeSpecialChars } from "utils/helpers";
|
||||
import { useToggleEditWidgetName } from "utils/hooks/dragResizeHooks";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { BindingText } from "pages/Editor/APIEditor/Form";
|
||||
import PropertyPaneHelpButton from "pages/Editor/PropertyPaneHelpButton";
|
||||
|
||||
import { Icon, Tooltip, Position, Classes } from "@blueprintjs/core";
|
||||
import { WidgetType } from "constants/WidgetConstants";
|
||||
|
|
@ -19,6 +19,7 @@ import { theme } from "constants/DefaultTheme";
|
|||
import { ControlIcons } from "icons/ControlIcons";
|
||||
import { FormIcons } from "icons/FormIcons";
|
||||
import { deleteSelectedWidget, copyWidget } from "actions/widgetActions";
|
||||
|
||||
const CopyIcon = ControlIcons.COPY_CONTROL;
|
||||
const DeleteIcon = FormIcons.DELETE_ICON;
|
||||
const Wrapper = styled.div`
|
||||
|
|
@ -151,18 +152,12 @@ const PropertyPaneTitle = memo((props: PropertyPaneTitleProps) => {
|
|||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
<div>
|
||||
<span>You can connect data from your API by adding </span>
|
||||
<BindingText>{`{{apiName.data}}`}</BindingText>
|
||||
<span> to a widget property</span>
|
||||
</div>
|
||||
}
|
||||
content={<span>Explore widget related docs</span>}
|
||||
position={Position.TOP}
|
||||
hoverOpenDelay={200}
|
||||
boundary="window"
|
||||
>
|
||||
<Icon color={theme.colors.paneSectionLabel} icon="help" iconSize={16} />
|
||||
<PropertyPaneHelpButton />
|
||||
</Tooltip>
|
||||
<Tooltip content="Close" position={Position.TOP} hoverOpenDelay={200}>
|
||||
<Icon
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { Component } from "react";
|
|||
import { Helmet } from "react-helmet";
|
||||
import { connect } from "react-redux";
|
||||
import { RouteComponentProps, withRouter } from "react-router-dom";
|
||||
import { Spinner } from "@blueprintjs/core";
|
||||
import { BuilderRouteParams } from "constants/routes";
|
||||
import { AppState } from "reducers";
|
||||
import MainContainer from "./MainContainer";
|
||||
|
|
@ -15,32 +16,22 @@ import {
|
|||
getIsPublishingApplication,
|
||||
getPublishingError,
|
||||
} from "selectors/editorSelectors";
|
||||
import { Hotkey, Hotkeys, Spinner } from "@blueprintjs/core";
|
||||
import { HotkeysTarget } from "@blueprintjs/core/lib/esnext/components/hotkeys/hotkeysTarget.js";
|
||||
import { initEditor } from "actions/initActions";
|
||||
import { initEditor, resetEditorRequest } from "actions/initActions";
|
||||
import { editorInitializer } from "utils/EditorUtils";
|
||||
import {
|
||||
ENTITY_EXPLORER_SEARCH_ID,
|
||||
WIDGETS_SEARCH_ID,
|
||||
} from "constants/Explorer";
|
||||
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
import { User } from "constants/userConstants";
|
||||
import ConfirmRunModal from "pages/Editor/ConfirmRunModal";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import {
|
||||
copyWidget,
|
||||
cutWidget,
|
||||
deleteSelectedWidget,
|
||||
pasteWidget,
|
||||
} from "actions/widgetActions";
|
||||
import { isMac } from "utils/helpers";
|
||||
import { getSelectedWidget } from "selectors/ui";
|
||||
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
|
||||
import Welcome from "./Welcome";
|
||||
import { getThemeDetails, ThemeMode } from "selectors/themeSelectors";
|
||||
import { ThemeProvider } from "styled-components";
|
||||
import { Theme } from "constants/DefaultTheme";
|
||||
import GlobalHotKeys from "./GlobalHotKeys";
|
||||
import { handlePathUpdated } from "actions/recentEntityActions";
|
||||
|
||||
import history from "utils/history";
|
||||
|
||||
type EditorProps = {
|
||||
currentApplicationId?: string;
|
||||
|
|
@ -52,115 +43,18 @@ type EditorProps = {
|
|||
isEditorInitializeError: boolean;
|
||||
errorPublishing: boolean;
|
||||
creatingOnboardingDatabase: boolean;
|
||||
copySelectedWidget: () => void;
|
||||
pasteCopiedWidget: () => void;
|
||||
deleteSelectedWidget: () => void;
|
||||
cutSelectedWidget: () => void;
|
||||
user?: User;
|
||||
selectedWidget?: string;
|
||||
lightTheme: Theme;
|
||||
resetEditorRequest: () => void;
|
||||
handlePathUpdated: (pathName: string) => void;
|
||||
};
|
||||
|
||||
type Props = EditorProps & RouteComponentProps<BuilderRouteParams>;
|
||||
|
||||
const getSelectedText = () => {
|
||||
if (typeof window.getSelection === "function") {
|
||||
const selectionObj = window.getSelection();
|
||||
return selectionObj && selectionObj.toString();
|
||||
}
|
||||
};
|
||||
|
||||
@HotkeysTarget
|
||||
class Editor extends Component<Props> {
|
||||
public stopPropagationIfWidgetSelected(e: KeyboardEvent): boolean {
|
||||
if (
|
||||
this.props.selectedWidget &&
|
||||
this.props.selectedWidget != MAIN_CONTAINER_WIDGET_ID &&
|
||||
!getSelectedText()
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
unlisten: any;
|
||||
|
||||
public renderHotkeys() {
|
||||
return (
|
||||
<Hotkeys>
|
||||
<Hotkey
|
||||
global={true}
|
||||
combo="mod + f"
|
||||
label="Search entities"
|
||||
onKeyDown={(e: any) => {
|
||||
const entitySearchInput = document.getElementById(
|
||||
ENTITY_EXPLORER_SEARCH_ID,
|
||||
);
|
||||
const widgetSearchInput = document.getElementById(
|
||||
WIDGETS_SEARCH_ID,
|
||||
);
|
||||
if (entitySearchInput) entitySearchInput.focus();
|
||||
if (widgetSearchInput) widgetSearchInput.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
<Hotkey
|
||||
global={true}
|
||||
combo="mod + c"
|
||||
label="Copy Widget"
|
||||
group="Canvas"
|
||||
onKeyDown={(e: any) => {
|
||||
if (this.stopPropagationIfWidgetSelected(e)) {
|
||||
this.props.copySelectedWidget();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Hotkey
|
||||
global={true}
|
||||
combo="mod + v"
|
||||
label="Paste Widget"
|
||||
group="Canvas"
|
||||
onKeyDown={() => {
|
||||
this.props.pasteCopiedWidget();
|
||||
}}
|
||||
/>
|
||||
<Hotkey
|
||||
global={true}
|
||||
combo="backspace"
|
||||
label="Delete Widget"
|
||||
group="Canvas"
|
||||
onKeyDown={(e: any) => {
|
||||
if (this.stopPropagationIfWidgetSelected(e) && isMac()) {
|
||||
this.props.deleteSelectedWidget();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Hotkey
|
||||
global={true}
|
||||
combo="del"
|
||||
label="Delete Widget"
|
||||
group="Canvas"
|
||||
onKeyDown={(e: any) => {
|
||||
if (this.stopPropagationIfWidgetSelected(e)) {
|
||||
this.props.deleteSelectedWidget();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Hotkey
|
||||
global={true}
|
||||
combo="mod + x"
|
||||
label="Cut Widget"
|
||||
group="Canvas"
|
||||
onKeyDown={(e: any) => {
|
||||
if (this.stopPropagationIfWidgetSelected(e)) {
|
||||
this.props.cutSelectedWidget();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
public state = {
|
||||
registered: false,
|
||||
};
|
||||
|
|
@ -173,6 +67,8 @@ class Editor extends Component<Props> {
|
|||
if (applicationId && pageId) {
|
||||
this.props.initEditor(applicationId, pageId);
|
||||
}
|
||||
this.props.handlePathUpdated(window.location.pathname);
|
||||
this.unlisten = history.listen(this.handleHistoryChange);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props, nextState: { registered: boolean }) {
|
||||
|
|
@ -191,6 +87,15 @@ class Editor extends Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.resetEditorRequest();
|
||||
if (typeof this.unlisten === "function") this.unlisten();
|
||||
}
|
||||
|
||||
handleHistoryChange = (location: any) => {
|
||||
this.props.handlePathUpdated(location.pathname);
|
||||
};
|
||||
|
||||
public render() {
|
||||
if (this.props.creatingOnboardingDatabase) {
|
||||
return <Welcome />;
|
||||
|
|
@ -216,7 +121,9 @@ class Editor extends Component<Props> {
|
|||
<meta charSet="utf-8" />
|
||||
<title>Editor | Appsmith</title>
|
||||
</Helmet>
|
||||
<MainContainer />
|
||||
<GlobalHotKeys>
|
||||
<MainContainer />
|
||||
</GlobalHotKeys>
|
||||
</div>
|
||||
<ConfirmRunModal />
|
||||
</DndProvider>
|
||||
|
|
@ -242,10 +149,9 @@ const mapDispatchToProps = (dispatch: any) => {
|
|||
return {
|
||||
initEditor: (applicationId: string, pageId: string) =>
|
||||
dispatch(initEditor(applicationId, pageId)),
|
||||
copySelectedWidget: () => dispatch(copyWidget(true)),
|
||||
pasteCopiedWidget: () => dispatch(pasteWidget()),
|
||||
deleteSelectedWidget: () => dispatch(deleteSelectedWidget(true)),
|
||||
cutSelectedWidget: () => dispatch(cutWidget()),
|
||||
resetEditorRequest: () => dispatch(resetEditorRequest()),
|
||||
handlePathUpdated: (pathName: string) =>
|
||||
dispatch(handlePathUpdated(pathName)),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const AuthCard = styled(Card)`
|
|||
text-align: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
${(props) => getTypographyByKey(props, "authCardHeader")}
|
||||
${(props) => getTypographyByKey(props, "cardHeader")}
|
||||
color: ${(props) => props.theme.colors.auth.headingText};
|
||||
}
|
||||
& .form-message-container {
|
||||
|
|
@ -120,13 +120,13 @@ export const FormActions = styled.div`
|
|||
`;
|
||||
|
||||
export const SignUpLinkSection = styled.div`
|
||||
${(props) => getTypographyByKey(props, "authCardSubheader")}
|
||||
${(props) => getTypographyByKey(props, "cardSubheader")}
|
||||
color: ${(props) => props.theme.colors.auth.text};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const ForgotPasswordLink = styled.div`
|
||||
${(props) => getTypographyByKey(props, "authCardSubheader")}
|
||||
${(props) => getTypographyByKey(props, "cardSubheader")}
|
||||
color: ${(props) => props.theme.colors.auth.text};
|
||||
text-align: center;
|
||||
margin-top: ${(props) => props.theme.spaces[11]}px;
|
||||
|
|
|
|||
|
|
@ -5,11 +5,8 @@ import {
|
|||
ReduxAction,
|
||||
} from "constants/ReduxActionConstants";
|
||||
import { WidgetProps } from "widgets/BaseWidget";
|
||||
import {
|
||||
UpdateCanvasLayout,
|
||||
UpdateWidgetPropertyPayload,
|
||||
} from "actions/controlActions";
|
||||
import { set, uniqBy } from "lodash";
|
||||
import { UpdateCanvasLayout } from "actions/controlActions";
|
||||
import { set } from "lodash";
|
||||
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
|
||||
|
||||
const initialState: CanvasWidgetsReduxState = {};
|
||||
|
|
@ -37,32 +34,6 @@ const canvasWidgetsReducer = createImmerReducer(initialState, {
|
|||
) => {
|
||||
set(state[MAIN_CONTAINER_WIDGET_ID], "rightColumn", action.payload.width);
|
||||
},
|
||||
[ReduxActionTypes.UPDATE_WIDGET_PROPERTY]: (
|
||||
state: CanvasWidgetsReduxState,
|
||||
action: ReduxAction<UpdateWidgetPropertyPayload>,
|
||||
) => {
|
||||
const { dynamicUpdates, updates, widgetId } = action.payload;
|
||||
// We loop over all updates
|
||||
Object.entries(updates).forEach(([propertyPath, propertyValue]) => {
|
||||
// since property paths could be nested, we use lodash set method
|
||||
set(state[widgetId], propertyPath, propertyValue);
|
||||
});
|
||||
|
||||
if (dynamicUpdates && dynamicUpdates.dynamicBindingPathList.length) {
|
||||
const currentList = state[widgetId].dynamicBindingPathList || [];
|
||||
state[widgetId].dynamicBindingPathList = uniqBy(
|
||||
[...currentList, ...dynamicUpdates.dynamicBindingPathList],
|
||||
"key",
|
||||
);
|
||||
}
|
||||
if (dynamicUpdates && dynamicUpdates.dynamicTriggerPathList.length) {
|
||||
const currentList = state[widgetId].dynamicTriggerPathList || [];
|
||||
state[widgetId].dynamicTriggerPathList = uniqBy(
|
||||
[...currentList, ...dynamicUpdates.dynamicTriggerPathList],
|
||||
"key",
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface CanvasWidgetsReduxState {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import { ImportReduxState } from "reducers/uiReducers/importReducer";
|
|||
import { HelpReduxState } from "./uiReducers/helpReducer";
|
||||
import { ApiNameReduxState } from "./uiReducers/apiNameReducer";
|
||||
import { ExplorerReduxState } from "./uiReducers/explorerReducer";
|
||||
import { PageCanvasStructureReduxState } from "./uiReducers/pageCanvasStructure";
|
||||
import { PageCanvasStructureReduxState } from "reducers/uiReducers/pageCanvasStructureReducer";
|
||||
import { ConfirmRunActionReduxState } from "./uiReducers/confirmRunActionReducer";
|
||||
import { AppDataState } from "reducers/entityReducers/appReducer";
|
||||
import { DatasourceNameReduxState } from "./uiReducers/datasourceNameReducer";
|
||||
|
|
@ -38,6 +38,7 @@ import { EvaluatedTreeState } from "./evaluationReducers/treeReducer";
|
|||
import { EvaluationDependencyState } from "./evaluationReducers/dependencyReducer";
|
||||
import { PageWidgetsReduxState } from "./uiReducers/pageWidgetsReducer";
|
||||
import { OnboardingState } from "./uiReducers/onBoardingReducer";
|
||||
import { GlobalSearchReduxState } from "./uiReducers/globalSearchReducer";
|
||||
import { ReleasesState } from "./uiReducers/releasesReducer";
|
||||
import { LoadingEntitiesState } from "./evaluationReducers/loadingEntitiesReducer";
|
||||
|
||||
|
|
@ -77,6 +78,7 @@ export interface AppState {
|
|||
datasourceName: DatasourceNameReduxState;
|
||||
theme: ThemeState;
|
||||
onBoarding: OnboardingState;
|
||||
globalSearch: GlobalSearchReduxState;
|
||||
releases: ReleasesState;
|
||||
};
|
||||
entities: {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const initialState: ApplicationsReduxState = {
|
|||
applicationList: [],
|
||||
creatingApplication: {},
|
||||
deletingApplication: false,
|
||||
forkingApplication: false,
|
||||
duplicatingApplication: false,
|
||||
userOrgs: [],
|
||||
isSavingOrgInfo: false,
|
||||
|
|
@ -176,6 +177,39 @@ const applicationsReducer = createReducer(initialState, {
|
|||
createApplicationError: ERROR_MESSAGE_CREATE_APPLICATION,
|
||||
};
|
||||
},
|
||||
[ReduxActionTypes.FORK_APPLICATION_INIT]: (state: ApplicationsReduxState) => {
|
||||
return { ...state, forkingApplication: true };
|
||||
},
|
||||
[ReduxActionTypes.FORK_APPLICATION_SUCCESS]: (
|
||||
state: ApplicationsReduxState,
|
||||
action: ReduxAction<{ orgId: string; application: ApplicationPayload }>,
|
||||
) => {
|
||||
const _organizations = state.userOrgs.map((org: Organization) => {
|
||||
if (org.organization.id === action.payload.orgId) {
|
||||
const applications = org.applications;
|
||||
org.applications = [...applications, action.payload.application];
|
||||
return {
|
||||
...org,
|
||||
};
|
||||
}
|
||||
return org;
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
forkingApplication: false,
|
||||
applicationList: [...state.applicationList, action.payload.application],
|
||||
userOrgs: _organizations,
|
||||
};
|
||||
},
|
||||
[ReduxActionErrorTypes.FORK_APPLICATION_ERROR]: (
|
||||
state: ApplicationsReduxState,
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
forkingApplication: false,
|
||||
};
|
||||
},
|
||||
[ReduxActionTypes.SAVING_ORG_INFO]: (state: ApplicationsReduxState) => {
|
||||
return {
|
||||
...state,
|
||||
|
|
@ -299,6 +333,7 @@ export interface ApplicationsReduxState {
|
|||
creatingApplication: creatingApplicationMap;
|
||||
createApplicationError?: string;
|
||||
deletingApplication: boolean;
|
||||
forkingApplication: boolean;
|
||||
duplicatingApplication: boolean;
|
||||
currentApplication?: ApplicationPayload;
|
||||
userOrgs: Organization[];
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ const initialState: EditorReduxState = {
|
|||
};
|
||||
|
||||
const editorReducer = createReducer(initialState, {
|
||||
[ReduxActionTypes.RESET_EDITOR_SUCCESS]: (state: EditorReduxState) => {
|
||||
return { ...state, initialized: false };
|
||||
},
|
||||
[ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS]: (state: EditorReduxState) => {
|
||||
return { ...state, initialized: true };
|
||||
},
|
||||
|
|
|
|||
49
app/client/src/reducers/uiReducers/globalSearchReducer.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { createReducer } from "utils/AppsmithUtils";
|
||||
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
import { RecentEntity } from "components/editorComponents/GlobalSearch/utils";
|
||||
|
||||
const initialState: GlobalSearchReduxState = {
|
||||
query: "", // used to prefill when opened via contextual help links
|
||||
modalOpen: false,
|
||||
recentEntities: [],
|
||||
recentEntitiesRestored: false,
|
||||
};
|
||||
|
||||
const globalSearchReducer = createReducer(initialState, {
|
||||
[ReduxActionTypes.SET_GLOBAL_SEARCH_QUERY]: (
|
||||
state: GlobalSearchReduxState,
|
||||
action: ReduxAction<string>,
|
||||
) => ({ ...state, query: action.payload }),
|
||||
[ReduxActionTypes.TOGGLE_SHOW_GLOBAL_SEARCH_MODAL]: (
|
||||
state: GlobalSearchReduxState,
|
||||
) => ({ ...state, modalOpen: !state.modalOpen }),
|
||||
[ReduxActionTypes.SET_RECENT_ENTITIES]: (
|
||||
state: GlobalSearchReduxState,
|
||||
action: ReduxAction<Array<RecentEntity>>,
|
||||
) => ({
|
||||
...state,
|
||||
recentEntities: action.payload,
|
||||
}),
|
||||
[ReduxActionTypes.RESET_RECENT_ENTITIES]: (
|
||||
state: GlobalSearchReduxState,
|
||||
) => ({
|
||||
...state,
|
||||
recentEntities: [],
|
||||
recentEntitiesRestored: false,
|
||||
}),
|
||||
[ReduxActionTypes.RESTORE_RECENT_ENTITIES_SUCCESS]: (
|
||||
state: GlobalSearchReduxState,
|
||||
) => ({
|
||||
...state,
|
||||
recentEntitiesRestored: true,
|
||||
}),
|
||||
});
|
||||
|
||||
export interface GlobalSearchReduxState {
|
||||
query: string;
|
||||
modalOpen: boolean;
|
||||
recentEntities: Array<RecentEntity>;
|
||||
recentEntitiesRestored: boolean;
|
||||
}
|
||||
|
||||
export default globalSearchReducer;
|
||||
|
|
@ -21,9 +21,10 @@ import explorerReducer from "./explorerReducer";
|
|||
import confirmRunActionReducer from "./confirmRunActionReducer";
|
||||
import themeReducer from "./themeReducer";
|
||||
import datasourceNameReducer from "./datasourceNameReducer";
|
||||
import pageCanvasStructureReducer from "./pageCanvasStructure";
|
||||
import pageCanvasStructureReducer from "reducers/uiReducers/pageCanvasStructureReducer";
|
||||
import pageWidgetsReducer from "./pageWidgetsReducer";
|
||||
import onBoardingReducer from "./onBoardingReducer";
|
||||
import globalSearchReducer from "./globalSearchReducer";
|
||||
import releasesReducer from "./releasesReducer";
|
||||
|
||||
const uiReducer = combineReducers({
|
||||
|
|
@ -52,6 +53,7 @@ const uiReducer = combineReducers({
|
|||
theme: themeReducer,
|
||||
confirmRunAction: confirmRunActionReducer,
|
||||
onBoarding: onBoardingReducer,
|
||||
globalSearch: globalSearchReducer,
|
||||
releases: releasesReducer,
|
||||
});
|
||||
export default uiReducer;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createImmerReducer } from "utils/AppsmithUtils";
|
||||
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
|
||||
import { DSL } from "./pageCanvasStructure";
|
||||
import { DSL } from "reducers/uiReducers/pageCanvasStructureReducer";
|
||||
import { WidgetProps } from "widgets/BaseWidget";
|
||||
import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer";
|
||||
|
||||
|
|
|
|||
|
|
@ -587,6 +587,12 @@ function* executeAppAction(action: ReduxAction<ExecuteActionPayload>) {
|
|||
const { dynamicString, event, responseData } = action.payload;
|
||||
log.debug({ dynamicString, responseData });
|
||||
|
||||
if (dynamicString === undefined) {
|
||||
if (event.callback) event.callback({ success: false });
|
||||
log.error("Executing undefined action", event);
|
||||
return;
|
||||
}
|
||||
|
||||
const triggers = yield call(
|
||||
evaluateDynamicTrigger,
|
||||
dynamicString,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import ApplicationApi, {
|
|||
DuplicateApplicationRequest,
|
||||
FetchApplicationsResponse,
|
||||
FetchUsersApplicationsOrgsResponse,
|
||||
ForkApplicationRequest,
|
||||
OrganizationApplicationObject,
|
||||
PublishApplicationRequest,
|
||||
PublishApplicationResponse,
|
||||
|
|
@ -52,6 +53,7 @@ import {
|
|||
getCurrentPageId,
|
||||
} from "selectors/editorSelectors";
|
||||
import { showCompletionDialog } from "./OnboardingSagas";
|
||||
import { deleteRecentAppEntities } from "utils/storage";
|
||||
|
||||
const getDefaultPageId = (
|
||||
pages?: ApplicationPagePayload[],
|
||||
|
|
@ -288,6 +290,7 @@ export function* deleteApplicationSaga(
|
|||
type: ReduxActionTypes.DELETE_APPLICATION_SUCCESS,
|
||||
payload: response.data,
|
||||
});
|
||||
yield call(deleteRecentAppEntities, request.applicationId);
|
||||
}
|
||||
} catch (error) {
|
||||
yield put({
|
||||
|
|
@ -447,6 +450,44 @@ export function* createApplicationSaga(
|
|||
}
|
||||
}
|
||||
|
||||
export function* forkApplicationSaga(
|
||||
action: ReduxAction<ForkApplicationRequest>,
|
||||
) {
|
||||
try {
|
||||
const response: ApiResponse = yield call(
|
||||
ApplicationApi.forkApplication,
|
||||
action.payload,
|
||||
);
|
||||
const isValidResponse = yield validateResponse(response);
|
||||
if (isValidResponse) {
|
||||
yield put(resetCurrentApplication());
|
||||
const application: ApplicationPayload = {
|
||||
...response.data,
|
||||
defaultPageId: getDefaultPageId(response.data.pages),
|
||||
};
|
||||
yield put({
|
||||
type: ReduxActionTypes.FORK_APPLICATION_SUCCESS,
|
||||
payload: {
|
||||
orgId: action.payload.organizationId,
|
||||
application,
|
||||
},
|
||||
});
|
||||
const pageURL = BUILDER_PAGE_URL(
|
||||
application.id,
|
||||
application.defaultPageId,
|
||||
);
|
||||
history.push(pageURL);
|
||||
}
|
||||
} catch (error) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.FORK_APPLICATION_ERROR,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function* applicationSagas() {
|
||||
yield all([
|
||||
takeLatest(
|
||||
|
|
@ -464,6 +505,7 @@ export default function* applicationSagas() {
|
|||
getAllApplicationSaga,
|
||||
),
|
||||
takeLatest(ReduxActionTypes.FETCH_APPLICATION_INIT, fetchApplicationSaga),
|
||||
takeLatest(ReduxActionTypes.FORK_APPLICATION_INIT, forkApplicationSaga),
|
||||
takeLatest(ReduxActionTypes.CREATE_APPLICATION_INIT, createApplicationSaga),
|
||||
takeLatest(
|
||||
ReduxActionTypes.SET_DEFAULT_APPLICATION_PAGE_INIT,
|
||||
|
|
|
|||
|
|
@ -43,11 +43,24 @@ const evalErrorHandler = (errors: EvalError[]) => {
|
|||
errors.forEach((error) => {
|
||||
switch (error.type) {
|
||||
case EvalErrorTypes.DEPENDENCY_ERROR: {
|
||||
Toaster.show({
|
||||
text: error.message,
|
||||
variant: Variant.danger,
|
||||
});
|
||||
Sentry.captureException(new Error(error.message));
|
||||
if (error.context) {
|
||||
// Add more info about node for the toast
|
||||
const { node, entityType } = error.context;
|
||||
Toaster.show({
|
||||
text: `${error.message} Node was: ${node}`,
|
||||
variant: Variant.danger,
|
||||
});
|
||||
// Send the generic error message to sentry for better grouping
|
||||
Sentry.captureException(new Error(error.message), {
|
||||
tags: {
|
||||
node,
|
||||
entityType,
|
||||
},
|
||||
// Level is warning because it could be a user error
|
||||
level: Sentry.Severity.Warning,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case EvalErrorTypes.EVAL_TREE_ERROR: {
|
||||
|
|
|
|||
89
app/client/src/sagas/GlobalSearchSagas.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
|
||||
import {
|
||||
all,
|
||||
call,
|
||||
put,
|
||||
takeLatest,
|
||||
select,
|
||||
putResolve,
|
||||
take,
|
||||
} from "redux-saga/effects";
|
||||
import { setRecentAppEntities, fetchRecentAppEntities } from "utils/storage";
|
||||
import {
|
||||
restoreRecentEntitiesSuccess,
|
||||
setRecentEntities,
|
||||
} from "actions/globalSearchActions";
|
||||
import { AppState } from "reducers";
|
||||
import { getIsEditorInitialized } from "selectors/editorSelectors";
|
||||
import { RecentEntity } from "components/editorComponents/GlobalSearch/utils";
|
||||
|
||||
export function* updateRecentEntity(actionPayload: ReduxAction<RecentEntity>) {
|
||||
try {
|
||||
const recentEntitiesRestored = yield select(
|
||||
(state: AppState) => state.ui.globalSearch.recentEntitiesRestored,
|
||||
);
|
||||
const isEditorInitialised = yield select(getIsEditorInitialized);
|
||||
|
||||
const waitForEffects = [];
|
||||
|
||||
if (!isEditorInitialised) {
|
||||
waitForEffects.push(take(ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS));
|
||||
}
|
||||
|
||||
if (!recentEntitiesRestored) {
|
||||
waitForEffects.push(
|
||||
take(ReduxActionTypes.RESTORE_RECENT_ENTITIES_SUCCESS),
|
||||
);
|
||||
}
|
||||
|
||||
yield all(waitForEffects);
|
||||
|
||||
const { payload: entity } = actionPayload;
|
||||
let recentEntities = yield select(
|
||||
(state: AppState) => state.ui.globalSearch.recentEntities,
|
||||
);
|
||||
|
||||
recentEntities = recentEntities.slice();
|
||||
|
||||
const existingIndex = recentEntities.findIndex(
|
||||
(recentEntity: { type: string; id: string }) =>
|
||||
recentEntity.id === entity.id,
|
||||
);
|
||||
|
||||
if (existingIndex === -1) {
|
||||
recentEntities.unshift(entity);
|
||||
recentEntities = recentEntities.slice(0, 5);
|
||||
} else {
|
||||
recentEntities.splice(existingIndex, 1);
|
||||
recentEntities.unshift(entity);
|
||||
}
|
||||
|
||||
yield put(setRecentEntities(recentEntities));
|
||||
if (entity?.params?.applicationId) {
|
||||
yield call(
|
||||
setRecentAppEntities,
|
||||
recentEntities,
|
||||
entity?.params?.applicationId,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e, "error");
|
||||
}
|
||||
}
|
||||
|
||||
export function* restoreRecentEntities(actionPayload: ReduxAction<string>) {
|
||||
const { payload: appId } = actionPayload;
|
||||
const recentAppEntities = yield call(fetchRecentAppEntities, appId);
|
||||
yield putResolve(setRecentEntities(recentAppEntities));
|
||||
yield put(restoreRecentEntitiesSuccess());
|
||||
}
|
||||
|
||||
export default function* globalSearchSagas() {
|
||||
yield all([
|
||||
takeLatest(ReduxActionTypes.UPDATE_RECENT_ENTITY, updateRecentEntity),
|
||||
takeLatest(
|
||||
ReduxActionTypes.RESTORE_RECENT_ENTITIES_REQUEST,
|
||||
restoreRecentEntities,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
|
@ -36,6 +36,11 @@ import { getDefaultPageId } from "./selectors";
|
|||
import { populatePageDSLsSaga } from "./PageSagas";
|
||||
import log from "loglevel";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import {
|
||||
restoreRecentEntitiesRequest,
|
||||
resetRecentEntities,
|
||||
} from "actions/globalSearchActions";
|
||||
import { resetEditorSuccess } from "actions/initActions";
|
||||
|
||||
function* initializeEditorSaga(
|
||||
initializeEditorAction: ReduxAction<InitializeEditorPayload>,
|
||||
|
|
@ -52,6 +57,8 @@ function* initializeEditorSaga(
|
|||
put(fetchApplication(applicationId, APP_MODE.EDIT)),
|
||||
]);
|
||||
|
||||
yield put(restoreRecentEntitiesRequest(applicationId));
|
||||
|
||||
const resultOfPrimaryCalls = yield race({
|
||||
success: all([
|
||||
take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS),
|
||||
|
|
@ -218,6 +225,11 @@ export function* initializeAppViewerSaga(
|
|||
}
|
||||
}
|
||||
|
||||
function* resetEditorSaga() {
|
||||
yield put(resetEditorSuccess());
|
||||
yield put(resetRecentEntities());
|
||||
}
|
||||
|
||||
export default function* watchInitSagas() {
|
||||
yield all([
|
||||
takeLatest(ReduxActionTypes.INITIALIZE_EDITOR, initializeEditorSaga),
|
||||
|
|
@ -225,5 +237,6 @@ export default function* watchInitSagas() {
|
|||
ReduxActionTypes.INITIALIZE_PAGE_VIEWER,
|
||||
initializeAppViewerSaga,
|
||||
),
|
||||
takeLatest(ReduxActionTypes.RESET_EDITOR_REQUEST, resetEditorSaga),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ import { generateReactKey } from "utils/generators";
|
|||
import { forceOpenPropertyPane } from "actions/widgetActions";
|
||||
import { navigateToCanvas } from "pages/Editor/Explorer/Widgets/WidgetEntity";
|
||||
import {
|
||||
updateWidgetProperty,
|
||||
batchUpdateWidgetProperty,
|
||||
updateWidgetPropertyRequest,
|
||||
} from "../actions/controlActions";
|
||||
import OnSubmitGif from "assets/gifs/onsubmit.gif";
|
||||
|
|
@ -146,14 +146,16 @@ function* listenForWidgetAdditions() {
|
|||
selectedWidget.tableData === initialTableData
|
||||
) {
|
||||
yield put(
|
||||
updateWidgetProperty(selectedWidget.widgetId, {
|
||||
tableData: [],
|
||||
columnSizeMap: {
|
||||
avatar: 20,
|
||||
name: 30,
|
||||
batchUpdateWidgetProperty(selectedWidget.widgetId, {
|
||||
modify: {
|
||||
tableData: [],
|
||||
columnSizeMap: {
|
||||
avatar: 20,
|
||||
name: 30,
|
||||
},
|
||||
migrated: false,
|
||||
...getStandupTableDimensions(),
|
||||
},
|
||||
migrated: false,
|
||||
...getStandupTableDimensions(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
@ -209,9 +211,11 @@ function* listenForAddInputWidget() {
|
|||
),
|
||||
);
|
||||
yield put(
|
||||
updateWidgetProperty(inputWidget.widgetId, {
|
||||
...getStandupInputDimensions(),
|
||||
...getStandupInputProps(),
|
||||
batchUpdateWidgetProperty(inputWidget.widgetId, {
|
||||
modify: {
|
||||
...getStandupInputDimensions(),
|
||||
...getStandupInputProps(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
yield put(setCurrentSubstep(2));
|
||||
|
|
@ -219,12 +223,12 @@ function* listenForAddInputWidget() {
|
|||
yield put(showIndicator(OnboardingStep.ADD_INPUT_WIDGET));
|
||||
}
|
||||
|
||||
const helperConfig = yield select(
|
||||
const helperConfig: OnboardingHelperConfig = yield select(
|
||||
(state) => state.ui.onBoarding.helperStepConfig,
|
||||
);
|
||||
const onSubmitGifUrl = OnSubmitGif;
|
||||
|
||||
if (helperConfig?.image.src !== onSubmitGifUrl) {
|
||||
if (helperConfig.image?.src !== onSubmitGifUrl) {
|
||||
yield put(
|
||||
setHelperConfig({
|
||||
...helperConfig,
|
||||
|
|
@ -306,11 +310,13 @@ function* listenForSuccessfulBinding() {
|
|||
|
||||
if (bindSuccessful) {
|
||||
yield put(
|
||||
updateWidgetProperty(selectedWidget.widgetId, {
|
||||
columnTypeMap: {
|
||||
avatar: {
|
||||
type: "image",
|
||||
format: "",
|
||||
batchUpdateWidgetProperty(selectedWidget.widgetId, {
|
||||
modify: {
|
||||
columnTypeMap: {
|
||||
avatar: {
|
||||
type: "image",
|
||||
format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ import { Variant } from "components/ads/common";
|
|||
import { migrateIncorrectDynamicBindingPathLists } from "utils/migrations/IncorrectDynamicBindingPathLists";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { ERROR_CODES } from "constants/ApiConstants";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import DEFAULT_TEMPLATE from "templates/default";
|
||||
|
||||
const getWidgetName = (state: AppState, widgetId: string) =>
|
||||
state.entities.canvasWidgets[widgetId];
|
||||
|
|
@ -303,6 +305,7 @@ function* savePageSaga(action: ReduxAction<{ isRetry?: boolean }>) {
|
|||
pageId: savePageRequest.pageId,
|
||||
},
|
||||
);
|
||||
AnalyticsUtil.logEvent("PAGE_SAVE", savePageRequest);
|
||||
try {
|
||||
// Store the updated DSL in the pageDSLs reducer
|
||||
yield put({
|
||||
|
|
@ -376,11 +379,18 @@ function* savePageSaga(action: ReduxAction<{ isRetry?: boolean }>) {
|
|||
},
|
||||
});
|
||||
} else {
|
||||
const correctWidget = migrateIncorrectDynamicBindingPathLists(
|
||||
const correctedWidget = migrateIncorrectDynamicBindingPathLists(
|
||||
widgets[widgetId],
|
||||
);
|
||||
AnalyticsUtil.logEvent("CORRECT_BAD_BINDING", {
|
||||
error: incorrectBindingError,
|
||||
correctWidget: correctedWidget,
|
||||
});
|
||||
yield put(
|
||||
updateAndSaveLayout({ ...widgets, [widgetId]: correctWidget }, true),
|
||||
updateAndSaveLayout(
|
||||
{ ...widgets, [widgetId]: correctedWidget },
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -744,13 +754,17 @@ function* fetchPageDSLSaga(pageId: string) {
|
|||
}
|
||||
} catch (error) {
|
||||
yield put({
|
||||
type: ReduxActionTypes.FETCH_PAGE_DSL_ERROR,
|
||||
type: ReduxActionErrorTypes.FETCH_PAGE_DSL_ERROR,
|
||||
payload: {
|
||||
pageId: pageId,
|
||||
error,
|
||||
show: false,
|
||||
show: true,
|
||||
},
|
||||
});
|
||||
return {
|
||||
pageId: pageId,
|
||||
dsl: DEFAULT_TEMPLATE,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||