Merge pull request #3473 from appsmithorg/release

Release v1.4.3 🔍
This commit is contained in:
Hetu Nandu 2021-03-10 15:52:49 +05:30 committed by GitHub
commit 0051c3f78f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
153 changed files with 5360 additions and 812 deletions

View File

@ -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",

View File

@ -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");
});

View File

@ -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);
});
});
});
});

View File

@ -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"
}

View 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
}
)
}
)

View 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
}
)
}
)

View File

@ -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")

View File

@ -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",

View File

@ -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

View 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,
});

View File

@ -14,3 +14,11 @@ export const initEditor = (
pageId,
},
});
export const resetEditorRequest = () => ({
type: ReduxActionTypes.RESET_EDITOR_REQUEST,
});
export const resetEditorSuccess = () => ({
type: ReduxActionTypes.RESET_EDITOR_SUCCESS,
});

View File

@ -0,0 +1,6 @@
import { ReduxActionTypes } from "constants/ReduxActionConstants";
export const handlePathUpdated = (pathName: string) => ({
type: ReduxActionTypes.HANDLE_PATH_UPDATED,
payload: { pathName },
});

View File

@ -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,

View File

@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 870 KiB

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -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

View File

@ -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

View 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

View 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

View File

@ -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

View 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

View File

@ -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

View 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

View File

@ -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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -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

View File

@ -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>

View File

@ -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;

View File

@ -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"
>

View File

@ -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()}

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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" }}
>
&#10024; Press <StyledKey>&#8629;</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;

View File

@ -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;

View File

@ -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;

View File

@ -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>&#8984;</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);

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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 &amp; 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);
});
});

View File

@ -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(
`&lt;${algoliaHighlightTag}&gt;|&lt;/${algoliaHighlightTag}&gt;`,
"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;

View File

@ -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;

View File

@ -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,
}));
};

View File

@ -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}

View File

@ -43,6 +43,7 @@ export interface ControlData {
dataType?: InputType;
isRequired?: boolean;
hidden?: HiddenType;
placeholderText?: string;
}
export interface ControlFunctions {

View File

@ -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 ? (

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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;
};

View File

@ -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";

View File

@ -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);

View File

@ -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;
}
});
},
);
}
}
});
}

View File

@ -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");

View File

@ -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>
)}

View 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;

View File

@ -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>
);

View File

@ -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],

View File

@ -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],

View File

@ -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;
};
};

View File

@ -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";

View File

@ -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";

View File

@ -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 = {

View File

@ -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;

View File

@ -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;

View 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);

View 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;

View 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;

View File

@ -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

View File

@ -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)),
};
};

View File

@ -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;

View File

@ -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 {

View File

@ -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: {

View File

@ -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[];

View File

@ -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 };
},

View 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;

View File

@ -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;

View File

@ -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";

View File

@ -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,

View File

@ -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,

View File

@ -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: {

View 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,
),
]);
}

View File

@ -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),
]);
}

View File

@ -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: "",
},
},
},
}),

View File

@ -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,
};
}
}

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