diff --git a/.github/workflows/build-client-server.yml b/.github/workflows/build-client-server.yml index 52253d4d03b4..550758e0fcbd 100644 --- a/.github/workflows/build-client-server.yml +++ b/.github/workflows/build-client-server.yml @@ -131,7 +131,6 @@ jobs: secrets: inherit with: pr: ${{fromJson(needs.file-check.outputs.pr)}} - matrix: ${{needs.file-check.outputs.matrix_count}} ci-test-limited-existing-docker-image: needs: [file-check] @@ -143,7 +142,6 @@ jobs: with: pr: ${{fromJson(needs.file-check.outputs.pr)}} previous-workflow-run-id: ${{ fromJson(needs.file-check.outputs.runId) }} - matrix: ${{ needs.file-check.outputs.matrix_count }} ci-test-limited-result: needs: [file-check, ci-test-limited] diff --git a/.github/workflows/ci-test-custom-script.yml b/.github/workflows/ci-test-custom-script.yml index a1e2429c858a..e7f877654c11 100644 --- a/.github/workflows/ci-test-custom-script.yml +++ b/.github/workflows/ci-test-custom-script.yml @@ -367,7 +367,7 @@ jobs: working-directory: "." run: | mkdir -p ~/dockerlogs - docker logs appsmith 2>&1 > ~/dockerlogs/dockerlogs-${{ matrix.job }}.txt + docker logs appsmith &> ~/dockerlogs/dockerlogs-${{ matrix.job }}.txt # Upload docker logs - name: Upload failed test list artifact diff --git a/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilModal_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilModal_spec.ts new file mode 100644 index 000000000000..f9c14d70c4ea --- /dev/null +++ b/app/client/cypress/e2e/Regression/ClientSide/Anvil/AnvilModal_spec.ts @@ -0,0 +1,167 @@ +import { + agHelper, + anvilLayout, + locators, + propPane, +} from "../../../../support/Objects/ObjectsCore"; +import { ANVIL_EDITOR_TEST } from "../../../../support/Constants"; +import { anvilLocators } from "../../../../support/Pages/Anvil/Locators"; +import EditorNavigation, { + EntityType, +} from "../../../../support/Pages/EditorNavigation"; + +describe( + `${ANVIL_EDITOR_TEST}: Anvil tests for Modals`, + { tags: ["@tag.Anvil"] }, + () => { + before(() => { + // Cleanup the canvas before each test + agHelper.SelectAllWidgets(); + agHelper.PressDelete(); + }); + it("1. Verify opening a modal by clicking on a button", () => { + // drop a modal widget + anvilLayout.dnd.DragDropNewAnvilWidgetNVerify( + anvilLocators.WDSMODAL, + 10, + 10, + { + skipWidgetSearch: true, + dropTargetDetails: { + dropModal: true, + }, + }, + ); + // press escape and close modal + agHelper.PressEscape(); + // add a button + anvilLayout.dnd.DragDropNewAnvilWidgetNVerify( + anvilLocators.WDSBUTTON, + 10, + 10, + { + skipWidgetSearch: true, + }, + ); + propPane.EnterJSContext("onClick", "{{showModal(Modal1.name);}}"); + agHelper.GetNClick(locators._enterPreviewMode); + agHelper.GetNClick(anvilLocators.anvilWidgetNameSelector("Button1")); + agHelper.AssertElementExist( + anvilLocators.anvilWidgetNameSelector("Modal1"), + ); + }); + it("2. Verify closing a modal using the close icon button", () => { + agHelper.GetNClick( + anvilLocators.anvilModalCloseIconButtonSelector("Modal1"), + ); + agHelper.AssertElementAbsence( + anvilLocators.anvilWidgetNameSelector("Modal1"), + ); + }); + it("3. Verify closing a modal by clicking outside the modal area", () => { + // open modal + agHelper.GetNClick(anvilLocators.anvilWidgetNameSelector("Button1")); + agHelper.AssertElementExist( + anvilLocators.anvilWidgetNameSelector("Modal1"), + ); + // click on overlay top position + agHelper.GetNClick( + anvilLocators.anvilModalOverlay, + 0, + false, + 500, + false, + false, + "top", + ); + agHelper.AssertElementAbsence( + anvilLocators.anvilWidgetNameSelector("Modal1"), + ); + }); + it("4. Verify closing a modal using the ESC key", () => { + // open modal + agHelper.GetNClick(anvilLocators.anvilWidgetNameSelector("Button1")); + agHelper.AssertElementExist( + anvilLocators.anvilWidgetNameSelector("Modal1"), + ); + // press escape + agHelper.PressEscape(); + agHelper.AssertElementAbsence( + anvilLocators.anvilWidgetNameSelector("Modal1"), + ); + agHelper.GetNClick(locators._exitPreviewMode); + }); + it("5. verify onClose function of Modal", () => { + EditorNavigation.SelectEntityByName("Modal1", EntityType.Widget); + propPane.EnterJSContext("onClose", "{{showAlert('onCloseTest');}}"); + agHelper.GetNClick(locators._enterPreviewMode); + //close modal via footer close button + agHelper.GetNClick( + anvilLocators.anvilModalFooterCloseButtonSelector("Modal1"), + ); + //verify alert + agHelper.ValidateToastMessage("onCloseTest"); + agHelper.GetNClick(locators._exitPreviewMode); + }); + it("6. Verify onSubmit function on Modal", () => { + EditorNavigation.SelectEntityByName("Modal1", EntityType.Widget); + propPane.EnterJSContext("onSubmit", "{{showAlert('onSubmitTest');}}"); + agHelper.GetNClick(locators._enterPreviewMode); + //close modal via submit button + agHelper.GetNClick( + anvilLocators.anvilModalFooterSubmitButtonSelector("Modal1"), + ); + //verify alert + agHelper.ValidateToastMessage("onSubmitTest"); + agHelper.GetNClick(locators._exitPreviewMode); + }); + it("7. Verify DnD on Modal", () => { + EditorNavigation.SelectEntityByName("Modal1", EntityType.Widget); + // add a widget to modal + anvilLayout.dnd.DragDropNewAnvilWidgetNVerify( + anvilLocators.WDSBUTTON, + 10, + 10, + { + skipWidgetSearch: true, + dropTargetDetails: { + name: "Modal1", + }, + }, + ); + // verify newly added button + agHelper.AssertElementExist( + anvilLocators.anvilWidgetNameSelector("Button2"), + ); + }); + it("8. Verify different modal sizes", () => { + // select all widgets and delete + agHelper.PressEscape(); + agHelper.SelectAllWidgets(); + agHelper.PressDelete(); + // add a modal widget + anvilLayout.dnd.DragDropNewAnvilWidgetNVerify( + anvilLocators.WDSMODAL, + 10, + 10, + { + skipWidgetSearch: true, + dropTargetDetails: { + dropModal: true, + }, + }, + ); + agHelper + .GetElement(anvilLocators.anvilWidgetNameSelector("Modal1")) + .matchImageSnapshot("anvilModalMediumSize"); + propPane.SelectPropertiesDropDown("size", "Small"); + agHelper + .GetElement(anvilLocators.anvilWidgetNameSelector("Modal1")) + .matchImageSnapshot("anvilModalSmallSize"); + propPane.SelectPropertiesDropDown("size", "Large"); + agHelper + .GetElement(anvilLocators.anvilWidgetNameSelector("Modal1")) + .matchImageSnapshot("anvilModalLargeSize"); + }); + }, +); diff --git a/app/client/cypress/e2e/Regression/ServerSide/OnLoadTests/OnLoadActions_Spec.ts b/app/client/cypress/e2e/Regression/ServerSide/OnLoadTests/OnLoadActions_Spec.ts index 922d7081629a..302bc1c1c137 100644 --- a/app/client/cypress/e2e/Regression/ServerSide/OnLoadTests/OnLoadActions_Spec.ts +++ b/app/client/cypress/e2e/Regression/ServerSide/OnLoadTests/OnLoadActions_Spec.ts @@ -29,7 +29,8 @@ describe( agHelper.AddDsl("onPageLoadActionsDsl"); EditorNavigation.SelectEntityByName("Page1", EntityType.Page); cy.url().then((url) => { - const pageid = url.split("/")[5]?.split("-").pop(); + const pageid = agHelper.extractPageIdFromUrl(url); + expect(pageid).to.not.be.null; cy.log(pageid + "page id"); cy.request("GET", "api/v1/pages/" + pageid).then((response) => { const respBody = JSON.stringify(response.body); diff --git a/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalLargeSize.snap.png b/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalLargeSize.snap.png new file mode 100644 index 000000000000..f91964ccb85a Binary files /dev/null and b/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalLargeSize.snap.png differ diff --git a/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalMediumSize.snap.png b/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalMediumSize.snap.png new file mode 100644 index 000000000000..af945bafb7f0 Binary files /dev/null and b/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalMediumSize.snap.png differ diff --git a/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalSmallSize.snap.png b/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalSmallSize.snap.png new file mode 100644 index 000000000000..487c58b37a2e Binary files /dev/null and b/app/client/cypress/snapshots/AnvilModal_spec.ts/anvilModalSmallSize.snap.png differ diff --git a/app/client/cypress/support/Pages/AggregateHelper.ts b/app/client/cypress/support/Pages/AggregateHelper.ts index ad63fcb32caf..9f5bc761b51a 100644 --- a/app/client/cypress/support/Pages/AggregateHelper.ts +++ b/app/client/cypress/support/Pages/AggregateHelper.ts @@ -133,15 +133,32 @@ export class AggregateHelper { }); } + public extractPageIdFromUrl(url: string): null | string { + const parts = url.split("/"); + + if (parts[3] !== "app") { + // Not a app URL. + return null; + } + + // Extract the page ID, either as an ObjectID or as a UUID. + return ( + parts[5]?.match( + /[0-9a-f]{24}$|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, + )?.[0] ?? null + ); + } + public AddDsl( dslFile: string, elementToCheckPresenceaftDslLoad: string | "" = "", // reloadWithoutCache = true, ) { - let pageid: string, layoutId; + let layoutId; let appId: string | null; cy.fixture(dslFile).then((val) => { cy.url().then((url) => { - pageid = url.split("/")[5]?.split("-").pop() as string; + const pageid = this.extractPageIdFromUrl(url); + expect(pageid).to.not.be.null; //Fetch the layout id cy.request("GET", "api/v1/pages/" + pageid).then((response: any) => { const respBody = JSON.stringify(response.body); diff --git a/app/client/cypress/support/Pages/Anvil/AnvilDnDHelper.ts b/app/client/cypress/support/Pages/Anvil/AnvilDnDHelper.ts index 24576a632c69..3056294a1e98 100644 --- a/app/client/cypress/support/Pages/Anvil/AnvilDnDHelper.ts +++ b/app/client/cypress/support/Pages/Anvil/AnvilDnDHelper.ts @@ -11,6 +11,7 @@ import { anvilLocators } from "./Locators"; interface DropTargetDetails { id?: string; name?: string; + dropModal?: boolean; } interface DragDropWidgetOptions { @@ -26,11 +27,14 @@ export class AnvilDnDHelper { dropTarget?: DropTargetDetails, ) => { if (dropTarget) { + if (dropTarget.dropModal) { + return anvilLocators.anvilDetachedWidgetsDropArena; + } if (dropTarget.id) { return `#${dropTarget.id}`; } if (dropTarget.name) { - return `${getWidgetSelector(dropTarget.name.toLowerCase() as any)} ${ + return `${anvilLocators.anvilWidgetNameSelector(dropTarget.name)} ${ anvilLocators.anvilDnDListener }`; } @@ -64,7 +68,10 @@ export class AnvilDnDHelper { eventConstructor: "MouseEvent", force: true, }); - cy.get(this.locator._anvilDnDHighlight); + if (!options.dropTargetDetails?.dropModal) { + // no need to show highlight for modal drop + cy.get(this.locator._anvilDnDHighlight); + } cy.get(dropAreaSelector).first().trigger("mouseup", xPos, yPos, { eventConstructor: "MouseEvent", force: true, diff --git a/app/client/cypress/support/Pages/Anvil/AnvilLayout.ts b/app/client/cypress/support/Pages/Anvil/AnvilLayout.ts index 707ae72eb827..beb7358083a4 100644 --- a/app/client/cypress/support/Pages/Anvil/AnvilLayout.ts +++ b/app/client/cypress/support/Pages/Anvil/AnvilLayout.ts @@ -9,6 +9,11 @@ export class AnvilLayout { const widgetSelector = anvilLocators.anvilWidgetNameSelector(widgetName); cy.get(widgetSelector).should("not.exist"); } + + public verifyAnvilModalIsClosed(widgetName: string) { + this.verifyWidgetDoesNotExist(widgetName); + } + public verifyParentChildRelationship(parentName: string, childName: string) { const parentWidgetSelector = anvilLocators.anvilWidgetNameSelector(parentName); diff --git a/app/client/cypress/support/Pages/Anvil/Locators/index.ts b/app/client/cypress/support/Pages/Anvil/Locators/index.ts index 0195e6883b0b..b6d3e6b90844 100644 --- a/app/client/cypress/support/Pages/Anvil/Locators/index.ts +++ b/app/client/cypress/support/Pages/Anvil/Locators/index.ts @@ -9,12 +9,25 @@ const anvilWidgetBasedSelectors = { anvilWidgetNameSelector: (widgetName: string) => { return `[${AnvilDataAttributes.WIDGET_NAME}="${widgetName}"]`; }, + anvilModalOverlay: 'div[data-floating-ui-portal] > div[data-status="open"]', anvilSelectedWidget: `${anvilWidgetSelector}[data-selected=true]`, anvilWidgetTypeSelector: (widgetType: string) => { return `.t--widget-${widgetType}`; }, }; +const anvilModalWidgetSelectors = { + anvilModalCloseIconButtonSelector: (widgetName: string) => { + return `${anvilWidgetBasedSelectors.anvilWidgetNameSelector(widgetName)} > div > div > button[data-icon-button]`; + }, + anvilModalFooterCloseButtonSelector: (widgetName: string) => { + return `${anvilWidgetBasedSelectors.anvilWidgetNameSelector(widgetName)} > div > div:last-child > button[data-button]:first-child`; + }, + anvilModalFooterSubmitButtonSelector: (widgetName: string) => { + return `${anvilWidgetBasedSelectors.anvilWidgetNameSelector(widgetName)} > div > div:last-child > button[data-button]:last-child`; + }, +}; + // sections and zones based selectors const anvilSectionAndZonesBasedSelectors = { anvilZoneDistributionValue: "[data-testid=t--anvil-zone-distribution-value]", @@ -29,6 +42,8 @@ const anvilSectionAndZonesBasedSelectors = { // dnd based selectors const anvilDnDBasedSelectors = { anvilDnDListener: "[data-type=anvil-dnd-listener]", + anvilDetachedWidgetsDropArena: + "[data-testid=t--anvil-detached-widgets-drop-arena]", mainCanvasSelector: `#${getAnvilCanvasId(MAIN_CONTAINER_WIDGET_ID)}`, }; @@ -39,12 +54,14 @@ const anvilWidgetsLocators = { WDSINPUT: "wdsinputwidget", WDSSWITCH: "wdsswitchwidget", WDSCHECKBOX: "wdscheckboxwidget", + WDSMODAL: "wdsmodalwidget", SECTION: "sectionwidget", ZONE: "zonewidget", }; export const anvilLocators = { ...anvilWidgetBasedSelectors, + ...anvilModalWidgetSelectors, ...anvilWidgetsLocators, ...anvilSectionAndZonesBasedSelectors, ...anvilDnDBasedSelectors, diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 9730cae88f54..e2cb4db75b02 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -371,7 +371,8 @@ Cypress.Commands.add("addDsl", (dsl) => { if (RapidMode.config.enabled && RapidMode.config.usesDSL) { pageid = RapidMode.config.pageID; } else { - pageid = url.split("/")[5]?.split("-").pop(); + pageid = agHelper.extractPageIdFromUrl(url); + expect(pageid).to.not.be.null; } //Fetch the layout id diff --git a/app/client/packages/design-system/widgets/src/components/Modal/src/Modal.tsx b/app/client/packages/design-system/widgets/src/components/Modal/src/Modal.tsx index 7f6c000e728a..a3fdaee54b51 100644 --- a/app/client/packages/design-system/widgets/src/components/Modal/src/Modal.tsx +++ b/app/client/packages/design-system/widgets/src/components/Modal/src/Modal.tsx @@ -7,8 +7,8 @@ import clsx from "clsx"; export const Modal = (props: ModalProps) => { const { children, + dataAttributes = {}, overlayClassName, - size = "medium", triggerRef, ...rest } = props; @@ -17,7 +17,7 @@ export const Modal = (props: ModalProps) => { // don't forget to change the transition-duration CSS as well {children} diff --git a/app/client/packages/design-system/widgets/src/components/Modal/src/types.ts b/app/client/packages/design-system/widgets/src/components/Modal/src/types.ts index 1c5bb1abd77d..d83d30903c44 100644 --- a/app/client/packages/design-system/widgets/src/components/Modal/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/Modal/src/types.ts @@ -3,7 +3,6 @@ import type { PopoverProps, } from "@design-system/headless"; import type { ReactNode } from "react"; -import type { SIZES } from "../../../shared"; export interface ModalProps extends Pick< @@ -16,10 +15,7 @@ export interface ModalProps | "dismissClickOutside" >, Pick { - /** Size of the Modal - * @default medium - */ - size?: keyof typeof SIZES; + dataAttributes?: Record; /** The children of the component. */ children: ReactNode; } diff --git a/app/client/packages/design-system/widgets/src/components/Modal/stories/ModalExamples.tsx b/app/client/packages/design-system/widgets/src/components/Modal/stories/ModalExamples.tsx index 0b0a560b4cce..259a108db717 100644 --- a/app/client/packages/design-system/widgets/src/components/Modal/stories/ModalExamples.tsx +++ b/app/client/packages/design-system/widgets/src/components/Modal/stories/ModalExamples.tsx @@ -51,10 +51,10 @@ export const ModalExamples = () => { }} /> @@ -121,10 +121,10 @@ export const ModalExamples = () => { diff --git a/app/client/packages/design-system/widgets/src/testing/ComplexForm.tsx b/app/client/packages/design-system/widgets/src/testing/ComplexForm.tsx index ee2ff6d668f3..906860533f5a 100644 --- a/app/client/packages/design-system/widgets/src/testing/ComplexForm.tsx +++ b/app/client/packages/design-system/widgets/src/testing/ComplexForm.tsx @@ -151,10 +151,10 @@ export const ComplexForm = () => { Ok diff --git a/app/client/src/Globals.d.ts b/app/client/src/Globals.d.ts index e2e0de448b50..2bf7e67e4224 100644 --- a/app/client/src/Globals.d.ts +++ b/app/client/src/Globals.d.ts @@ -3,4 +3,5 @@ declare module "*.txt" { export default filePath; } +declare let CDN_URL: string; declare module "echarts-gl"; diff --git a/app/client/src/assets/icons/ads/enter.svg b/app/client/src/assets/icons/ads/enter.svg new file mode 100644 index 000000000000..fd5566defca2 --- /dev/null +++ b/app/client/src/assets/icons/ads/enter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/assets/images/scim.png b/app/client/src/assets/images/scim.png new file mode 100644 index 000000000000..79df1714bb1a Binary files /dev/null and b/app/client/src/assets/images/scim.png differ diff --git a/app/client/src/assets/lottie/guided-tour-indicator.json.txt b/app/client/src/assets/lottie/guided-tour-indicator.json.txt index 4df1c90f8149..7c9e70de0066 100644 --- a/app/client/src/assets/lottie/guided-tour-indicator.json.txt +++ b/app/client/src/assets/lottie/guided-tour-indicator.json.txt @@ -1 +1 @@ -{"v":"5.5.8","fr":30,"ip":0,"op":55,"w":30,"h":30,"nm":"dot notification 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"palette 01","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":0,"s":[80,80,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":13,"s":[110,110,100]},{"t":43,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[11.25,11.25],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":11.25,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.30980399251,0.160784006119,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"palette 01","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"palette 02","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":17.5,"s":[35]},{"t":35,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":0,"s":[60,60,100]},{"t":35,"s":[150,150,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,18],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":18,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.30980399251,0.160784006119,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"palette 02","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file +{"v":"5.5.8","fr":30,"ip":0,"op":55,"w":30,"h":30,"nm":"dot notification 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"palette 01","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":0,"s":[80,80,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":13,"s":[110,110,100]},{"t":43,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[11.25,11.25],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":11.25,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.30980399251,0.160784006119,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"palette 01","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"palette 02","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":17.5,"s":[35]},{"t":35,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":0,"s":[60,60,100]},{"t":35,"s":[150,150,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,18],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":18,"ix":4},"nm":"Trazado de rectángulo 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.30980399251,0.160784006119,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Relleno 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformar"}],"nm":"palette 02","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} diff --git a/app/client/src/ce/utils/serviceWorkerUtils.test.ts b/app/client/src/ce/utils/serviceWorkerUtils.test.ts index 11514036c46d..4160d0fb01cc 100644 --- a/app/client/src/ce/utils/serviceWorkerUtils.test.ts +++ b/app/client/src/ce/utils/serviceWorkerUtils.test.ts @@ -442,6 +442,17 @@ describe("serviceWorkerUtils", () => { const key = prefetchApiService.getRequestKey(request); expect(key).toBe("GET:https://app.appsmith.com/:branchname:main"); }); + + it("should only append branchname header in request key", () => { + const request = new Request("https://app.appsmith.com", { + method: "GET", + }); + request.headers.append("branchname", "main"); + request.headers.append("another-header-key", "another-header-value"); + request.headers.append("Content-Type", "application/json"); + const key = prefetchApiService.getRequestKey(request); + expect(key).toBe("GET:https://app.appsmith.com/:branchname:main"); + }); }); describe("aqcuireFetchMutex", () => { diff --git a/app/client/src/ce/utils/serviceWorkerUtils.ts b/app/client/src/ce/utils/serviceWorkerUtils.ts index 4fc8ba81541a..e78b890cec8f 100644 --- a/app/client/src/ce/utils/serviceWorkerUtils.ts +++ b/app/client/src/ce/utils/serviceWorkerUtils.ts @@ -157,15 +157,23 @@ export class PrefetchApiService { cacheMaxAge = 2 * 60 * 1000; // 2 minutes in milliseconds // Mutex to lock the fetch and cache operation prefetchFetchMutexMap = new Map(); + // Header keys used to create the unique request key + headerKeys = ["branchname"]; constructor() {} // Function to get the request key getRequestKey = (request: Request) => { - const headersKey = Array.from(request.headers.entries()) - .map(([key, value]) => `${key}:${value}`) - .join(","); - return `${request.method}:${request.url}:${headersKey}`; + let requestKey = `${request.method}:${request.url}`; + + this.headerKeys.forEach((headerKey) => { + const headerValue = request.headers.get(headerKey); + if (headerValue) { + requestKey += `:${headerKey}:${headerValue}`; + } + }); + + return requestKey; }; // Function to acquire the fetch mutex for a request diff --git a/app/client/src/components/propertyControls/ButtonListControl.tsx b/app/client/src/components/propertyControls/ButtonListControl.tsx index 9932776b2c71..ea02ab300133 100644 --- a/app/client/src/components/propertyControls/ButtonListControl.tsx +++ b/app/client/src/components/propertyControls/ButtonListControl.tsx @@ -195,7 +195,7 @@ class ButtonListControl extends BaseControl< itemType: isSeparator ? "SEPARATOR" : "BUTTON", isSeparator, isVisible: true, - buttonVariant: "filled", + variant: "filled", }, }; diff --git a/app/client/src/globalStyles/popover.ts b/app/client/src/globalStyles/popover.ts index ac4b52502718..dd725ecba0c9 100644 --- a/app/client/src/globalStyles/popover.ts +++ b/app/client/src/globalStyles/popover.ts @@ -45,4 +45,14 @@ export const PopoverStyles = createGlobalStyle` } } } + .ai-window { + .bp3-popover2 { + & { + border: 1px solid !important; + border-radius: var(--ads-v2-border-radius); + border-color: var(--ads-v2-color-border) !important; + box-shadow: var(--ads-v2-shadow-popovers) !important; + } + } + } `; diff --git a/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx b/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx index 8aca04abe879..ab66050c8cbb 100644 --- a/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx +++ b/app/client/src/layoutSystems/anvil/common/AnvilFlexComponent.tsx @@ -15,6 +15,7 @@ import { Layers } from "constants/Layers"; import { noop } from "utils/AppsmithUtils"; import { convertFlexGrowToFlexBasis } from "../sectionSpaceDistributor/utils/spaceDistributionEditorUtils"; import styles from "./styles.module.css"; +import { AnvilDataAttributes } from "widgets/anvil/constants"; const anvilWidgetStyleProps: CSSProperties = { position: "relative", @@ -44,13 +45,13 @@ export const AnvilFlexComponent = forwardRef( onClick = noop, onClickCapture = noop, widgetId, + widgetName, widgetSize, widgetType, }: AnvilFlexComponentProps, ref: any, ) => { const _className = `${className} ${styles.anvilWidgetWrapper}`; - const widgetConfigProps = useMemo(() => { const widgetConfig: | (Partial & WidgetConfigProps & { type: string }) @@ -99,12 +100,15 @@ export const AnvilFlexComponent = forwardRef( } return data; }, [widgetConfigProps, widgetSize, flexGrow]); - + const testDataAttributes = { + [AnvilDataAttributes.WIDGET_NAME]: widgetName, + }; // Render the Anvil Flex Component using the Flex component from WDS return ( + { if (ref.current) { - ref.current.setAttribute(AnvilDataAttributes.WIDGET_NAME, widgetName); ref.current.setAttribute( AnvilDataAttributes.IS_SELECTED_WIDGET, isSelected ? "true" : "false", diff --git a/app/client/src/layoutSystems/anvil/integrations/sagas/anvilDraggingSagas/anvilDraggingSagas.test.ts b/app/client/src/layoutSystems/anvil/integrations/sagas/anvilDraggingSagas/anvilDraggingSagas.test.ts new file mode 100644 index 000000000000..fb1ffd3ab6b4 --- /dev/null +++ b/app/client/src/layoutSystems/anvil/integrations/sagas/anvilDraggingSagas/anvilDraggingSagas.test.ts @@ -0,0 +1,232 @@ +import { select } from "redux-saga/effects"; +import { addWidgetsSaga, moveWidgetsSaga } from "."; +import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; +import { generateReactKey } from "@shared/dsl/src/migrate/utils"; +import { LayoutComponentTypes } from "layoutSystems/anvil/utils/anvilTypes"; +import { expectSaga } from "redux-saga-test-plan"; +import { getWidgets } from "sagas/selectors"; +import { registerWidgets } from "WidgetProvider/factory/registrationHelper"; +import { SectionWidget } from "widgets/anvil/SectionWidget"; +import { ZoneWidget } from "widgets/anvil/ZoneWidget"; +import { WDSButtonWidget } from "widgets/wds/WDSButtonWidget"; +import { + getCanvasWidth, + getIsAutoLayoutMobileBreakPoint, +} from "selectors/editorSelectors"; +import { getCurrentlyOpenAnvilDetachedWidgets } from "../../modalSelectors"; +import { getDataTree } from "selectors/dataTreeSelectors"; +import { getLayoutSystemType } from "selectors/layoutSystemSelectors"; +import { registerLayoutComponents } from "layoutSystems/anvil/utils/layouts/layoutUtils"; +import { getIsAnvilLayout } from "../../selectors"; +import { selectWidgetInitAction } from "actions/widgetSelectionActions"; +import { SelectionRequestType } from "sagas/WidgetSelectUtils"; +import { WDSModalWidget } from "widgets/wds/WDSModalWidget"; +import { generateMockDataWithTwoSections } from "./mockData.helper"; +import type { AnvilMoveWidgetsPayload } from "../../actions/actionTypes"; +import { + AnvilReduxActionTypes, + type AnvilNewWidgetsPayload, +} from "../../actions/actionTypes"; +import { AnvilDraggedWidgetTypesEnum } from "layoutSystems/anvil/editor/canvasArenas/types"; +import { + FlexLayerAlignment, + ResponsiveBehavior, +} from "layoutSystems/common/utils/constants"; +import { mockAnvilHighlightInfo } from "mocks/mockHighlightInfo"; + +describe("", () => { + beforeAll(() => { + registerLayoutComponents(); + registerWidgets([ + SectionWidget, + ZoneWidget, + WDSButtonWidget, + WDSModalWidget, + ]); + }); + // Successfully adds a new widget to the main canvas + it("should successfully add a new widget to the main canvas", async () => { + const mainCanvasLayoutId = generateReactKey(); + const newWidgetId = generateReactKey(); + const allWidgets: any = { + [MAIN_CONTAINER_WIDGET_ID]: { + widgetName: "Main Container", + widgetId: MAIN_CONTAINER_WIDGET_ID, + children: [], + layout: [ + { + layoutId: mainCanvasLayoutId, + layoutType: LayoutComponentTypes.ALIGNED_LAYOUT_COLUMN, + layout: [], + }, + ], + }, + }; + const payload: AnvilNewWidgetsPayload = { + dragMeta: { + draggedOn: "MAIN_CANVAS", + draggedWidgetTypes: AnvilDraggedWidgetTypesEnum.WIDGETS, + }, + highlight: mockAnvilHighlightInfo({ + alignment: FlexLayerAlignment.Start, + canvasId: MAIN_CONTAINER_WIDGET_ID, + layoutId: mainCanvasLayoutId, + layoutOrder: [mainCanvasLayoutId], + }), + newWidget: { + width: 100, + height: 50, + newWidgetId, + type: "WDS_BUTTON_WIDGET", + detachFromLayout: false, + }, + }; + const actionPayload = { + type: AnvilReduxActionTypes.ANVIL_ADD_NEW_WIDGET, + payload, + }; + const { effects } = await expectSaga(addWidgetsSaga, actionPayload) + .provide([ + [select(getWidgets), allWidgets], + [select(getCanvasWidth), 100], + [select(getIsAutoLayoutMobileBreakPoint), false], + [select(getCurrentlyOpenAnvilDetachedWidgets), []], + [select(getDataTree), {}], + [select(getLayoutSystemType), "ANVIL"], + [select(getIsAnvilLayout), true], + ]) + .run(); + const widgetSelectPutEffect = effects.put[effects.put.length - 1]; + expect(widgetSelectPutEffect.payload.action).toEqual( + selectWidgetInitAction(SelectionRequestType.Create, [newWidgetId]), + ); + const updateWidgetsPutEffect = effects.put[effects.put.length - 2]; + expect(updateWidgetsPutEffect.payload.action.type).toBe("UPDATE_LAYOUT"); + // check if new widget was added to main canvas by wrapping it in a section and zone + const updatedWidgets = + updateWidgetsPutEffect.payload.action.payload.widgets; + const mainCanvasWidget = updatedWidgets[MAIN_CONTAINER_WIDGET_ID]; + const sectionWidgetId = mainCanvasWidget.children[0]; + const sectionWidget = updatedWidgets[sectionWidgetId]; + const zoneWidgetId = sectionWidget.children[0]; + const zoneWidget = updatedWidgets[zoneWidgetId]; + expect(zoneWidget.children).toContain(newWidgetId); + }); + it("should successfully add a new modal widget to the main canvas", async () => { + const mainCanvasLayoutId = generateReactKey(); + const newModalId = generateReactKey(); + const allWidgets: any = { + [MAIN_CONTAINER_WIDGET_ID]: { + widgetName: "Main Container", + widgetId: MAIN_CONTAINER_WIDGET_ID, + children: [], + layout: [ + { + layoutId: mainCanvasLayoutId, + layoutType: LayoutComponentTypes.ALIGNED_LAYOUT_COLUMN, + layout: [], + }, + ], + }, + }; + const payload: AnvilNewWidgetsPayload = { + dragMeta: { + draggedOn: "MAIN_CANVAS", + draggedWidgetTypes: AnvilDraggedWidgetTypesEnum.WIDGETS, + }, + highlight: mockAnvilHighlightInfo({ + alignment: FlexLayerAlignment.Start, + canvasId: MAIN_CONTAINER_WIDGET_ID, + layoutId: mainCanvasLayoutId, + layoutOrder: [mainCanvasLayoutId], + }), + newWidget: { + width: 100, + height: 50, + newWidgetId: newModalId, + type: "WDS_MODAL_WIDGET", + detachFromLayout: true, + }, + }; + const actionPayload = { + type: AnvilReduxActionTypes.ANVIL_ADD_NEW_WIDGET, + payload, + }; + + const { effects } = await expectSaga(addWidgetsSaga, actionPayload) + .provide([ + [select(getWidgets), allWidgets], + [select(getCanvasWidth), 100], + [select(getIsAutoLayoutMobileBreakPoint), false], + [select(getCurrentlyOpenAnvilDetachedWidgets), []], + [select(getDataTree), {}], + [select(getLayoutSystemType), "ANVIL"], + [select(getIsAnvilLayout), true], + ]) + .run(); + const widgetSelectPutEffect = effects.put[effects.put.length - 1]; + expect(widgetSelectPutEffect.payload.action).toEqual( + selectWidgetInitAction(SelectionRequestType.Create, [newModalId]), + ); + const updateWidgetsPutEffect = effects.put[effects.put.length - 2]; + expect(updateWidgetsPutEffect.payload.action.type).toBe("UPDATE_LAYOUT"); + // check if new widget was added to main canvas by wrapping it in a section and zone + const updatedWidgets = + updateWidgetsPutEffect.payload.action.payload.widgets; + const mainCanvasWidget = updatedWidgets[MAIN_CONTAINER_WIDGET_ID]; + const modalWidgetId = mainCanvasWidget.children[0]; + expect(modalWidgetId).toContain(newModalId); + }); + + it("should successfully move widget to the main canvas", async () => { + const { allWidgets, mainCanvasLayoutId, section1Id, section2Id } = + generateMockDataWithTwoSections(); + const payload: AnvilMoveWidgetsPayload = { + dragMeta: { + draggedOn: "MAIN_CANVAS", + draggedWidgetTypes: AnvilDraggedWidgetTypesEnum.SECTION, + }, + movedWidgets: [ + { + widgetId: section2Id, + type: "SECTION_WIDGET", + parentId: MAIN_CONTAINER_WIDGET_ID, + responsiveBehavior: ResponsiveBehavior.Fill, + }, + ], + highlight: mockAnvilHighlightInfo({ + alignment: FlexLayerAlignment.Start, + rowIndex: 0, + canvasId: MAIN_CONTAINER_WIDGET_ID, + layoutId: mainCanvasLayoutId, + layoutOrder: [mainCanvasLayoutId], + }), + }; + const actionPayload = { + type: AnvilReduxActionTypes.ANVIL_MOVE_WIDGET, + payload, + }; + const { effects } = await expectSaga(moveWidgetsSaga, actionPayload) + .provide([ + [select(getWidgets), allWidgets], + [select(getCanvasWidth), 100], + [select(getIsAutoLayoutMobileBreakPoint), false], + [select(getCurrentlyOpenAnvilDetachedWidgets), []], + [select(getDataTree), {}], + [select(getLayoutSystemType), "ANVIL"], + [select(getIsAnvilLayout), true], + ]) + .run(); + const updateWidgetsPutEffect = effects.put[effects.put.length - 1]; + expect(updateWidgetsPutEffect.payload.action.type).toBe("UPDATE_LAYOUT"); + // expect section2 to be moved to the first position in layout + const updatedWidgets = + updateWidgetsPutEffect.payload.action.payload.widgets; + const mainCanvasWidget = updatedWidgets[MAIN_CONTAINER_WIDGET_ID]; + const mainCanvasLayout = mainCanvasWidget.layout[0]; + const firstWidgetRow = mainCanvasLayout.layout[0]; + const secondWidgetRow = mainCanvasLayout.layout[1]; + expect(firstWidgetRow.layout[0].widgetId).toBe(section2Id); + expect(secondWidgetRow.layout[0].widgetId).toBe(section1Id); + }); +}); diff --git a/app/client/src/layoutSystems/anvil/integrations/sagas/anvilDraggingSagas.ts b/app/client/src/layoutSystems/anvil/integrations/sagas/anvilDraggingSagas/index.ts similarity index 96% rename from app/client/src/layoutSystems/anvil/integrations/sagas/anvilDraggingSagas.ts rename to app/client/src/layoutSystems/anvil/integrations/sagas/anvilDraggingSagas/index.ts index fbaea2af140b..b8a8581ca605 100644 --- a/app/client/src/layoutSystems/anvil/integrations/sagas/anvilDraggingSagas.ts +++ b/app/client/src/layoutSystems/anvil/integrations/sagas/anvilDraggingSagas/index.ts @@ -10,12 +10,12 @@ import type { WidgetLayoutProps, } from "layoutSystems/anvil/utils/anvilTypes"; import { getWidget, getWidgets } from "sagas/selectors"; -import { addWidgetsToPreset } from "../../utils/layouts/update/additionUtils"; +import { addWidgetsToPreset } from "../../../utils/layouts/update/additionUtils"; import type { AnvilMoveWidgetsPayload, AnvilNewWidgetsPayload, -} from "../actions/actionTypes"; -import { AnvilReduxActionTypes } from "../actions/actionTypes"; +} from "../../actions/actionTypes"; +import { AnvilReduxActionTypes } from "../../actions/actionTypes"; import { generateDefaultLayoutPreset } from "layoutSystems/anvil/layoutComponents/presets/DefaultLayoutPreset"; import { selectWidgetInitAction } from "actions/widgetSelectionActions"; import { SelectionRequestType } from "sagas/WidgetSelectUtils"; @@ -39,7 +39,7 @@ import { addNewWidgetToDsl, getCreateWidgetPayload, } from "layoutSystems/anvil/utils/widgetAdditionUtils"; -import { updateAndSaveAnvilLayout } from "../../utils/anvilChecksUtils"; +import { updateAndSaveAnvilLayout } from "../../../utils/anvilChecksUtils"; import { moveWidgetsToZone } from "layoutSystems/anvil/utils/layouts/update/zoneUtils"; // Function to retrieve highlighting information for the last row in the main canvas layout @@ -201,7 +201,9 @@ export function* addNewChildToDSL( } // function to handle the addition of new widgets to the Anvil layout -function* addWidgetsSaga(actionPayload: ReduxAction) { +export function* addWidgetsSaga( + actionPayload: ReduxAction, +) { try { const start = performance.now(); @@ -297,7 +299,9 @@ function* addWidgetToGenericLayout( * Remove widgets from current parents and layouts. * Add to new parent and layout. */ -function* moveWidgetsSaga(actionPayload: ReduxAction) { +export function* moveWidgetsSaga( + actionPayload: ReduxAction, +) { try { const start = performance.now(); const { diff --git a/app/client/src/layoutSystems/anvil/integrations/sagas/anvilDraggingSagas/mockData.helper.ts b/app/client/src/layoutSystems/anvil/integrations/sagas/anvilDraggingSagas/mockData.helper.ts new file mode 100644 index 000000000000..559884b8a082 --- /dev/null +++ b/app/client/src/layoutSystems/anvil/integrations/sagas/anvilDraggingSagas/mockData.helper.ts @@ -0,0 +1,113 @@ +import { generateReactKey } from "@shared/dsl/src/migrate/utils"; +import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; +import { LayoutComponentTypes } from "layoutSystems/anvil/utils/anvilTypes"; +import { FlexLayerAlignment } from "layoutSystems/common/utils/constants"; + +export const generateMockDataWithTwoSections = () => { + const mainCanvasLayoutId = generateReactKey(); + const section1Id = generateReactKey(); + const section2Id = generateReactKey(); + const zone1Id = generateReactKey(); + const zone2Id = generateReactKey(); + const section1Layout = { + layoutType: LayoutComponentTypes.SECTION, + layout: [ + { + widgetId: zone1Id, + alignment: FlexLayerAlignment.Start, + widgetType: "ZONE_WIDGET", + }, + ], + }; + const section2Layout = { + layoutType: LayoutComponentTypes.SECTION, + layout: [ + { + widgetId: zone2Id, + alignment: FlexLayerAlignment.Start, + widgetType: "ZONE_WIDGET", + }, + ], + }; + const allWidgets: any = { + [MAIN_CONTAINER_WIDGET_ID]: { + widgetName: "Main Container", + widgetId: MAIN_CONTAINER_WIDGET_ID, + children: [section1Id, section2Id], + layout: [ + { + layoutId: mainCanvasLayoutId, + layoutType: LayoutComponentTypes.ALIGNED_LAYOUT_COLUMN, + childTemplate: { + insertChild: true, + layoutId: "", + layoutType: LayoutComponentTypes.WIDGET_ROW, + layout: [], + }, + layout: [ + { + layoutType: LayoutComponentTypes.WIDGET_ROW, + layout: [ + { + widgetId: section1Id, + alignment: FlexLayerAlignment.Start, + widgetType: "SECTION_WIDGET", + }, + ], + }, + { + layoutType: LayoutComponentTypes.WIDGET_ROW, + layout: [ + { + widgetId: section2Id, + alignment: FlexLayerAlignment.Start, + widgetType: "SECTION_WIDGET", + }, + ], + }, + ], + }, + ], + }, + [section1Id]: { + widgetName: "Section 1", + type: "SECTION_WIDGET", + widgetId: section1Id, + children: [zone1Id], + layout: [section1Layout], + zoneCount: 1, + parentId: MAIN_CONTAINER_WIDGET_ID, + }, + [section2Id]: { + widgetName: "Section 2", + type: "SECTION_WIDGET", + widgetId: section2Id, + children: [zone2Id], + layout: [section2Layout], + zoneCount: 1, + parentId: MAIN_CONTAINER_WIDGET_ID, + }, + [zone1Id]: { + widgetName: "Zone 1", + type: "ZONE_WIDGET", + widgetId: zone1Id, + children: [], + parentId: section1Id, + }, + [zone2Id]: { + widgetName: "Zone 2", + type: "ZONE_WIDGET", + widgetId: zone2Id, + children: [], + parentId: section2Id, + }, + }; + return { + allWidgets, + mainCanvasLayoutId, + section1Id, + section2Id, + zone1Id, + zone2Id, + }; +}; diff --git a/app/client/src/layoutSystems/anvil/viewer/canvas/AnvilDetachedWidgets.tsx b/app/client/src/layoutSystems/anvil/viewer/canvas/AnvilDetachedWidgets.tsx index 650cde35595e..09e5c1d138ff 100644 --- a/app/client/src/layoutSystems/anvil/viewer/canvas/AnvilDetachedWidgets.tsx +++ b/app/client/src/layoutSystems/anvil/viewer/canvas/AnvilDetachedWidgets.tsx @@ -1,6 +1,5 @@ import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; import { useDetachedChildren } from "layoutSystems/anvil/common/hooks/detachedWidgetHooks"; -import { getAnvilWidgetDOMId } from "layoutSystems/common/utils/LayoutElementPositionsObserver/utils"; import { renderChildWidget } from "layoutSystems/common/utils/canvasUtils"; import React from "react"; import { useSelector } from "react-redux"; @@ -15,9 +14,7 @@ export const AnvilDetachedWidgets = () => { {detachedChildren.map((child) => renderChildWidget({ childWidgetData: child as WidgetProps, - defaultWidgetProps: { - className: `${getAnvilWidgetDOMId(child.widgetId)}`, - }, + defaultWidgetProps: {}, noPad: false, // Adding these properties as the type insists on providing this // while it is not required for detached children diff --git a/app/client/src/pages/AdminSettings/FormGroup/CopyUrlForm.tsx b/app/client/src/pages/AdminSettings/FormGroup/CopyUrlForm.tsx index 0aee861f107f..1e39e8c4dd3a 100644 --- a/app/client/src/pages/AdminSettings/FormGroup/CopyUrlForm.tsx +++ b/app/client/src/pages/AdminSettings/FormGroup/CopyUrlForm.tsx @@ -7,6 +7,8 @@ import { Icon, Input, Text, toast, Tooltip } from "design-system"; export const BodyContainer = styled.div` width: 100%; padding: 0 0 16px; + .ads-v2-input__input-section-input, + .ads-v2-input__input-section-icon[data-has-onclick="true"], .ads-v2-input__input-section-icon[data-has-onclick="true"] * { cursor: pointer !important; } diff --git a/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx b/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx index a5d8592d14e8..a6afacbfcc26 100644 --- a/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx +++ b/app/client/src/pages/Editor/Explorer/EntityExplorer.tsx @@ -49,7 +49,7 @@ const NoResult = styled(NonIdealState)` svg { height: 52px; width: 144px; - } + } div { diff --git a/app/client/src/pages/Editor/gitSync/ReconnectDatasourceModal.tsx b/app/client/src/pages/Editor/gitSync/ReconnectDatasourceModal.tsx index e832a27cdf0d..11beaa2dfaeb 100644 --- a/app/client/src/pages/Editor/gitSync/ReconnectDatasourceModal.tsx +++ b/app/client/src/pages/Editor/gitSync/ReconnectDatasourceModal.tsx @@ -577,7 +577,7 @@ function ReconnectDatasourceModal() { return ( { : RenderModes.PAGE; }; +export const getIsViewMode = (state: AppState) => + state.entities.app.mode === APP_MODE.PUBLISHED; + export const getViewModePageList = createSelector( getPageList, getCurrentPageId, diff --git a/app/client/src/utils/BrandingUtils.ts b/app/client/src/utils/BrandingUtils.ts index eef24cdd4dcf..53169ccee35d 100644 --- a/app/client/src/utils/BrandingUtils.ts +++ b/app/client/src/utils/BrandingUtils.ts @@ -202,3 +202,16 @@ export const faivconImageValidator = ( callback && callback(e); }; }; + +// NOTE: the reason why the json parsing is out of selector is we don't +// want to do the parsing everytime selector is called +let cachedTenantConfigParsed = {}; +const cachedTenantConfig = localStorage.getItem("tenantConfig"); + +try { + if (cachedTenantConfig) { + cachedTenantConfigParsed = JSON.parse(cachedTenantConfig); + } +} catch (e) {} + +export { cachedTenantConfigParsed }; diff --git a/app/client/src/widgets/ButtonGroupWidget/component/index.tsx b/app/client/src/widgets/ButtonGroupWidget/component/index.tsx index c165962359c1..0ff9ccb4182d 100644 --- a/app/client/src/widgets/ButtonGroupWidget/component/index.tsx +++ b/app/client/src/widgets/ButtonGroupWidget/component/index.tsx @@ -40,6 +40,7 @@ interface ButtonData { label?: string; iconName?: string; } + // Extract props influencing to width change const getButtonData = ( groupButtons: Record, diff --git a/app/client/src/widgets/anvil/constants.ts b/app/client/src/widgets/anvil/constants.ts index 1edc90a76760..c60988dd4bf6 100644 --- a/app/client/src/widgets/anvil/constants.ts +++ b/app/client/src/widgets/anvil/constants.ts @@ -13,6 +13,7 @@ export enum Elevations { * The data attribute that will be used to identify the anvil widget name in the DOM. */ export const AnvilDataAttributes = { + MODAL_SIZE: "data-size", WIDGET_NAME: "data-widget-name", IS_SELECTED_WIDGET: "data-selected", }; diff --git a/app/client/src/widgets/wds/WDSBaseInputWidget/config/contentConfig.ts b/app/client/src/widgets/wds/WDSBaseInputWidget/config/contentConfig.ts index 491e069a3ad5..25aaeabd6e82 100644 --- a/app/client/src/widgets/wds/WDSBaseInputWidget/config/contentConfig.ts +++ b/app/client/src/widgets/wds/WDSBaseInputWidget/config/contentConfig.ts @@ -27,7 +27,7 @@ export const propertyPaneContentConfig = [ propertyName: "regex", label: "Regex", controlType: "INPUT_TEXT", - placeholderText: "^\\w+@[a-zA-Z_]+?\\.[a-zA-Z]{2,3}$", + placeholderText: "^\\w+@[a-zA-Z_]$", isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.REGEX }, @@ -37,7 +37,7 @@ export const propertyPaneContentConfig = [ propertyName: "validation", label: "Valid", controlType: "INPUT_TEXT", - placeholderText: "{{ Input1.text.length > 0 }}", + placeholderText: "{{ Input1.isValid }}", isBindProperty: true, isTriggerProperty: false, validation: { @@ -68,8 +68,7 @@ export const propertyPaneContentConfig = [ propertyName: "tooltip", label: "Tooltip", controlType: "INPUT_TEXT", - placeholderText: - "The tooltip may include relevant information or instructions", + placeholderText: "Tooltips show contextual help", isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.TEXT }, diff --git a/app/client/src/widgets/wds/WDSModalWidget/widget/index.tsx b/app/client/src/widgets/wds/WDSModalWidget/widget/index.tsx index b42c6fc5f8fa..99aa94d8cf4f 100644 --- a/app/client/src/widgets/wds/WDSModalWidget/widget/index.tsx +++ b/app/client/src/widgets/wds/WDSModalWidget/widget/index.tsx @@ -24,6 +24,9 @@ import { call } from "redux-saga/effects"; import { pasteWidgetsIntoMainCanvas } from "layoutSystems/anvil/utils/paste/mainCanvasPasteUtils"; import { ModalLayoutProvider } from "layoutSystems/anvil/layoutComponents/ModalLayoutProvider"; import styles from "./styles.module.css"; +import { getAnvilWidgetDOMId } from "layoutSystems/common/utils/LayoutElementPositionsObserver/utils"; +import { widgetTypeClassname } from "widgets/WidgetUtils"; +import { AnvilDataAttributes } from "widgets/anvil/constants"; class WDSModalWidget extends BaseWidget { static type = "WDS_MODAL_WIDGET"; @@ -85,7 +88,7 @@ class WDSModalWidget extends BaseWidget { } onModalClose = () => { - if (this.props.onClose) { + if (!this.props.disableWidgetInteraction && this.props.onClose) { super.executeAction({ triggerPropertyName: "onClose", dynamicString: this.props.onClose, @@ -121,24 +124,35 @@ class WDSModalWidget extends BaseWidget { return this.props.isVisible; }; + getModalClassNames = () => { + const { disableWidgetInteraction, type, widgetId } = this.props; + return `${getAnvilWidgetDOMId(widgetId)} ${widgetTypeClassname(type)} ${ + disableWidgetInteraction ? styles.disableModalInteraction : "" + }`; + }; + getWidgetView() { const closeText = this.props.cancelButtonText || "Cancel"; const submitText = this.props.showSubmitButton ? this.props.submitButtonText || "Submit" : undefined; - const contentClassName = `${this.props.className} ${ - this.props.disableWidgetInteraction ? styles.disableModalInteraction : "" - }`; + const modalClassNames = `${this.getModalClassNames()}`; return ( this.setState({ isVisible: val })} - size={this.props.size} > {this.state.isVisible && ( - + {this.props.showHeader && ( { iconAlign, iconName, label, - onClick, value, valueChange, valueChangeColor, @@ -22,7 +21,6 @@ export const StatsComponent = (props: StatsComponentProps) => { direction={iconAlign === "end" ? "row-reverse" : "row"} gap="spacing-2" isInner - onClick={onClick} > {iconName && iconName !== "(none)" && ( diff --git a/app/client/src/widgets/wds/WDSStatsWidget/config/propertyPaneConfig/contentConfig.ts b/app/client/src/widgets/wds/WDSStatsWidget/config/propertyPaneConfig/contentConfig.ts index 145b237ce094..1feee935bd5f 100644 --- a/app/client/src/widgets/wds/WDSStatsWidget/config/propertyPaneConfig/contentConfig.ts +++ b/app/client/src/widgets/wds/WDSStatsWidget/config/propertyPaneConfig/contentConfig.ts @@ -159,18 +159,4 @@ export const propertyPaneContentConfig = [ }, ], }, - { - sectionName: "Events", - children: [ - { - helpText: "when the button is clicked", - propertyName: "onClick", - label: "onClick", - controlType: "ACTION_SELECTOR", - isJSConvertible: true, - isBindProperty: true, - isTriggerProperty: true, - }, - ], - }, ]; diff --git a/app/client/src/widgets/wds/WDSStatsWidget/widget/index.tsx b/app/client/src/widgets/wds/WDSStatsWidget/widget/index.tsx index 85a32d9cbf11..5dafafb8b976 100644 --- a/app/client/src/widgets/wds/WDSStatsWidget/widget/index.tsx +++ b/app/client/src/widgets/wds/WDSStatsWidget/widget/index.tsx @@ -7,7 +7,6 @@ import * as config from "../config"; import { StatsComponent } from "../component"; import type { StatsWidgetProps } from "./types"; import type { AnvilConfig } from "WidgetProvider/constants"; -import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import { Elevations } from "widgets/anvil/constants"; import { ContainerComponent } from "widgets/anvil/Container"; @@ -51,32 +50,13 @@ class WDSStatsWidget extends BaseWidget { } getWidgetView() { - const onClick = () => { - if (this.props.onClick) { - this.setState({ isLoading: true }); - - super.executeAction({ - triggerPropertyName: "onClick", - dynamicString: this.props.onClick, - event: { - type: EventType.ON_CLICK, - }, - }); - - return; - } - }; - return ( - + ); } diff --git a/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/autocompleteConfig.ts b/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/autocompleteConfig.ts index 8691188452fa..e0faa817381e 100644 --- a/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/autocompleteConfig.ts +++ b/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/autocompleteConfig.ts @@ -6,7 +6,6 @@ export const autocompleteConfig = { "!url": "https://docs.appsmith.com/widget-reference/switch-group", isVisible: DefaultAutocompleteDefinitions.isVisible, isDisabled: "bool", - isValid: "bool", options: "[$__dropdownOption__$]", selectedValues: "[string]", }; diff --git a/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/defaultsConfig.ts b/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/defaultsConfig.ts index 07bbf0693c7f..ab11c53dfe2b 100644 --- a/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/defaultsConfig.ts +++ b/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/defaultsConfig.ts @@ -10,7 +10,6 @@ export const defaultsConfig = { ], defaultSelectedValues: ["BLUE", "RED"], isDisabled: false, - isRequired: false, isVisible: true, labelPosition: "end", label: "Label", diff --git a/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/propertyPaneConfig/contentConfig.ts b/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/propertyPaneConfig/contentConfig.ts index ca7a89e6d2ee..e7176e538703 100644 --- a/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/propertyPaneConfig/contentConfig.ts +++ b/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/propertyPaneConfig/contentConfig.ts @@ -130,23 +130,6 @@ export const propertyPaneContentConfig = [ }, ], }, - { - sectionName: "Validations", - children: [ - { - propertyName: "isRequired", - label: "Required", - helpText: "Makes input to the widget mandatory", - controlType: "SWITCH", - isJSConvertible: true, - isBindProperty: true, - isTriggerProperty: false, - validation: { - type: ValidationTypes.BOOLEAN, - }, - }, - ], - }, { sectionName: "General", children: [ diff --git a/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/settersConfig.ts b/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/settersConfig.ts index 2a036bc6b02e..83c1e7296c64 100644 --- a/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/settersConfig.ts +++ b/app/client/src/widgets/wds/WDSSwitchGroupWidget/config/settersConfig.ts @@ -8,10 +8,6 @@ export const settersConfig = { path: "isDisabled", type: "boolean", }, - setRequired: { - path: "isRequired", - type: "boolean", - }, setSelectedOptions: { path: "defaultSelectedValues", type: "array", diff --git a/app/client/src/widgets/wds/WDSSwitchGroupWidget/widget/helpers.ts b/app/client/src/widgets/wds/WDSSwitchGroupWidget/widget/helpers.ts deleted file mode 100644 index 0e1603890e3e..000000000000 --- a/app/client/src/widgets/wds/WDSSwitchGroupWidget/widget/helpers.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Validation } from "widgets/wds/WDSInputWidget/widget/types"; - -import type { SwitchGroupWidgetProps } from "./types"; - -export function validateInput(props: SwitchGroupWidgetProps): Validation { - if (!props.isValid && props.isDirty) { - return { - validationStatus: "invalid", - errorMessage: "Please select an option", - }; - } - - return { - validationStatus: "valid", - errorMessage: "", - }; -} diff --git a/app/client/src/widgets/wds/WDSSwitchGroupWidget/widget/index.tsx b/app/client/src/widgets/wds/WDSSwitchGroupWidget/widget/index.tsx index 5c0edb59e35b..4a9faf072e52 100644 --- a/app/client/src/widgets/wds/WDSSwitchGroupWidget/widget/index.tsx +++ b/app/client/src/widgets/wds/WDSSwitchGroupWidget/widget/index.tsx @@ -5,7 +5,6 @@ import type { WidgetState } from "widgets/BaseWidget"; import type { SetterConfig } from "entities/AppTheming"; import type { AnvilConfig } from "WidgetProvider/constants"; import { Switch, ToggleGroup } from "@design-system/widgets"; -import type { DerivedPropertiesMap } from "WidgetProvider/factory"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import { @@ -18,7 +17,6 @@ import { settersConfig, methodsConfig, } from "../config"; -import { validateInput } from "./helpers"; import type { SwitchGroupWidgetProps, OptionProps } from "./types"; class WDSSwitchGroupWidget extends BaseWidget< @@ -65,13 +63,6 @@ class WDSSwitchGroupWidget extends BaseWidget< }; } - static getDerivedPropertiesMap(): DerivedPropertiesMap { - return { - value: `{{this.selectedValues}}`, - isValid: `{{ this.isRequired ? !!this.selectedValues.length : true }}`, - }; - } - static getMetaPropertiesMap(): Record { return { selectedValues: undefined, @@ -118,14 +109,10 @@ class WDSSwitchGroupWidget extends BaseWidget< ...rest } = this.props; - const validation = validateInput(this.props); - return ( [] = - diff( - oldUnEvalTreeWithStringifiedJSFunctions, - localUnEvalTreeWithStringifiedJSFunctions, - ) || []; + const differences: Diff[] = profileFn( + "unEvalTreeWithStringifiedJSFunctionsDiff", + undefined, + webworkerTelemetry, + () => + diff( + oldUnEvalTreeWithStringifiedJSFunctions, + localUnEvalTreeWithStringifiedJSFunctions, + ) || [], + ); // Since eval tree is listening to possible events that don't cause differences // We want to check if no diffs are present and bail out early if (differences.length === 0) { @@ -628,7 +633,10 @@ export default class DataTreeEvaluator { const updateDependencyStartTime = performance.now(); // TODO => Optimize using dataTree diff - this.allKeys = getAllPaths(localUnEvalTreeWithStringifiedJSFunctions); + + this.allKeys = profileFn("getAllPaths", undefined, webworkerTelemetry, () => + getAllPaths(localUnEvalTreeWithStringifiedJSFunctions), + ); // Find all the paths that have changed as part of the difference and update the // global dependency map if an existing dynamic binding has now become legal const { diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 5036011a6334..35a1d672d59f 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -32896,9 +32896,9 @@ __metadata: linkType: hard "tinymce@npm:^7.0.0 || ^6.0.0 || ^5.5.1": - version: 7.2.0 - resolution: "tinymce@npm:7.2.0" - checksum: 75af9f1e950938e568b252b68b00e4265a395853f984011a9e269e67a044ac06076c3175f70cd44042452eafcc34273d807bcaa558c579024926b1f28dcb2ba8 + version: 7.0.0 + resolution: "tinymce@npm:7.0.0" + checksum: 1f2dc383c1c6f4efb8d3f4223577a8f2b450f6cc62b2861c80467495d68c2453cbd8b646416a0376282fb0b6341c194c698503149037347133b7b936fb7c717b languageName: node linkType: hard diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageDTO.java index fccb245d80ea..3b61de875c91 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageDTO.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageDTO.java @@ -78,8 +78,7 @@ public class PageDTO { @JsonView(Views.Public.class) DefaultResources defaultResources; - // TODO: get this clarified for GIT annotation - @JsonView({Views.Public.class, Git.class}) + @JsonView(Views.Public.class) Map> dependencyMap; public void sanitiseToExportDBObject() { diff --git a/deploy/docker/base.dockerfile b/deploy/docker/base.dockerfile index 6ce8453cc854..e80efeb95ad7 100644 --- a/deploy/docker/base.dockerfile +++ b/deploy/docker/base.dockerfile @@ -27,12 +27,12 @@ RUN set -o xtrace \ && echo "deb http://apt.postgresql.org/pub/repos/apt $(grep CODENAME /etc/lsb-release | cut -d= -f2)-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list \ && curl --silent --show-error --location https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \ && apt update \ - && apt-get install --no-install-recommends --yes mongodb-org redis postgresql-13 \ + && apt-get install --no-install-recommends --yes mongodb-org redis postgresql-13 postgresql-14 \ # Create a symlink to the current version of PostgreSQL && ln -s /usr/lib/postgresql/13 /usr/lib/postgresql/current \ && apt-get clean -ENV PATH="/usr/lib/postgresql/13/bin:${PATH}" +ENV PATH="/usr/lib/postgresql/14/bin:${PATH}" # Install Java RUN set -o xtrace \ diff --git a/deploy/docker/fs/opt/appsmith/entrypoint.sh b/deploy/docker/fs/opt/appsmith/entrypoint.sh index 9b1fddb74792..c0338a67e47c 100644 --- a/deploy/docker/fs/opt/appsmith/entrypoint.sh +++ b/deploy/docker/fs/opt/appsmith/entrypoint.sh @@ -425,7 +425,9 @@ init_postgres() { # Postgres does not allow it's server to be run with super user access, we use user postgres and the file system owner also needs to be the same user postgres chown -R postgres:postgres "$POSTGRES_DB_PATH" "$TMP/pg-runtime" - if [[ ! -e "$POSTGRES_DB_PATH/PG_VERSION" ]]; then + if [[ -e "$POSTGRES_DB_PATH/PG_VERSION" ]]; then + /opt/appsmith/pg-upgrade.sh + else tlog "Initializing local Postgres data folder" su postgres -c "env PATH='$PATH' initdb -D $POSTGRES_DB_PATH" fi diff --git a/deploy/docker/fs/opt/appsmith/pg-upgrade.sh b/deploy/docker/fs/opt/appsmith/pg-upgrade.sh new file mode 100644 index 000000000000..e646276c0cf3 --- /dev/null +++ b/deploy/docker/fs/opt/appsmith/pg-upgrade.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +set -o errexit +set -o nounset + +# This script will upgrade Postgres to the "current" version of Postgres, if needed. + +# Assumptions: +# 1. Postgres is currently not running. The caller of this script ensures that _no_ version of Postgres server is currently running. +# 2. The newest version of Postgres we want to use, is already installed. +# 3. Postgres is installed via apt package manager only. + +# Contract: +# 1. Don't install old version of Postgres, if it's already installed. +# 2. Use absolute paths to all Postgres executables, don't rely on any of them to be coming from "\$PATH". +# 3. Be idempotent across versions. +# 4. When we can't proceed due to any exceptional scenarios, communicate clearly. +# 5. Mark old/stale/deprecated data with a date, so it can be deleted with confidence later. + +# Check if any Postgres server is running +if pgrep -x "postgres" > /dev/null; then + echo "Error: A Postgres server is currently running. Please stop it before proceeding with the upgrade." + exit 1 +fi + +postgres_path=/usr/lib/postgresql + +pg_data_dir=/appsmith-stacks/data/postgres/main + +old_version="" +if [[ -f "$pg_data_dir/PG_VERSION" ]]; then + old_version="$(cat "$pg_data_dir/PG_VERSION")" +fi + +if [[ -z "$old_version" ]]; then + tlog "No existing Postgres data found, not upgrading anything." >&2 + exit +fi + +if [[ -f "$pg_data_dir/postmaster.pid" ]]; then + tlog "Previous Postgres was not shutdown cleanly. Please start and stop Postgres $old_version properly with 'supervisorctl' only." >&2 + exit 1 +fi + +top_available_version="$(postgres --version | grep -o '[[:digit:]]\+' | head -1)" + +declare -a to_uninstall +to_uninstall=() + +# 13 to 14 +if [[ "$old_version" == 13 && "$top_available_version" > "$old_version" ]]; then + if [[ ! -e "$postgres_path/$old_version" ]]; then + apt-get update + apt-get install --yes "postgresql-$old_version" + to_uninstall+=("postgresql-$old_version") + fi + + new_version="$((old_version + 1))" + new_data_dir="$pg_data_dir-$new_version" + + # `pg_upgrade` writes log to current folder. So change to a temp folder first. + rm -rf "$TMP/pg_upgrade" "$new_data_dir" + mkdir -p "$TMP/pg_upgrade" "$new_data_dir" + chown -R postgres "$TMP/pg_upgrade" "$new_data_dir" + cd "$TMP/pg_upgrade" + + # Required by the temporary Postgres server started by `pg_upgrade`. + chown postgres /etc/ssl/private/ssl-cert-snakeoil.key + chmod 0600 /etc/ssl/private/ssl-cert-snakeoil.key + + su postgres --command " + set -o errexit + set -o xtrace + '$postgres_path/$new_version/bin/initdb' --pgdata='$new_data_dir' + '$postgres_path/$new_version/bin/pg_upgrade' \ + --old-datadir='$pg_data_dir' \ + --new-datadir='$new_data_dir' \ + --old-bindir='$postgres_path/$old_version/bin' \ + --new-bindir='$postgres_path/$new_version/bin' + " + + date -u '+%FT%T.%3NZ' > "$pg_data_dir/deprecated-on.txt" + mv -v "$pg_data_dir" "$pg_data_dir-$old_version" + mv -v "$new_data_dir" "$pg_data_dir" + + # Dangerous generated script that deletes the now updated data folder. + rm -fv "$TMP/pg_upgrade/delete_old_cluster.sh" +fi + +if [[ -n "${#to_uninstall[@]}" ]]; then + apt-get purge "${to_uninstall[@]}" + apt-get clean +fi + +echo "== Fin ==" diff --git a/deploy/docker/tests/pg-upgrade/run.sh b/deploy/docker/tests/pg-upgrade/run.sh new file mode 100755 index 000000000000..10de912c0809 --- /dev/null +++ b/deploy/docker/tests/pg-upgrade/run.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# A script to test Postgres upgrades. WIP. + +set -o errexit +set -o nounset +set -o xtrace + +from_tag=appsmith/appsmith-ce:v1.28 +to_tag=appsmith/appsmith-ce:latest + +container_name=appsmith-pg-upgrade-test +port=20080 + +docker rm -f "$container_name" +docker volume rm --force "$container_name" + +# TODO: Add `--pull always` for images that have a manifest? + +docker volume create "$container_name" +docker run \ + --name "$container_name" \ + --detach \ + --publish "$port":80 \ + --volume "$container_name":/appsmith-stacks \ + "$from_tag" + +wait-for-supervisor() { + while ! docker exec "$container_name" test -e /tmp/appsmith/supervisor.sock; do + sleep 1 + done + sleep 2 +} + +wait-for-supervisor + +docker exec "$container_name" bash -exc ' +supervisorctl status \ + | awk '\''$1 != "postgres" && $1 != "stdout" {print $1}'\'' \ + | xargs supervisorctl stop + +# Insert some sample data +su postgres -c "psql -h 127.0.0.1 -c \" +create table t (id serial, name text); +insert into t values (1, '\''one'\''); +insert into t values (2, '\''two'\''); +insert into t values (3, '\''three'\''); +\"" + +supervisorctl stop postgres + +cat /appsmith-stacks/data/postgres/main/PG_VERSION +' + +docker rm -f "$container_name" + +docker run \ + --name "$container_name" \ + --detach \ + --publish "$port":80 \ + --volume "$container_name":/appsmith-stacks \ + "$to_tag" + +wait-for-supervisor + +status=0 + +if [[ 14 != "$(docker exec "$container_name" cat /appsmith-stacks/data/postgres/main/PG_VERSION)" ]]; then + echo "Version isn't 14" + status=1 +else + sample_table_contents="$(su postgres -c 'psql -h 127.0.0.1 -c "select * from t"')" + expected_contents=' id | name +----+------- + 1 | one + 2 | two + 3 | three +(3 rows)' + if ! diff <(echo "$expected_contents") <(su postgres -c 'psql -h 127.0.0.1 -c "select * from t"'); then + status=1 + echo "Table contents mismatch. Found this:" + su postgres -c 'psql -h 127.0.0.1 -c "select * from t"' + echo "Instead of this:" + echo "$expected_contents" + fi +fi + +docker exec -it "$container_name" bash + +docker rm --force "$container_name" +docker volume rm --force "$container_name" + +exit "$status"