diff --git a/app/client/cypress/e2e/Regression/ClientSide/Binding/JSObjectToListWidget_Spec.ts b/app/client/cypress/e2e/Regression/ClientSide/Binding/JSObjectToListWidget_Spec.ts index 9e40b6520e4c..eedf46751a57 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Binding/JSObjectToListWidget_Spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/Binding/JSObjectToListWidget_Spec.ts @@ -2,6 +2,8 @@ import * as _ from "../../../../support/Objects/ObjectsCore"; import EditorNavigation, { EntityType, } from "../../../../support/Pages/EditorNavigation"; +import BottomPane from "../../../../support/Pages/IDE/BottomPane"; + let valueToTest: any, jsName: any; describe( @@ -17,7 +19,7 @@ describe( _.dataManager.dsValues[_.dataManager.defaultEnviorment].mockApiUrl, ); _.apiPage.RunAPI(); - _.agHelper.GetNClick(_.dataSources._queryResponse("JSON")); + BottomPane.response.switchResponseType("JSON"); _.apiPage.ReadApiResponsebyKey("name"); cy.get("@apiResp").then((value) => { valueToTest = value; diff --git a/app/client/cypress/e2e/Regression/ClientSide/BugTests/Binding_Bug28287_Spec.ts b/app/client/cypress/e2e/Regression/ClientSide/BugTests/Binding_Bug28287_Spec.ts index d5d4c62b71a0..5a38feeaf456 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/BugTests/Binding_Bug28287_Spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/BugTests/Binding_Bug28287_Spec.ts @@ -8,6 +8,7 @@ import { import EditorNavigation, { EntityType, } from "../../../../support/Pages/EditorNavigation"; +import BottomPane from "../../../../support/Pages/IDE/BottomPane"; let dsName: any; let queryName: string; @@ -43,7 +44,11 @@ describe( EditorNavigation.SelectEntityByName(queryName, EntityType.Query); - agHelper.AssertElementVisibility(dataSources._queryResponse("TABLE")); + BottomPane.response.switchToResponseTab(); + + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("TABLE"), + ); }); }); }, 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 92a8ce84c282..f3215412c138 100644 --- a/app/client/cypress/e2e/Regression/ServerSide/OnLoadTests/OnLoadActions_Spec.ts +++ b/app/client/cypress/e2e/Regression/ServerSide/OnLoadTests/OnLoadActions_Spec.ts @@ -13,6 +13,7 @@ import { import EditorNavigation, { EntityType, } from "../../../../support/Pages/EditorNavigation"; +import BottomPane from "../../../../support/Pages/IDE/BottomPane"; describe( "Layout OnLoad Actions tests", @@ -55,7 +56,7 @@ describe( ); apiPage.RunAPI(); - agHelper.GetNClick(dataSources._queryResponse("JSON")); + BottomPane.response.switchResponseType("JSON"); apiPage.CreateAndFillApi( "http://host.docker.internal:5001/v1/favqs/qotd", @@ -64,7 +65,7 @@ describe( ); apiPage.EnterHeader("dependency", "{{RandomUser.data}}"); //via Params tab apiPage.RunAPI(); - agHelper.GetNClick(dataSources._queryResponse("JSON")); + BottomPane.response.switchResponseType("JSON"); apiPage.CreateAndFillApi( "http://host.docker.internal:5001/v1/boredapi/activity", @@ -73,7 +74,7 @@ describe( ); apiPage.EnterHeader("dependency", "{{InspiringQuotes.data.data}}"); apiPage.RunAPI(); - agHelper.GetNClick(dataSources._queryResponse("JSON")); + BottomPane.response.switchResponseType("JSON"); apiPage.CreateAndFillApi( "http://host.docker.internal:5001/v1/genderize/sampledata", @@ -82,7 +83,7 @@ describe( ); apiPage.EnterParams("name", "{{RandomUser.data[0].name}}"); //via Params tab apiPage.RunAPI(); - agHelper.GetNClick(dataSources._queryResponse("JSON")); + BottomPane.response.switchResponseType("JSON"); //Adding dependency in right order matters! EditorNavigation.SelectEntityByName("Image1", EntityType.Widget); @@ -162,7 +163,7 @@ describe( value: "{{RandomUser.data[0].name}}", }); // verifies Bug 10055 apiPage.RunAPI(); - agHelper.GetNClick(dataSources._queryResponse("JSON")); + BottomPane.response.switchResponseType("JSON"); deployMode.DeployApp( locators._widgetInDeployed("textwidget"), diff --git a/app/client/cypress/e2e/Regression/ServerSide/QueryPane/Mongo1_spec.ts b/app/client/cypress/e2e/Regression/ServerSide/QueryPane/Mongo1_spec.ts index 5ed89ec798df..08fe4506112e 100644 --- a/app/client/cypress/e2e/Regression/ServerSide/QueryPane/Mongo1_spec.ts +++ b/app/client/cypress/e2e/Regression/ServerSide/QueryPane/Mongo1_spec.ts @@ -16,6 +16,7 @@ import EditorNavigation, { EntityType, } from "../../../../support/Pages/EditorNavigation"; import PageList from "../../../../support/Pages/PageList"; +import BottomPane from "../../../../support/Pages/IDE/BottomPane"; let dsName: any; @@ -429,8 +430,12 @@ describe( parseInt(JSON.stringify(resObj.response.body.data.body.n)), ).to.eq(3); }); - agHelper.AssertElementVisibility(dataSources._queryResponse("JSON")); - agHelper.AssertElementVisibility(dataSources._queryResponse("RAW")); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("JSON"), + ); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("RAW"), + ); agHelper.ActionContextMenuWithInPane({ action: "Delete", entityType: entityItems.Query, @@ -462,8 +467,12 @@ describe( parseInt(JSON.stringify(resObj.response.body.data.body.nModified)), ).to.eq(0); }); - agHelper.AssertElementVisibility(dataSources._queryResponse("JSON")); - agHelper.AssertElementVisibility(dataSources._queryResponse("RAW")); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("JSON"), + ); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("RAW"), + ); agHelper.ActionContextMenuWithInPane({ action: "Delete", entityType: entityItems.Query, @@ -505,8 +514,12 @@ describe( parseInt(JSON.stringify(resObj.response.body.data.body.nModified)), ).to.eq(2); }); - agHelper.AssertElementVisibility(dataSources._queryResponse("JSON")); - agHelper.AssertElementVisibility(dataSources._queryResponse("RAW")); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("JSON"), + ); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("RAW"), + ); agHelper.ActionContextMenuWithInPane({ action: "Delete", entityType: entityItems.Query, @@ -543,8 +556,12 @@ describe( parseInt(JSON.stringify(resObj.response.body.data.body.nModified)), ).to.eq(1); }); - agHelper.AssertElementVisibility(dataSources._queryResponse("JSON")); - agHelper.AssertElementVisibility(dataSources._queryResponse("RAW")); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("JSON"), + ); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("RAW"), + ); agHelper.ActionContextMenuWithInPane({ action: "Delete", entityType: entityItems.Query, @@ -570,8 +587,12 @@ describe( parseInt(JSON.stringify(resObj.response.body.data.body.n)), ).to.eq(0); }); - agHelper.AssertElementVisibility(dataSources._queryResponse("JSON")); - agHelper.AssertElementVisibility(dataSources._queryResponse("RAW")); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("JSON"), + ); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("RAW"), + ); agHelper.ActionContextMenuWithInPane({ action: "Delete", entityType: entityItems.Query, @@ -599,8 +620,12 @@ describe( parseInt(JSON.stringify(resObj.response.body.data.body.n)), ).to.eq(1); }); - agHelper.AssertElementVisibility(dataSources._queryResponse("JSON")); - agHelper.AssertElementVisibility(dataSources._queryResponse("RAW")); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("JSON"), + ); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("RAW"), + ); agHelper.ActionContextMenuWithInPane({ action: "Delete", entityType: entityItems.Query, @@ -632,8 +657,12 @@ describe( 2, ); }); - agHelper.AssertElementVisibility(dataSources._queryResponse("JSON")); - agHelper.AssertElementVisibility(dataSources._queryResponse("RAW")); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("JSON"), + ); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("RAW"), + ); agHelper.ActionContextMenuWithInPane({ action: "Delete", entityType: entityItems.Query, @@ -653,8 +682,12 @@ describe( 7, ); }); - agHelper.AssertElementVisibility(dataSources._queryResponse("JSON")); - agHelper.AssertElementVisibility(dataSources._queryResponse("RAW")); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("JSON"), + ); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("RAW"), + ); agHelper.ActionContextMenuWithInPane({ action: "Delete", entityType: entityItems.Query, @@ -686,8 +719,12 @@ describe( JSON.parse(JSON.stringify(resObj.response.body.data.body.values[1])), ).to.eql("51e062189c6ae665454e301d"); }); - agHelper.AssertElementVisibility(dataSources._queryResponse("JSON")); - agHelper.AssertElementVisibility(dataSources._queryResponse("RAW")); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("JSON"), + ); + agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("RAW"), + ); agHelper.ActionContextMenuWithInPane({ action: "Delete", entityType: entityItems.Query, diff --git a/app/client/cypress/e2e/Regression/ServerSide/QueryPane/Querypane_Mongo_Spec.js b/app/client/cypress/e2e/Regression/ServerSide/QueryPane/Querypane_Mongo_Spec.js index b3764037d926..f1f7adcb0577 100644 --- a/app/client/cypress/e2e/Regression/ServerSide/QueryPane/Querypane_Mongo_Spec.js +++ b/app/client/cypress/e2e/Regression/ServerSide/QueryPane/Querypane_Mongo_Spec.js @@ -17,6 +17,7 @@ import { apiPage, } from "../../../../support/Objects/ObjectsCore"; import { Widgets } from "../../../../support/Pages/DataSources"; +import BottomPane from "../../../../support/Pages/IDE/BottomPane"; let datasourceName; @@ -70,7 +71,7 @@ describe( dataSources.EnterQuery(`{"find": "listingAndReviews","limit": 10}`); agHelper.FocusElement(locators._codeMirrorTextArea); dataSources.RunQuery(); - dataSources.CheckResponseRecordsCount(10); + BottomPane.response.validateRecordCount(10); cy.deleteQueryUsingContext(); }); @@ -92,7 +93,7 @@ describe( fieldValue: "listingAndReviews", }); dataSources.RunQuery(); - dataSources.CheckResponseRecordsCount(10); + BottomPane.response.validateRecordCount(10); agHelper.EnterValue("{beds : {$lte: 2}}", { propFieldName: "", @@ -100,7 +101,7 @@ describe( inputFieldName: "Query", }); dataSources.RunQuery(); - dataSources.CheckResponseRecordsCount(10); + BottomPane.response.validateRecordCount(10); agHelper.EnterValue("{number_of_reviews: -1}", { propFieldName: "", @@ -108,7 +109,7 @@ describe( inputFieldName: "Sort", }); //sort descending dataSources.RunQuery(); - dataSources.CheckResponseRecordsCount(10); + BottomPane.response.validateRecordCount(10); agHelper.EnterValue("{house_rules: 1, description:1}", { propFieldName: "", @@ -130,7 +131,7 @@ describe( "Response is not as expected for Find commmand with multiple conditions", ); }); - dataSources.CheckResponseRecordsCount(5); + BottomPane.response.validateRecordCount(5); agHelper.EnterValue("2", { propFieldName: "", @@ -144,7 +145,7 @@ describe( "Response is not as expected for Find commmand with multiple conditions", ); }); - dataSources.CheckResponseRecordsCount(5); + BottomPane.response.validateRecordCount(5); cy.deleteQueryUsingContext(); }); @@ -432,7 +433,7 @@ describe( ); dataSources.RunQuery(); - dataSources.CheckResponseRecordsCount(3); + BottomPane.response.validateRecordCount(3); dataSources.AssertTableInVirtuosoList(datasourceName, "NonAsciiTest"); diff --git a/app/client/cypress/e2e/Regression/ServerSide/QueryPane/S3_1_spec.js b/app/client/cypress/e2e/Regression/ServerSide/QueryPane/S3_1_spec.js index 339eb14bb812..2bd496ead58f 100644 --- a/app/client/cypress/e2e/Regression/ServerSide/QueryPane/S3_1_spec.js +++ b/app/client/cypress/e2e/Regression/ServerSide/QueryPane/S3_1_spec.js @@ -1,5 +1,7 @@ /// +import BottomPane from "../../../../support/Pages/IDE/BottomPane"; + const queryLocators = require("../../../../locators/QueryEditor.json"); const generatePage = require("../../../../locators/GeneratePage.json"); const formControls = require("../../../../locators/FormControl.json"); @@ -189,8 +191,8 @@ describe( cy.typeValueNValidate(fileName, formControls.s3ListPrefix); dataSources.RunQuery({ toValidateResponse: false }); - agHelper.GetNClick(dataSources._queryResponse("TABLE")); - agHelper.GetNClick(dataSources._queryResponse("JSON")); + BottomPane.response.switchResponseType("TABLE"); + BottomPane.response.switchResponseType("JSON"); cy.wait("@postExecute").then(({ response }) => { expect(response.body.data.isExecutionSuccess).to.eq(true); diff --git a/app/client/cypress/support/Pages/DataSources.ts b/app/client/cypress/support/Pages/DataSources.ts index b56ad7792f35..059bf5413b88 100644 --- a/app/client/cypress/support/Pages/DataSources.ts +++ b/app/client/cypress/support/Pages/DataSources.ts @@ -11,6 +11,7 @@ import EditorNavigation, { import datasource from "../../locators/DatasourcesEditor.json"; import PageList from "./PageList"; import { anvilLocators } from "./Anvil/Locators"; +import BottomPane from "./IDE/BottomPane"; export const DataSourceKVP = { Postgres: "PostgreSQL", @@ -1137,17 +1138,18 @@ export class DataSources { tableCheck = true, ) { this.RunQuery(); - tableCheck && - this.agHelper.AssertElementVisibility(this._queryResponse("TABLE")); - this.agHelper.AssertElementVisibility(this._queryResponse("JSON")); - this.agHelper.AssertElementVisibility(this._queryResponse("RAW")); - this.CheckResponseRecordsCount(expectedRecordsCount); - } - - public CheckResponseRecordsCount(expectedRecordCount: number) { - this.agHelper.AssertElementVisibility( - this._queryRecordResult(expectedRecordCount), - ); + if (tableCheck) { + this.agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("TABLE"), + ); + this.agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("JSON"), + ); + this.agHelper.AssertElementVisibility( + BottomPane.response.getResponseTypeSelector("RAW"), + ); + } + BottomPane.response.validateRecordCount(expectedRecordsCount); } public CreateDataSource( diff --git a/app/client/cypress/support/Pages/IDE/BottomPane/Response.ts b/app/client/cypress/support/Pages/IDE/BottomPane/Response.ts new file mode 100644 index 000000000000..ca1db0fa7ca1 --- /dev/null +++ b/app/client/cypress/support/Pages/IDE/BottomPane/Response.ts @@ -0,0 +1,21 @@ +class Response { + private ResponseTab = "//button[@data-testid='t--tab-RESPONSE_TAB']"; + + public switchToResponseTab(): void { + cy.xpath(this.ResponseTab).click({ force: true }); + } + + public getResponseTypeSelector(type: string): string { + return `//div[@data-testid='t--response-tab-segmented-control']//span[text()='${type}']`; + } + + public switchResponseType(type: string): void { + this.switchToResponseTab(); + cy.xpath(this.getResponseTypeSelector(type)).click({ force: true }); + } + + // TODO: Implement this method when response UI is ready + public validateRecordCount(count: number): void {} +} + +export { Response }; diff --git a/app/client/cypress/support/Pages/IDE/BottomPane/index.ts b/app/client/cypress/support/Pages/IDE/BottomPane/index.ts new file mode 100644 index 000000000000..055d7812dd5b --- /dev/null +++ b/app/client/cypress/support/Pages/IDE/BottomPane/index.ts @@ -0,0 +1,11 @@ +import { Response } from "./Response"; + +class BottomPane { + public readonly response: Response; + + constructor() { + this.response = new Response(); + } +} + +export default new BottomPane(); diff --git a/app/client/package.json b/app/client/package.json index 9b822e0f11ae..dc06ada8d35f 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -72,16 +72,16 @@ "@mantine/hooks": "^5.10.1", "@newrelic/browser-agent": "^1.255.0", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/auto-instrumentations-web": "^0.41.0", - "@opentelemetry/context-zone": "1.25.1", - "@opentelemetry/core": "^1.26.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.52.1", - "@opentelemetry/exporter-trace-otlp-proto": "0.52.1", - "@opentelemetry/instrumentation": "0.52.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-metrics": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1", - "@opentelemetry/sdk-trace-web": "1.25.1", + "@opentelemetry/auto-instrumentations-web": "^0.42.0", + "@opentelemetry/context-zone": "^1.27.0", + "@opentelemetry/core": "^1.27.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.54.2", + "@opentelemetry/exporter-trace-otlp-http": "^0.54.2", + "@opentelemetry/instrumentation": "^0.54.2", + "@opentelemetry/resources": "^1.27.0", + "@opentelemetry/sdk-metrics": "^1.27.0", + "@opentelemetry/sdk-trace-base": "^1.27.0", + "@opentelemetry/sdk-trace-web": "^1.27.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@react-spring/web": "^9.7.4", "@react-types/shared": "^3.23.0", diff --git a/app/client/packages/design-system/widgets/package.json b/app/client/packages/design-system/widgets/package.json index 799d1e31679e..126c104ff3d7 100644 --- a/app/client/packages/design-system/widgets/package.json +++ b/app/client/packages/design-system/widgets/package.json @@ -29,11 +29,13 @@ "react-aria-components": "^1.2.1", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", + "react-transition-group": "^4.4.5", "remark-gfm": "^4.0.0" }, "devDependencies": { "@types/fs-extra": "^11.0.4", "@types/react-syntax-highlighter": "^15.5.13", + "@types/react-transition-group": "^4.4.11", "eslint-plugin-storybook": "^0.6.10" }, "peerDependencies": { diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/index.ts b/app/client/packages/design-system/widgets/src/components/Sheet/index.ts new file mode 100644 index 000000000000..3bd16e178a03 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/src/Sheet.tsx b/app/client/packages/design-system/widgets/src/components/Sheet/src/Sheet.tsx new file mode 100644 index 000000000000..3b5261f223a8 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/src/Sheet.tsx @@ -0,0 +1,62 @@ +import clsx from "clsx"; +import React, { forwardRef, type Ref, useRef } from "react"; +import { + Modal as HeadlessModal, + Dialog as HeadlessDialog, + ModalOverlay as HeadlessModalOverlay, +} from "react-aria-components"; +import { CSSTransition } from "react-transition-group"; + +import styles from "./styles.module.css"; +import type { SheetProps } from "./types"; + +export function _Sheet(props: SheetProps, ref: Ref) { + const { + children, + className, + isOpen, + onEntered, + onExited, + onOpenChange, + position = "start", + ...rest + } = props; + const root = document.body.querySelector( + "[data-theme-provider]", + ) as HTMLButtonElement; + + const overlayRef = useRef(); + + return ( + + + + + {children} + + + + + ); +} + +export const Sheet = forwardRef(_Sheet); diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/src/index.ts b/app/client/packages/design-system/widgets/src/components/Sheet/src/index.ts new file mode 100644 index 000000000000..c415d8b0d501 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/src/index.ts @@ -0,0 +1,2 @@ +export { Sheet } from "./Sheet"; +export { DialogTrigger as SheetTrigger } from "react-aria-components"; diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Sheet/src/styles.module.css new file mode 100644 index 000000000000..27dfe571139d --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/src/styles.module.css @@ -0,0 +1,117 @@ +.sheet { + position: fixed; + background: var(--color-bg-elevation-3); + width: 80%; +} + +.overlay { + display: flex; + align-items: center; + justify-content: center; + background: var(--color-bg-neutral-opacity); + z-index: var(--z-index-99); + contain: strict; + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; +} + +.dialog { + height: 100%; +} + +.overlay[data-entering] { + animation: fade-in 0.3s ease-in-out; +} + +.overlay[data-exiting] { + animation: fade-out 0.3s ease-in-out; +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +.start { + top: 0; + left: 0; + bottom: 0; + max-width: 90%; + height: 100%; +} + +.start[data-entering] { + animation: slide-in-start 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.start[data-exiting] { + animation: slide-out-start 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.end { + top: 0; + right: 0; + bottom: 0; + max-width: 90%; + height: 100%; +} + +.end[data-entering] { + animation: slide-in-end 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.end[data-exiting] { + animation: slide-out-end 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes slide-in-start { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +@keyframes slide-out-start { + from { + transform: translateX(0); + } + to { + transform: translateX(-100%); + } +} + +@keyframes slide-in-end { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } +} + +@keyframes slide-out-end { + from { + transform: translateX(0); + } + to { + transform: translateX(100%); + } +} diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/src/types.ts b/app/client/packages/design-system/widgets/src/components/Sheet/src/types.ts new file mode 100644 index 000000000000..d66e9d5a8906 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/src/types.ts @@ -0,0 +1,12 @@ +import type { ModalOverlayProps as HeadlessModalOverlayProps } from "react-aria-components"; + +export interface SheetProps + extends Omit { + /** + * The position from which the sheet slides in + * @default 'start' + */ + position?: "start" | "end"; + onEntered?: () => void; + onExited?: () => void; +} diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/stories/Sheet.stories.tsx b/app/client/packages/design-system/widgets/src/components/Sheet/stories/Sheet.stories.tsx new file mode 100644 index 000000000000..ab336257e155 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/stories/Sheet.stories.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { Sheet } from "../index"; +import { SimpleSheet } from "./SimpleSheet"; + +const meta: Meta = { + title: "WDS/Widgets/Sheet", + component: Sheet, + render: (args) => , +}; + +export default meta; +type Story = StoryObj; + +// Default story with left position (start) +export const Default: Story = { + args: { + position: "start", + }, +}; + +// Right position (end) +export const RightPositioned: Story = { + args: { + position: "end", + }, +}; diff --git a/app/client/packages/design-system/widgets/src/components/Sheet/stories/SimpleSheet.tsx b/app/client/packages/design-system/widgets/src/components/Sheet/stories/SimpleSheet.tsx new file mode 100644 index 000000000000..200e37d00841 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sheet/stories/SimpleSheet.tsx @@ -0,0 +1,20 @@ +import React, { useState } from "react"; +import { Button } from "../../Button"; +import { Sheet } from "../index"; +import type { SheetProps } from "../src/types"; + +export const SimpleSheet = (props: Omit) => { + const [isOpen, setIsOpen] = useState(props.isOpen); + + return ( + <> + + +
+

Sheet Content

+

This is an example of sheet content.

+
+
+ + ); +}; diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/index.ts b/app/client/packages/design-system/widgets/src/components/Sidebar/index.ts new file mode 100644 index 000000000000..3bd16e178a03 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/src/Sidebar.tsx b/app/client/packages/design-system/widgets/src/components/Sidebar/src/Sidebar.tsx new file mode 100644 index 000000000000..c8b231cef3cb --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/src/Sidebar.tsx @@ -0,0 +1,72 @@ +import clsx from "clsx"; +import * as React from "react"; +import { type Ref, useRef } from "react"; +import { Sheet } from "../../Sheet"; +import { useSidebar } from "./use-sidebar"; +import styles from "./styles.module.css"; +import type { SidebarProps } from "./types"; +import { CSSTransition } from "react-transition-group"; + +const _Sidebar = (props: SidebarProps, ref: Ref) => { + const { + children, + className, + collapsible = "offcanvas", + onEntered, + onExited, + side = "start", + variant = "sidebar", + ...rest + } = props; + const { isMobile, setOpen, state } = useSidebar(); + const sidebarRef = useRef(); + + if (collapsible === "none") { + return ( +
+ {children} +
+ ); + } + + if (Boolean(isMobile)) { + return ( + + {children} + + ); + } + + return ( + +
+
+
+
{children}
+
+
+ + ); +}; + +export const Sidebar = React.forwardRef(_Sidebar); diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarContent.tsx b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarContent.tsx new file mode 100644 index 000000000000..bb90417a91b1 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarContent.tsx @@ -0,0 +1,22 @@ +import clsx from "clsx"; +import React, { type ComponentProps, type Ref } from "react"; + +import styles from "./styles.module.css"; + +const _SidebarContent = ( + props: ComponentProps<"div">, + ref: Ref, +) => { + const { className, ...rest } = props; + + return ( +
+ ); +}; + +export const SidebarContent = React.forwardRef(_SidebarContent); diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarInset.tsx b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarInset.tsx new file mode 100644 index 000000000000..f6c3a1d0bc57 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarInset.tsx @@ -0,0 +1,22 @@ +import clsx from "clsx"; +import * as React from "react"; +import type { ComponentProps, Ref } from "react"; + +import styles from "./styles.module.css"; + +export const _SidebarInset = ( + props: ComponentProps<"main">, + ref: Ref, +) => { + const { className, ...rest } = props; + + return ( +
+ ); +}; + +export const SidebarInset = React.forwardRef(_SidebarInset); diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarProvider.tsx b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarProvider.tsx new file mode 100644 index 000000000000..35d688f23837 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarProvider.tsx @@ -0,0 +1,96 @@ +import clsx from "clsx"; +import React, { type Ref, useCallback, useState } from "react"; + +import styles from "./styles.module.css"; +import { SidebarContext } from "./context"; +import { useIsMobile } from "./use-mobile"; +import { SIDEBAR_CONSTANTS } from "./constants"; +import type { SidebarContextType, SidebarProviderProps } from "./types"; + +export const _SidebarProvider = ( + props: SidebarProviderProps, + ref: Ref, +) => { + const { + children, + className, + defaultOpen = true, + isOpen: openProp, + onOpen: setOpenProp, + style, + ...rest + } = props; + const isMobile = useIsMobile(); + + const [_open, _setOpen] = useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value; + + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } + }, + [setOpenProp, open], + ); + + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpen((open) => !open) : setOpen((open) => !open); + }, [isMobile, setOpen]); + + React.useEffect( + function handleKeyboardShortcuts() { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_CONSTANTS.KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + + return () => window.removeEventListener("keydown", handleKeyDown); + }, + [toggleSidebar, isMobile], + ); + + const state = open ? "expanded" : "collapsed"; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, toggleSidebar], + ); + + return ( + +
+ {children} +
+
+ ); +}; + +export const SidebarProvider = React.forwardRef(_SidebarProvider); diff --git a/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarTrigger.tsx b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarTrigger.tsx new file mode 100644 index 000000000000..4649d3ae877c --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Sidebar/src/SidebarTrigger.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { Button } from "@appsmith/wds"; + +import { useSidebar } from "./use-sidebar"; + +interface SidebarTriggerProps { + onPress?: () => void; +} + +export const SidebarTrigger = (props: SidebarTriggerProps) => { + const { onPress } = props; + const { state, toggleSidebar } = useSidebar(); + + return ( +