Merge branch 'release' into fix/10319-select-api-response
This commit is contained in:
commit
01539dd52d
|
|
@ -530,13 +530,13 @@ jobs:
|
|||
path: app/rts/node_modules/
|
||||
|
||||
- name: Build docker image
|
||||
if: success() && github.ref == 'refs/heads/release' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
||||
if: steps.run_result.outputs.run_result != 'success'
|
||||
working-directory: "."
|
||||
run: |
|
||||
docker build -t fatcontainer .
|
||||
|
||||
- name: Load docker image
|
||||
if: success() && github.ref == 'refs/heads/release' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
|
||||
if: steps.run_result.outputs.run_result != 'success'
|
||||
env:
|
||||
APPSMITH_LICENSE_KEY: ${{ secrets.APPSMITH_LICENSE_KEY }}
|
||||
working-directory: "."
|
||||
|
|
|
|||
1045
app/client/cypress/fixtures/gitImport.json
Normal file
1045
app/client/cypress/fixtures/gitImport.json
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -94,19 +94,28 @@ describe("Slug URLs", () => {
|
|||
);
|
||||
});
|
||||
|
||||
cy.get(".t--upgrade").click({ force: true });
|
||||
|
||||
cy.get(".t--upgrade-confirm").click({ force: true });
|
||||
|
||||
cy.wait("@getPagesForCreateApp").then((intercept) => {
|
||||
const { application, pages } = intercept.response.body.data;
|
||||
const defaultPage = pages.find((p) => p.isDefault);
|
||||
|
||||
cy.Createpage("NewPage");
|
||||
cy.get("@currentPageId").then((currentPageId) => {
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.pathname).includes(
|
||||
`/${application.slug}/${defaultPage.slug}-${defaultPage.id}`,
|
||||
`/applications/${application.id}/pages/${currentPageId}`,
|
||||
);
|
||||
});
|
||||
|
||||
cy.get(".t--upgrade").click({ force: true });
|
||||
|
||||
cy.get(".t--upgrade-confirm").click({ force: true });
|
||||
|
||||
cy.wait("@getPagesForCreateApp").then((intercept) => {
|
||||
const { application, pages } = intercept.response.body.data;
|
||||
const currentPage = pages.find((p) => p.id === currentPageId);
|
||||
|
||||
cy.location().should((loc) => {
|
||||
expect(loc.pathname).includes(
|
||||
`/${application.slug}/${currentPage.slug}-${currentPage.id}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
import gitSyncLocators from "../../../../locators/gitSyncLocators";
|
||||
const homePage = require("../../../../locators/HomePage");
|
||||
const reconnectDatasourceModal = require("../../../../locators/ReconnectLocators");
|
||||
let repoName;
|
||||
let appName;
|
||||
|
||||
describe("Git import flow", function() {
|
||||
before(() => {
|
||||
cy.NavigateToHome();
|
||||
cy.createOrg();
|
||||
cy.wait("@createOrg").then((interception) => {
|
||||
const newOrganizationName = interception.response.body.data.name;
|
||||
cy.CreateAppForOrg(newOrganizationName, newOrganizationName);
|
||||
});
|
||||
});
|
||||
it("Import an app from JSON with Postgres, MySQL, Mongo db", () => {
|
||||
cy.get(homePage.homeIcon).click();
|
||||
cy.get(homePage.optionsIcon)
|
||||
.first()
|
||||
.click();
|
||||
cy.get(homePage.orgImportAppOption).click({ force: true });
|
||||
cy.get(homePage.orgImportAppModal).should("be.visible");
|
||||
cy.xpath(homePage.uploadLogo).attachFile("gitImport.json");
|
||||
cy.wait("@importNewApplication").then((interception) => {
|
||||
cy.wait(100);
|
||||
// should check reconnect modal opening
|
||||
// const { isPartialImport } = interception.response.body.data;
|
||||
// if (isPartialImport) {
|
||||
// should reconnect button
|
||||
cy.get(reconnectDatasourceModal.Modal).should("be.visible");
|
||||
cy.ReconnectDatasource("TEDPostgres");
|
||||
cy.wait(1000);
|
||||
cy.fillPostgresDatasourceForm();
|
||||
cy.testSaveDatasource();
|
||||
cy.wait(2000);
|
||||
// commenting until bug12535 is closed
|
||||
/* cy.ReconnectDatasource("TEDMySQL");
|
||||
cy.wait(2000);
|
||||
cy.fillMySQLDatasourceForm();
|
||||
cy.testSaveDatasource();
|
||||
cy.wait(2000);
|
||||
cy.ReconnectDatasource("TEDMongo");
|
||||
cy.wait(2000);
|
||||
cy.fillMongoDatasourceForm();
|
||||
cy.testSaveDatasource();
|
||||
cy.wait(2000);
|
||||
// } else {
|
||||
cy.get(homePage.toastMessage).should(
|
||||
"contain",
|
||||
"Application imported successfully",
|
||||
); */
|
||||
cy.get(reconnectDatasourceModal.SkipToAppBtn).click({ force: true });
|
||||
cy.wait(2000);
|
||||
cy.get(".tbody")
|
||||
.first()
|
||||
.should("contain.text", "Test user 7");
|
||||
cy.generateUUID().then((uid) => {
|
||||
repoName = uid;
|
||||
cy.createTestGithubRepo(repoName);
|
||||
cy.connectToGitRepo(repoName);
|
||||
});
|
||||
});
|
||||
});
|
||||
it("Import an app from Git and reconnect Postgres, MySQL and Mongo db ", () => {
|
||||
cy.NavigateToHome();
|
||||
cy.createOrg();
|
||||
cy.wait("@createOrg").then((interception) => {
|
||||
const newOrganizationName = interception.response.body.data.name;
|
||||
cy.CreateAppForOrg(newOrganizationName, "gitImport");
|
||||
});
|
||||
cy.get(homePage.homeIcon).click();
|
||||
cy.get(homePage.optionsIcon)
|
||||
.first()
|
||||
.click();
|
||||
cy.get(homePage.orgImportAppOption).click({ force: true });
|
||||
cy.get(".t--import-json-card")
|
||||
.next()
|
||||
.click();
|
||||
cy.importAppFromGit(repoName);
|
||||
// cy.wait("@importNewApplication").then((interception) => {
|
||||
cy.wait(100);
|
||||
// should check reconnect modal opening
|
||||
// const { isPartialImport } = interception.response.body.data;
|
||||
// if (isPartialImport) {
|
||||
// should reconnect button
|
||||
cy.get(reconnectDatasourceModal.Modal).should("be.visible");
|
||||
cy.ReconnectDatasource("TEDPostgres");
|
||||
cy.wait(1000);
|
||||
cy.fillPostgresDatasourceForm();
|
||||
cy.testSaveDatasource();
|
||||
cy.wait(1000);
|
||||
/* cy.ReconnectDatasource("TEDMySQL");
|
||||
cy.wait(1000);
|
||||
cy.fillMySQLDatasourceForm();
|
||||
cy.testSaveDatasource();
|
||||
cy.wait(1000);
|
||||
cy.ReconnectDatasource("TEDMongo");
|
||||
cy.wait(1000);
|
||||
cy.fillMongoDatasourceForm();
|
||||
cy.testSaveDatasource();
|
||||
cy.wait(2000);
|
||||
} else {
|
||||
cy.get(homePage.toastMessage).should(
|
||||
"contain",
|
||||
"Application imported successfully",
|
||||
);
|
||||
} */
|
||||
cy.get(reconnectDatasourceModal.SkipToAppBtn).click({ force: true });
|
||||
});
|
||||
it("Verfiy imported app should have all the data binding visible", () => {
|
||||
// verify postgres data binded to table
|
||||
cy.get(".tbody")
|
||||
.first()
|
||||
.should("contain.text", "Test user 7");
|
||||
// verify MySQL data binded to table
|
||||
// cy.get(".tbody").last().should("contain.text", "New Config")
|
||||
// verify api response binded to input widget
|
||||
cy.xpath("//input[@value='this is a test']");
|
||||
// verify js object binded to input widget
|
||||
cy.xpath("//input[@value='Success']");
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.deleteTestGithubRepo(repoName);
|
||||
});
|
||||
});
|
||||
|
|
@ -1908,6 +1908,7 @@ Cypress.Commands.add("Createpage", (pageName) => {
|
|||
cy.get(pages.editName).click({ force: true });
|
||||
cy.get(pages.editInput).type(pageName + "{enter}");
|
||||
pageidcopy = pageName;
|
||||
cy.wrap(pageId).as("currentPageId");
|
||||
}
|
||||
cy.get(generatePage.buildFromScratchActionCard).click();
|
||||
cy.get("#loading").should("not.exist");
|
||||
|
|
@ -2341,6 +2342,7 @@ Cypress.Commands.add(
|
|||
cy.get(datasourceEditor["databaseName"])
|
||||
.clear()
|
||||
.type(datasourceFormData["mongo-databaseName"]);
|
||||
cy.get(datasourceEditor.sectionAuthentication).click();
|
||||
// cy.get(datasourceEditor["username"]).type(
|
||||
// datasourceFormData["mongo-username"],
|
||||
// );
|
||||
|
|
@ -2374,7 +2376,6 @@ Cypress.Commands.add(
|
|||
cy.get(datasourceEditor.databaseName)
|
||||
.clear()
|
||||
.type(databaseName);
|
||||
|
||||
cy.get(datasourceEditor.sectionAuthentication).click();
|
||||
cy.get(datasourceEditor.username).type(
|
||||
datasourceFormData["postgres-username"],
|
||||
|
|
@ -2382,6 +2383,7 @@ Cypress.Commands.add(
|
|||
cy.get(datasourceEditor.password).type(
|
||||
datasourceFormData["postgres-password"],
|
||||
);
|
||||
cy.get(datasourceEditor.sectionAuthentication).click();
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -2408,6 +2410,7 @@ Cypress.Commands.add(
|
|||
cy.get(datasourceEditor.password).type(
|
||||
datasourceFormData["mysql-password"],
|
||||
);
|
||||
cy.get(datasourceEditor.sectionAuthentication).click();
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -2531,6 +2534,7 @@ Cypress.Commands.add(
|
|||
: datasourceFormData["smtp-host"];
|
||||
cy.get(datasourceEditor.host).type(hostAddress);
|
||||
cy.get(datasourceEditor.port).type(datasourceFormData["smtp-port"]);
|
||||
|
||||
cy.get(datasourceEditor.sectionAuthentication).click();
|
||||
cy.get(datasourceEditor.username).type(datasourceFormData["smtp-username"]);
|
||||
cy.get(datasourceEditor.password).type(datasourceFormData["smtp-password"]);
|
||||
|
|
@ -3067,7 +3071,7 @@ Cypress.Commands.add("startServerAndRoutes", () => {
|
|||
|
||||
cy.route("POST", "api/v1/git/connect/*").as("connectGitRepo");
|
||||
cy.route("POST", "api/v1/git/commit/*").as("commit");
|
||||
|
||||
cy.route("POST", "/api/v1/git/import/*").as("importFromGit");
|
||||
cy.route("PUT", "api/v1/collections/actions/refactor").as("renameJsAction");
|
||||
|
||||
cy.route("POST", "/api/v1/collections/actions").as("createNewJSCollection");
|
||||
|
|
@ -3811,6 +3815,78 @@ Cypress.Commands.add(
|
|||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add(
|
||||
"importAppFromGit",
|
||||
(repo, shouldCommit = true, assertConnectFailure) => {
|
||||
const testEmail = "test@test.com";
|
||||
const testUsername = "testusername";
|
||||
const owner = Cypress.env("TEST_GITHUB_USER_NAME");
|
||||
|
||||
let generatedKey;
|
||||
cy.intercept(
|
||||
{
|
||||
url: "api/v1/git/connect/*",
|
||||
hostname: window.location.host,
|
||||
},
|
||||
(req) => {
|
||||
req.headers["origin"] = "Cypress";
|
||||
},
|
||||
);
|
||||
cy.intercept("GET", "api/v1/git/import/keys").as(`generateKey-${repo}`);
|
||||
cy.get(gitSyncLocators.gitRepoInput).type(
|
||||
`git@github.com:${owner}/${repo}.git`,
|
||||
);
|
||||
cy.get(gitSyncLocators.generateDeployKeyBtn).click();
|
||||
cy.wait(`@generateKey-${repo}`).then((result) => {
|
||||
generatedKey = result.response.body.data.publicKey;
|
||||
generatedKey = generatedKey.slice(0, generatedKey.length - 1);
|
||||
// fetch the generated key and post to the github repo
|
||||
cy.request({
|
||||
method: "POST",
|
||||
url: `${GITHUB_API_BASE}/repos/${Cypress.env(
|
||||
"TEST_GITHUB_USER_NAME",
|
||||
)}/${repo}/keys`,
|
||||
headers: {
|
||||
Authorization: `token ${Cypress.env("GITHUB_PERSONAL_ACCESS_TOKEN")}`,
|
||||
},
|
||||
body: {
|
||||
title: "key0",
|
||||
key: generatedKey,
|
||||
},
|
||||
});
|
||||
|
||||
cy.get(gitSyncLocators.useGlobalGitConfig).click();
|
||||
|
||||
cy.get(gitSyncLocators.gitConfigNameInput).type(
|
||||
`{selectall}${testUsername}`,
|
||||
);
|
||||
cy.get(gitSyncLocators.gitConfigEmailInput).type(
|
||||
`{selectall}${testEmail}`,
|
||||
);
|
||||
// click on the connect button and verify
|
||||
cy.get(gitSyncLocators.connectSubmitBtn).click();
|
||||
|
||||
if (!assertConnectFailure) {
|
||||
// check for connect success
|
||||
cy.wait("@importFromGit").should(
|
||||
"have.nested.property",
|
||||
"response.body.responseMeta.status",
|
||||
201,
|
||||
);
|
||||
} else {
|
||||
cy.wait("@importFromGit").then((interception) => {
|
||||
const status = interception.response.body.responseMeta.status;
|
||||
expect(status).to.be.gte(400);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add("ReconnectDatasource", (datasource) => {
|
||||
cy.xpath(`//span[text()='${datasource}']`).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add("clearPropertyValue", (value) => {
|
||||
cy.get(".CodeMirror textarea")
|
||||
.eq(value)
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ export function updateURLFactory(params: Optional<BaseURLBuilderParams>) {
|
|||
BASE_URL_BUILDER_PARAMS = { ...BASE_URL_BUILDER_PARAMS, ...params };
|
||||
}
|
||||
|
||||
export const getRouteBuilderParams = () => BASE_URL_BUILDER_PARAMS;
|
||||
|
||||
/**
|
||||
* Do not export this method directly. Please write wrappers for your URLs.
|
||||
* Uses applicationVersion attribute to determine whether to use slug URLs or legacy URLs.
|
||||
|
|
|
|||
5
app/client/src/assets/icons/ads/js.svg
Normal file
5
app/client/src/assets/icons/ads/js.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.8 20.2V3.8H12H15.1686L17.4343 6.06569L19.2 7.83137V12V20.2H4.8Z" fill="#ffffff" stroke="#575757" stroke-width="1.6"/>
|
||||
<path d="M7.25391 16.0625C7.25391 17.3672 8.10938 18.1562 9.47656 18.1562C10.8438 18.1562 11.6641 17.4297 11.6641 16.1055V12.3633H10.0078V16.0938C10.0078 16.5352 9.8125 16.7695 9.44922 16.7695C9.07422 16.7695 8.83984 16.5039 8.83984 16.0625H7.25391Z" fill="#575757"/>
|
||||
<path d="M12.7227 16.3555C12.7305 17.4727 13.6484 18.1562 15.1445 18.1562C16.6836 18.1562 17.6055 17.4297 17.6055 16.2109C17.6055 15.2969 17.0977 14.7773 16 14.5859L15.2305 14.4531C14.6914 14.3594 14.4609 14.2031 14.4609 13.9336C14.4609 13.6211 14.7422 13.4258 15.1719 13.4258C15.6133 13.4258 15.9609 13.6836 15.9727 14.0234H17.4883C17.4766 12.9414 16.5273 12.207 15.1367 12.207C13.7266 12.207 12.8164 12.9492 12.8164 14.0977C12.8164 15.0078 13.3789 15.6016 14.4141 15.7812L15.1602 15.9141C15.7656 16.0234 15.9766 16.1484 15.9766 16.4102C15.9766 16.7266 15.668 16.9336 15.2031 16.9336C14.6719 16.9336 14.3125 16.707 14.293 16.3555H12.7227Z" fill="#575757"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,6 +1,9 @@
|
|||
import {
|
||||
CANNOT_MERGE_DUE_TO_UNCOMMITTED_CHANGES,
|
||||
CANNOT_PULL_WITH_LOCAL_UNCOMMITTED_CHANGES,
|
||||
CHANGES_ONLY_MIGRATION,
|
||||
CHANGES_ONLY_USER,
|
||||
CHANGES_USER_AND_MIGRATION,
|
||||
COMMIT_AND_PUSH,
|
||||
COMMIT_CHANGES,
|
||||
COMMIT_TO,
|
||||
|
|
@ -256,10 +259,25 @@ describe("git-sync messages", () => {
|
|||
key: "ERROR_GIT_INVALID_REMOTE",
|
||||
value: "Remote repo doesn't exist or is unreachable.",
|
||||
},
|
||||
{
|
||||
key: "CHANGES_ONLY_USER",
|
||||
value: "Changes since last commit",
|
||||
},
|
||||
{
|
||||
key: "CHANGES_ONLY_MIGRATION",
|
||||
value: "Appsmith update changes since last commit",
|
||||
},
|
||||
{
|
||||
key: "CHANGES_USER_AND_MIGRATION",
|
||||
value: "Appsmith update and user changes since last commit",
|
||||
},
|
||||
];
|
||||
const functions = [
|
||||
CANNOT_MERGE_DUE_TO_UNCOMMITTED_CHANGES,
|
||||
CANNOT_PULL_WITH_LOCAL_UNCOMMITTED_CHANGES,
|
||||
CHANGES_ONLY_MIGRATION,
|
||||
CHANGES_ONLY_USER,
|
||||
CHANGES_USER_AND_MIGRATION,
|
||||
COMMITTING_AND_PUSHING_CHANGES,
|
||||
COMMIT_AND_PUSH,
|
||||
COMMIT_CHANGES,
|
||||
|
|
|
|||
|
|
@ -588,7 +588,6 @@ export const GIT_DISCONNECT_POPUP_MAIN_HEADING = () => `Are you sure ?`;
|
|||
|
||||
export const GIT_CONNECTION = () => "Git Connection";
|
||||
export const GIT_IMPORT = () => "Git Import";
|
||||
export const DEPLOY = () => "Deploy";
|
||||
export const MERGE = () => "Merge";
|
||||
export const GIT_SETTINGS = () => "Git Settings";
|
||||
export const CONNECT_TO_GIT = () => "Connect to git repository";
|
||||
|
|
@ -618,7 +617,6 @@ export const CHECK_DP = () => "CHECK";
|
|||
export const DEPLOY_TO_CLOUD = () => "Deploy to cloud";
|
||||
export const DEPLOY_WITHOUT_GIT = () =>
|
||||
"Deploy your application without version control";
|
||||
export const DEPLOY_YOUR_APPLICATION = () => "Deploy your application";
|
||||
export const COMMIT_CHANGES = () => "Commit changes";
|
||||
export const COMMIT_TO = () => "Commit to";
|
||||
export const COMMIT_AND_PUSH = () => "Commit & push";
|
||||
|
|
@ -735,6 +733,16 @@ export const CONNECTING_TO_REPO_DISABLED = () =>
|
|||
export const DURING_ONBOARDING_TOUR = () => "during the onboarding tour";
|
||||
export const MERGED_SUCCESSFULLY = () => "Merged successfully";
|
||||
|
||||
// GIT DEPLOY begin
|
||||
export const DEPLOY = () => "Deploy";
|
||||
export const DEPLOY_YOUR_APPLICATION = () => "Deploy your application";
|
||||
export const CHANGES_ONLY_USER = () => "Changes since last commit";
|
||||
export const CHANGES_ONLY_MIGRATION = () =>
|
||||
"Appsmith update changes since last commit";
|
||||
export const CHANGES_USER_AND_MIGRATION = () =>
|
||||
"Appsmith update and user changes since last commit";
|
||||
// GIT DEPLOY end
|
||||
|
||||
// GIT ERRORS begin
|
||||
export const ERROR_GIT_AUTH_FAIL = () =>
|
||||
"Please make sure that regenerated SSH key is added and has write access to the repo.";
|
||||
|
|
@ -1149,4 +1157,12 @@ export const CLEAN_URL_UPDATE = {
|
|||
name: () => "Update URLs",
|
||||
shortDesc: () =>
|
||||
"All URLs in your applications will update to a new readable format that includes the application and page names.",
|
||||
description: [
|
||||
() =>
|
||||
"All URLs in your applications will be updated to match our new style. This will make your apps easier to find, and URLs easier to remember.",
|
||||
(url: string) =>
|
||||
`The current app’s URL will be:<br /><code style="line-break: anywhere; padding: 2px 4px; line-height: 22px">${url}</code>`,
|
||||
],
|
||||
disclaimer: () =>
|
||||
"Existing references to <strong>appsmith.URL.fullpath</strong> and <strong>appsmith.URL.pathname</strong> properties will behave differently.",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -70,9 +70,10 @@ import { ReactComponent as WorkspaceIcon } from "assets/icons/ads/organizationIc
|
|||
import { ReactComponent as SettingIcon } from "assets/icons/control/settings.svg";
|
||||
import { ReactComponent as DropdownIcon } from "assets/icons/ads/dropdown.svg";
|
||||
import { ReactComponent as ChatIcon } from "assets/icons/ads/app-icons/chat.svg";
|
||||
import { ReactComponent as JsIcon } from "assets/icons/ads/js.svg";
|
||||
|
||||
import styled from "styled-components";
|
||||
import { CommonComponentProps, Classes } from "./common";
|
||||
import { Classes, CommonComponentProps } from "./common";
|
||||
import { noop } from "lodash";
|
||||
import { theme } from "constants/DefaultTheme";
|
||||
import Spinner from "./Spinner";
|
||||
|
|
@ -90,6 +91,7 @@ import CheckLineIcon from "remixicon-react/CheckLineIcon";
|
|||
import CloseLineIcon from "remixicon-react/CloseLineIcon";
|
||||
import CloseCircleIcon from "remixicon-react/CloseCircleFillIcon";
|
||||
import CommentContextMenu from "remixicon-react/More2FillIcon";
|
||||
import More2FillIcon from "remixicon-react/More2FillIcon";
|
||||
import CompassesLine from "remixicon-react/CompassesLineIcon";
|
||||
import ContextMenuIcon from "remixicon-react/MoreFillIcon";
|
||||
import CreateNewIcon from "remixicon-react/AddLineIcon";
|
||||
|
|
@ -102,8 +104,10 @@ import Download from "remixicon-react/DownloadCloud2LineIcon";
|
|||
import DuplicateIcon from "remixicon-react/FileCopyLineIcon";
|
||||
import EditIcon from "remixicon-react/PencilFillIcon";
|
||||
import EditLineIcon from "remixicon-react/EditLineIcon";
|
||||
import EditUnderlineIcon from "remixicon-react/EditLineIcon";
|
||||
import Emoji from "remixicon-react/EmotionLineIcon";
|
||||
import ExpandMore from "remixicon-react/ArrowDownSLineIcon";
|
||||
import DownArrowIcon from "remixicon-react/ArrowDownSLineIcon";
|
||||
import ExpandLess from "remixicon-react/ArrowUpSLineIcon";
|
||||
import EyeOn from "remixicon-react/EyeLineIcon";
|
||||
import EyeOff from "remixicon-react/EyeOffLineIcon";
|
||||
|
|
@ -122,7 +126,6 @@ import KeyIcon from "remixicon-react/Key2LineIcon";
|
|||
import LeftArrowIcon2 from "remixicon-react/ArrowLeftSLineIcon";
|
||||
import Link2 from "remixicon-react/LinkIcon";
|
||||
import LeftArrowIcon from "remixicon-react/ArrowLeftLineIcon";
|
||||
import More2FillIcon from "remixicon-react/More2FillIcon";
|
||||
import NewsPaperLine from "remixicon-react/NewspaperLineIcon";
|
||||
import OvalCheck from "remixicon-react/CheckboxCircleLineIcon";
|
||||
import OvalCheckFill from "remixicon-react/CheckboxCircleFillIcon";
|
||||
|
|
@ -137,10 +140,8 @@ import Trash from "remixicon-react/DeleteBinLineIcon";
|
|||
import UpArrow from "remixicon-react/ArrowUpSFillIcon";
|
||||
import WarningIcon from "remixicon-react/ErrorWarningFillIcon";
|
||||
import WarningLineIcon from "remixicon-react/ErrorWarningLineIcon";
|
||||
import EditUnderlineIcon from "remixicon-react/EditLineIcon";
|
||||
import LogoutIcon from "remixicon-react/LogoutBoxRLineIcon";
|
||||
import ShareLineIcon from "remixicon-react/ShareLineIcon";
|
||||
import DownArrowIcon from "remixicon-react/ArrowDownSLineIcon";
|
||||
import LoaderLineIcon from "remixicon-react/LoaderLineIcon";
|
||||
import WidgetIcon from "remixicon-react/FunctionLineIcon";
|
||||
import RefreshLineIcon from "remixicon-react/RefreshLineIcon";
|
||||
|
|
@ -344,6 +345,7 @@ const ICON_LOOKUP = {
|
|||
hamburger: <HamburgerIcon />,
|
||||
help: <HelpIcon />,
|
||||
info: <InfoIcon />,
|
||||
js: <JsIcon />,
|
||||
key: <KeyIcon />,
|
||||
lightning: <LightningIcon />,
|
||||
link: <LinkIcon />,
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ export const Colors = {
|
|||
|
||||
WARNING_SOLID: "#FEB811",
|
||||
WARNING_SOLID_HOVER: "#EFA903",
|
||||
WARNING_ORANGE: "#FFF8E2",
|
||||
WARNING_OUTLINE_HOVER: "#FFFAE9",
|
||||
WARNING_GHOST_HOVER: "#FBEED0",
|
||||
|
||||
|
|
|
|||
|
|
@ -683,8 +683,8 @@ export const ReduxActionTypes = {
|
|||
SET_TEMPLATE_NOTIFICATION_SEEN: "SET_TEMPLATE_NOTIFICATION_SEEN",
|
||||
GET_TEMPLATE_NOTIFICATION_SEEN: "GET_TEMPLATE_NOTIFICATION_SEEN",
|
||||
GET_SIMILAR_TEMPLATES_INIT: "GET_SIMILAR_TEMPLATES_INIT",
|
||||
GET_SIMILAR_TEMPLATES_SUCCESS: "GET_SIMILAR_TEMPLATES_SUCCESS",
|
||||
/* This action constants is for identifying the status of the updates of the entities */
|
||||
GET_SIMILAR_TEMPLATES_SUCCESS:
|
||||
"GET_SIMILAR_TEMPLATES_SUCCESS" /* This action constants is for identifying the status of the updates of the entities */,
|
||||
ENTITY_UPDATE_STARTED: "ENTITY_UPDATE_STARTED",
|
||||
ENTITY_UPDATE_SUCCESS: "ENTITY_UPDATE_SUCCESS",
|
||||
FETCH_PLUGIN_AND_JS_ACTIONS_SUCCESS: "FETCH_PLUGIN_AND_JS_ACTIONS_SUCCESS",
|
||||
|
|
@ -925,6 +925,7 @@ export interface PromisePayload {
|
|||
reject: any;
|
||||
resolve: any;
|
||||
}
|
||||
|
||||
export interface ReduxActionWithPromise<T> extends ReduxAction<T> {
|
||||
payload: T & PromisePayload;
|
||||
}
|
||||
|
|
@ -989,6 +990,8 @@ export interface ApplicationPayload {
|
|||
modifiedAt?: string;
|
||||
pages: ApplicationPagePayload[];
|
||||
applicationVersion: ApplicationVersion;
|
||||
isAutoUpdate?: boolean;
|
||||
isManualUpdate?: boolean;
|
||||
}
|
||||
|
||||
export type OrganizationDetails = {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
Category,
|
||||
Icon,
|
||||
IconSize,
|
||||
IconWrapper,
|
||||
Size,
|
||||
Text,
|
||||
TextType,
|
||||
|
|
@ -18,23 +19,15 @@ import React, { useState } from "react";
|
|||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
getCurrentApplicationId,
|
||||
getCurrentPageId,
|
||||
selectApplicationVersion,
|
||||
selectURLSlugs,
|
||||
} from "selectors/editorSelectors";
|
||||
import styled from "styled-components";
|
||||
import { useLocalStorage } from "utils/hooks/localstorage";
|
||||
import { createMessage, CLEAN_URL_UPDATE } from "@appsmith/constants/messages";
|
||||
|
||||
const updates = [
|
||||
{
|
||||
name: createMessage(CLEAN_URL_UPDATE.name),
|
||||
shortDesc: createMessage(CLEAN_URL_UPDATE.shortDesc),
|
||||
description: [
|
||||
"All URLs in your applications will update to a new readable format that includes the application and page names.",
|
||||
'Existing references to <code style="background:#ebebeb;padding:2px 5px;border-radius:2px">appsmith.URL.fullpath</code> and <code style="background:#ebebeb;padding:2px 5px;border-radius:2px">appsmith.URL.pathname</code> properties will behave differently.',
|
||||
],
|
||||
version: ApplicationVersion.SLUG_URL,
|
||||
},
|
||||
];
|
||||
import { useLocation } from "react-router";
|
||||
import DisclaimerIcon from "remixicon-react/ErrorWarningLineIcon";
|
||||
|
||||
function RedDot() {
|
||||
return (
|
||||
|
|
@ -51,6 +44,9 @@ const StyledList = styled.ul`
|
|||
li {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 19px;
|
||||
letter-spacing: -0.24px;
|
||||
margin: 4px 0;
|
||||
a {
|
||||
color: rgb(248, 106, 43);
|
||||
}
|
||||
|
|
@ -70,16 +66,44 @@ const StyledIconContainer = styled.div`
|
|||
border-radius: 50%;
|
||||
`;
|
||||
|
||||
const DisclaimerContainer = styled.div`
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
background: ${Colors.WARNING_ORANGE};
|
||||
color: ${Colors.BROWN};
|
||||
margin: 24px 0 0;
|
||||
`;
|
||||
|
||||
const BodyContainer = styled.div`
|
||||
.close-modal > svg {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
`;
|
||||
|
||||
function UpdatesModal({
|
||||
applicationVersion,
|
||||
closeModal,
|
||||
latestVersion,
|
||||
showModal,
|
||||
updates,
|
||||
}: {
|
||||
showModal: boolean;
|
||||
closeModal: () => void;
|
||||
latestVersion: ApplicationVersion;
|
||||
applicationVersion: ApplicationVersion;
|
||||
updates: {
|
||||
name: string;
|
||||
shortDesc: string;
|
||||
description: string[];
|
||||
version: ApplicationVersion;
|
||||
disclaimer: {
|
||||
desc: string;
|
||||
};
|
||||
}[];
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const applicationId = useSelector(getCurrentApplicationId);
|
||||
|
|
@ -97,7 +121,7 @@ function UpdatesModal({
|
|||
scrollContents
|
||||
width={600}
|
||||
>
|
||||
<div className="p-6">
|
||||
<BodyContainer className="p-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center justify-start">
|
||||
<StyledIconContainer>
|
||||
|
|
@ -110,6 +134,7 @@ function UpdatesModal({
|
|||
<Text type={TextType.H1}>Product Updates</Text>
|
||||
</div>
|
||||
<Icon
|
||||
className="close-modal"
|
||||
fillColor={Colors.SCORPION}
|
||||
name="close-modal"
|
||||
onClick={closeModal}
|
||||
|
|
@ -126,6 +151,14 @@ function UpdatesModal({
|
|||
<li dangerouslySetInnerHTML={{ __html: desc }} key={idx} />
|
||||
))}
|
||||
</StyledList>
|
||||
<DisclaimerContainer>
|
||||
<IconWrapper size={IconSize.XXXL}>
|
||||
<DisclaimerIcon color={Colors.WARNING_SOLID} />
|
||||
</IconWrapper>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{ __html: update.disclaimer.desc }}
|
||||
/>
|
||||
</DisclaimerContainer>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-end gap-2 items-center">
|
||||
|
|
@ -157,7 +190,7 @@ function UpdatesModal({
|
|||
text="Update"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BodyContainer>
|
||||
</ModalComponent>
|
||||
);
|
||||
}
|
||||
|
|
@ -168,6 +201,36 @@ function ManualUpgrades() {
|
|||
"",
|
||||
);
|
||||
const applicationVersion = useSelector(selectApplicationVersion);
|
||||
const applicationId = useSelector(getCurrentApplicationId);
|
||||
const pageId = useSelector(getCurrentPageId);
|
||||
const { applicationSlug, pageSlug } = useSelector(selectURLSlugs);
|
||||
const location = useLocation();
|
||||
|
||||
const updates = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
name: createMessage(CLEAN_URL_UPDATE.name),
|
||||
shortDesc: createMessage(CLEAN_URL_UPDATE.shortDesc),
|
||||
description: CLEAN_URL_UPDATE.description.map((formatter) =>
|
||||
createMessage(
|
||||
formatter.bind(
|
||||
null,
|
||||
window.location.href.replace(
|
||||
`/applications/${applicationId}/pages/${pageId}`,
|
||||
`/${applicationSlug}/${pageSlug}-${pageId}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
disclaimer: {
|
||||
severity: "MODERATE",
|
||||
desc: createMessage(CLEAN_URL_UPDATE.disclaimer),
|
||||
},
|
||||
version: ApplicationVersion.SLUG_URL,
|
||||
},
|
||||
],
|
||||
[location, applicationSlug, pageSlug, pageId, applicationId],
|
||||
);
|
||||
const latestVersion = React.useMemo(
|
||||
() => updates.reduce((max, u) => (max > u.version ? max : u.version), 0),
|
||||
[],
|
||||
|
|
@ -225,6 +288,7 @@ function ManualUpgrades() {
|
|||
}}
|
||||
latestVersion={latestVersion}
|
||||
showModal={showModal}
|
||||
updates={updates}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import { render, screen } from "test/testUtils";
|
||||
import BranchButton from "./BranchButton";
|
||||
import React from "react";
|
||||
|
||||
describe("BranchButton", () => {
|
||||
it("renders properly", async () => {
|
||||
render(<BranchButton />);
|
||||
const buttonContainer = await screen.queryByTestId(
|
||||
"t--branch-button-container",
|
||||
);
|
||||
expect(buttonContainer).not.toBeNull();
|
||||
const currentBranch = await screen.queryByTestId(
|
||||
"t--branch-button-currentBranch",
|
||||
);
|
||||
expect(currentBranch?.innerHTML).toContain("*");
|
||||
});
|
||||
});
|
||||
|
|
@ -20,19 +20,24 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
|
|||
const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& .label {
|
||||
color: ${(props) => props.theme.colors.editorBottomBar.branchBtnText};
|
||||
${(props) => getTypographyByKey(props, "p1")};
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
& .icon {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
margin: 0 ${(props) => props.theme.spaces[4]}px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover svg path {
|
||||
fill: ${Colors.CHARCOAL};
|
||||
}
|
||||
|
||||
& .label {
|
||||
width: 100px;
|
||||
overflow: hidden;
|
||||
|
|
@ -57,6 +62,7 @@ function BranchButton() {
|
|||
return (
|
||||
<Popover2
|
||||
content={<BranchList setIsPopupOpen={setIsOpen} />}
|
||||
data-testid={"t--git-branch-button-popover"}
|
||||
hasBackdrop
|
||||
isOpen={isOpen}
|
||||
minimal
|
||||
|
|
@ -78,11 +84,18 @@ function BranchButton() {
|
|||
hoverOpenDelay={1}
|
||||
position={Position.TOP_LEFT}
|
||||
>
|
||||
<ButtonContainer className="t--branch-button">
|
||||
<ButtonContainer
|
||||
className="t--branch-button"
|
||||
data-testid={"t--branch-button-container"}
|
||||
>
|
||||
<div className="icon">
|
||||
<Icon name="git-branch" size={IconSize.XXXXL} />
|
||||
</div>
|
||||
<div className="label" ref={labelTarget}>
|
||||
<div
|
||||
className="label"
|
||||
data-testid={"t--branch-button-currentBranch"}
|
||||
ref={labelTarget}
|
||||
>
|
||||
{currentBranch}
|
||||
{!status?.isClean && "*"}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -72,18 +72,19 @@ const QuickActionButtonContainer = styled.div<{ disabled?: boolean }>`
|
|||
|
||||
.count {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
height: ${(props) => props.theme.spaces[7]}px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: ${Colors.WHITE};
|
||||
background-color: ${Colors.BLACK};
|
||||
top: -8px;
|
||||
left: 18px;
|
||||
border-radius: 50%;
|
||||
top: ${(props) => -1 * props.theme.spaces[3]}px;
|
||||
left: ${(props) => props.theme.spaces[8]}px;
|
||||
border-radius: ${(props) => props.theme.spaces[3]}px;
|
||||
${(props) => getTypographyByKey(props, "p3")};
|
||||
z-index: 1;
|
||||
padding: ${(props) => props.theme.spaces[1]}px
|
||||
${(props) => props.theme.spaces[2]}px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -91,15 +92,6 @@ const capitalizeFirstLetter = (string = " ") => {
|
|||
return string.charAt(0).toUpperCase() + string.toLowerCase().slice(1);
|
||||
};
|
||||
|
||||
// const SpinnerContainer = styled.div`
|
||||
// margin-left: ${(props) => props.theme.spaces[2]}px;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// width: 29px;
|
||||
// height: 26px;
|
||||
// `;
|
||||
|
||||
function QuickActionButton({
|
||||
className = "",
|
||||
count = 0,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Title } from "../components/StyledComponents";
|
||||
import { Space, Title } from "../components/StyledComponents";
|
||||
import {
|
||||
DEPLOY_YOUR_APPLICATION,
|
||||
COMMIT_TO,
|
||||
createMessage,
|
||||
CHANGES_ONLY_MIGRATION,
|
||||
CHANGES_ONLY_USER,
|
||||
CHANGES_USER_AND_MIGRATION,
|
||||
COMMIT_AND_PUSH,
|
||||
COMMIT_TO,
|
||||
COMMITTING_AND_PUSHING_CHANGES,
|
||||
createMessage,
|
||||
DEPLOY_YOUR_APPLICATION,
|
||||
FETCH_GIT_STATUS,
|
||||
GIT_NO_UPDATED_TOOLTIP,
|
||||
GIT_UPSTREAM_CHANGES,
|
||||
|
|
@ -18,18 +21,17 @@ import Button, { Size } from "components/ads/Button";
|
|||
import { LabelContainer } from "components/ads/Checkbox";
|
||||
|
||||
import {
|
||||
getConflictFoundDocUrlDeploy,
|
||||
getGitCommitAndPushError,
|
||||
getGitStatus,
|
||||
getIsFetchingGitStatus,
|
||||
getIsCommitSuccessful,
|
||||
getIsCommittingInProgress,
|
||||
getIsFetchingGitStatus,
|
||||
getIsPullingProgress,
|
||||
getPullFailed,
|
||||
getGitCommitAndPushError,
|
||||
getUpstreamErrorDocUrl,
|
||||
getConflictFoundDocUrlDeploy,
|
||||
} from "selectors/gitSyncSelectors";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { Space } from "../components/StyledComponents";
|
||||
import { Colors } from "constants/Colors";
|
||||
import { getTypographyByKey, Theme } from "constants/DefaultTheme";
|
||||
|
||||
|
|
@ -40,7 +42,6 @@ import {
|
|||
fetchGitStatusInit,
|
||||
gitPullInit,
|
||||
} from "actions/gitSyncActions";
|
||||
import { getIsCommitSuccessful } from "selectors/gitSyncSelectors";
|
||||
import StatusLoader from "../components/StatusLoader";
|
||||
import { clearCommitSuccessfulState } from "../../../../actions/gitSyncActions";
|
||||
import Statusbar, {
|
||||
|
|
@ -56,11 +57,15 @@ import Icon, { IconSize } from "components/ads/Icon";
|
|||
|
||||
import { isMac } from "utils/helpers";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import { getApplicationLastDeployedAt } from "selectors/editorSelectors";
|
||||
import {
|
||||
getApplicationLastDeployedAt,
|
||||
getCurrentApplication,
|
||||
} from "selectors/editorSelectors";
|
||||
import GIT_ERROR_CODES from "constants/GitErrorCodes";
|
||||
import useAutoGrow from "utils/hooks/useAutoGrow";
|
||||
|
||||
const Section = styled.div`
|
||||
margin-top: ${(props) => props.theme.spaces[11]}px;
|
||||
margin-bottom: ${(props) => props.theme.spaces[11]}px;
|
||||
`;
|
||||
|
||||
|
|
@ -132,9 +137,21 @@ function Deploy() {
|
|||
const currentBranch = gitMetaData?.branchName;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const currentApplication = useSelector(getCurrentApplication);
|
||||
const isAutoUpdate = currentApplication?.isAutoUpdate || false;
|
||||
const isManualUpdate = currentApplication?.isManualUpdate || true;
|
||||
const changeReason = isAutoUpdate
|
||||
? isManualUpdate
|
||||
? CHANGES_USER_AND_MIGRATION
|
||||
: CHANGES_ONLY_MIGRATION
|
||||
: CHANGES_ONLY_USER;
|
||||
const changeReasonText = createMessage(changeReason);
|
||||
|
||||
const handleCommit = (doPush: boolean) => {
|
||||
AnalyticsUtil.logEvent("GS_COMMIT_AND_PUSH_BUTTON_CLICK", {
|
||||
source: "GIT_DEPLOY_MODAL",
|
||||
isAutoUpdate,
|
||||
isManualUpdate,
|
||||
});
|
||||
if (currentBranch) {
|
||||
dispatch(
|
||||
|
|
@ -197,9 +214,15 @@ function Deploy() {
|
|||
const autogrowHeight = useAutoGrow(commitMessageDisplay, 37);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Container data-testid={"t--deploy-tab-container"}>
|
||||
<Title>{createMessage(DEPLOY_YOUR_APPLICATION)}</Title>
|
||||
<Section>
|
||||
<Text
|
||||
data-testid={"t--git-deploy-change-reason-text"}
|
||||
type={TextType.P1}
|
||||
>
|
||||
{changeReasonText}
|
||||
</Text>
|
||||
<GitChanged />
|
||||
<Row>
|
||||
<SectionTitle>
|
||||
|
|
|
|||
|
|
@ -3,97 +3,132 @@ import styled from "constants/DefaultTheme";
|
|||
import { Classes } from "components/ads/common";
|
||||
import Text, { TextType } from "components/ads/Text";
|
||||
import { Colors } from "constants/Colors";
|
||||
import Icon, { IconName, IconSize } from "components/ads/Icon";
|
||||
import Icon, { IconSize } from "components/ads/Icon";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
getGitStatus,
|
||||
getIsFetchingGitStatus,
|
||||
} from "selectors/gitSyncSelectors";
|
||||
import { GitStatusData } from "../../../../reducers/uiReducers/gitSyncReducer";
|
||||
|
||||
const Skeleton = styled.div`
|
||||
width: 135px;
|
||||
const DummyChange = styled.div`
|
||||
width: 50%;
|
||||
height: ${(props) => props.theme.spaces[9]}px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${Colors.GREY_2} 0%,
|
||||
rgba(240, 240, 240, 0) 100%
|
||||
);
|
||||
margin-right: ${(props) => props.theme.spaces[8] + 5}px;
|
||||
margin-top: ${(props) => props.theme.spaces[7]}px;
|
||||
margin-bottom: ${(props) => props.theme.spaces[7]}px;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 178px;
|
||||
height: ${(props) => props.theme.spaces[9]}px;
|
||||
margin-bottom: ${(props) => props.theme.spaces[7]}px;
|
||||
display: flex;
|
||||
|
||||
.${Classes.ICON} {
|
||||
margin-right: ${(props) => props.theme.spaces[3]}px;
|
||||
}
|
||||
|
||||
.${Classes.TEXT} {
|
||||
padding-top: ${(props) => props.theme.spaces[1] - 2}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const GitChangedRow = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
const Statuses = styled.div`
|
||||
margin-top: ${(props) => props.theme.spaces[7]}px;
|
||||
margin-bottom: ${(props) => props.theme.spaces[11]}px;
|
||||
`;
|
||||
|
||||
export enum Kind {
|
||||
widget = "widget",
|
||||
query = "query",
|
||||
commit = "commit",
|
||||
// pullRequest = "pullRequest",
|
||||
WIDGET = "WIDGET",
|
||||
QUERY = "QUERY",
|
||||
COMMIT = "COMMIT",
|
||||
JS_OBJECT = "JS_OBJECT",
|
||||
}
|
||||
|
||||
type GitSyncProps = {
|
||||
type: Kind;
|
||||
type StatusProps = {
|
||||
iconName: string;
|
||||
message: string;
|
||||
hasValue: boolean;
|
||||
};
|
||||
|
||||
function GitStatus(props: GitSyncProps) {
|
||||
const { type } = props;
|
||||
const status: any = useSelector(getGitStatus);
|
||||
const loading = useSelector(getIsFetchingGitStatus);
|
||||
// const loading = true;
|
||||
let message = "",
|
||||
iconName: IconName;
|
||||
switch (type) {
|
||||
case Kind.widget:
|
||||
message = `${status?.modifiedPages || 0} page${
|
||||
(status?.modifiedPages || 0) === 1 ? "" : "s"
|
||||
} updated`;
|
||||
iconName = "widget";
|
||||
break;
|
||||
case Kind.query:
|
||||
message = `${status?.modifiedQueries || 0} ${
|
||||
(status?.modifiedQueries || 0) === 1 ? "query" : "queries"
|
||||
} modified`;
|
||||
iconName = "query";
|
||||
break;
|
||||
case Kind.commit:
|
||||
message = `${status?.aheadCount || 0} commit${
|
||||
(status?.aheadCount || 0) === 1 ? "" : "s"
|
||||
} to push`;
|
||||
iconName = "git-commit";
|
||||
break;
|
||||
}
|
||||
return loading ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
type StatusMap = {
|
||||
[key in Kind]: (status: GitStatusData) => StatusProps;
|
||||
};
|
||||
|
||||
const STATUS_MAP: StatusMap = {
|
||||
[Kind.WIDGET]: (status: GitStatusData) => ({
|
||||
message: `${status?.modifiedPages || 0} ${
|
||||
(status?.modifiedPages || 0) <= 1 ? "page" : "pages"
|
||||
} updated`,
|
||||
iconName: "widget",
|
||||
hasValue: (status?.modifiedPages || 0) > 0,
|
||||
}),
|
||||
[Kind.QUERY]: (status: GitStatusData) => ({
|
||||
message: `${status?.modifiedQueries || 0} ${
|
||||
(status?.modifiedQueries || 0) <= 1 ? "query" : "queries"
|
||||
} modified`,
|
||||
iconName: "query",
|
||||
hasValue: (status?.modifiedQueries || 0) > 0,
|
||||
}),
|
||||
[Kind.COMMIT]: (status: GitStatusData) => ({
|
||||
message: commitMessage(status),
|
||||
iconName: "git-commit",
|
||||
hasValue: (status?.aheadCount || 0) > 0 || (status?.behindCount || 0) > 0,
|
||||
}),
|
||||
[Kind.JS_OBJECT]: (status: GitStatusData) => ({
|
||||
message: `${status?.modifiedJSObjects || 0} JS ${
|
||||
(status?.modifiedJSObjects || 0) <= 1 ? "Object" : "Objects"
|
||||
} modified`,
|
||||
iconName: "js",
|
||||
hasValue: (status?.modifiedJSObjects || 0) > 0,
|
||||
}),
|
||||
};
|
||||
|
||||
function commitMessage(status: GitStatusData) {
|
||||
const aheadCount = status?.aheadCount || 0;
|
||||
const behindCount = status?.behindCount || 0;
|
||||
const aheadMessage =
|
||||
aheadCount > 0
|
||||
? (aheadCount || 0) === 1
|
||||
? `${aheadCount || 0} commit ahead`
|
||||
: `${aheadCount || 0} commits ahead`
|
||||
: null;
|
||||
const behindMessage =
|
||||
behindCount > 0
|
||||
? (behindCount || 0) === 1
|
||||
? `${behindCount || 0} commit behind`
|
||||
: `${behindCount || 0} commits behind `
|
||||
: null;
|
||||
return [aheadMessage, behindMessage].filter((i) => i !== null).join(" and ");
|
||||
}
|
||||
|
||||
function Status(props: Partial<StatusProps>) {
|
||||
const { iconName, message } = props;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Icon fillColor={Colors.GREY_10} name={iconName} size={IconSize.XXL} />
|
||||
<Icon name={iconName} size={IconSize.XXL} />
|
||||
<Text type={TextType.P3}>{message}</Text>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GitChanged() {
|
||||
const gitStatus: any = useSelector(getGitStatus);
|
||||
return (
|
||||
<GitChangedRow>
|
||||
<GitStatus type={Kind.widget} />
|
||||
<GitStatus type={Kind.query} />
|
||||
{gitStatus?.aheadCount > 0 && <GitStatus type={Kind.commit} />}
|
||||
</GitChangedRow>
|
||||
const status: GitStatusData = useSelector(getGitStatus) as GitStatusData;
|
||||
const loading = useSelector(getIsFetchingGitStatus);
|
||||
const statuses = [Kind.WIDGET, Kind.QUERY, Kind.COMMIT, Kind.JS_OBJECT]
|
||||
.map((type: Kind) => STATUS_MAP[type](status))
|
||||
.map((s) =>
|
||||
s.hasValue ? <Status {...s} key={`change-status-${s.iconName}`} /> : null,
|
||||
)
|
||||
.filter((s) => !!s);
|
||||
return loading ? (
|
||||
<DummyChange data-testid={"t--git-change-loading-dummy"} />
|
||||
) : (
|
||||
<Statuses data-testid={"t--git-change-statuses"}>{statuses}</Statuses>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -450,6 +450,7 @@ export type GitStatusData = {
|
|||
modifiedPages: number;
|
||||
modifiedQueries: number;
|
||||
remoteBranch: string;
|
||||
modifiedJSObjects: number;
|
||||
};
|
||||
|
||||
type GitErrorPayloadType = {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ import { failFastApiCalls } from "./InitSagas";
|
|||
import { Datasource } from "entities/Datasource";
|
||||
import { GUIDED_TOUR_STEPS } from "pages/Editor/GuidedTour/constants";
|
||||
import { PLACEHOLDER_APP_SLUG, PLACEHOLDER_PAGE_SLUG } from "constants/routes";
|
||||
import { updateSlugNamesInURL } from "utils/helpers";
|
||||
import { builderURL, generateTemplateURL, viewerURL } from "RouteBuilder";
|
||||
import { getDefaultPageId as selectDefaultPageId } from "./selectors";
|
||||
import PageApi from "api/PageApi";
|
||||
|
|
@ -357,9 +356,6 @@ export function* updateApplicationSaga(
|
|||
type: ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE,
|
||||
payload: response.data,
|
||||
});
|
||||
updateSlugNamesInURL({
|
||||
applicationSlug: response.data.slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -52,11 +52,7 @@ import {
|
|||
takeLeading,
|
||||
} from "redux-saga/effects";
|
||||
import history from "utils/history";
|
||||
import {
|
||||
captureInvalidDynamicBindingPath,
|
||||
isNameValid,
|
||||
updateSlugNamesInURL,
|
||||
} from "utils/helpers";
|
||||
import { captureInvalidDynamicBindingPath, isNameValid } from "utils/helpers";
|
||||
import { extractCurrentDSL } from "utils/WidgetPropsUtils";
|
||||
import { checkIfMigrationIsNeeded } from "utils/DSLMigrations";
|
||||
import {
|
||||
|
|
@ -319,7 +315,7 @@ export function* fetchPublishedPageSaga(
|
|||
// Update the canvas
|
||||
yield put(initCanvasLayout(canvasWidgetsPayload));
|
||||
// set current page
|
||||
yield put(updateCurrentPage(pageId));
|
||||
yield put(updateCurrentPage(pageId, response.data.slug));
|
||||
// dispatch fetch page success
|
||||
yield put(
|
||||
fetchPublishedPageSuccess(
|
||||
|
|
@ -599,9 +595,6 @@ export function* updatePageSaga(action: ReduxAction<UpdatePageRequest>) {
|
|||
payload: response.data,
|
||||
});
|
||||
}
|
||||
updateSlugNamesInURL({
|
||||
pageSlug: response.data.slug,
|
||||
});
|
||||
} catch (error) {
|
||||
yield put({
|
||||
type: ReduxActionErrorTypes.UPDATE_PAGE_ERROR,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ import { rootSaga } from "sagas";
|
|||
import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProduction";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
import { updateURLFactory } from "RouteBuilder";
|
||||
import { getRouteBuilderParams, updateURLFactory } from "RouteBuilder";
|
||||
import { updateSlugNamesInURL } from "utils/helpers";
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
const sentryReduxEnhancer = Sentry.createReduxEnhancer({
|
||||
|
|
@ -42,11 +43,21 @@ const routeParamsMiddleware: Middleware = () => (next: any) => (
|
|||
case ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE: {
|
||||
const { slug } = action.payload;
|
||||
updateURLFactory({ applicationSlug: slug });
|
||||
updateSlugNamesInURL({
|
||||
applicationSlug: slug,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ReduxActionTypes.SWITCH_CURRENT_PAGE_ID:
|
||||
case ReduxActionTypes.UPDATE_PAGE_SUCCESS: {
|
||||
const { id, slug } = action.payload;
|
||||
const id = action.payload.id;
|
||||
const slug = action.payload.slug;
|
||||
const { pageId } = getRouteBuilderParams();
|
||||
// Update page slug in URL only if the current page is renamed
|
||||
if (pageId === id)
|
||||
updateSlugNamesInURL({
|
||||
pageSlug: slug,
|
||||
});
|
||||
updateURLFactory({ pageId: id, pageSlug: slug });
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -710,6 +710,8 @@ export const getUpdatedRoute = (
|
|||
|
||||
export const updateSlugNamesInURL = (params: Record<string, string>) => {
|
||||
const { pathname, search } = window.location;
|
||||
// Do not update old URLs
|
||||
if (isURLDeprecated(pathname)) return;
|
||||
const newURL = getUpdatedRoute(pathname, params);
|
||||
history.replace(newURL + search);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -591,9 +591,9 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
/**
|
||||
* This function will take the application reference object to hydrate the application in mongoDB
|
||||
*
|
||||
* @param organizationId organization to which application is going to be stored
|
||||
* @param applicationJson application resource which contains necessary information to save the application
|
||||
* @param applicationId application which needs to be saved with the updated resources
|
||||
* @param organizationId organization to which application is going to be stored
|
||||
* @param applicationJson application resource which contains necessary information to import the application
|
||||
* @param applicationId application which needs to be saved with the updated resources
|
||||
* @return Updated application
|
||||
*/
|
||||
public Mono<Application> importApplicationInOrganization(String organizationId,
|
||||
|
|
@ -689,6 +689,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
// Check for duplicate datasources to avoid duplicates in target organization
|
||||
.flatMap(datasource -> {
|
||||
|
||||
final String importedDatasourceName = datasource.getName();
|
||||
// Check if the datasource has gitSyncId and if it's already in DB
|
||||
if (datasource.getGitSyncId() != null
|
||||
&& savedDatasourcesGitIdToDatasourceMap.containsKey(datasource.getGitSyncId())) {
|
||||
|
|
@ -702,7 +703,11 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
datasource.setPluginId(null);
|
||||
AppsmithBeanUtils.copyNestedNonNullProperties(datasource, existingDatasource);
|
||||
existingDatasource.setStructure(null);
|
||||
return datasourceService.update(existingDatasource.getId(), existingDatasource);
|
||||
return datasourceService.update(existingDatasource.getId(), existingDatasource)
|
||||
.map(datasource1 -> {
|
||||
datasourceMap.put(importedDatasourceName, datasource1.getId());
|
||||
return datasource1;
|
||||
});
|
||||
}
|
||||
|
||||
// This is explicitly copied over from the map we created before
|
||||
|
|
@ -719,11 +724,11 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
updateAuthenticationDTO(datasource, decryptedFields);
|
||||
}
|
||||
|
||||
return createUniqueDatasourceIfNotPresent(existingDatasourceFlux, datasource, organizationId, applicationId);
|
||||
})
|
||||
.map(datasource -> {
|
||||
datasourceMap.put(datasource.getName(), datasource.getId());
|
||||
return datasource;
|
||||
return createUniqueDatasourceIfNotPresent(existingDatasourceFlux, datasource, organizationId)
|
||||
.map(datasource1 -> {
|
||||
datasourceMap.put(importedDatasourceName, datasource1.getId());
|
||||
return datasource1;
|
||||
});
|
||||
})
|
||||
.collectList();
|
||||
})
|
||||
|
|
@ -1726,9 +1731,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
*/
|
||||
private Mono<Datasource> createUniqueDatasourceIfNotPresent(Flux<Datasource> existingDatasourceFlux,
|
||||
Datasource datasource,
|
||||
String organizationId,
|
||||
String applicationId) {
|
||||
|
||||
String organizationId) {
|
||||
/*
|
||||
1. If same datasource is present return
|
||||
2. If unable to find the datasource create a new datasource with unique name and return
|
||||
|
|
@ -1743,16 +1746,8 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
|
|||
}
|
||||
|
||||
return existingDatasourceFlux
|
||||
.map(ds -> {
|
||||
final DatasourceConfiguration dsAuthConfig = ds.getDatasourceConfiguration();
|
||||
if (dsAuthConfig != null && dsAuthConfig.getAuthentication() != null) {
|
||||
dsAuthConfig.getAuthentication().setAuthenticationResponse(null);
|
||||
dsAuthConfig.getAuthentication().setAuthenticationType(null);
|
||||
}
|
||||
return ds;
|
||||
})
|
||||
// For git import exclude datasource configuration
|
||||
.filter(ds -> applicationId != null ? ds.getName().equals(datasource.getName()) : ds.softEquals(datasource))
|
||||
.filter(ds -> ds.getName().equals(datasource.getName()) && datasource.getPluginId().equals(ds.getPluginId()))
|
||||
.next() // Get the first matching datasource, we don't need more than one here.
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
if (datasourceConfig != null && datasourceConfig.getAuthentication() != null) {
|
||||
|
|
|
|||
|
|
@ -1702,7 +1702,7 @@ public class GitServiceTest {
|
|||
StepVerifier
|
||||
.create(applicationMono)
|
||||
.expectErrorMatches(throwable -> throwable instanceof AppsmithException
|
||||
&& throwable.getMessage().equals(AppsmithError.GIT_ACTION_FAILED.getMessage("checkout", "origin/branchInLocal already exists in remote")))
|
||||
&& throwable.getMessage().equals(AppsmithError.GIT_ACTION_FAILED.getMessage("checkout", "origin/branchInLocal already exists in local - branchInLocal")))
|
||||
.verify();
|
||||
}
|
||||
|
||||
|
|
@ -2486,7 +2486,7 @@ public class GitServiceTest {
|
|||
|
||||
@Test
|
||||
@WithUserDetails(value = "api_user")
|
||||
public void importApplicationFromGit_validRequestWithDuplicateDatasourceOfSameTypeCancelledMidway_Success() throws GitAPIException, IOException {
|
||||
public void importApplicationFromGit_validRequestWithDuplicateDatasourceOfSameTypeCancelledMidway_Success() {
|
||||
Organization organization = new Organization();
|
||||
organization.setName("gitImportOrgCancelledMidway");
|
||||
final String testOrgId = organizationService.create(organization)
|
||||
|
|
@ -2497,6 +2497,7 @@ public class GitServiceTest {
|
|||
GitAuth gitAuth = gitService.generateSSHKey().block();
|
||||
|
||||
ApplicationJson applicationJson = createAppJson(filePath).block();
|
||||
applicationJson.getExportedApplication().setName(null);
|
||||
applicationJson.getDatasourceList().get(0).setName("db-auth-testGitImportRepo");
|
||||
|
||||
String pluginId = pluginRepository.findByPackageName("mongo-plugin").block().getId();
|
||||
|
|
@ -2521,7 +2522,7 @@ public class GitServiceTest {
|
|||
|
||||
// Wait for git clone to complete
|
||||
Mono<Application> gitConnectedAppFromDbMono = Mono.just(testOrgId)
|
||||
.flatMap(application -> {
|
||||
.flatMap(ignore -> {
|
||||
try {
|
||||
// Before fetching the git connected application, sleep for 5 seconds to ensure that the clone
|
||||
// completes
|
||||
|
|
|
|||
|
|
@ -2182,5 +2182,111 @@ public class ImportExportApplicationServiceTests {
|
|||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUserDetails(value = "api_user")
|
||||
public void importApplication_datasourceWithSameNameAndDifferentPlugin_importedWithValidActionsAndSuffixedDatasource() {
|
||||
|
||||
ApplicationJson applicationJson = createAppJson("test_assets/ImportExportServiceTest/valid-application.json").block();
|
||||
|
||||
Organization testOrganization = new Organization();
|
||||
testOrganization.setName("Duplicate datasource with different plugin org");
|
||||
testOrganization = organizationService.create(testOrganization).block();
|
||||
|
||||
Datasource testDatasource = new Datasource();
|
||||
// Chose any plugin except for mongo, as json static file has mongo plugin for datasource
|
||||
Plugin postgreSQLPlugin = pluginRepository.findByName("PostgreSQL").block();
|
||||
testDatasource.setPluginId(postgreSQLPlugin.getId());
|
||||
testDatasource.setOrganizationId(testOrganization.getId());
|
||||
final String datasourceName = applicationJson.getDatasourceList().get(0).getName();
|
||||
testDatasource.setName(datasourceName);
|
||||
datasourceService.create(testDatasource).block();
|
||||
|
||||
final Mono<Application> resultMono = importExportApplicationService.importApplicationInOrganization(testOrganization.getId(), applicationJson);
|
||||
|
||||
StepVerifier
|
||||
.create(resultMono
|
||||
.flatMap(application -> Mono.zip(
|
||||
Mono.just(application),
|
||||
datasourceService.findAllByOrganizationId(application.getOrganizationId(), MANAGE_DATASOURCES).collectList(),
|
||||
newActionService.findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null).collectList()
|
||||
)))
|
||||
.assertNext(tuple -> {
|
||||
final Application application = tuple.getT1();
|
||||
final List<Datasource> datasourceList = tuple.getT2();
|
||||
final List<NewAction> actionList = tuple.getT3();
|
||||
|
||||
assertThat(application.getName()).isEqualTo("valid_application");
|
||||
|
||||
List<String> datasourceNameList = new ArrayList<>();
|
||||
assertThat(datasourceList).isNotEmpty();
|
||||
datasourceList.forEach(datasource -> {
|
||||
assertThat(datasource.getOrganizationId()).isEqualTo(application.getOrganizationId());
|
||||
datasourceNameList.add(datasource.getName());
|
||||
});
|
||||
// Check if both suffixed and newly imported datasource are present
|
||||
assertThat(datasourceNameList).contains(datasourceName, datasourceName + " #1");
|
||||
|
||||
assertThat(actionList).isNotEmpty();
|
||||
actionList.forEach(newAction -> {
|
||||
ActionDTO actionDTO = newAction.getUnpublishedAction();
|
||||
assertThat(actionDTO.getDatasource()).isNotNull();
|
||||
});
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithUserDetails(value = "api_user")
|
||||
public void importApplication_datasourceWithSameNameAndPlugin_importedWithValidActionsWithoutSuffixedDatasource() {
|
||||
|
||||
ApplicationJson applicationJson = createAppJson("test_assets/ImportExportServiceTest/valid-application.json").block();
|
||||
|
||||
Organization testOrganization = new Organization();
|
||||
testOrganization.setName("Duplicate datasource with same plugin org");
|
||||
testOrganization = organizationService.create(testOrganization).block();
|
||||
|
||||
Datasource testDatasource = new Datasource();
|
||||
// Chose plugin same as mongo, as json static file has mongo plugin for datasource
|
||||
Plugin postgreSQLPlugin = pluginRepository.findByName("MongoDB").block();
|
||||
testDatasource.setPluginId(postgreSQLPlugin.getId());
|
||||
testDatasource.setOrganizationId(testOrganization.getId());
|
||||
final String datasourceName = applicationJson.getDatasourceList().get(0).getName();
|
||||
testDatasource.setName(datasourceName);
|
||||
datasourceService.create(testDatasource).block();
|
||||
|
||||
final Mono<Application> resultMono = importExportApplicationService.importApplicationInOrganization(testOrganization.getId(), applicationJson);
|
||||
|
||||
StepVerifier
|
||||
.create(resultMono
|
||||
.flatMap(application -> Mono.zip(
|
||||
Mono.just(application),
|
||||
datasourceService.findAllByOrganizationId(application.getOrganizationId(), MANAGE_DATASOURCES).collectList(),
|
||||
newActionService.findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null).collectList()
|
||||
)))
|
||||
.assertNext(tuple -> {
|
||||
final Application application = tuple.getT1();
|
||||
final List<Datasource> datasourceList = tuple.getT2();
|
||||
final List<NewAction> actionList = tuple.getT3();
|
||||
|
||||
assertThat(application.getName()).isEqualTo("valid_application");
|
||||
|
||||
List<String> datasourceNameList = new ArrayList<>();
|
||||
assertThat(datasourceList).isNotEmpty();
|
||||
datasourceList.forEach(datasource -> {
|
||||
assertThat(datasource.getOrganizationId()).isEqualTo(application.getOrganizationId());
|
||||
datasourceNameList.add(datasource.getName());
|
||||
});
|
||||
// Check that there are no datasources are created with suffix names as datasource's are of same plugin
|
||||
assertThat(datasourceNameList).contains(datasourceName);
|
||||
|
||||
assertThat(actionList).isNotEmpty();
|
||||
actionList.forEach(newAction -> {
|
||||
ActionDTO actionDTO = newAction.getUnpublishedAction();
|
||||
assertThat(actionDTO.getDatasource()).isNotNull();
|
||||
});
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user