Merge pull request #5908 from appsmithorg/release-frozen

Release
This commit is contained in:
Trisha Anand 2021-07-16 17:53:04 +05:30 committed by GitHub
commit 1dae62a09e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
354 changed files with 12553 additions and 7361 deletions

View File

@ -71,6 +71,7 @@ jobs:
- name: Build and test
env:
APPSMITH_MONGODB_URI: "mongodb://localhost:27017/mobtools"
APPSMITH_CLOUD_SERVICES_BASE_URL: "https://release-cs.appsmith.com"
APPSMITH_REDIS_URL: "redis://127.0.0.1:6379"
APPSMITH_ENCRYPTION_PASSWORD: "password"
APPSMITH_ENCRYPTION_SALT: "salt"
@ -199,7 +200,7 @@ jobs:
if(check.length == 0) {
const head_sha = pull.head.sha;
const { data: completed_at } = await github.checks.create({
owner: context.repo.owner,
owner: context.repo.owner,
repo: context.repo.repo,
head_sha: head_sha,
name: process.env.job,
@ -443,7 +444,7 @@ jobs:
if(check.length == 0) {
const head_sha = pull.head.sha;
const { data: completed_at } = await github.checks.create({
owner: context.repo.owner,
owner: context.repo.owner,
repo: context.repo.repo,
head_sha: head_sha,
name: process.env.job,
@ -512,7 +513,7 @@ jobs:
if(check.length == 0) {
const head_sha = pull.head.sha;
const { data: completed_at } = await github.checks.create({
owner: context.repo.owner,
owner: context.repo.owner,
repo: context.repo.repo,
head_sha: head_sha,
name: process.env.job,

View File

@ -83,6 +83,7 @@ jobs:
- name: Build and test
env:
APPSMITH_MONGODB_URI: "mongodb://localhost:27017/mobtools"
APPSMITH_CLOUD_SERVICES_BASE_URL: "https://release-cs.appsmith.com"
APPSMITH_REDIS_URL: "redis://127.0.0.1:6379"
APPSMITH_ENCRYPTION_PASSWORD: "password"
APPSMITH_ENCRYPTION_SALT: "salt"

View File

@ -6,7 +6,7 @@ GIT_SHA=$(eval git rev-parse HEAD)
echo $GIT_SHA
echo "Sentry Auth Token: $SENTRY_AUTH_TOKEN"
REACT_APP_SENTRY_RELEASE=$GIT_SHA EXTEND_ESLINT=true craco --max-old-space-size=4096 build --config craco.build.config.js
REACT_APP_SENTRY_RELEASE=$GIT_SHA REACT_APP_CLIENT_LOG_LEVEL=ERROR EXTEND_ESLINT=true craco --max-old-space-size=4096 build --config craco.build.config.js
rm ./build/static/js/*.js.map
echo "build finished"

View File

@ -0,0 +1,48 @@
{
"dsl": {
"widgetName": "MainContainer",
"backgroundColor": "none",
"rightColumn": 1280,
"snapColumns": 64,
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0,
"bottomRow": 1300,
"containerStyle": "none",
"snapRows": 125,
"parentRowSpace": 1,
"type": "CANVAS_WIDGET",
"canExtend": true,
"version": 23,
"minHeight": 1310,
"parentColumnSpace": 1,
"dynamicTriggerPathList": [],
"dynamicBindingPathList": [],
"leftColumn": 0,
"children": [
{
"widgetName": "Divider1",
"rightColumn": 23,
"widgetId": "i5kplngd19",
"topRow": 25,
"bottomRow": 75,
"parentRowSpace": 10,
"isVisible": true,
"type": "DIVIDER_WIDGET",
"version": 1,
"parentId": "0",
"isLoading": false,
"parentColumnSpace": 19.75,
"dynamicTriggerPathList": [],
"leftColumn": 15,
"dynamicBindingPathList": [],
"orientation": "horizontal",
"capType": "nc",
"capSide": -1,
"strokeStyle": "solid",
"dividerColor": "black",
"thickness": 2
}
]
}
}

View File

@ -7,11 +7,9 @@ describe("API Panel Test Functionality", function() {
cy.NavigateToAPI_Panel();
cy.log("Navigation to API Panel screen successful");
cy.CreateAPI("FirstAPI");
cy.get("textarea").should(
"have.attr",
"placeholder",
"https://mock-api.appsmith.com/users",
);
cy.get(".CodeMirror-placeholder")
.first()
.should("have.text", "https://mock-api.appsmith.com/users");
cy.log("Creation of FirstAPI Action successful");
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods);
cy.SaveAndRunAPI();
@ -29,25 +27,20 @@ describe("API Panel Test Functionality", function() {
cy.NavigateToAPI_Panel();
cy.CreateAPI("CrashTestAPI");
cy.SelectAction(testdata.postAction);
cy.xpath(apiwidget.headerKey)
cy.get(apiwidget.headerKey)
.first()
.focus({ force: true })
.type("{uparrow}", { force: true })
.type("{ctrl}{shift}{downarrow}", { force: true })
.type("{backspace}", { force: true });
.click({ force: true })
.type("{uparrow}", { parseSpecialCharSequences: true })
.type("{ctrl}{shift}{downarrow}", { parseSpecialCharSequences: true })
.type("{backspace}", { parseSpecialCharSequences: true });
// assert so that this fails
cy.xpath(apiwidget.headerKey).should("be.visible");
cy.xpath(apiwidget.headerKey).should("have.value", "");
cy.get(apiwidget.headerKey).should("be.visible");
cy.get(apiwidget.headerKey).should("have.value", "");
});
it("Should correctly parse query params", function() {
cy.NavigateToAPI_Panel();
cy.CreateAPI("APIWithQueryParams");
cy.get("textarea").should(
"have.attr",
"placeholder",
"https://mock-api.appsmith.com/users",
);
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methodWithQueryParam);
cy.ValidateQueryParams({
key: "q",

View File

@ -12,7 +12,7 @@ describe("API Panel request body", function() {
cy.contains(apiEditor.bodyTab).click();
cy.contains(testdata.apiFormDataBodyType).click();
cy.xpath(apiwidget.formEncoded).should("be.visible");
cy.get(apiwidget.formEncoded).should("be.visible");
cy.DeleteAPI();
});

View File

@ -15,7 +15,6 @@ describe("Debugger logs", function() {
});
it("Reset debugger state", function() {
cy.openPropertyPane("buttonwidget");
cy.get(".t--property-control-visible")
.find(".t--js-toggle")
.click();

View File

@ -0,0 +1,23 @@
const dsl = require("../../../../fixtures/DividerDsl.json");
const explorer = require("../../../../locators/explorerlocators.json");
describe("Divider Widget Functionality", function() {
beforeEach(() => {
cy.addDsl(dsl);
});
it("Add new Divider", () => {
cy.get(explorer.addWidget).click();
cy.dragAndDropToCanvas("dividerwidget", { x: 300, y: 300 });
cy.get(".t--divider-widget").should("exist");
});
it("Open Existing Divider from created Widgets list", () => {
cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(Widgets)").click({
multiple: true,
});
cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(Divider1)").click({
multiple: true,
});
});
});

View File

@ -23,7 +23,6 @@ describe("Image Widget Functionality", function() {
);
cy.get(viewWidgetsPage.defaultImage)
.click({ force: true })
.type(this.data.command)
.type(this.data.defaultimage);
/**
* @param{URL} ImageUrl

View File

@ -0,0 +1,46 @@
const widgetsPage = require("../../../../locators/Widgets.json");
const dsl = require("../../../../fixtures/tableNewDsl.json");
describe("Table Widget row multi select validation", function() {
before(() => {
cy.addDsl(dsl);
});
it("Test multi select column shows when enableMultirowselection is true", function() {
cy.openPropertyPane("tablewidget");
cy.get(widgetsPage.toggleEnableMultirowselection)
.first()
.click({ force: true });
cy.get(".t--table-multiselect-header")
.first()
.should("be.visible");
cy.get(".t--table-multiselect")
.first()
.should("be.visible");
});
it("Test click on header cell selects all row", function() {
// click on header check cell
cy.get(".t--table-multiselect-header")
.first()
.click({ force: true });
// check if rows selected
cy.get(".tr").should("have.class", "selected-row");
});
it("Test click on single row cell changes header select cell state", function() {
// un select all rows
cy.get(".t--table-multiselect-header")
.first()
.click({ force: true });
// click on first row select box
cy.get(".t--table-multiselect")
.first()
.click({ force: true });
// check if header cell is in half check state
cy.get(".t--table-multiselect-header-half-check-svg")
.first()
.should("be.visible");
});
});

View File

@ -32,6 +32,11 @@ describe("Test Suite to validate copy/paste table Widget", function() {
200,
);
cy.get(commonlocators.toastAction).should("be.visible");
//Check after copying new table widget should not have any warnings
cy.get('.t--widget-propertypane-toggle [name="warning"]').should(
"not.exist",
);
/*
cy.get(commonlocators.toastAction)
.contains("UNDO")

View File

@ -264,16 +264,16 @@ describe("Table Widget Functionality", function() {
const tabValue = tabData;
expect(tabValue).to.be.equal("Lindsay Ferguson");
cy.log("the value is" + tabValue);
cy.get(publish.compactMode).click();
cy.get(publish.compactOpt)
cy.get(publish.rowHeight).click();
cy.get(publish.rowHeightOpt)
.contains("Tall")
.click();
cy.scrollTabledataPublish("3", "3").then((tabData) => {
const tabValue = tabData;
expect(tabValue).to.be.equal("Byron Fields");
});
cy.get(publish.compactMode).click();
cy.get(publish.compactOpt)
cy.get(publish.rowHeight).click();
cy.get(publish.rowHeightOpt)
.contains("Short")
.click();
cy.readTabledataPublish("4", "3").then((tabData) => {

View File

@ -51,7 +51,7 @@ describe("Video Widget Functionality", function() {
cy.get(".CodeMirror textarea")
.first()
.blur();
cy.get(widgetsPage.autoPlay).click();
cy.get(widgetsPage.autoPlay).click({ force: true });
cy.wait("@updateLayout").should(
"have.nested.property",
"response.body.responseMeta.status",
@ -62,7 +62,7 @@ describe("Video Widget Functionality", function() {
cy.get(commonlocators.toastMsg).should("be.visible");
cy.get(commonlocators.toastMsg).contains("Play success");
*/
cy.get(widgetsPage.autoPlay).click();
cy.get(widgetsPage.autoPlay).click({ force: true });
cy.wait("@updateLayout").should(
"have.nested.property",
"response.body.responseMeta.status",

View File

@ -6,29 +6,23 @@ const dynamicInputLocators = require("../../../../locators/DynamicInput.json");
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
describe("Dynamic input autocomplete", () => {
beforeEach(() => {
before(() => {
cy.addDsl(dsl);
});
it("opens autocomplete for bindings", () => {
cy.openPropertyPane("buttonwidget");
cy.get(dynamicInputLocators.input)
.first()
.focus()
.type("{ctrl}{shift}{downarrow}")
.then(($cm) => {
if ($cm.val() !== "") {
cy.get(dynamicInputLocators.input)
.first()
.clear({
force: true,
});
}
.click({ force: true })
.type("{uparrow}", { parseSpecialCharSequences: true })
.type("{ctrl}{shift}{downarrow}", { parseSpecialCharSequences: true })
.type("{backspace}", { parseSpecialCharSequences: true })
.then(() => {
cy.get(dynamicInputLocators.input)
.first()
.click({ force: true })
.type("{{", {
force: true,
parseSpecialCharSequences: false,
parseSpecialCharSequences: true,
});
// Tests if autocomplete will open
@ -42,9 +36,11 @@ describe("Dynamic input autocomplete", () => {
// Tests if "No suggestions" message will pop if you type any garbage
cy.get(dynamicInputLocators.input)
.first()
.type("garbage", {
force: true,
parseSpecialCharSequences: false,
.click({ force: true })
.type("{uparrow}", { parseSpecialCharSequences: true })
.type("{ctrl}{shift}{downarrow}", { parseSpecialCharSequences: true })
.type("{{ garbage", {
parseSpecialCharSequences: true,
})
.then(() => {
cy.get(".CodeMirror-Tern-tooltip").should(
@ -53,21 +49,16 @@ describe("Dynamic input autocomplete", () => {
);
});
});
cy.evaluateErrorMessage("ReferenceError: garbage is not defined");
});
it("opens current value popup", () => {
// Test on widgets pane
cy.openPropertyPane("buttonwidget");
cy.get(dynamicInputLocators.input)
.first()
.focus();
cy.assertEvaluatedValuePopup("string");
// Test on api pane
cy.NavigateToAPI_Panel();
cy.get(apiwidget.createapi).click({ force: true });
cy.wait("@createNewApi");
cy.xpath(apiwidget.headerValue)
.first()
.focus();
cy.get(apiwidget.headerValue).within(() => {
cy.get("textarea").click({ force: true });
});
cy.assertEvaluatedValuePopup("string");
});
});

View File

@ -5,6 +5,7 @@ const datasource = require("../../../../locators/DatasourcesEditor.json");
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
const commonlocators = require("../../../../locators/commonlocators.json");
const explorer = require("../../../../locators/explorerlocators.json");
const pages = require("../../../../locators/Pages.json");
const pageid = "MyPage";
let datasourceName;
@ -59,7 +60,7 @@ describe("Entity explorer tests related to query and datasource", function() {
/* eslint-disable */
cy.wait(2000);
cy.go("back");
cy.NavigateToQueryEditor();
cy.contains(".t--datasource-name", datasourceName)
.find(queryLocators.editDatasourceButton)
@ -121,7 +122,12 @@ describe("Entity explorer tests related to query and datasource", function() {
cy.runQuery();
cy.deleteQuery();
cy.get(commonlocators.entityExplorersearch).clear({ force: true });
cy.wait(500);
cy.NavigateToQueryEditor();
cy.get(pages.integrationActiveTab)
.should("be.visible")
.click({ force: true });
cy.contains(".t--datasource-name", datasourceName)
.find(".t--edit-datasource")
.click();

View File

@ -81,7 +81,7 @@ describe("Checkbox Widget Functionality", function() {
cy.get(publish.checkboxWidget).click();
cy.get(widgetsPage.formButtonWidget)
.contains("Submit")
.should("have.attr", "disabled");
.should("have.class", "bp3-disabled");
cy.get(publish.checkboxWidget).click();
cy.get(widgetsPage.formButtonWidget)

View File

@ -49,7 +49,6 @@ describe("Radio Widget Functionality", function() {
cy.get(formWidgetsPage.radioOnSelectionChangeDropdown)
.get(commonlocators.dropdownSelectButton)
.click({ force: true })
.type(this.data.command)
.type("2");
cy.PublishtheApp();
});

View File

@ -2,6 +2,9 @@
// const explorer = require("../../../../locators/explorerlocators.json");
const homePage = require("../../../../locators/HomePage.json");
const commonlocators = require("../../../../locators/commonlocators.json");
const pages = require("../../../../locators/Pages.json");
const datasourceEditor = require("../../../../locators/DatasourcesEditor.json");
const datasource = require("../../../../locators/DatasourcesEditor.json");
describe("Onboarding", function() {
it("Onboarding flow", function() {
@ -38,9 +41,6 @@ describe("Onboarding", function() {
.should("be.visible")
.click({ force: true });
cy.get(".t--create-query").click();
cy.wait("@createNewApi");
// Create and run query
// Using the cheat option to create the action with 30 sec timeout
cy.get(".t--onboarding-cheat-action")

View File

@ -51,14 +51,7 @@ describe("Leave organization test spec", function() {
cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1"));
cy.visit("/applications");
cy.get(homePage.orgList.concat(newOrganizationName).concat(")"))
.scrollIntoView()
.should("be.visible")
.within(() => {
cy.get(homePage.orgNamePopover)
.first()
.click();
});
cy.openOrgOptionsPopup(newOrganizationName);
cy.get(homePage.orgNamePopoverContent)
.find("a")
.should("have.length", 1)

View File

@ -23,7 +23,9 @@ describe("Pages", function() {
cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(Page1)").click({
multiple: true,
});
cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(APIs)").click({
cy.get(
".bp3-icon-caret-right ~ .t--entity-name:contains(Datasources)",
).click({
multiple: true,
});
cy.get(`.t--entity-name:contains(${apiName})`).should("have.length", 2);

View File

@ -0,0 +1,21 @@
const dsl = require("../../../../fixtures/TextTabledsl.json");
describe("Property pane CTA to add an action", function() {
before(() => {
cy.addDsl(dsl);
});
it("Check if CTA is shown when there is no action", function() {
cy.openPropertyPane("tablewidget");
cy.get(".t--propertypane-connect-cta").should("be.visible");
});
it("Check if CTA does not exist when there is an action", function() {
cy.NavigateToAPI_Panel();
cy.CreateAPI("FirstAPI");
cy.SearchEntityandOpen("Table1");
cy.get(".t--propertypane-connect-cta").should("not.exist");
});
});

View File

@ -1,6 +1,7 @@
const datasource = require("../../../../locators/DatasourcesEditor.json");
const queryLocators = require("../../../../locators/QueryEditor.json");
const commonlocators = require("../../../../locators/commonlocators.json");
const queryEditor = require("../../../../locators/QueryEditor.json");
describe("Check datasource doc links", function() {
let postgresDatasourceName;
@ -44,7 +45,8 @@ describe("Check datasource doc links", function() {
});
it("Delete the query and datasources", function() {
cy.get(queryLocators.deleteQuery).click();
cy.get(queryEditor.queryMoreAction).click();
cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",

View File

@ -1,5 +1,6 @@
const datasource = require("../../../../locators/DatasourcesEditor.json");
const queryLocators = require("../../../../locators/QueryEditor.json");
const queryEditor = require("../../../../locators/QueryEditor.json");
describe("Switch datasource", function() {
let postgresDatasourceName;
@ -92,7 +93,8 @@ describe("Switch datasource", function() {
});
it("Delete the query and datasources", function() {
cy.get(queryLocators.deleteQuery).click();
cy.get(queryEditor.queryMoreAction).click();
cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",

View File

@ -23,5 +23,7 @@ describe("Widget Selection", function() {
cy.get(`.t--widget-propertypane-toggle`)
.not(`[style *= "background: rgb(255, 224, 210);"]`) // Excluding focused widgets.
.should("have.length", 2);
cy.get(`.t--multi-selection-box`).should("have.length", 1);
});
});

View File

@ -1,10 +1,14 @@
const ApiEditor = require("../../../../locators/ApiEditor.json");
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
const pages = require("../../../../locators/Pages.json");
describe("Test curl import flow", function() {
it("Test curl import flow Run and Delete", function() {
localStorage.setItem("ApiPaneV2", "ApiPaneV2");
cy.NavigateToApiEditor();
cy.get(pages.integrationCreateNew)
.should("be.visible")
.click({ force: true });
cy.get(ApiEditor.curlImage).click({ force: true });
cy.get("textarea").type("curl -X GET https://mock-api.appsmith.com/users");
cy.importCurl();
@ -27,7 +31,5 @@ describe("Test curl import flow", function() {
cy.get("@deleteAction").then((response) => {
cy.expect(response.response.body.responseMeta.success).to.eq(true);
});
cy.get(ApiEditor.ApiHomePage).should("be.visible");
cy.get(ApiEditor.formActionButtons).should("not.exist");
});
});

View File

@ -14,11 +14,8 @@ describe("API Panel Test Functionality", function() {
});
it("PUT Action test API fetaure", function() {
cy.log("Login Successful");
cy.NavigateToAPI_Panel();
cy.log("Navigation to API Panel screen successful");
cy.CreateAPI("FirstAPI");
cy.log("Creation of FirstAPI Action successful");
cy.SelectAction(testdata.putAction);
cy.EnterSourceDetailsWithbody(
testdata.baseUrl,

View File

@ -1,10 +1,14 @@
const ApiEditor = require("../../../../locators/ApiEditor.json");
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
const pages = require("../../../../locators/Pages.json");
describe("Test curl import flow", function() {
it("Test curl import flow for POST action with JSON body", function() {
localStorage.setItem("ApiPaneV2", "ApiPaneV2");
cy.NavigateToApiEditor();
cy.get(pages.integrationCreateNew)
.should("be.visible")
.click({ force: true });
cy.get(ApiEditor.curlImage).click({ force: true });
cy.get("textarea").type(
'curl -d \'{"name":"morpheus","job":"leader"}\' -H Content-Type:application/json -X POST https://mock-api.appsmith.com/echo/post',
@ -30,6 +34,9 @@ describe("Test curl import flow", function() {
it("Test curl import flow for POST action with multipart form data", function() {
localStorage.setItem("ApiPaneV2", "ApiPaneV2");
cy.NavigateToApiEditor();
cy.get(pages.integrationCreateNew)
.should("be.visible")
.click({ force: true });
cy.get(ApiEditor.curlImage).click({ force: true });
cy.get("textarea").type(
"curl --request POST http://httpbin.org/post -F 'randomKey=randomValue' --form 'randomKey2=\"randomValue2\"'",

View File

@ -1,5 +1,6 @@
const datasource = require("../../../../locators/DatasourcesEditor.json");
const queryEditor = require("../../../../locators/QueryEditor.json");
const datasourceEditor = require("../../../../locators/DatasourcesEditor.json");
let datasourceName;
@ -21,14 +22,18 @@ describe("Postgres datasource test cases", function() {
it("Create a new query from the datasource editor", function() {
cy.saveDatasource();
cy.get(datasource.createQuerty).click();
// cy.get(datasource.createQuerty).click();
cy.get(`${datasourceEditor.datasourceCard} ${datasource.createQuerty}`)
.last()
.click();
cy.wait("@createNewApi").should(
"have.nested.property",
"response.body.responseMeta.status",
201,
);
cy.get(queryEditor.deleteQuery).click();
cy.get(queryEditor.queryMoreAction).click();
cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",

View File

@ -100,7 +100,6 @@ describe("Entity explorer tests related to copy query", function() {
cy.log("sliced id :" + updatedName);
cy.EditEntityNameByDoubleClick(datasourceName, updatedName);
cy.SearchEntityandOpen(updatedName);
cy.testSaveDatasource();
cy.hoverAndClick();
cy.get(apiwidget.delete).click({ force: true });
//This is check to make sure if a datasource is active 409

View File

@ -63,7 +63,8 @@ describe("Entity explorer datasource structure", function() {
201,
);
cy.get(queryEditor.deleteQuery).click();
cy.get(queryEditor.queryMoreAction).click();
cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",
@ -72,7 +73,8 @@ describe("Entity explorer datasource structure", function() {
cy.GlobalSearchEntity("MyQuery");
cy.get(`.t--entity-name:contains(MyQuery)`).click();
cy.get(queryEditor.deleteQuery).click();
cy.get(queryEditor.queryMoreAction).click();
cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",
@ -151,7 +153,8 @@ describe("Entity explorer datasource structure", function() {
200,
);
cy.get(queryEditor.deleteQuery).click();
cy.get(queryEditor.queryMoreAction).click();
cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",

View File

@ -41,7 +41,8 @@ describe("Confirm run action", function() {
200,
);
cy.get(queryEditor.deleteQuery).click();
cy.get(queryEditor.queryMoreAction).click();
cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",

View File

@ -1,5 +1,6 @@
const queryLocators = require("../../../../locators/QueryEditor.json");
const datasource = require("../../../../locators/DatasourcesEditor.json");
const pages = require("../../../../locators/Pages.json");
let datasourceName;
@ -39,6 +40,9 @@ describe("Create a query with a postgres datasource, run, save and then delete t
});
it("Create, runs and delete another query", () => {
cy.NavigateToQueryEditor();
cy.get(pages.integrationActiveTab)
.should("be.visible")
.click({ force: true });
cy.contains(".t--datasource-name", datasourceName)
.find(queryLocators.createQuery)
.click();
@ -53,6 +57,9 @@ describe("Create a query with a postgres datasource, run, save and then delete t
});
it("Deletes a datasource", () => {
cy.NavigateToQueryEditor();
cy.get(pages.integrationActiveTab)
.should("be.visible")
.click({ force: true });
cy.contains(".t--datasource-name", datasourceName)
.find(".t--edit-datasource")
.click();

View File

@ -13,6 +13,8 @@
"sectionAuthentication": "[data-cy=section-Authentication]",
"PostgresEntity": ".t--entity-name:contains(PostgreSQL)",
"createQuerty": ".t--create-query",
"activeDatasourceList": ".t--active-datasource-list",
"datasourceCard": ".t--datasource",
"editDatasource": ".t--edit-datasource",
"defaultDatabaseName": "input[name='datasourceConfiguration.connection.defaultDatabaseName']",
"selConnectionType": "[data-cy='datasourceConfiguration.connection.type']",

View File

@ -1,5 +1,5 @@
{
"input": ".CodeMirror textarea",
"input": ".CodeEditorTarget",
"hints": "ul.CodeMirror-hints",
"evaluatedValue": ".t--CodeEditor-evaluatedValue",
"bindingPrompt": ".t--no-binding-prompt"

View File

@ -9,7 +9,9 @@
"Menuaction": ".bp3-overlay-open>.bp3-transition-container",
"Delete": ":nth-child(2) > .bp3-menu-item",
"apiEditorIcon": ".t--nav-link-api-editor",
"addEntityAPI": ".apis .t--entity-add-btn",
"addEntityAPI": ".datasources .t--entity-add-btn",
"integrationCreateNew": "[data-cy=t--tab-CREATE_NEW]",
"integrationActiveTab": "[data-cy=t--tab-ACTIVE]",
"entityWidget": ".t--entity-name:contains('Widgets')",
"entityTable": ".t--entity-name:contains('Table1')",
"entityText": ".t--entity-name:contains('Text1')",

View File

@ -5,6 +5,8 @@
"runQuery": ".t--run-query",
"saveQuery": ".t--save-query",
"deleteQuery": ".t--delete-query",
"queryMoreAction": ".t--more-action-menu",
"deleteUsingContext": ".t--apiFormDeleteBtn",
"createQuery": ".t--create-query",
"addQueryEntity": ".//div[contains(@class,'t--entity group queries')]//div[contains(@class,'t--entity-add-btn')]",
"addDatasource": ".t--add-datasource",

View File

@ -27,7 +27,7 @@
"createMarker": ".t--property-control-createnewmarker [type='checkbox']",
"zoomLevel": ".t--property-control-zoomlevel svg",
"sourceImage": ".t--property-control-image .CodeMirror-code",
"defaultImage": ".t--property-control-defaultimage .CodeMirror-code",
"defaultImage": ".t--property-control-defaultimage .CodeMirror textarea",
"Chartlabel": ".t--draggable-chartwidget g:nth-child(5) text",
"PieChartLabel": ".t--draggable-chartwidget g g:nth-child(1) g text",
"pickMyLocation": ".t--draggable-mapwidget div[title='Pick My Location']"

View File

@ -8,8 +8,8 @@
"buttonWidget": ".t--draggable-buttonwidget",
"buttonStyleDropdown": ".t--property-control-buttonstyle [name='downArrow']",
"buttonBackground": ".sc-ecQjpJ > div > .bp3-button",
"copyWidget": ":nth-child(2) > .bp3-popover-target > .sc-bdnylx > svg",
"removeWidget": ":nth-child(3) > .bp3-popover-target > .sc-bdnylx > svg",
"copyWidget": ".t--copy-widget svg",
"removeWidget": ".t--delete-widget svg",
"propertypaneText": ".t--propertypane .bp3-panel-stack-view",
"formButtonWidget": ".t--widget-formbuttonwidget",
"textWidget": ".t--draggable-textwidget",
@ -75,7 +75,7 @@
"verticalBottom": ".t--icon-tab-BOTTOM",
"textColor": ".t--property-control-textcolor input",
"backgroundcolorPicker": ".t--property-control-backgroundcolor input",
"greenColor": "//div[@color='rgb(3, 179, 101)']",
"greenColor": "//div[@color='#03b365']",
"toggleJsColor": ".t--property-control-textcolor .t--js-toggle",
"backgroundColor": ".t--property-control-cellbackground input",
"toggleJsBcgColor": ".t--property-control-cellbackground .t--js-toggle",
@ -105,5 +105,6 @@
"switchWidgetActive": ".t--switch-widget-active",
"switchWidgetInactive": ".t--switch-widget-inactive",
"switchWidgetLoading": ".t--switch-widget-loading",
"chartPlotGroup": "g.raphael-group-63-plot-group"
"chartPlotGroup": "g.raphael-group-63-plot-group",
"toggleEnableMultirowselection": ".t--property-control-enablemultirowselection .bp3-control-indicator"
}

View File

@ -12,12 +12,11 @@
"path": ".t--path >div textarea",
"editResourceUrl": ".t--dataSourceField",
"autoSuggest": "//div[contains(@id,'react-select')]",
"headerKey": "(//div[contains(@class,'t--actionConfiguration.headers[0].key.0')]//textarea)[2]",
"headerValue": "(//div[contains(@class,'t--actionConfiguration.headers[0].value.0')]//textarea)[2]",
"postDefaultContentTypeHeaderKey": "(//div[contains(@class,'t--actionConfiguration.headers[2].key.2')]//textarea)[2]",
"queryKey": "(//div[contains(@class,'t--actionConfiguration.queryParameters[0].key.0')]//textarea)[2]",
"queryValue": "(//div[contains(@class,'t--actionConfiguration.queryParameters[0].value.0')]//textarea)[2]",
"formEncoded": "(//div[contains(@class,'t--actionConfiguration.bodyFormData[0].key.0')]//textarea)[2]",
"headerKey": ".t--actionConfiguration\\.headers\\[0\\]\\.key\\.0",
"headerValue": ".t--actionConfiguration\\.headers\\[0\\]\\.value\\.0",
"queryKey": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.key\\.0",
"queryValue": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.value\\.0",
"formEncoded": ".t--actionConfiguration\\.bodyFormData\\[0\\]\\.key\\.0",
"responseStatus": "//div[@id='root']",
"responseText": ".CodeMirror-line > [role='presentation']",
"createApiOnSideBar": "button:contains('Create new API')",
@ -54,6 +53,6 @@
"onPageLoad": "[data-cy=executeOnLoad]",
"renameEntity": ".single-select >div:contains('Edit Name')",
"paramsTab": "//li//span[text()='Params']",
"paramKey": "(//div[contains(@class,'t--actionConfiguration.queryParameters[0].key.0')]//textarea)[1]",
"paramValue": "(//div[contains(@class,'t--actionConfiguration.queryParameters[0].value.0')]//textarea)[1]"
"paramKey": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.key\\.0",
"paramValue": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.value\\.0"
}

View File

@ -13,9 +13,9 @@
"optionchangetextDropdown": " .t--property-control-onoptionchange .CodeMirror-code",
"optionchangetextDatePicker": " .t--property-control-ondateselected .CodeMirror-code",
"optionchangetextCheckbox": ".t--property-control-oncheckchange .CodeMirror-code",
"optionchangetextSwitch": ".t--property-control-onchange .CodeMirror-code",
"optionchangetextSwitch": ".t--property-control-onchange .CodeMirror textarea",
"optionchangetextInput": ".t--property-control-ontextchanged .CodeMirror-code",
"optionchangeRadioselect": ".t--property-control-onselectionchange .CodeMirror-code",
"optionchangeRadioselect": ".t--property-control-onselectionchange .CodeMirror textarea",
"optionalignment": ".t--property-control-alignment .bp3-popover-target",
"onMarkerclick": ".t--property-control-onmarkerclick .CodeMirror-code",
"success": "div[type='success'] span",

View File

@ -3,7 +3,7 @@
"NoQueryMsg": "p:contains('No DB Queries yet.')",
"NoWidgetsMsg": "p:contains('No widgets yet.')",
"AddPage": ".pages .t--entity-add-btn",
"addEntityAPI": ".apis .t--entity-add-btn",
"addEntityAPI": ".datasources .t--entity-add-btn",
"entityWidget": ".t--entity-name:contains('Widgets')",
"entityTable": ".t--entity-name:contains('Table1')",
"entityText": ".t--entity-name:contains('Text1')",
@ -13,7 +13,7 @@
"editName": ".single-select >div:contains('Edit Name')",
"deletePage": ".single-select >div:contains('Delete')",
"refreshStructure": ".single-select >div:contains('Refresh')",
"entityQuery": ".t--entity-name:contains('Queries')",
"entityQuery": ".t--entity-name:contains('Datasources')",
"datasourceStructure": ".t--entity.datasourceStructure",
"datasourceColumn": ".t--datasource-column",
"templateMenuIcon": ".t--template-menu-trigger",
@ -25,6 +25,6 @@
"addWidget":".widgets .t--entity-add-btn",
"dropHere":".appsmith_widget_0",
"closeWidgets":".t--close-widgets-sidebar",
"addDBQueryEntity": ".dbqueries .t--entity-add-btn",
"addDBQueryEntity": ".datasources .t--entity-add-btn",
"editEntity": ".t--entity-name input"
}

View File

@ -31,8 +31,8 @@
"inputValue": ".t--table-filter-value-input",
"canvas": ".canvas",
"removeFilter": ".t--table-filter-remove-btn",
"compactMode": ".t--table-compact-mode-toggle-btn",
"compactOpt": ".t--table-compact-mode-option",
"rowHeight": ".t--table-compact-mode-toggle-btn",
"rowHeightOpt": ".t--table-compact-mode-option",
"visibilityMode": ".t--table-column-visibility-toggle-btn",
"visibilityOpt": ".option-title",
"containerWidget": ".t--widget-containerwidget",

View File

@ -24,6 +24,12 @@ const viewWidgetsPage = require("../locators/ViewWidgets.json");
let pageidcopy = " ";
export const initLocalstorage = () => {
cy.window().then((window) => {
window.localStorage.setItem("ShowCommentsButtonToolTip", "");
});
};
Cypress.Commands.add("createOrg", () => {
cy.get(homePage.createOrg)
.should("be.visible")
@ -373,6 +379,8 @@ Cypress.Commands.add("LogintoApp", (uname, pword) => {
"response.body.responseMeta.status",
200,
);
initLocalstorage();
});
Cypress.Commands.add("LoginFromAPI", (uname, pword) => {
@ -509,6 +517,9 @@ Cypress.Commands.add("NavigateToAPI_Panel", () => {
cy.get(pages.addEntityAPI)
.should("be.visible")
.click({ force: true });
cy.get(pages.integrationCreateNew)
.should("be.visible")
.click({ force: true });
cy.get("#loading").should("not.exist");
});
@ -742,16 +753,14 @@ Cypress.Commands.add(
"EnterSourceDetailsWithHeader",
(baseUrl, v1method, hKey, hValue) => {
cy.enterDatasourceAndPath(baseUrl, v1method);
cy.xpath(apiwidget.headerKey)
cy.get(apiwidget.headerKey)
.first()
.click({ force: true })
.type(hKey, { force: true })
.should("have.value", hKey);
cy.xpath(apiwidget.headerValue)
.type(hKey, { parseSpecialCharSequences: true });
cy.get(apiwidget.headerValue)
.first()
.click({ force: true })
.type(hValue, { force: true })
.should("have.value", hValue);
.type(hValue, { parseSpecialCharSequences: true });
cy.WaitAutoSave();
},
);
@ -844,22 +853,20 @@ Cypress.Commands.add(
"EnterSourceDetailsWithQueryParam",
(baseUrl, v1method, hKey, hValue, qKey, qValue) => {
cy.enterDatasourceAndPath(baseUrl, v1method);
cy.xpath(apiwidget.headerKey)
cy.get(apiwidget.headerKey)
.first()
.click({ force: true })
.type(hKey, { force: true })
.should("have.value", hKey);
cy.xpath(apiwidget.headerValue)
.type(hKey, { parseSpecialCharSequences: true });
cy.get(apiwidget.headerValue)
.first()
.click({ force: true })
.type(hValue, { force: true })
.should("have.value", hValue);
cy.xpath(apiwidget.queryKey)
.type(hValue, { parseSpecialCharSequences: true });
cy.get(apiwidget.queryKey)
.first()
.click({ force: true })
.type(qKey, { force: true })
.should("have.value", qKey);
cy.xpath(apiwidget.queryValue)
cy.get(apiwidget.queryValue)
.first()
.click({ force: true })
.type(qValue, { force: true })
@ -880,6 +887,9 @@ Cypress.Commands.add(
Cypress.Commands.add("CreationOfUniqueAPIcheck", (apiname) => {
cy.get(pages.addEntityAPI).click();
cy.get(pages.integrationCreateNew)
.should("be.visible")
.click({ force: true });
cy.get(apiwidget.createapi).click({ force: true });
cy.wait("@createNewApi");
// cy.wait("@getUser");
@ -1722,9 +1732,7 @@ Cypress.Commands.add("getAlert", (alertcss) => {
cy.get(alertcss)
.click({ force: true })
.type("{command}{A}{del}")
.type("hello")
.should("not.to.be.empty");
.type("hello");
cy.get(".t--open-dropdown-Select-type").click({ force: true });
cy.get(".bp3-popover-content .bp3-menu li")
.contains("Success")
@ -1754,9 +1762,7 @@ Cypress.Commands.add(
cy.get(alertcss)
.click({ force: true })
.type("{command}{A}{del}")
.type("hello")
.should("not.to.be.empty");
.type("hello");
cy.get(".t--open-dropdown-Select-type").click({ force: true });
cy.get(".bp3-popover-content .bp3-menu li")
.contains("Success")
@ -1768,7 +1774,7 @@ Cypress.Commands.add("addQueryFromLightningMenu", (QueryName) => {
cy.get(commonlocators.dropdownSelectButton)
.first()
.click({ force: true })
.selectOnClickOption("Execute a DB Query")
.selectOnClickOption("Execute a Query")
.selectOnClickOption(QueryName);
});
@ -1776,7 +1782,7 @@ Cypress.Commands.add("addAPIFromLightningMenu", (ApiName) => {
cy.get(commonlocators.dropdownSelectButton)
.first()
.click({ force: true })
.selectOnClickOption("Call An API")
.selectOnClickOption("Execute a Query")
.selectOnClickOption(ApiName);
});
@ -1849,7 +1855,11 @@ Cypress.Commands.add("testSaveDeleteDatasource", () => {
200,
);
cy.get(datasourceEditor.editDatasource).click();
cy.get(
`${datasourceEditor.datasourceCard} ${datasourceEditor.editDatasource}`,
)
.last()
.click();
cy.get(".t--delete-datasource").click();
cy.wait("@deleteDatasource").should(
@ -1872,7 +1882,9 @@ Cypress.Commands.add("NavigateToDatasourceEditor", () => {
cy.get(explorer.addDBQueryEntity)
.last()
.click({ force: true });
cy.get(queryEditor.addDatasource).click();
cy.get(pages.integrationCreateNew)
.should("be.visible")
.click({ force: true });
});
Cypress.Commands.add("NavigateToQueryEditor", () => {
@ -1902,7 +1914,11 @@ Cypress.Commands.add("saveDatasource", () => {
Cypress.Commands.add("testSaveDatasource", () => {
cy.saveDatasource();
cy.get(datasourceEditor.editDatasource).click();
cy.get(
`${datasourceEditor.datasourceCard} ${datasourceEditor.editDatasource}`,
)
.last()
.click();
cy.testDatasource();
});
@ -1960,7 +1976,9 @@ Cypress.Commands.add("createPostgresDatasource", () => {
Cypress.Commands.add("deleteDatasource", (datasourceName) => {
cy.NavigateToQueryEditor();
cy.get(pages.integrationActiveTab)
.should("be.visible")
.click({ force: true });
cy.contains(".t--datasource-name", datasourceName)
.find(".t--edit-datasource")
.click();
@ -2020,7 +2038,8 @@ Cypress.Commands.add("runAndDeleteQuery", () => {
200,
);
cy.get(queryEditor.deleteQuery).click();
cy.get(queryEditor.queryMoreAction).click();
cy.get(queryEditor.deleteUsingContext).click();
cy.wait("@deleteAction").should(
"have.nested.property",
"response.body.responseMeta.status",
@ -2044,7 +2063,7 @@ Cypress.Commands.add("executeDbQuery", (queryName) => {
.click({ force: true })
.get("ul.bp3-menu")
.children()
.contains("Execute a DB Query")
.contains("Execute a Query")
.click({ force: true })
.get("ul.bp3-menu")
.children()
@ -2168,9 +2187,12 @@ Cypress.Commands.add("copyWidget", (widget, widgetLocator) => {
.children()
.last()
.invoke("text")
.then((originalWidget) => {
.then((x) => {
cy.log(x);
let originalWidget = x.replaceAll("x", "");
originalWidget = originalWidget.replaceAll(/\u200B/g, "");
cy.log(originalWidget);
cy.get(widgetsPage.copyWidget).click();
cy.get(widgetsPage.copyWidget).click({ force: true });
cy.reload();
// Wait for the widget to be appear in the DOM and press Ctrl/Cmd + V to paste the button.
cy.get(widgetLocator).should("be.visible");
@ -2181,7 +2203,10 @@ Cypress.Commands.add("copyWidget", (widget, widgetLocator) => {
.children()
.last()
.invoke("text")
.then((copiedWidget) => {
.then((y) => {
cy.log(y);
let copiedWidget = y.replaceAll("x", "");
copiedWidget = copiedWidget.replaceAll(/\u200B/g, "");
cy.log(copiedWidget);
expect(originalWidget).to.be.equal(copiedWidget);
});
@ -2190,7 +2215,7 @@ Cypress.Commands.add("copyWidget", (widget, widgetLocator) => {
Cypress.Commands.add("deleteWidget", (widget) => {
// Delete the button widget
cy.get(widgetsPage.removeWidget).click();
cy.get(widgetsPage.removeWidget).click({ force: true });
cy.get(widgetsPage.deleteToast).should("have.text", "UNDO");
});
@ -2212,6 +2237,9 @@ Cypress.Commands.add("UpdateChartType", (typeOfChart) => {
Cypress.Commands.add("createAndFillApi", (url, parameters) => {
cy.NavigateToApiEditor();
cy.get(pages.integrationCreateNew)
.should("be.visible")
.click({ force: true });
cy.testCreateApiButton();
cy.get("@createNewApi").then((response) => {
cy.get(ApiEditor.ApiNameField).should("be.visible");
@ -2495,13 +2523,13 @@ Cypress.Commands.add("ValidatePaginationInputData", () => {
Cypress.Commands.add("callApi", (apiname) => {
cy.get(commonlocators.callApi)
.first()
.click();
.click({ force: true });
cy.get(commonlocators.singleSelectMenuItem)
.contains("Call An API")
.click();
.contains("Execute a Query")
.click({ force: true });
cy.get(commonlocators.selectMenuItem)
.contains(apiname)
.click();
.click({ force: true });
});
Cypress.Commands.add("assertPageSave", () => {
@ -2512,10 +2540,16 @@ Cypress.Commands.add("ValidateQueryParams", (param) => {
cy.xpath(apiwidget.paramsTab)
.should("be.visible")
.click({ force: true });
cy.xpath(apiwidget.paramKey)
.first()
.contains(param.key);
cy.xpath(apiwidget.paramValue)
.first()
.contains(param.value);
cy.validateCodeEditorContent(apiwidget.paramKey, param.key);
cy.validateCodeEditorContent(apiwidget.paramValue, param.value);
});
Cypress.Commands.add(
"validateCodeEditorContent",
(selector, contentToValidate) => {
cy.get(selector).within(() => {
cy.get(".CodeMirror-code").should("have.text", contentToValidate);
});
},
);

View File

@ -18,6 +18,7 @@ let appId;
// Import commands.js using ES2015 syntax:
import "./commands";
import { initLocalstorage } from "./commands";
Cypress.on("uncaught:exception", (err, runnable) => {
// returning false here prevents Cypress from
@ -31,11 +32,13 @@ Cypress.on("fail", (error, runnable) => {
});
before(function() {
initLocalstorage();
cy.startServerAndRoutes();
// Clear indexedDB
cy.window().then((window) => {
window.indexedDB.deleteDatabase("Appsmith");
});
const username = Cypress.env("USERNAME");
const password = Cypress.env("PASSWORD");
cy.LoginFromAPI(username, password);
@ -58,6 +61,7 @@ before(function() {
});
beforeEach(function() {
initLocalstorage();
Cypress.Cookies.preserveOnce("SESSION", "remember_token");
cy.startServerAndRoutes();
});

View File

@ -27,10 +27,10 @@ if [ "$target" == "ci" ]; then
# On the CI server run the tests in parallel
# This requires the projectId and the record_key to be configured in your environment variables. By default this is defined on the CI server
echo "Got the Build ID: $BUILD_ID"
$(npm bin)/cypress run --headless --browser chrome \
$(npm bin)/cypress run --headless \
--record --key "$CYPRESS_RECORD_KEY" --ci-build-id $BUILD_ID \
--parallel --group "Electrons on Gitlab CI" \
--spec "cypress/integration/Smoke_TestSuite/**/*.js"
else
$(npm bin)/cypress run --headless --browser chrome --spec "cypress/integration/Smoke_TestSuite/**/*.js"
$(npm bin)/cypress run --headless --browser chromium --spec "cypress/integration/Smoke_TestSuite/**/*.js"
fi

View File

@ -117,6 +117,7 @@
"react-mentions": "^4.1.1",
"react-paginating": "^1.4.0",
"react-player": "^2.3.1",
"react-rating": "^2.0.5",
"react-redux": "^7.1.3",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
@ -155,7 +156,7 @@
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "BROWSER=none EXTEND_ESLINT=true REACT_APP_ENVIRONMENT=DEVELOPMENT HOST=dev.appsmith.com craco start",
"start": "BROWSER=none EXTEND_ESLINT=true REACT_APP_ENVIRONMENT=DEVELOPMENT REACT_APP_CLIENT_LOG_LEVEL=debug HOST=dev.appsmith.com craco start",
"build": "./build.sh",
"build-local": "craco --max-old-space-size=4096 build --config craco.build.config.js",
"build-staging": "REACT_APP_ENVIRONMENT=STAGING craco --max-old-space-size=4096 build --config craco.build.config.js",

View File

@ -34,10 +34,10 @@
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="loader" style="width: 30vw;"></div>
<!--
To keep zIndex for tooltips higher than app comments, todo remove when migrating to Tooltip2
To keep zIndex for tooltips higher than app comments, todo remove when migrating to Tooltip2
Currently the className does not apply to the portal root, so we're unable to work with z-indexes based on that
-->
<div id="tooltip-root"></div>
<div id="tooltip-root"></div>
<div id="header-root"></div>
<div id="root"></div>
<script type="text/javascript">
@ -80,7 +80,7 @@
return window.localStorage.setItem(key, value);
}
};
// Note: The following handler is necessary for when we have a new deployment.
// What happens?
@ -103,7 +103,7 @@
const regex = /\/static\/js\/(\w.+).js$/;
const chunkLoadErrorMessage = "Unexpected token '<'"
if(regex.test(url) && url.indexOf(window.location.host) > -1 && message.includes(chunkLoadErrorMessage)) {
console.log("chunk load failed!", url);
console.error("chunk load failed!", url);
// First check if we've set the flag to not reload on chunk load fail
const donotReload = localStorageUtil.getItem("donotReloadOnChunkLoadFail");
if(!donotReload){
@ -131,7 +131,7 @@
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker.register("/pageService.js").catch(error => {
console.log("Service Worker Registration failed: " + error);
console.error("Service Worker Registration failed: " + error);
});
});
}

View File

@ -123,7 +123,11 @@ export const updateActionSuccess = (payload: { data: Action }) => {
};
};
export const deleteAction = (payload: { id: string; name: string }) => {
export const deleteAction = (payload: {
id: string;
name: string;
onSuccess?: () => void;
}) => {
return {
type: ReduxActionTypes.DELETE_ACTION_INIT,
payload,

View File

@ -70,3 +70,11 @@ export const updateBodyContentType = (
type: ReduxActionTypes.UPDATE_API_ACTION_BODY_CONTENT_TYPE,
payload: { title, apiId },
});
export const redirectToNewIntegrations = (
applicationId: string,
pageId: string,
): ReduxAction<{ applicationId: string; pageId: string }> => ({
type: ReduxActionTypes.REDIRECT_TO_NEW_INTEGRATIONS,
payload: { applicationId, pageId },
});

View File

@ -248,11 +248,15 @@ export const setAreCommentsEnabled = (flag: boolean) => ({
payload: flag,
});
export const fetchUnreadCommentThreadsCountRequest = () => ({
type: ReduxActionTypes.FETCH_UNREAD_COMMENT_THREADS_COUNT_REQUEST,
export const fetchUnreadCommentThreadsCountSuccess = (count?: number) => ({
type: ReduxActionTypes.FETCH_UNREAD_COMMENT_THREADS_COUNT_SUCCESS,
payload: count,
});
export const fetchUnreadCommentThreadsCountSuccess = (flag: boolean) => ({
type: ReduxActionTypes.FETCH_UNREAD_COMMENT_THREADS_COUNT_SUCCESS,
payload: flag,
export const incrementThreadUnreadCount = () => ({
type: ReduxActionTypes.INCREMENT_COMMENT_THREAD_UNREAD_COUNT,
});
export const decrementThreadUnreadCount = () => ({
type: ReduxActionTypes.DECREMENT_COMMENT_THREAD_UNREAD_COUNT,
});

View File

@ -137,6 +137,29 @@ export const fetchDatasources = () => {
};
};
export const fetchMockDatasources = () => {
return {
type: ReduxActionTypes.FETCH_MOCK_DATASOURCES_INIT,
};
};
export const addMockDatasourceToOrg = (
name: string,
organizationId: string,
pluginId: string,
packageName: string,
): ReduxAction<{
name: string;
organizationId: string;
pluginId: string;
packageName: string;
}> => {
return {
type: ReduxActionTypes.ADD_MOCK_DATASOURCES_INIT,
payload: { name, packageName, pluginId, organizationId },
};
};
export const initDatasourcePane = (
pluginType: string,
urlId?: string,

View File

@ -83,6 +83,11 @@ export const initCanvasLayout = (
};
};
export const setLastUpdatedTime = (payload: number): ReduxAction<number> => ({
type: ReduxActionTypes.SET_LAST_UPDATED_TIME,
payload,
});
export const savePageSuccess = (payload: SavePageResponse) => {
return {
type: ReduxActionTypes.SAVE_PAGE_SUCCESS,

View File

@ -31,11 +31,12 @@ export interface ApplicationResponsePayload {
pages?: ApplicationPagePayload[];
appIsExample: boolean;
appLayout?: AppLayoutConfig;
unreadCommentThreads?: number;
}
// export interface FetchApplicationResponse extends ApiResponse {
// data: ApplicationResponsePayload & { pages: ApplicationPagePayload[] };
// }
export interface FetchApplicationResponse extends ApiResponse {
data: ApplicationResponsePayload & { pages: ApplicationPagePayload[] };
}
export interface FetchApplicationsResponse extends ApiResponse {
data: Array<ApplicationResponsePayload & { pages: ApplicationPagePayload[] }>;
@ -154,13 +155,13 @@ class ApplicationApi extends Api {
static fetchApplication(
applicationId: string,
): AxiosPromise<FetchApplicationsResponse> {
): AxiosPromise<FetchApplicationResponse> {
return Api.get(ApplicationApi.baseURL + applicationId);
}
static fetchApplicationForViewMode(
applicationId: string,
): AxiosPromise<FetchApplicationsResponse> {
): AxiosPromise<FetchApplicationResponse> {
return Api.get(ApplicationApi.baseURL + `view/${applicationId}`);
}

View File

@ -77,11 +77,6 @@ class CommentsApi extends Api {
data: request,
});
}
// eslint-disable-next-line
static fetchUnreadCommentThreads(applicationId: string) {
return Promise.resolve({ data: { count: 10 } });
}
}
export default CommentsApi;

View File

@ -63,6 +63,26 @@ class DatasourcesApi extends API {
DatasourcesApi.url + `/${id}/structure?ignoreCache=${ignoreCache}`,
);
}
static fetchMockDatasources(): AxiosPromise<
GenericApiResponse<Datasource[]>
> {
return API.get(DatasourcesApi.url + "/mocks");
}
static addMockDbToDatasources(
name: string,
organizationId: string,
pluginId: string,
packageName: string,
): Promise<any> {
return API.post(DatasourcesApi.url + `/mocks`, {
name,
organizationId,
pluginId,
packageName,
});
}
}
export default DatasourcesApi;

View File

@ -0,0 +1,3 @@
<svg width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.92006 1.18808L7.51929 0.805547C7.46589 0.754465 7.40437 0.729004 7.33482 0.729004C7.26545 0.729004 7.20396 0.754465 7.15055 0.805547L4.00004 3.81269L0.849671 0.805628C0.796241 0.754545 0.734752 0.729084 0.665289 0.729084C0.595798 0.729084 0.534309 0.754545 0.480906 0.805628L0.0801884 1.18818C0.0266733 1.23916 1.18406e-09 1.29785 1.32213e-09 1.36419C1.46009e-09 1.43046 0.0267575 1.48916 0.0801884 1.54013L3.81566 5.10589C3.86906 5.15689 3.93058 5.18238 4.00004 5.18238C4.06951 5.18238 4.13091 5.15689 4.18428 5.10589L7.92006 1.54013C7.97347 1.48913 8 1.43044 8 1.36419C8 1.29785 7.97347 1.23916 7.92006 1.18808Z" fill="#090707"/>
</svg>

After

Width:  |  Height:  |  Size: 743 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="9" viewBox="0 0 14 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1603 0.969728L9.53673 1.56501L12.3118 4.21393L-5.87492e-07 4.21393L-7.39946e-07 5.05579L12.3118 5.0558L9.53673 7.70468L10.1603 8.29997L14 4.63483L10.1603 0.969728Z" fill="#090707"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 730 B

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" >
<path d="M13.3327 7.33341H5.21935L8.94601 3.60675L7.99935 2.66675L2.66602 8.00008L7.99935 13.3334L8.93935 12.3934L5.21935 8.66675H13.3327V7.33341Z" fill="#A2A6A8" id="svg_1" class="" transform="rotate(-180 7.999359607696534,8.00007438659668) "/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" stroke="#a2a6a8" strokeWidth="2"><path d="M1.784 8h11.351"/><path fill="#a2a6a8" d="M6.605 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z"/></svg>

After

Width:  |  Height:  |  Size: 213 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none">
<path fill="#A2A6A8" fill-opacity="1" stroke="#A2A6A8" stroke-opacity="1" stroke-width="2" stroke-dasharray="none" stroke-linejoin="miter" stroke-linecap="butt" fill-rule="nonzero" opacity="1" d="M2.3243240536870218,7.999999884012587 L13.675675405038383,7.999999884012587 " />
</svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5 0H3.14706V1.41176H14.1747V13.6471H15.5V0ZM12.8529 2.82353H0.5V16H12.8529V2.82353Z" fill="#090707"/>
</svg>

After

Width:  |  Height:  |  Size: 258 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.8956 1.29262C1.95273 0.235792 3.66667 0.235792 4.7238 1.29262C5.78093 2.34945 5.78093 4.06291 4.7238 5.11975C4.60724 5.23627 4.4827 5.33995 4.35193 5.43078L5.448 6.019L6.08506 5.32306L12.1055 2.08076C13.0124 1.59232 14.1323 1.88302 14.6929 2.72638L14.7766 2.8641L7.397 6.838L14.7766 10.8125L14.6929 10.9502C14.1323 11.7935 13.0124 12.0842 12.1055 11.5958L6.08506 8.35351L5.586 7.807L4.21474 8.5457C4.30828 8.60855 4.39877 8.67787 4.48558 8.75365L4.62104 8.88025C5.67817 9.93709 5.67817 11.6505 4.62105 12.7074C3.56392 13.7642 1.84998 13.7642 0.792846 12.7074C-0.264282 11.6505 -0.264282 9.93709 0.792846 8.88025C1.12818 8.54502 1.5296 8.31612 1.95519 8.19356L4.264 6.95L2.21011 5.84551C1.78495 5.7494 1.3785 5.54968 1.03107 5.24634L0.8956 5.11975C-0.161527 4.06291 -0.161527 2.34946 0.8956 1.29262ZM3.52628 9.75678C3.00747 9.34607 2.25163 9.38029 1.77233 9.85946C1.25615 10.3755 1.25616 11.2121 1.77233 11.7282C2.2885 12.2442 3.12539 12.2442 3.64156 11.7282C4.15774 11.2121 4.15774 10.3755 3.64156 9.85946L3.52628 9.75678ZM12.0683 6.07527L12.1767 6.08198C12.5771 6.1361 12.8876 6.44438 12.9408 6.8351L12.9489 6.95462C12.9489 7.44005 12.5549 7.83397 12.0693 7.83397C11.5837 7.83397 11.1897 7.44005 11.1897 6.96244C11.1853 6.5546 11.4597 6.2104 11.8336 6.107L11.962 6.08307L12.0683 6.07527ZM15.1194 6.06362L15.2271 6.07033C15.6282 6.12445 15.9387 6.43272 15.9919 6.82345L16 6.94297C16 7.4284 15.606 7.82232 15.1204 7.82232C14.6348 7.82232 14.2408 7.4284 14.2408 6.95078C14.2364 6.54294 14.5108 6.19875 14.8847 6.09534L15.0131 6.07141L15.1194 6.06362ZM3.62903 2.16915C3.11022 1.75843 2.35439 1.79266 1.87508 2.27183C1.35891 2.78786 1.35891 3.62451 1.87508 4.14054C2.39126 4.65657 3.22814 4.65657 3.74432 4.14054C4.26049 3.62451 4.26049 2.78786 3.74432 2.27183L3.62903 2.16915Z" fill="#4B4848"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg width="48" height="24" fill="#fff"><path d="M11 16v-3h10v-2H11V8l-4 4 4 4zm-8 4h2V4H3v16zM36.378 7.73v3h-10v2h10v3l4-4-4-4zm8-4h-2v16h2v-16z"/></svg>

After

Width:  |  Height:  |  Size: 154 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
<path fill="#fff" d="M8 11H11V21H13V11H16L12 7L8 11ZM4 3V5H20V3H4Z" class="selected" transform="rotate(-90 12.000000000000002,12) "/>
</svg>

After

Width:  |  Height:  |  Size: 216 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
<path fill="#fff" d="M8 11H11V21H13V11H16L12 7L8 11ZM4 3V5H20V3H4Z" transform="rotate(90 12,12.000000000000002) "/>
</svg>

After

Width:  |  Height:  |  Size: 200 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 0H1.4H2.54546V1.39814H1.4V2.54546H0V1.39814V0ZM3.81818 0H6.36364V1.39814H3.81818V0ZM1.4 3.81818H0V6.36364H1.4V3.81818ZM1.4 7.63636H0V10.1818H1.4V7.63636ZM12.6 3.81818H14V6.36364H12.6V3.81818ZM14 7.63636H12.6V10.1818H14V7.63636ZM7.63636 0H10.1818V1.39814H7.63636V0ZM6.36364 12.6H3.81818V14H6.36364V12.6ZM7.63636 12.6H10.1818V14H7.63636V12.6ZM0 14H1.4H2.54546V12.6H1.4V11.4546H0V12.6V14ZM12.6 0H14V1.39814V2.54546H12.6V1.39814H11.4546V0H12.6ZM14 14H12.6H11.4546V12.6H12.6V11.4546H14V12.6V14Z" fill="#4B4848"/>
</svg>

After

Width:  |  Height:  |  Size: 663 B

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 6H10V9H6V6Z" fill="#939090"/>
<path d="M13 6H15V9H13V6Z" fill="#939090"/>
<path d="M1 6H3V9H1V6Z" fill="#939090"/>
</svg>

After

Width:  |  Height:  |  Size: 229 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#939090"><path d="M13 7.5a1 1 0 1 1 2 0v1a1 1 0 1 1-2 0v-1zm-12 0a1 1 0 1 1 2 0v1a1 1 0 1 1-2 0v-1zm8.5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 1 1 3 0z"/></svg>

After

Width:  |  Height:  |  Size: 220 B

View File

@ -0,0 +1,4 @@
<svg width="9" height="12" viewBox="0 0 9 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.79102 11.1094H4.11328V10.2832H3.87891C3.05273 10.2832 2.71875 9.90234 2.71875 8.96484V7.07812C2.71875 6.23438 2.29688 5.79492 1.43555 5.70703V5.56641C2.29688 5.47852 2.71875 5.03906 2.71875 4.19531V2.32031C2.71875 1.38281 3.05273 1.00195 3.87891 1.00195H4.11328V0.175781H3.79102C2.4082 0.175781 1.75781 0.826172 1.75781 2.19141V3.83203C1.75781 4.76953 1.43555 5.08008 0.46875 5.08008V6.19336C1.43555 6.19336 1.75781 6.50391 1.75781 7.44141V9.09375C1.75781 10.459 2.4082 11.1094 3.79102 11.1094Z" fill="#4B4848"/>
<path d="M8.13305 11.1094H8.45531V10.2832H8.22094C7.39477 10.2832 7.06078 9.90234 7.06078 8.96484V7.07812C7.06078 6.23438 6.63891 5.79492 5.77758 5.70703V5.56641C6.63891 5.47852 7.06078 5.03906 7.06078 4.19531V2.32031C7.06078 1.38281 7.39477 1.00195 8.22094 1.00195H8.45531V0.175781H8.13305C6.75023 0.175781 6.09984 0.826172 6.09984 2.19141V3.83203C6.09984 4.76953 5.77758 5.08008 4.81078 5.08008V6.19336C5.77758 6.19336 6.09984 6.50391 6.09984 7.44141V9.09375C6.09984 10.459 6.75023 11.1094 8.13305 11.1094Z" fill="#4B4848"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.38281 11.8271C6.71777 11.8271 6.99805 11.5605 6.99805 11.2324V6.68652H11.4141C11.7422 6.68652 12.0225 6.40625 12.0225 6.07129C12.0225 5.73633 11.7422 5.46289 11.4141 5.46289H6.99805V0.910156C6.99805 0.582031 6.71777 0.31543 6.38281 0.31543C6.04785 0.31543 5.77441 0.582031 5.77441 0.910156V5.46289H1.35156C1.02344 5.46289 0.743164 5.73633 0.743164 6.07129C0.743164 6.40625 1.02344 6.68652 1.35156 6.68652H5.77441V11.2324C5.77441 11.5605 6.04785 11.8271 6.38281 11.8271Z" fill="#4B4848"/>
</svg>

After

Width:  |  Height:  |  Size: 603 B

View File

@ -0,0 +1 @@
<svg width="20" height="4" viewBox="0 0 20 4" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="20" height="3"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.9545 4L14.2934 8.12197C14.4556 8.44862 14.8847 8.75905 15.2469 8.81139L20 9.75293L16.6923 13.2405C16.43 13.4949 16.2662 13.9968 16.3281 14.3559L16.8704 19.0588L12.5437 17.0908C12.2195 16.9211 11.6892 16.9211 11.3649 17.0908L7.03854 19.0572L7.58084 14.3543C7.64276 13.9952 7.47897 13.4929 7.21665 13.2388L4 9.75127L8.6621 8.80973C9.02462 8.75739 9.45373 8.44697 9.61552 8.12031L11.9545 4Z" fill="#EAEAEA"/>
</svg>

After

Width:  |  Height:  |  Size: 521 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 10.474V13.5261C20 13.7558 19.8138 13.9423 19.5838 13.9423H13.9423V19.5838C13.9423 19.8139 13.7558 20 13.5261 20H10.474C10.2444 20 10.0578 19.8138 10.0578 19.5838V13.9423H4.41619C4.18625 13.9423 4 13.7558 4 13.5261V10.474C4 10.2442 4.18625 10.0578 4.41619 10.0578H10.0578V4.41619C10.0578 4.18616 10.2443 4 10.474 4H13.5261C13.7558 4 13.9423 4.18616 13.9423 4.41619V10.0578H19.5838C19.8139 10.0578 20 10.2442 20 10.474Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 550 B

View File

@ -203,16 +203,25 @@ const reduceReactions = (
reactions.reduce(
(res: Record<string, ComponentReaction>, reaction: Reaction) => {
const { byUsername, emoji } = reaction;
const sameAsCurrent = byUsername === username;
if (res[reaction.emoji]) {
res[reaction.emoji].count++;
if (!sameAsCurrent) {
res[reaction.emoji].users = [
...(res[reaction.emoji].users || []),
byUsername,
];
}
} else {
const users = !sameAsCurrent ? [byUsername] : [];
res[emoji] = {
count: 1,
reactionEmoji: emoji,
users,
} as ComponentReaction;
}
if (byUsername === username) {
if (sameAsCurrent) {
res[reaction.emoji].active = true;
}

View File

@ -7,6 +7,7 @@ import ScrollToLatest from "./ScrollToLatest";
import {
addCommentToThreadRequest,
markThreadAsReadRequest,
resetVisibleThread,
setCommentResolutionRequest,
} from "actions/commentActions";
@ -21,6 +22,7 @@ import { RawDraftContentState } from "draft-js";
import styled from "styled-components";
import { animated } from "react-spring";
import { AppState } from "reducers";
import { useEffect } from "react";
const ThreadContainer = styled(animated.div)<{
visible?: boolean;
@ -79,6 +81,12 @@ function CommentThreadContainer({
state.ui.comments.visibleCommentThreadId === commentThreadId,
);
useEffect(() => {
if (isVisible && !commentThread?.isViewed) {
dispatch(markThreadAsReadRequest(commentThreadId));
}
}, [isVisible, commentThread?.isViewed]);
// Check if the comments window is scrolled to the bottom
// We don't autoscroll for the user receiving the updates
// for better UX, instead we'd show a helper message to indicate

View File

@ -13,9 +13,10 @@ function ShowcaseCarouselModal({ children }: { children: React.ReactNode }) {
isOpen
left={25}
onClose={() => {
console.log("handle close");
// TODO (rishabh) handle close
}}
overlayClassName="comments-onboarding-carousel"
portalClassName="comments-onboarding-carousel-portal"
scrollContents
width={325}
zIndex={Layers.appComments}

View File

@ -4,11 +4,6 @@ import Text, { TextType } from "components/ads/Text";
import ShowcaseCarousel, { Steps } from "components/ads/ShowcaseCarousel";
import ProfileForm, { PROFILE_FORM } from "./ProfileForm";
import CommentsCarouselModal from "./CommentsCarouselModal";
import CommentsOnboardingStep1 from "assets/images/comments-onboarding/step-1.png";
import CommentsOnboardingStep2 from "assets/images/comments-onboarding/step-2.png";
import CommentsOnboardingStep3 from "assets/images/comments-onboarding/step-3.png";
import CommentsOnboardingStep4 from "assets/images/comments-onboarding/step-4.png";
import CommentsOnboardingStep5 from "assets/images/comments-onboarding/step-5.png";
import styled, { withTheme } from "styled-components";
import { Theme } from "constants/DefaultTheme";
@ -26,37 +21,42 @@ import { setCommentsIntroSeen } from "utils/storage";
import { updateUserDetails } from "actions/userActions";
import { S3_BUCKET_URL } from "constants/ThirdPartyConstants";
const getBanner = (step: number) =>
`${S3_BUCKET_URL}/comments/step-${step}.png`;
const introSteps = [
{
title: "Introducing Live Comments",
content:
"We are introducing live comments. From now on you will be able to comment on your apps, tag other people and exchange thoughts in threads. Click Next to learn more about comments and start commenting.",
banner: CommentsOnboardingStep1,
banner: getBanner(1),
hideBackBtn: true,
},
{
title: "Give feedback",
content:
"Comment on your co-workers work and share your thoughts on what works and what needs change.",
banner: CommentsOnboardingStep2,
banner: getBanner(2),
},
{
title: "Invite other people to your conversations",
content:
"When leaving a comment you can tag other people by writing @ and their name. This way the person you tagged will get a notification and an e-mail that you tagged them in a comment.",
banner: CommentsOnboardingStep3,
banner: getBanner(3),
},
{
title: "Tag a comment to a widget",
content:
"If you click on a component while in a comment mode you will tag that comment to that widget. This way if the widget is moved the comment will be moved as well. You can disconnect the comment and widget y simply moving the the comment away from the widget.",
banner: CommentsOnboardingStep4,
banner: getBanner(4),
},
{
title: "You are all set!",
content:
"By clicking on the comments icon in the top right corner you will activate the collaboration mode and will be able to start a thread or answer to someone elses comment.",
banner: CommentsOnboardingStep5,
banner: getBanner(5),
},
];
@ -72,7 +72,7 @@ const StyledImg = styled.img`
function IntroStep(props: {
title: string;
content: string;
banner: typeof CommentsOnboardingStep1;
banner: string;
theme: Theme;
}) {
return (

View File

@ -70,7 +70,7 @@ const defaultProps = {
size: Size.small,
isLoading: false,
disabled: false,
fill: false,
fill: undefined,
tag: "a",
};

View File

@ -64,7 +64,7 @@ const ColorTab = styled.div<{ color: string }>`
`;
const defaultColors: string[] = [
"rgb(3, 179, 101)",
"#03b365",
"#FFC13D",
"#38AFF4",
"#DD4B34",

View File

@ -1,9 +1,10 @@
import React, { useState, useEffect, useCallback } from "react";
import React, { ReactNode, useState, useEffect, useCallback } from "react";
import Icon, { IconName, IconSize } from "./Icon";
import { CommonComponentProps, Classes } from "./common";
import Text, { TextType } from "./Text";
import { Popover, Position } from "@blueprintjs/core";
import styled from "constants/DefaultTheme";
import { Colors } from "constants/Colors";
export type DropdownOption = {
label?: string;
@ -16,6 +17,22 @@ export type DropdownOption = {
onSelect?: (value?: string) => void;
};
export interface DefaultDropDownValueNodeProps {
selected: DropdownOption;
showLabelOnly?: boolean;
isOpen?: boolean;
}
export interface RenderDropdownOptionType {
option: DropdownOption;
optionClickHandler?: (dropdownOption: DropdownOption) => void;
}
type RenderOption = ({
option,
optionClickHandler,
}: RenderDropdownOptionType) => ReactNode;
export type DropdownProps = CommonComponentProps & {
options: DropdownOption[];
selected: DropdownOption;
@ -25,7 +42,10 @@ export type DropdownProps = CommonComponentProps & {
showLabelOnly?: boolean;
optionWidth?: string;
showDropIcon?: boolean;
headerLabel?: string;
SelectedValueNode?: typeof DefaultDropDownValueNode;
bgColor?: string;
renderOption?: RenderOption;
};
export const DropdownContainer = styled.div<{ width: string; height: string }>`
@ -38,12 +58,15 @@ const Selected = styled.div<{
isOpen: boolean;
disabled?: boolean;
height: string;
bgColor?: string;
}>`
padding: ${(props) => props.theme.spaces[2]}px
${(props) => props.theme.spaces[3]}px;
background: ${(props) =>
props.disabled
? props.theme.colors.dropdown.header.disabledBg
: !!props.bgColor
? props.bgColor
: props.theme.colors.dropdown.header.bg};
display: flex;
align-items: center;
@ -53,21 +76,31 @@ const Selected = styled.div<{
cursor: pointer;
${(props) =>
props.isOpen
? `border: 1px solid ${props.theme.colors.info.main}`
? `border: 1px solid ${
!!props.bgColor ? props.bgColor : props.theme.colors.info.main
}`
: props.disabled
? `border: 1px solid ${props.theme.colors.dropdown.header.disabledBg}`
: `border: 1px solid ${props.theme.colors.dropdown.header.bg}`};
: `border: 1px solid ${
!!props.bgColor
? props.bgColor
: props.theme.colors.dropdown.header.bg
}`};
${(props) =>
props.isOpen && !props.disabled ? "box-sizing: border-box" : null};
${(props) =>
props.isOpen && !props.disabled
props.isOpen && !props.disabled && !props.bgColor
? "box-shadow: 0px 0px 4px 4px rgba(203, 72, 16, 0.18)"
: null};
.${Classes.TEXT} {
${(props) =>
props.disabled
? `color: ${props.theme.colors.dropdown.header.disabledText}`
: `color: ${props.theme.colors.dropdown.header.text}`};
: `color: ${
!!props.bgColor
? Colors.WHITE
: props.theme.colors.dropdown.header.text
}`};
}
`;
@ -159,6 +192,12 @@ const StyledSubText = styled(Text)`
}
`;
const HeaderWrapper = styled.div`
color: #6d6d6d;
font-size: 10px;
padding: 0px 7px 7px 7px;
`;
const SelectedDropDownHolder = styled.div`
display: flex;
align-items: center;
@ -185,17 +224,17 @@ const SelectedIcon = styled(Icon)`
}
`;
function DefaultDropDownValueNode({
export function DefaultDropDownValueNode({
selected,
showLabelOnly,
}: {
selected: DropdownOption;
showLabelOnly?: boolean;
}) {
}: DefaultDropDownValueNodeProps) {
return (
<SelectedDropDownHolder>
{selected.icon ? (
<SelectedIcon name={selected.icon} size={IconSize.XXS} />
<SelectedIcon
name={selected.icon}
size={selected.iconSize || IconSize.XXS}
/>
) : null}
<Text type={TextType.P1}>
{showLabelOnly ? selected.label : selected.value}
@ -209,6 +248,7 @@ export default function Dropdown(props: DropdownProps) {
onSelect,
showDropIcon = true,
SelectedValueNode = DefaultDropDownValueNode,
renderOption,
} = { ...props };
const [isOpen, setIsOpen] = useState<boolean>(false);
const [selected, setSelected] = useState<DropdownOption>(props.selected);
@ -242,6 +282,7 @@ export default function Dropdown(props: DropdownProps) {
position={Position.BOTTOM_LEFT}
>
<Selected
bgColor={props.bgColor}
className={props.className}
disabled={props.disabled}
height={props.height || "38px"}
@ -249,13 +290,23 @@ export default function Dropdown(props: DropdownProps) {
onClick={() => setIsOpen(!isOpen)}
>
<SelectedValueNode
isOpen={isOpen}
selected={selected}
showLabelOnly={props.showLabelOnly}
/>
{showDropIcon && <Icon name="downArrow" size={IconSize.XXS} />}
</Selected>
<DropdownWrapper width={props.optionWidth || "260px"}>
{props.headerLabel && (
<HeaderWrapper>{props.headerLabel}</HeaderWrapper>
)}
{props.options.map((option: DropdownOption, index: number) => {
if (renderOption) {
return renderOption({
option,
optionClickHandler,
});
}
return (
<OptionWrapper
className="t--dropdown-option"

View File

@ -2,6 +2,7 @@ import React from "react";
import styled from "styled-components";
import EmojiPicker from "./EmojiPicker";
import { IconSize } from "./Icon";
import TooltipComponent from "./Tooltip";
const Container = styled.div`
display: flex;
@ -46,10 +47,29 @@ const Count = styled.div<{ active?: boolean }>`
white-space: nowrap;
`;
const ReactionsByContainer = styled.span`
max-width: 200px;
display: inline-block;
`;
function ReactionsBy(props: { reaction: Reaction }) {
const { reaction } = props;
if (!reaction?.users) return null;
const isSliced = reaction?.users.length > 5;
const users = reaction?.users.slice(0, 5);
if (isSliced) users.push("...");
if (reaction.active) users.unshift("You");
return <ReactionsByContainer>{users.join(", ")}</ReactionsByContainer>;
}
export type Reaction = {
count: number;
reactionEmoji: string;
active?: boolean;
users?: Array<string>;
};
export type Reactions = Record<string, Reaction>;
@ -98,6 +118,7 @@ function EmojiReactions({
};
if (reactions[emojiData].count === 0) delete reactions[emojiData];
} else {
addOrRemove = ReactionOperation.ADD;
reactions[emojiData] = {
active: true,
reactionEmoji: emojiData,
@ -124,16 +145,23 @@ function EmojiReactions({
<Container>
{!hideReactions &&
transformReactions(reactions).map((reaction: Reaction) => (
<Bubble
active={reaction.active}
<TooltipComponent
boundary={"viewport"}
content={<ReactionsBy reaction={reaction} />}
disabled={!reaction.users || reaction.users.length === 0}
key={reaction.reactionEmoji}
onClick={(e) => handleSelectReaction(e, reaction.reactionEmoji)}
modifiers={{ preventOverflow: { enabled: true } }}
>
<span>{reaction.reactionEmoji}</span>
{reaction.count > 1 && (
<Count active={reaction.active}>{reaction.count}</Count>
)}
</Bubble>
<Bubble
active={reaction.active}
onClick={(e) => handleSelectReaction(e, reaction.reactionEmoji)}
>
<span>{reaction.reactionEmoji}</span>
{reaction.count > 1 && (
<Count active={reaction.active}>{reaction.count}</Count>
)}
</Bubble>
</TooltipComponent>
))}
{!hideReactions ? (
<Bubble>

View File

@ -3,6 +3,7 @@ import { ReactComponent as DeleteIcon } from "assets/icons/ads/delete.svg";
import { ReactComponent as BookIcon } from "assets/icons/ads/book.svg";
import { ReactComponent as BugIcon } from "assets/icons/ads/bug.svg";
import { ReactComponent as CancelIcon } from "assets/icons/ads/cancel.svg";
import { ReactComponent as ExpandMore } from "assets/icons/ads/expand-more.svg";
import { ReactComponent as CrossIcon } from "assets/icons/ads/cross.svg";
import { ReactComponent as OpenIcon } from "assets/icons/ads/open.svg";
import { ReactComponent as UserIcon } from "assets/icons/ads/user.svg";
@ -39,6 +40,7 @@ import { ReactComponent as NoResponseIcon } from "assets/icons/ads/no-response.s
import { ReactComponent as LightningIcon } from "assets/icons/ads/lightning.svg";
import { ReactComponent as AddMoreIcon } from "assets/icons/ads/add-more.svg";
import { ReactComponent as RightArrowIcon } from "assets/icons/ads/right-arrow.svg";
import { ReactComponent as TrendingFlat } from "assets/icons/ads/trending-flat.svg";
import { ReactComponent as DatasourceIcon } from "assets/icons/ads/datasource.svg";
import { ReactComponent as PlayIcon } from "assets/icons/ads/play.svg";
import { ReactComponent as DesktopIcon } from "assets/icons/ads/desktop.svg";
@ -66,6 +68,11 @@ import { ReactComponent as Reaction } from "assets/icons/comments/reaction.svg";
import { ReactComponent as Reaction2 } from "assets/icons/comments/reaction-2.svg";
import { ReactComponent as Upload } from "assets/icons/ads/upload.svg";
import { ReactComponent as Download } from "assets/icons/ads/download.svg";
import { ReactComponent as ArrowForwardIcon } from "assets/icons/control/arrow_forward.svg";
import { ReactComponent as CapSolidIcon } from "assets/icons/control/cap_solid.svg";
import { ReactComponent as CapDotIcon } from "assets/icons/control/cap_dot.svg";
import { ReactComponent as LineDottedIcon } from "assets/icons/control/line_dotted.svg";
import { ReactComponent as LineDashedIcon } from "assets/icons/control/line_dashed.svg";
import styled from "styled-components";
import { CommonComponentProps, Classes } from "./common";
import { noop } from "lodash";
@ -126,6 +133,7 @@ export const IconCollection = [
"cancel",
"cross",
"delete",
"expand-more",
"open",
"user",
"general",
@ -162,6 +170,7 @@ export const IconCollection = [
"lightning",
"add-more",
"right-arrow",
"trending-flat",
"datasource",
"play",
"desktop",
@ -193,6 +202,11 @@ export const IconCollection = [
"unpin",
"reaction",
"reaction-2",
"arrow-forward",
"cap-solid",
"cap-dot",
"line-dotted",
"line-dashed",
] as const;
export type IconName = typeof IconCollection[number];
@ -274,6 +288,9 @@ const Icon = forwardRef(
case "delete":
returnIcon = <DeleteIcon />;
break;
case "expand-more":
returnIcon = <ExpandMore />;
break;
case "open":
returnIcon = <OpenIcon />;
break;
@ -386,6 +403,9 @@ const Icon = forwardRef(
case "right-arrow":
returnIcon = <RightArrowIcon />;
break;
case "trending-flat":
returnIcon = <TrendingFlat />;
break;
case "datasource":
returnIcon = <DatasourceIcon />;
break;
@ -489,6 +509,26 @@ const Icon = forwardRef(
returnIcon = <Download />;
break;
case "arrow-forward":
returnIcon = <ArrowForwardIcon />;
break;
case "cap-solid":
returnIcon = <CapSolidIcon />;
break;
case "cap-dot":
returnIcon = <CapDotIcon />;
break;
case "line-dotted":
returnIcon = <LineDottedIcon />;
break;
case "line-dashed":
returnIcon = <LineDashedIcon />;
break;
default:
returnIcon = null;
break;

View File

@ -3,7 +3,8 @@ import styled from "styled-components";
import { ControlIcons, ControlIconName } from "icons/ControlIcons";
const ItemWrapper = styled.div<{ selected: boolean }>`
width: 32px;
width: auto;
padding: 0 5px;
height: 32px;
display: flex;
align-items: center;
@ -30,6 +31,7 @@ const FlexWrapper = styled.div`
export interface IconTabOption {
icon: string;
value: string;
width?: number;
}
interface IconTabsComponentProps {
@ -41,21 +43,23 @@ interface IconTabsComponentProps {
function IconTabsComponent(props: IconTabsComponentProps) {
return (
<FlexWrapper>
{props.options.map((option: IconTabOption, index: number) => {
const controlIconName: ControlIconName = option.icon;
const ControlIcon = ControlIcons[controlIconName];
const isSelected = props.value === option.value;
return (
<ItemWrapper
className={`t--icon-tab-${option.value}`}
key={index}
onClick={() => props.selectOption(option.value)}
selected={isSelected}
>
<ControlIcon height={24} width={24} />
</ItemWrapper>
);
})}
{props.options.map(
({ icon, value, width = 24 }: IconTabOption, index: number) => {
const controlIconName: ControlIconName = icon;
const ControlIcon = ControlIcons[controlIconName];
const isSelected = props.value === value;
return (
<ItemWrapper
className={`t--icon-tab-${value}`}
key={index}
onClick={() => props.selectOption(value)}
selected={isSelected}
>
<ControlIcon height={24} width={width} />
</ItemWrapper>
);
},
)}
</FlexWrapper>
);
}

View File

@ -14,7 +14,10 @@ export type TabProp = {
iconSize?: IconSize;
};
const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
const TabsWrapper = styled.div<{
shouldOverflow?: boolean;
vertical?: boolean;
}>`
border-radius: 0px;
height: 100%;
.react-tabs {
@ -27,7 +30,8 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
.react-tabs__tab-list {
margin: 0px;
display: flex;
align-items: center;
flex-direction: ${(props) => (!!props.vertical ? "column" : "row")};
align-items: ${(props) => (!!props.vertical ? "flex-start" : "center")};
border-bottom: none;
color: ${(props) => props.theme.colors.tabs.normal};
path {
@ -45,8 +49,10 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
display: flex;
align-items: center;
justify-content: flex-start;
padding: ${(props) => props.theme.spaces[3] - 1}px 0
${(props) => props.theme.spaces[4]}px 0;
padding: ${(props) => props.theme.spaces[3] - 1}px
${(props) => (props.vertical ? `${props.theme.spaces[4] - 1}px` : 0)}
${(props) => props.theme.spaces[4]}px
${(props) => (props.vertical ? `${props.theme.spaces[4] - 1}px` : 0)};
margin-right: ${(props) => props.theme.spaces[12] - 3}px;
text-align: center;
display: inline-flex;
@ -72,10 +78,15 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
&::after {
content: "";
position: absolute;
width: 100%;
bottom: ${(props) => props.theme.spaces[0] - 1}px;
width: ${(props) =>
props.vertical ? `${props.theme.spaces[1] - 2}px` : "100%"};
bottom: ${(props) =>
props.vertical ? "0%" : `${props.theme.spaces[0] - 1}px`};
top: ${(props) =>
props.vertical ? `${props.theme.spaces[0] - 1}px` : "100%"};
left: ${(props) => props.theme.spaces[0]}px;
height: ${(props) => props.theme.spaces[1] - 2}px;
height: ${(props) =>
props.vertical ? "100%" : `${props.theme.spaces[1] - 2}px`};
background-color: ${(props) => props.theme.colors.info.main};
}
}
@ -83,10 +94,15 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
&::after {
content: "";
position: absolute;
width: 100%;
bottom: ${(props) => props.theme.spaces[0] - 1}px;
width: ${(props) =>
props.vertical ? `${props.theme.spaces[1] - 2}px` : "100%"};
bottom: ${(props) =>
props.vertical ? "0%" : `${props.theme.spaces[0] - 1}px`};
top: ${(props) =>
props.vertical ? `${props.theme.spaces[0] - 1}px` : "100%"};
left: ${(props) => props.theme.spaces[0]}px;
height: ${(props) => props.theme.spaces[1] - 2}px;
height: ${(props) =>
props.vertical ? "100%" : `${props.theme.spaces[1] - 2}px`};
background-color: ${(props) => props.theme.colors.info.main};
}
box-shadow: none;
@ -101,7 +117,7 @@ const TabTitleWrapper = styled.div`
display: flex;
align-items: center;
.${Classes.ICON} {
margin-right: ${(props) => props.theme.spaces[3]}px;
margin-right: ${(props) => props.theme.spaces[2]}px;
}
`;
@ -110,7 +126,7 @@ export const TabTitle = styled.span`
font-weight: ${(props) => props.theme.typography.h5.fontWeight};
line-height: ${(props) => props.theme.typography.h5.lineHeight - 3}px;
letter-spacing: ${(props) => props.theme.typography.h5.letterSpacing}px;
margin: 0 5px;
margin: 0;
`;
export const TabCount = styled.div`
@ -127,6 +143,7 @@ type TabbedViewComponentType = CommonComponentProps & {
selectedIndex?: number;
onSelect?: (tabIndex: number) => void;
overflow?: boolean;
vertical?: boolean;
};
export function TabComponent(props: TabbedViewComponentType) {
@ -134,6 +151,7 @@ export function TabComponent(props: TabbedViewComponentType) {
<TabsWrapper
data-cy={props.cypressSelector}
shouldOverflow={props.overflow}
vertical={props.vertical}
>
<Tabs
onSelect={(index: number) => {
@ -143,7 +161,7 @@ export function TabComponent(props: TabbedViewComponentType) {
>
<TabList>
{props.tabs.map((tab) => (
<Tab key={tab.key}>
<Tab data-cy={`t--tab-${tab.key}`} key={tab.key}>
<TabTitleWrapper>
{tab.icon ? (
<Icon

View File

@ -8,6 +8,7 @@ type Variant = "dark" | "light";
type TooltipProps = CommonComponentProps & {
content: JSX.Element | string;
disabled?: boolean;
position?: Position;
children: JSX.Element | React.ReactNode;
variant?: Variant;
@ -30,6 +31,7 @@ function TooltipComponent(props: TooltipProps) {
autoFocus={props.autoFocus}
boundary={props.boundary || "scrollParent"}
content={props.content}
disabled={props.disabled}
hoverOpenDelay={props.hoverOpenDelay}
isOpen={props.isOpen}
minimal={props.minimal}

View File

@ -19,6 +19,7 @@ export type TreeDropdownOption = DropdownOption & {
children?: TreeDropdownOption[];
className?: string;
type?: string;
icon?: React.ReactNode;
};
type Setter = (value: TreeDropdownOption, defaultVal?: string) => void;

View File

@ -31,7 +31,14 @@ const StyledContainerComponent = styled.div<
box-shadow: ${(props) =>
props.selected ? "0px 0px 0px 3px rgba(59,130,246,0.5)" : "none"};
z-index: ${(props) => (props.focused ? "3" : props.selected ? "2" : "1")};
${(props) => (props.shouldScrollContents ? scrollContents : "")}
${(props) =>
props.shouldScrollContents === true
? scrollContents
: props.shouldScrollContents === false
? css`
overflow: hidden;
`
: ""}
&:hover {
z-index: ${(props) => (props.onClickCapture ? "2" : "1")};

View File

@ -2,9 +2,11 @@ import * as React from "react";
import { ComponentProps } from "./BaseComponent";
import styled from "styled-components";
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import { Colors } from "constants/Colors";
export interface StyledImageProps {
defaultImageUrl: string;
enableRotation?: boolean;
imageUrl?: string;
backgroundColor?: string;
showHoverPointer?: boolean;
@ -42,6 +44,46 @@ const Wrapper = styled.div`
}
`;
const ControlBtnWrapper = styled.div`
position: absolute;
top: 2px;
right: 2px;
padding: 5px 0px;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
background: white;
`;
const ControlBtn = styled.div`
height: 25px;
width: 45px;
color: white;
padding: 0px 10px;
display: inline-block;
&.separator {
border-right: 1px solid ${Colors.ALTO2};
}
& > div {
cursor: pointer;
height: 100%;
width: 100%;
padding: 4px;
transition: background 0.2s linear;
& > svg {
height: 16px;
width: 17px;
}
&: hover {
background: #ebebeb;
}
}
`;
enum ZoomingState {
MAX_ZOOMED_OUT = "MAX_ZOOMED_OUT",
MAX_ZOOMED_IN = "MAX_ZOOMED_IN",
@ -50,6 +92,8 @@ class ImageComponent extends React.Component<
ImageComponentProps,
{
imageError: boolean;
showImageControl: boolean;
imageRotation: number;
zoomingState: ZoomingState;
}
> {
@ -59,11 +103,14 @@ class ImageComponent extends React.Component<
this.isPanning = false;
this.state = {
imageError: false,
showImageControl: false,
imageRotation: 0,
zoomingState: ZoomingState.MAX_ZOOMED_OUT,
};
}
render() {
const { maxZoomLevel } = this.props;
const { imageRotation } = this.state;
const zoomActive =
maxZoomLevel !== undefined && maxZoomLevel > 1 && !this.isPanning;
const isZoomingIn = this.state.zoomingState === ZoomingState.MAX_ZOOMED_OUT;
@ -72,7 +119,10 @@ class ImageComponent extends React.Component<
cursor = isZoomingIn ? "zoom-in" : "zoom-out";
}
return (
<Wrapper>
<Wrapper
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
<TransformWrapper
defaultScale={1}
doubleClick={{
@ -122,45 +172,162 @@ class ImageComponent extends React.Component<
}}
>
{({ zoomIn, zoomOut }: any) => (
<TransformComponent>
<StyledImage
className={this.props.isLoading ? "bp3-skeleton" : ""}
imageError={this.state.imageError}
{...this.props}
data-testid="styledImage"
onClick={(event: React.MouseEvent<HTMLElement>) => {
if (!this.isPanning) {
if (isZoomingIn) {
zoomIn(event);
} else {
zoomOut(event);
<>
{this.renderImageControl()}
<TransformComponent>
<StyledImage
className={this.props.isLoading ? "bp3-skeleton" : ""}
imageError={this.state.imageError}
{...this.props}
data-testid="styledImage"
onClick={(event: React.MouseEvent<HTMLElement>) => {
if (!this.isPanning) {
if (isZoomingIn) {
zoomIn(event);
} else {
zoomOut(event);
}
this.props.onClick && this.props.onClick(event);
}
this.props.onClick && this.props.onClick(event);
}
this.isPanning = false;
}}
style={{
cursor,
}}
>
{/* Used for running onImageError and onImageLoad Functions since Background Image doesn't have the functionality */}
<img
alt={this.props.widgetName}
onError={this.onImageError}
onLoad={this.onImageLoad}
src={this.props.imageUrl}
style={{
display: "none",
this.isPanning = false;
}}
/>
</StyledImage>
</TransformComponent>
style={{
cursor,
transform: `rotate(${imageRotation}deg)`,
}}
>
{/* Used for running onImageError and onImageLoad Functions since Background Image doesn't have the functionality */}
<img
alt={this.props.widgetName}
onError={this.onImageError}
onLoad={this.onImageLoad}
src={this.props.imageUrl}
style={{
display: "none",
}}
/>
</StyledImage>
</TransformComponent>
</>
)}
</TransformWrapper>
</Wrapper>
);
}
renderImageControl = () => {
const { enableDownload, enableRotation } = this.props;
const { showImageControl } = this.state;
if (showImageControl && (enableRotation || enableDownload)) {
return (
<ControlBtnWrapper>
{enableRotation && (
<>
<ControlBtn onClick={this.handleImageRotate(false)}>
<div>
<svg
fill="none"
height="12"
viewBox="0 0 12 12"
width="12"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.28492 1.81862C3.27446 0.939565 4.57489 0.400391 6.00002 0.400391C9.08724 0.400391 11.6 2.91317 11.6 6.00039C11.6 9.08761 9.08724 11.6004 6.00002 11.6004C2.91281 11.6004 0.400024 9.08761 0.400024 6.00039H1.33336C1.33336 8.58317 3.41724 10.6671 6.00002 10.6671C8.58281 10.6671 10.6667 8.58317 10.6667 6.00039C10.6667 3.41761 8.58281 1.33372 6.00002 1.33372C4.82777 1.33372 3.76447 1.7682 2.94573 2.47943L4.13336 3.66706H1.33336V0.867057L2.28492 1.81862Z"
fill="#858282"
stroke="#858282"
strokeWidth="0.5"
/>
</svg>
</div>
</ControlBtn>
<ControlBtn
className="separator"
onClick={this.handleImageRotate(true)}
>
<div>
<svg
fill="none"
height="12"
viewBox="0 0 12 12"
width="12"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.400024 6.00039C0.400024 2.91317 2.91281 0.400391 6.00002 0.400391C7.42515 0.400391 8.72559 0.939565 9.71513 1.81862L10.6667 0.867057V3.66706H7.86669L9.05432 2.47943C8.23558 1.7682 7.17228 1.33372 6.00002 1.33372C3.41724 1.33372 1.33336 3.41761 1.33336 6.00039C1.33336 8.58317 3.41724 10.6671 6.00002 10.6671C8.58281 10.6671 10.6667 8.58317 10.6667 6.00039H11.6C11.6 9.08761 9.08724 11.6004 6.00002 11.6004C2.91281 11.6004 0.400024 9.08761 0.400024 6.00039Z"
fill="#858282"
stroke="#858282"
strokeWidth="0.5"
/>
</svg>
</div>
</ControlBtn>
</>
)}
{enableDownload && (
<ControlBtn onClick={this.handleImageDownload}>
<div>
<svg fill="none" height="20" viewBox="0 0 20 20" width="20">
<path
clipRule="evenodd"
d="M15.4547 16.4284H13.117H6.88326H4.54559C2.8243 16.4284 1.42871 14.8933 1.42871 12.9999C1.42871 11.3987 2.43157 10.0641 3.7804 9.68786C3.93001 6.28329 6.47884 3.57129 9.61053 3.57129C12.7072 3.57129 15.2349 6.22243 15.4352 9.57386C17.183 9.56015 18.5716 11.1167 18.5716 12.9999C18.5716 14.8933 17.176 16.4284 15.4547 16.4284ZM12.7266 11.4286L9.99929 14.8572L7.27202 11.4286L8.83045 11.4286L8.83045 8.00004L11.1681 8.00003V11.4286L12.7266 11.4286Z"
fill="#939090"
fillRule="evenodd"
/>
</svg>
</div>
</ControlBtn>
)}
</ControlBtnWrapper>
);
}
};
handleImageRotate = (rotateRight: boolean) => (e: any) => {
const { imageRotation } = this.state;
const nextRotation = rotateRight ? imageRotation + 90 : imageRotation - 90;
this.setState({ imageRotation: nextRotation % 360 });
if (!!e) {
e.preventDefault();
e.stopPropagation();
}
};
handleImageDownload = (e: any) => {
const { imageUrl, widgetId } = this.props;
const fileName = `${widgetId}-download`;
const xhr = new XMLHttpRequest();
xhr.open("GET", imageUrl, true);
xhr.responseType = "blob";
xhr.onload = function() {
const urlCreator = window.URL || window.webkitURL;
const imageUrl = urlCreator.createObjectURL(this.response);
const tag = document.createElement("a");
tag.href = imageUrl;
tag.download = fileName;
document.body.appendChild(tag);
tag.click();
document.body.removeChild(tag);
window.URL.revokeObjectURL(imageUrl);
};
xhr.send();
if (!!e) {
e.preventDefault();
e.stopPropagation();
}
};
onMouseEnter = () => this.setState({ showImageControl: true });
onMouseLeave = () => this.setState({ showImageControl: false });
onImageError = () => {
this.setState({
imageError: true,
@ -180,6 +347,8 @@ export interface ImageComponentProps extends ComponentProps {
isLoading: boolean;
showHoverPointer?: boolean;
maxZoomLevel: number;
enableRotation?: boolean;
enableDownload?: boolean;
objectFit: string;
disableDrag: (disabled: boolean) => void;
onClick?: (event: React.MouseEvent<HTMLElement>) => void;

View File

@ -64,6 +64,7 @@ export function PositionedContainer(props: PositionedContainerProps) {
className={containerClassName}
data-testid="test-widget"
id={props.widgetId}
key={`positioned-container-${props.widgetId}`}
onClick={stopEventPropagation}
// Positioned Widget is the top enclosure for all widgets and clicks on/inside the widget should not be propogated/bubbled out of this Container.
onClickCapture={openPropPane}

View File

@ -31,7 +31,6 @@ describe("TableUtilities", () => {
"url",
true,
);
// console.log(JSON.stringify(sortedTableData));
expect(sortedTableData).toStrictEqual(expected);
});
});

View File

@ -1,10 +1,12 @@
import React, { useRef } from "react";
import { reduce } from "lodash";
import {
useTable,
usePagination,
useBlockLayout,
useResizeColumns,
useRowSelect,
Row,
} from "react-table";
import {
TableWrapper,
@ -12,7 +14,12 @@ import {
TableHeaderInnerWrapper,
} from "./TableStyledWrappers";
import { ReactTableFilter } from "components/designSystems/appsmith/TableComponent/TableFilters";
import { TableHeaderCell, renderEmptyRows } from "./TableUtilities";
import {
TableHeaderCell,
renderEmptyRows,
renderCheckBoxCell,
renderCheckBoxHeaderCell,
} from "./TableUtilities";
import TableHeader from "./TableHeader";
import { Classes } from "@blueprintjs/core";
import {
@ -47,6 +54,7 @@ interface TableProps {
}) => void;
pageNo: number;
updatePageNo: (pageNo: number, event?: EventType) => void;
multiRowSelection?: boolean;
nextPageClick: () => void;
prevPageClick: () => void;
serverSidePaginationEnabled: boolean;
@ -54,6 +62,10 @@ interface TableProps {
selectedRowIndices: number[];
disableDrag: () => void;
enableDrag: () => void;
toggleAllRowSelect: (
isSelect: boolean,
pageData: Row<Record<string, unknown>>[],
) => void;
triggerRowSelection: boolean;
searchTableData: (searchKey: any) => void;
filters?: ReactTableFilter[];
@ -166,6 +178,29 @@ export function Table(props: TableProps) {
const tableWrapperRef = useRef<HTMLDivElement | null>(null);
const tableBodyRef = useRef<HTMLDivElement | null>(null);
const tableHeaderWrapperRef = React.createRef<HTMLDivElement>();
const rowSelectionState = React.useMemo(() => {
// return : 0; no row selected | 1; all row selected | 2: some rows selected
if (!props.multiRowSelection) return null;
const selectedRowCount = reduce(
subPage,
(count, row) => {
return selectedRowIndices.includes(row.index) ? count + 1 : count;
},
0,
);
const result =
selectedRowCount === 0 ? 0 : selectedRowCount === subPage.length ? 1 : 2;
return result;
}, [selectedRowIndices, subPage]);
const handleAllRowSelectClick = (
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
) => {
// if all / some rows are selected we remove selection on click
// else select all rows
props.toggleAllRowSelect(!Boolean(rowSelectionState), subPage);
// loop over subPage rows and toggleRowSelected if required
e.stopPropagation();
};
const isHeaderVisible =
props.isVisibleSearch ||
props.isVisibleFilters ||
@ -250,30 +285,37 @@ export function Table(props: TableProps) {
onMouseLeave={props.enableDrag}
onMouseOver={props.disableDrag}
>
{headerGroups.map((headerGroup: any, index: number) => (
<div
{...headerGroup.getHeaderGroupProps()}
className="tr"
key={index}
>
{headerGroup.headers.map(
(column: any, columnIndex: number) => {
return (
<TableHeaderCell
column={column}
columnIndex={columnIndex}
columnName={column.Header}
isAscOrder={column.isAscOrder}
isHidden={column.isHidden}
isResizingColumn={isResizingColumn.current}
key={columnIndex}
sortTableColumn={props.sortTableColumn}
/>
);
},
)}
</div>
))}
{headerGroups.map((headerGroup: any, index: number) => {
const headerRowProps = {
...headerGroup.getHeaderGroupProps(),
style: { display: "flex" },
};
return (
<div {...headerRowProps} className="tr" key={index}>
{props.multiRowSelection &&
renderCheckBoxHeaderCell(
handleAllRowSelectClick,
rowSelectionState,
)}
{headerGroup.headers.map(
(column: any, columnIndex: number) => {
return (
<TableHeaderCell
column={column}
columnIndex={columnIndex}
columnName={column.Header}
isAscOrder={column.isAscOrder}
isHidden={column.isHidden}
isResizingColumn={isResizingColumn.current}
key={columnIndex}
sortTableColumn={props.sortTableColumn}
/>
);
},
)}
</div>
);
})}
{headerGroups.length === 0 &&
renderEmptyRows(
1,
@ -281,6 +323,7 @@ export function Table(props: TableProps) {
props.width,
subPage,
prepareRow,
props.multiRowSelection,
)}
</div>
<div
@ -292,18 +335,17 @@ export function Table(props: TableProps) {
>
{subPage.map((row, rowIndex) => {
prepareRow(row);
const rowProps = {
...row.getRowProps(),
style: { display: "flex" },
};
const isRowSelected =
row.index === selectedRowIndex ||
selectedRowIndices.includes(row.index);
return (
<div
{...row.getRowProps()}
className={
"tr" +
`${
row.index === selectedRowIndex ||
selectedRowIndices.includes(row.index)
? " selected-row"
: ""
}`
}
{...rowProps}
className={"tr" + `${isRowSelected ? " selected-row" : ""}`}
key={rowIndex}
onClick={(e) => {
row.toggleRowSelected();
@ -311,6 +353,8 @@ export function Table(props: TableProps) {
e.stopPropagation();
}}
>
{props.multiRowSelection &&
renderCheckBoxCell(isRowSelected)}
{row.cells.map((cell, cellIndex) => {
return (
<div
@ -334,6 +378,7 @@ export function Table(props: TableProps) {
props.width,
subPage,
prepareRow,
props.multiRowSelection,
)}
</div>
</div>

View File

@ -10,13 +10,14 @@ interface TableActionProps {
title: string;
children: React.ReactNode;
icon?: React.ReactNode;
titleColor?: string;
}
export const TableIconWrapper = styled.div<{
selected?: boolean;
disabled?: boolean;
titleColor?: string;
}>`
background: ${(props) => (props.selected ? Colors.Gallery : "transparent")};
height: 38px;
display: flex;
align-items: center;
@ -26,6 +27,8 @@ export const TableIconWrapper = styled.div<{
color: ${(props) => (props.selected ? Colors.CODE_GRAY : Colors.GRAY)};
.action-title {
margin-left: 4px;
white-space: nowrap;
color: ${(props) => props.titleColor || Colors.GRAY};
}
position: relative;
margin-left: 5px;
@ -48,9 +51,10 @@ function TableAction(props: TableActionProps) {
className={props.className}
onClick={handleIconClick}
selected={props.selected}
titleColor={props.titleColor}
>
<IconWrapper
color={props.selected ? Colors.CODE_GRAY : Colors.GRAY}
color={props.titleColor ? props.titleColor : Colors.GRAY}
height={20}
width={20}
>

View File

@ -184,7 +184,8 @@ function TableFilters(props: TableFilterProps) {
selectMenu(selected);
}}
selected={selected}
title="Filters"
title={`Filters${hasAnyFilters ? ` (${filters.length})` : ""}`}
titleColor={hasAnyFilters ? Colors.CODE_GRAY : Colors.GRAY}
>
<FilterIcon />
</TableAction>

View File

@ -49,9 +49,6 @@ export const TableWrapper = styled.div<{
display: table;
width: 100%;
${hideScrollbar};
.tr {
width: 100%;
}
.thead,
.tbody {
overflow: hidden;
@ -62,22 +59,15 @@ export const TableWrapper = styled.div<{
width: 100%;
overflow-y: auto;
${hideScrollbar};
.tr {
width: 100%;
}
}
.tr {
width: calc(100% - 8px);
overflow: hidden;
cursor: ${(props) => props.triggerRowSelection && "pointer"};
background: ${Colors.WHITE};
&.selected-row {
& > .td > div {
background: ${Colors.POLAR}!important;
}
background: ${Colors.POLAR}!important;
background: ${Colors.OPAQ_BLUE}!important;
&:hover {
background: ${Colors.POLAR};
background: ${Colors.OPAQ_BLUE};
}
}
&:hover {
@ -429,6 +419,46 @@ export const CellWrapper = styled.div<{
}
`;
export const CellCheckboxWrapper = styled(CellWrapper)<{ isChecked?: boolean }>`
justify-content: center;
width: 40px;
background: ${Colors.WHITE};
& > div {
background: ${(props) => (props.isChecked ? Colors.DANUBE : "")};
}
${(props) =>
props.isChecked
? `
background: #FAFAFA;
&:hover {
background: ${Colors.OPAQ_BLUE};
}
`
: `
&:hover {
& > div {
background: ${Colors.Gallery};
}
}
`}
`;
export const CellCheckbox = styled.div`
height: 15px;
width: 15px;
border: 0.5px solid ${Colors.GEYSER_LIGHT};
position: relative;
.th-svg {
display: block;
position: absolute;
left: -1px;
top: -1px;
height: 15px;
width: 15px;
}
`;
export const TableHeaderWrapper = styled.div<{
serverSidePaginationEnabled: boolean;
width: number;

View File

@ -2,6 +2,8 @@ import React, { useState } from "react";
import { MenuItem, Classes, Button as BButton } from "@blueprintjs/core";
import {
CellWrapper,
CellCheckboxWrapper,
CellCheckbox,
ActionWrapper,
SortIconWrapper,
DraggableHeaderWrapper,
@ -196,20 +198,86 @@ function TableAction(props: {
);
}
function CheckBoxLineIcon() {
return (
<svg
className="th-svg t--table-multiselect-header-half-check-svg"
fill="none"
height="15"
width="15"
>
<path
d="M11.183673404886235,7.5 H3.81632661819458 "
stroke="white"
strokeLinecap="round"
strokeLinejoin="round"
strokeOpacity="0.9"
/>
</svg>
);
}
function CheckBoxCheckIcon() {
return (
<svg className="th-svg" fill="none" height="15" width="15">
<path
d="M3.523326302644791,8.068102895600848 L5.7957131234862,10.340476082148596 L11.476673358442884,4.659524027768102 "
stroke="white"
strokeLinecap="round"
strokeLinejoin="round"
strokeOpacity="0.9"
/>
</svg>
);
}
export const renderCheckBoxCell = (isChecked: boolean) => (
<CellCheckboxWrapper
className="td t--table-multiselect"
isChecked={isChecked}
>
<CellCheckbox>{isChecked && <CheckBoxCheckIcon />}</CellCheckbox>
</CellCheckboxWrapper>
);
export const renderCheckBoxHeaderCell = (
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void,
checkState: number | null,
) => (
<CellCheckboxWrapper
className="th header-reorder t--table-multiselect-header"
isChecked={!!checkState}
onClick={onClick}
role="columnheader"
style={{ padding: "0px", justifyContent: "center" }}
>
<CellCheckbox>
{checkState === 1 && <CheckBoxCheckIcon />}
{checkState === 2 && <CheckBoxLineIcon />}
</CellCheckbox>
</CellCheckboxWrapper>
);
export const renderEmptyRows = (
rowCount: number,
columns: any,
tableWidth: number,
page: any,
prepareRow: any,
multiRowSelection = false,
) => {
const rows: string[] = new Array(rowCount).fill("");
if (page.length) {
const row = page[0];
return rows.map((item: string, index: number) => {
prepareRow(row);
const rowProps = {
...row.getRowProps(),
style: { display: "flex" },
};
return (
<div {...row.getRowProps()} className="tr" key={index}>
<div {...rowProps} className="tr" key={index}>
{multiRowSelection && renderCheckBoxCell(false)}
{row.cells.map((cell: any, cellIndex: number) => {
return (
<div {...cell.getCellProps()} className="td" key={cellIndex} />

View File

@ -4,6 +4,7 @@ import {
ReactTableColumnProps,
ReactTableFilter,
} from "components/designSystems/appsmith/TableComponent/Constants";
import { Row } from "react-table";
import Table from "components/designSystems/appsmith/TableComponent/Table";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import { isEqual } from "lodash";
@ -46,6 +47,8 @@ interface ReactTableComponentProps {
disableDrag: (disable: boolean) => void;
onRowClick: (rowData: Record<string, unknown>, rowIndex: number) => void;
onCommandClick: (dynamicTrigger: string, onComplete: () => void) => void;
selectAllRow: (pageData: Row<Record<string, unknown>>[]) => void;
unSelectAllRow: (pageData: Row<Record<string, unknown>>[]) => void;
updatePageNo: (pageNo: number, event?: EventType) => void;
sortTableColumn: (column: string, asc: boolean) => void;
nextPageClick: () => void;
@ -91,6 +94,7 @@ function ReactTableComponent(props: ReactTableComponentProps) {
isVisibleFilters,
isVisiblePagination,
isVisibleSearch,
multiRowSelection,
nextPageClick,
onRowClick,
pageNo,
@ -98,12 +102,14 @@ function ReactTableComponent(props: ReactTableComponentProps) {
prevPageClick,
searchKey,
searchTableData,
selectAllRow,
selectedRowIndex,
selectedRowIndices,
serverSidePaginationEnabled,
sortTableColumn: _sortTableColumn,
tableData,
triggerRowSelection,
unSelectAllRow,
updateCompactMode,
updatePageNo,
widgetId,
@ -222,6 +228,17 @@ function ReactTableComponent(props: ReactTableComponentProps) {
onRowClick(row.original, row.index);
};
const toggleAllRowSelect = (
isSelect: boolean,
pageData: Row<Record<string, unknown>>[],
) => {
if (isSelect) {
selectAllRow(pageData);
} else {
unSelectAllRow(pageData);
}
};
return (
<Table
applyFilter={applyFilter}
@ -245,6 +262,7 @@ function ReactTableComponent(props: ReactTableComponentProps) {
isVisibleFilters={isVisibleFilters}
isVisiblePagination={isVisiblePagination}
isVisibleSearch={isVisibleSearch}
multiRowSelection={multiRowSelection}
nextPageClick={nextPageClick}
pageNo={pageNo - 1}
pageSize={pageSize || 1}
@ -256,6 +274,7 @@ function ReactTableComponent(props: ReactTableComponentProps) {
selectedRowIndices={selectedRowIndices}
serverSidePaginationEnabled={serverSidePaginationEnabled}
sortTableColumn={sortTableColumn}
toggleAllRowSelect={toggleAllRowSelect}
triggerRowSelection={triggerRowSelection}
updateCompactMode={updateCompactMode}
updatePageNo={updatePageNo}

View File

@ -51,7 +51,7 @@ const AccentColorMap: Record<ButtonStyleName, string> = {
};
const ButtonWrapper = styled((props: ButtonStyleProps & IButtonProps) => (
<Button {..._.omit(props, ["accent", "filled"])} />
<Button {..._.omit(props, ["accent", "filled", "disabled"])} />
))<ButtonStyleProps>`
&&&& {
${ButtonColorStyles};
@ -131,7 +131,10 @@ type ButtonStyleProps = {
// To be used in any other part of the app
export function BaseButton(props: IButtonProps & ButtonStyleProps) {
return <ButtonWrapper {...props} />;
const className = props.disabled
? `${props.className} bp3-disabled`
: props.className;
return <ButtonWrapper {...props} className={className} />;
}
BaseButton.defaultProps = {

View File

@ -0,0 +1,191 @@
import React from "react";
import { ComponentProps } from "components/designSystems/appsmith/BaseComponent";
import styled from "styled-components";
import { isUndefined } from "lodash";
const DividerWrapper = styled.div<{
isHorizontal: boolean;
thickness: number;
showStartCap: boolean;
showEndCap: boolean;
}>`
height: 100%;
width: 100%;
position: relative;
display: flex;
overflow: hidden;
justify-content: center;
align-items: center;
${(props) => {
const paddingVal = props.thickness / 2;
let padStyle: string;
if (props.isHorizontal) {
padStyle = `padding: 0px ${
props.showEndCap ? paddingVal + "px" : "0px"
} 0px ${props.showStartCap ? paddingVal + "px" : "0px"};`;
} else {
padStyle = `padding: ${
props.showStartCap ? paddingVal + "px" : "0px"
} 0px ${props.showEndCap ? paddingVal + "px" : "0px"} 0px;`;
}
return padStyle;
}}
`;
const HorizontalDivider = styled.div<Partial<DividerComponentProps>>`
height: 0px;
width: 100%;
border-top: ${(props) =>
`${props.thickness || 1}px ${props.strokeStyle ||
"solid"} ${props.dividerColor || "black"};`};
`;
const VerticalDivider = styled.div<Partial<DividerComponentProps>>`
width: 0px;
height: 100%;
border-right: ${(props) =>
`${props.thickness || 1}px ${props.strokeStyle ||
"solid"} ${props.dividerColor || "black"};`};
`;
const CapWrapper = styled.div<{
isHorizontal: boolean;
isStartCap: boolean;
size: number;
}>`
position: absolute;
height: ${(props) => props.size}px;
width: ${(props) => props.size}px;
${(props) =>
props.isStartCap
? props.isHorizontal
? "left: 0px;"
: "top: 0px;"
: props.isHorizontal
? "right: 0px;"
: "bottom: 0px;"}
${(props) => (props.isHorizontal ? "top" : "left")}: 50%;
${(props) =>
props.isHorizontal
? "transform: translateY(-50%);"
: "transform: translateX(-50%);"};
svg {
display: block;
height: 100%;
width: 100%;
&.arrow {
${(
props, // rotate icon according to its pos
) =>
props.isStartCap
? props.isHorizontal
? "transform: rotate(0deg);"
: "transform: rotate(90deg);"
: props.isHorizontal
? "transform: rotate(180deg);"
: "transform: rotate(270deg);"}
path {
transform: translateX(-3px);
}
}
}
`;
class DividerComponent extends React.Component<DividerComponentProps> {
render() {
const {
capSide,
capType,
dividerColor,
orientation,
strokeStyle,
thickness,
} = this.props;
const showStartCap =
capType !== "nc" && (isUndefined(capSide) ? false : capSide <= 0);
const showEndCap =
capType !== "nc" && (isUndefined(capSide) ? false : capSide >= 0);
return (
<DividerWrapper
className="t--divider-widget"
isHorizontal={orientation === "horizontal"}
showEndCap={showEndCap}
showStartCap={showStartCap}
thickness={thickness || 1}
>
{orientation === "horizontal" ? (
<HorizontalDivider
data-testid="dividerHorizontal"
dividerColor={dividerColor}
strokeStyle={strokeStyle}
thickness={thickness}
/>
) : (
<VerticalDivider
data-testid="dividerVertical"
dividerColor={dividerColor}
strokeStyle={strokeStyle}
thickness={thickness}
/>
)}
{showStartCap && this.renderCap(true)}
{showEndCap && this.renderCap(false)}
</DividerWrapper>
);
}
renderCap = (isStartCap: boolean) => {
const { capType, dividerColor, orientation, thickness } = this.props;
const isHorizontal = orientation === "horizontal";
// size calculations
const strokeSize = thickness || 1;
const sizeMultiplier = capType === "dot" ? 3.5 : 5;
const capSize = strokeSize * sizeMultiplier;
const halfCapSize = capSize / 2;
return (
<CapWrapper
isHorizontal={isHorizontal}
isStartCap={isStartCap}
size={capSize}
>
{capType === "dot" ? (
<svg>
<circle
cx={halfCapSize}
cy={halfCapSize}
fill={dividerColor}
r={halfCapSize}
/>
</svg>
) : (
<svg className="arrow" height="14" viewBox="0 0 8 14" width="8">
<path
d="M7 13L1 7L7 1"
fill="none"
stroke={dividerColor}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
/>
</svg>
)}
</CapWrapper>
);
};
}
export interface DividerComponentProps extends ComponentProps {
capType: string;
capSide?: number;
orientation: string;
strokeStyle?: "solid" | "dashed" | "dotted";
dividerColor?: string;
thickness?: number;
className?: string;
}
export default DividerComponent;

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