chore: entity tabs replacement (#38989)
## Description Replacing entity tab bar and components with ADS templates. Fixes #37647 Fixes #37775 ## Automation /ok-to-test tags="@tag.All" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/13172995565> > Commit: f3db2d920ded4aec290af6bc48278a59a04b8db8 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13172995565&attempt=2" target="_blank">Cypress dashboard</a>. > Tags: `@tag.All` > Spec: > <hr>Thu, 06 Feb 2025 07:56:59 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Enhanced tab interaction: The add button now supports dynamic visibility and a loading indicator when new tabs are being created. - Improved editing: Tab labels now feature refined controls for entering and exiting edit mode, offering a more flexible user experience. - New `EntityTabsHeader` component added to enhance tab management. - Updated tab structure to utilize `DismissibleTab` for improved functionality. - Introduced new props for `DismissibleTabBar` to allow for additional styling and control over add button visibility. - New properties added to `DismissibleTab` for greater interactivity and customization. - Additional properties introduced to `EditableDismissibleTab` for enhanced editing capabilities. - **Refactor** - Streamlined the tab interface by replacing legacy components with modern, responsive alternatives, simplifying the overall design and interaction. - Enhanced event handling and state management for better performance and user experience. - Updated import paths and component structures for clarity and maintainability. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
parent
41160a26f6
commit
858ca47d3a
|
|
@ -110,7 +110,7 @@ describe("Focus Retention of Inputs", { tags: ["@tag.IDE"] }, function () {
|
||||||
cy.setQueryTimeout(10000);
|
cy.setQueryTimeout(10000);
|
||||||
|
|
||||||
EditorNavigation.SelectEntityByName("SQL_Query", EntityType.Query);
|
EditorNavigation.SelectEntityByName("SQL_Query", EntityType.Query);
|
||||||
cy.get(locators._queryName).should("contain.text", "SQL_Query");
|
cy.get(locators._activeEntityTab).should("contain.text", "SQL_Query");
|
||||||
pluginActionForm.toolbar.toggleSettings();
|
pluginActionForm.toolbar.toggleSettings();
|
||||||
agHelper.GetElement(dataSources._usePreparedStatement).should("be.focused");
|
agHelper.GetElement(dataSources._usePreparedStatement).should("be.focused");
|
||||||
EditorNavigation.SelectEntityByName("S3_Query", EntityType.Query);
|
EditorNavigation.SelectEntityByName("S3_Query", EntityType.Query);
|
||||||
|
|
|
||||||
|
|
@ -53,13 +53,13 @@ describe("Tabs Navigation", { tags: ["@tag.IDE"] }, () => {
|
||||||
agHelper.GetNClick(editorTabSelector("page1_query1"));
|
agHelper.GetNClick(editorTabSelector("page1_query1"));
|
||||||
|
|
||||||
agHelper
|
agHelper
|
||||||
.GetElement(locators._queryName)
|
.GetElement(locators._activeEntityTab)
|
||||||
.should("have.text", "Page1_Query1");
|
.should("have.text", "Page1_Query1");
|
||||||
|
|
||||||
agHelper.GetNClick(editorTabSelector("page1_query2"));
|
agHelper.GetNClick(editorTabSelector("page1_query2"));
|
||||||
|
|
||||||
agHelper
|
agHelper
|
||||||
.GetElement(locators._queryName)
|
.GetElement(locators._activeEntityTab)
|
||||||
.should("have.text", "Page1_Query2");
|
.should("have.text", "Page1_Query2");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -93,13 +93,13 @@ describe("Tabs Navigation", { tags: ["@tag.IDE"] }, () => {
|
||||||
agHelper.GetNClick(editorTabSelector("page2_query1"));
|
agHelper.GetNClick(editorTabSelector("page2_query1"));
|
||||||
|
|
||||||
agHelper
|
agHelper
|
||||||
.GetElement(locators._queryName)
|
.GetElement(locators._activeEntityTab)
|
||||||
.should("have.text", "Page2_Query1");
|
.should("have.text", "Page2_Query1");
|
||||||
|
|
||||||
agHelper.GetNClick(editorTabSelector("page2_query2"));
|
agHelper.GetNClick(editorTabSelector("page2_query2"));
|
||||||
|
|
||||||
agHelper
|
agHelper
|
||||||
.GetElement(locators._queryName)
|
.GetElement(locators._activeEntityTab)
|
||||||
.should("have.text", "Page2_Query2");
|
.should("have.text", "Page2_Query2");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -108,13 +108,13 @@ describe("Tabs Navigation", { tags: ["@tag.IDE"] }, () => {
|
||||||
agHelper.GetNClick(editorTabSelector("page1_query1"));
|
agHelper.GetNClick(editorTabSelector("page1_query1"));
|
||||||
|
|
||||||
agHelper
|
agHelper
|
||||||
.GetElement(locators._queryName)
|
.GetElement(locators._activeEntityTab)
|
||||||
.should("have.text", "Page1_Query1");
|
.should("have.text", "Page1_Query1");
|
||||||
|
|
||||||
agHelper.GetNClick(editorTabSelector("page1_query2"));
|
agHelper.GetNClick(editorTabSelector("page1_query2"));
|
||||||
|
|
||||||
agHelper
|
agHelper
|
||||||
.GetElement(locators._queryName)
|
.GetElement(locators._activeEntityTab)
|
||||||
.should("have.text", "Page1_Query2");
|
.should("have.text", "Page1_Query2");
|
||||||
|
|
||||||
PageLeftPane.switchSegment(PagePaneSegment.JS);
|
PageLeftPane.switchSegment(PagePaneSegment.JS);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ describe(
|
||||||
jsEditor.RenameJSObjectFromContextMenu("ChangedName1");
|
jsEditor.RenameJSObjectFromContextMenu("ChangedName1");
|
||||||
|
|
||||||
// Validate the new name of the JS Object
|
// Validate the new name of the JS Object
|
||||||
cy.get(jsEditor.listOfJsObjects).eq(0).contains("ChangedName1");
|
cy.get(jsEditor.listOfJsDismissibleTabs).eq(0).contains("ChangedName1");
|
||||||
|
|
||||||
// Create second JS file
|
// Create second JS file
|
||||||
jsEditor.CreateJSObject("", { prettify: false, toRun: false });
|
jsEditor.CreateJSObject("", { prettify: false, toRun: false });
|
||||||
|
|
@ -24,7 +24,7 @@ describe(
|
||||||
jsEditor.RenameJSObjectFromContextMenu("ChangedName3");
|
jsEditor.RenameJSObjectFromContextMenu("ChangedName3");
|
||||||
|
|
||||||
// Validate the new name of the 3rd JS Objcte
|
// Validate the new name of the 3rd JS Objcte
|
||||||
cy.get(jsEditor.listOfJsObjects).eq(2).contains("ChangedName3");
|
cy.get(jsEditor.listOfJsDismissibleTabs).eq(2).contains("ChangedName3");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -446,7 +446,7 @@ describe(
|
||||||
subAction: "Page1",
|
subAction: "Page1",
|
||||||
toastToValidate: "copied to page",
|
toastToValidate: "copied to page",
|
||||||
});
|
});
|
||||||
agHelper.GetNAssertContains(locators._queryName, "Query1Copy");
|
agHelper.GetNAssertContains(locators._activeEntityTab, "Query1Copy");
|
||||||
dataSources.runQueryAndVerifyResponseViews({ count: 2 });
|
dataSources.runQueryAndVerifyResponseViews({ count: 2 });
|
||||||
PageList.AddNewPage();
|
PageList.AddNewPage();
|
||||||
EditorNavigation.SelectEntityByName("Page1", EntityType.Page);
|
EditorNavigation.SelectEntityByName("Page1", EntityType.Page);
|
||||||
|
|
@ -456,7 +456,7 @@ describe(
|
||||||
toastToValidate: "moved to page",
|
toastToValidate: "moved to page",
|
||||||
});
|
});
|
||||||
agHelper.WaitUntilAllToastsDisappear();
|
agHelper.WaitUntilAllToastsDisappear();
|
||||||
agHelper.GetNAssertContains(locators._queryName, "Query1Copy");
|
agHelper.GetNAssertContains(locators._activeEntityTab, "Query1Copy");
|
||||||
dataSources.runQueryAndVerifyResponseViews({ count: 2 });
|
dataSources.runQueryAndVerifyResponseViews({ count: 2 });
|
||||||
agHelper.ActionContextMenuWithInPane({
|
agHelper.ActionContextMenuWithInPane({
|
||||||
action: "Delete",
|
action: "Delete",
|
||||||
|
|
|
||||||
|
|
@ -181,9 +181,9 @@ Cypress.Commands.add("createAndFillApi", (url, parameters) => {
|
||||||
dataSources.NavigateToDSCreateNew();
|
dataSources.NavigateToDSCreateNew();
|
||||||
cy.testCreateApiButton();
|
cy.testCreateApiButton();
|
||||||
cy.get("@createNewApi").then((response) => {
|
cy.get("@createNewApi").then((response) => {
|
||||||
cy.get(locator._queryName).should("be.visible");
|
cy.get(locator._activeEntityTab).should("be.visible");
|
||||||
expect(response.response.body.responseMeta.success).to.eq(true);
|
expect(response.response.body.responseMeta.success).to.eq(true);
|
||||||
cy.get(locator._queryName)
|
cy.get(locator._activeEntityTab)
|
||||||
.invoke("text")
|
.invoke("text")
|
||||||
.then((text) => {
|
.then((text) => {
|
||||||
const someText = text;
|
const someText = text;
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ export class CommonLocators {
|
||||||
_link = ".ads-v2-link";
|
_link = ".ads-v2-link";
|
||||||
_btnSpinner = ".ads-v2-spinner";
|
_btnSpinner = ".ads-v2-spinner";
|
||||||
_sidebar = ".t--sidebar";
|
_sidebar = ".t--sidebar";
|
||||||
_queryName = ".editor-tab.active > .ads-v2-text";
|
_activeEntityTab = ".editor-tab.active .ads-v2-text";
|
||||||
_queryNameTxt = ".editor-tab.active > .ads-v2-text input";
|
_activeEntityTabInput = ".editor-tab.active .ads-v2-text input";
|
||||||
_editIcon = ".t--action-name-edit-icon";
|
_editIcon = ".t--action-name-edit-icon";
|
||||||
_emptyCanvasCta = "[data-testid='canvas-ctas']";
|
_emptyCanvasCta = "[data-testid='canvas-ctas']";
|
||||||
_dsName = ".t--edit-datasource-name span";
|
_dsName = ".t--edit-datasource-name span";
|
||||||
|
|
|
||||||
|
|
@ -270,8 +270,8 @@ export class AggregateHelper {
|
||||||
|
|
||||||
public RenameQuery(renameVal: string, willFailError?: string) {
|
public RenameQuery(renameVal: string, willFailError?: string) {
|
||||||
this.rename({
|
this.rename({
|
||||||
nameLocator: this.locator._queryName,
|
nameLocator: this.locator._activeEntityTab,
|
||||||
textInputLocator: this.locator._queryNameTxt,
|
textInputLocator: this.locator._activeEntityTabInput,
|
||||||
renameVal,
|
renameVal,
|
||||||
dblClick: true,
|
dblClick: true,
|
||||||
willFailError,
|
willFailError,
|
||||||
|
|
@ -1162,7 +1162,7 @@ export class AggregateHelper {
|
||||||
|
|
||||||
public GetObjectName() {
|
public GetObjectName() {
|
||||||
//cy.get(this.locator._queryName).invoke("text").then((text) => cy.wrap(text).as("queryName")); or below syntax
|
//cy.get(this.locator._queryName).invoke("text").then((text) => cy.wrap(text).as("queryName")); or below syntax
|
||||||
return cy.get(this.locator._queryName).invoke("text");
|
return cy.get(this.locator._activeEntityTab).invoke("text");
|
||||||
}
|
}
|
||||||
|
|
||||||
public GetElementLength(selector: string) {
|
public GetElementLength(selector: string) {
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ export class ApiPage {
|
||||||
public settingsTriggerLocator = "[data-testid='t--js-settings-trigger']";
|
public settingsTriggerLocator = "[data-testid='t--js-settings-trigger']";
|
||||||
public splitPaneContextMenuTrigger = ".entity-context-menu";
|
public splitPaneContextMenuTrigger = ".entity-context-menu";
|
||||||
public moreActionsTrigger = "[data-testid='t--more-action-trigger']";
|
public moreActionsTrigger = "[data-testid='t--more-action-trigger']";
|
||||||
private apiNameInput = ".editor-tab.active > .ads-v2-text input";
|
private apiNameInput = this.locator._activeEntityTabInput;
|
||||||
public pageList = ".ads-v2-sub-menu > .ads-v2-menu__menu-item";
|
public pageList = ".ads-v2-sub-menu > .ads-v2-menu__menu-item";
|
||||||
|
|
||||||
CreateApi(
|
CreateApi(
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ export class JSEditor {
|
||||||
public settingsTriggerLocator = "[data-testid='t--js-settings-trigger']";
|
public settingsTriggerLocator = "[data-testid='t--js-settings-trigger']";
|
||||||
public contextMenuTriggerLocator = "[data-testid='t--more-action-trigger']";
|
public contextMenuTriggerLocator = "[data-testid='t--more-action-trigger']";
|
||||||
public runFunctionSelectLocator = "[data-testid='t--js-function-run']";
|
public runFunctionSelectLocator = "[data-testid='t--js-function-run']";
|
||||||
public listOfJsObjects = "[data-testid='t--tabs-container']>div>span";
|
public listOfJsDismissibleTabs =
|
||||||
|
"[data-testid='t--tabs-container'] .editor-tab";
|
||||||
|
|
||||||
public toolbar = new PluginEditorToolbar(
|
public toolbar = new PluginEditorToolbar(
|
||||||
this.runButtonLocator,
|
this.runButtonLocator,
|
||||||
|
|
@ -55,8 +56,8 @@ export class JSEditor {
|
||||||
private _onPageLoadSwitchStatus = (functionName: string) =>
|
private _onPageLoadSwitchStatus = (functionName: string) =>
|
||||||
`//div[contains(@class, '${functionName}-on-page-load-setting')]//label/input`;
|
`//div[contains(@class, '${functionName}-on-page-load-setting')]//label/input`;
|
||||||
|
|
||||||
private _jsObjName = ".editor-tab.active > .ads-v2-text";
|
private _jsObjName = this.locator._activeEntityTab;
|
||||||
public _jsObjTxt = ".editor-tab.active > .ads-v2-text input";
|
public _jsObjTxt = this.locator._activeEntityTabInput;
|
||||||
public _newJSobj = "span:contains('New JS object')";
|
public _newJSobj = "span:contains('New JS object')";
|
||||||
private _bindingsClose = ".t--entity-property-close";
|
private _bindingsClose = ".t--entity-property-close";
|
||||||
public _propertyList = ".binding";
|
public _propertyList = ".binding";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
import { DismissibleTab } from ".";
|
import { DismissibleTab } from "./DismissibleTab";
|
||||||
|
|
||||||
const meta: Meta<typeof DismissibleTab> = {
|
const meta: Meta<typeof DismissibleTab> = {
|
||||||
title: "ADS/Components/Dismissible Tab",
|
title: "ADS/Components/Dismissible Tab",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { Button as ADSButton } from "..";
|
import { Button as ADSButton } from "../Button";
|
||||||
|
|
||||||
export const Tab = styled.div`
|
export const Tab = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -9,7 +9,8 @@ export const Tab = styled.div`
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
gap: var(--ads-v2-spaces-2);
|
gap: var(--ads-v2-spaces-2);
|
||||||
height: 100%;
|
min-height: 32px;
|
||||||
|
max-height: 32px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--ads-v2-color-fg);
|
color: var(--ads-v2-color-fg);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -20,8 +21,7 @@ export const Tab = styled.div`
|
||||||
border-right: 1px solid transparent;
|
border-right: 1px solid transparent;
|
||||||
border-top: 3px solid transparent;
|
border-top: 3px solid transparent;
|
||||||
|
|
||||||
padding: var(--ads-v2-spaces-3);
|
padding: 0 var(--ads-v2-spaces-3) 2px var(--ads-v2-spaces-3);
|
||||||
padding-top: 6px;
|
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: var(--ads-v2-colors-control-field-default-bg);
|
background: var(--ads-v2-colors-control-field-default-bg);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { useCallback } from "react";
|
||||||
|
|
||||||
import clsx from "classnames";
|
import clsx from "classnames";
|
||||||
|
|
||||||
import { Icon } from "..";
|
import { Icon } from "../Icon";
|
||||||
|
|
||||||
import * as Styled from "./DismissibleTab.styles";
|
import * as Styled from "./DismissibleTab.styles";
|
||||||
import { DATA_TEST_ID } from "./constants";
|
import { DATA_TEST_ID } from "./constants";
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
export interface DismissibleTabProps {
|
export interface DismissibleTabProps {
|
||||||
|
/** The content of the tab. */
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
/** Used for custom styling, necessary for styled-components. */
|
||||||
|
className?: string;
|
||||||
|
/** Used for passing data-testid. */
|
||||||
dataTestId?: string;
|
dataTestId?: string;
|
||||||
|
/** Applies active styling. */
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
/** Callback when the tab is clicked. */
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
/** Callback when tab is closed. */
|
||||||
onClose: (e: React.MouseEvent) => void;
|
onClose: (e: React.MouseEvent) => void;
|
||||||
|
/** Callback when tab is double clicked. */
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,9 @@
|
||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
import {
|
import { DismissibleTab } from "./DismissibleTab";
|
||||||
DismissibleTab,
|
import { DismissibleTabBar } from "./DismissibleTabBar";
|
||||||
DismissibleTabBar,
|
import { type DismissibleTabBarProps } from "./DismissibleTabBar.types";
|
||||||
type DismissibleTabBarProps,
|
|
||||||
} from ".";
|
|
||||||
|
|
||||||
const meta: Meta<typeof DismissibleTabBar> = {
|
const meta: Meta<typeof DismissibleTabBar> = {
|
||||||
title: "ADS/Components/Dismissible Tab Bar",
|
title: "ADS/Components/Dismissible Tab Bar",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import styled, { css } from "styled-components";
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
import { Button } from "..";
|
import { Button } from "../Button";
|
||||||
|
|
||||||
export const animatedLeftBorder = (showLeftBorder: boolean) => css`
|
export const animatedLeftBorder = (showLeftBorder: boolean) => css`
|
||||||
transition: border-color 0.5s ease;
|
transition: border-color 0.5s ease;
|
||||||
|
|
@ -16,10 +16,11 @@ export const Root = styled.div<{
|
||||||
}>`
|
}>`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow-x: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 32px;
|
max-height: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
|
||||||
${({ $showLeftBorder }) => animatedLeftBorder($showLeftBorder ?? false)};
|
${({ $showLeftBorder }) => animatedLeftBorder($showLeftBorder ?? false)};
|
||||||
`;
|
`;
|
||||||
|
|
@ -28,7 +29,6 @@ export const TabsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--ads-v2-spaces-2);
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -41,11 +41,11 @@ export const PlusButtonContainer = styled.div<{ $showLeftBorder?: boolean }>`
|
||||||
position: sticky;
|
position: sticky;
|
||||||
right: 0;
|
right: 0;
|
||||||
border: none;
|
border: none;
|
||||||
min-width: 32px;
|
min-width: 28px;
|
||||||
height: 100%;
|
min-height: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: end;
|
||||||
|
|
||||||
${({ $showLeftBorder }) => animatedLeftBorder($showLeftBorder ?? false)};
|
${({ $showLeftBorder }) => animatedLeftBorder($showLeftBorder ?? false)};
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { noop } from "lodash";
|
import { noop } from "lodash";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { Spinner } from "../Spinner";
|
||||||
import { ScrollArea } from "../ScrollArea";
|
import { ScrollArea } from "../ScrollArea";
|
||||||
|
|
||||||
import * as Styled from "./DismissibleTabBar.styles";
|
import * as Styled from "./DismissibleTabBar.styles";
|
||||||
|
|
@ -14,13 +16,15 @@ export const SCROLL_AREA_OPTIONS = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const SCROLL_AREA_STYLE = {
|
const SCROLL_AREA_STYLE = {
|
||||||
height: 34,
|
height: 32,
|
||||||
top: 1,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DismissibleTabBar = ({
|
export const DismissibleTabBar = ({
|
||||||
children,
|
children,
|
||||||
|
className,
|
||||||
disableAdd = false,
|
disableAdd = false,
|
||||||
|
hideAdd = false,
|
||||||
|
isAddingNewTab,
|
||||||
onTabAdd,
|
onTabAdd,
|
||||||
}: DismissibleTabBarProps) => {
|
}: DismissibleTabBarProps) => {
|
||||||
const [isLeftIntersecting, setIsLeftIntersecting] = useState(false);
|
const [isLeftIntersecting, setIsLeftIntersecting] = useState(false);
|
||||||
|
|
@ -85,7 +89,10 @@ export const DismissibleTabBar = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Styled.Root $showLeftBorder={isLeftIntersecting}>
|
<Styled.Root
|
||||||
|
$showLeftBorder={isLeftIntersecting}
|
||||||
|
className={clsx(className)}
|
||||||
|
>
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
data-testid="t--editor-tabs"
|
data-testid="t--editor-tabs"
|
||||||
options={SCROLL_AREA_OPTIONS}
|
options={SCROLL_AREA_OPTIONS}
|
||||||
|
|
@ -99,8 +106,13 @@ export const DismissibleTabBar = ({
|
||||||
<Styled.StickySentinel ref={sentinelRightRef} />
|
<Styled.StickySentinel ref={sentinelRightRef} />
|
||||||
</Styled.TabsContainer>
|
</Styled.TabsContainer>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
{!hideAdd && (
|
||||||
<Styled.PlusButtonContainer $showLeftBorder={isRightIntersecting}>
|
<Styled.PlusButtonContainer $showLeftBorder={isRightIntersecting}>
|
||||||
|
{isAddingNewTab ? (
|
||||||
|
<Spinner size="md" />
|
||||||
|
) : (
|
||||||
<Styled.PlusButton
|
<Styled.PlusButton
|
||||||
|
data-testid="t--ide-tabs-add-button"
|
||||||
isDisabled={disableAdd}
|
isDisabled={disableAdd}
|
||||||
isIconButton
|
isIconButton
|
||||||
kind="tertiary"
|
kind="tertiary"
|
||||||
|
|
@ -108,7 +120,9 @@ export const DismissibleTabBar = ({
|
||||||
startIcon="add-line"
|
startIcon="add-line"
|
||||||
title="Add new tab"
|
title="Add new tab"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Styled.PlusButtonContainer>
|
</Styled.PlusButtonContainer>
|
||||||
|
)}
|
||||||
</Styled.Root>
|
</Styled.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,19 @@ import type React from "react";
|
||||||
import type { DismissibleTabProps } from "./DismissibleTab.types";
|
import type { DismissibleTabProps } from "./DismissibleTab.types";
|
||||||
|
|
||||||
export interface DismissibleTabBarProps {
|
export interface DismissibleTabBarProps {
|
||||||
|
/** The content of the tab bar. */
|
||||||
children:
|
children:
|
||||||
| React.ReactElement<DismissibleTabProps>
|
| React.ReactElement<DismissibleTabProps>
|
||||||
| React.ReactElement<DismissibleTabProps>[];
|
| React.ReactElement<DismissibleTabProps>[]
|
||||||
onTabAdd: () => void;
|
| React.ReactNode;
|
||||||
|
/** Used for custom styling, necessary for styled-components. */
|
||||||
|
className?: string;
|
||||||
|
/** Button is visible, but disabled & not clickable. */
|
||||||
disableAdd?: boolean;
|
disableAdd?: boolean;
|
||||||
|
/** Hides add button completely. */
|
||||||
|
hideAdd?: boolean;
|
||||||
|
/** Will display a loader in place of add button. */
|
||||||
|
isAddingNewTab?: boolean;
|
||||||
|
/** Callback tab is added. */
|
||||||
|
onTabAdd: () => void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
|
||||||
import { EditableDismissibleTab } from ".";
|
import { EditableDismissibleTab } from "./EditableDismissibleTab";
|
||||||
import { Icon } from "../..";
|
import { Icon } from "../../Icon";
|
||||||
|
|
||||||
const meta: Meta<typeof EditableDismissibleTab> = {
|
const meta: Meta<typeof EditableDismissibleTab> = {
|
||||||
title: "ADS/Templates/Editable Dismissible Tab",
|
title: "ADS/Templates/Editable Dismissible Tab",
|
||||||
|
|
@ -24,7 +24,7 @@ export const Basic: Story = {
|
||||||
dataTestId: "t--dismissible-tab",
|
dataTestId: "t--dismissible-tab",
|
||||||
icon: JSIcon(),
|
icon: JSIcon(),
|
||||||
name: "Hello",
|
name: "Hello",
|
||||||
canEdit: true,
|
isEditable: true,
|
||||||
|
|
||||||
onNameSave: console.log,
|
onNameSave: console.log,
|
||||||
validateName: (name: string) =>
|
validateName: (name: string) =>
|
||||||
|
|
|
||||||
|
|
@ -2,33 +2,38 @@ import React from "react";
|
||||||
import { noop } from "lodash";
|
import { noop } from "lodash";
|
||||||
import { useBoolean } from "usehooks-ts";
|
import { useBoolean } from "usehooks-ts";
|
||||||
|
|
||||||
import { DismissibleTab } from "../..";
|
import { DismissibleTab } from "../../DismissibleTab";
|
||||||
import { EditableEntityName } from "..";
|
import { EditableEntityName } from "../EditableEntityName";
|
||||||
|
|
||||||
import type { EditableDismissibleTabProps } from "./EditableDismissibleTab.types";
|
import type { EditableDismissibleTabProps } from "./EditableDismissibleTab.types";
|
||||||
|
|
||||||
export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
|
export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
|
||||||
const {
|
const {
|
||||||
canEdit,
|
|
||||||
dataTestId,
|
dataTestId,
|
||||||
icon,
|
icon,
|
||||||
isActive,
|
isActive,
|
||||||
isEditable = true,
|
isEditable = true,
|
||||||
|
isEditing: propIsEditing,
|
||||||
isLoading,
|
isLoading,
|
||||||
name,
|
name,
|
||||||
onClick,
|
onClick,
|
||||||
onClose,
|
onClose,
|
||||||
|
onEnterEditMode: propOnEnterEditMode,
|
||||||
|
onExitEditMode: propOnExitEditMode,
|
||||||
onNameSave,
|
onNameSave,
|
||||||
validateName,
|
validateName,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setFalse: exitEditMode,
|
setFalse: localOnExitEditMode,
|
||||||
setTrue: enterEditMode,
|
setTrue: localOnEnterEditMode,
|
||||||
value: isEditing,
|
value: localIsEditing,
|
||||||
} = useBoolean(false);
|
} = useBoolean(false);
|
||||||
|
|
||||||
const handleDoubleClick = isEditable ? enterEditMode : noop;
|
const isEditing = propIsEditing ?? localIsEditing;
|
||||||
|
const handleEnterEditMode = propOnEnterEditMode ?? localOnEnterEditMode;
|
||||||
|
const handleExitEditMode = propOnExitEditMode ?? localOnExitEditMode;
|
||||||
|
const handleDoubleClick = isEditable ? handleEnterEditMode : noop;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DismissibleTab
|
<DismissibleTab
|
||||||
|
|
@ -39,12 +44,12 @@ export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
|
||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleDoubleClick}
|
||||||
>
|
>
|
||||||
<EditableEntityName
|
<EditableEntityName
|
||||||
canEdit={canEdit}
|
canEdit={isEditable}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
name={name}
|
name={name}
|
||||||
onExitEditing={exitEditMode}
|
onExitEditing={handleExitEditMode}
|
||||||
onNameSave={onNameSave}
|
onNameSave={onNameSave}
|
||||||
validateName={validateName}
|
validateName={validateName}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,31 @@
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
export interface EditableDismissibleTabProps {
|
export interface EditableDismissibleTabProps {
|
||||||
|
/** Used for passing data-testid. */
|
||||||
dataTestId?: string;
|
dataTestId?: string;
|
||||||
|
/** Icon component to be displayed in the tab. */
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
|
/** Passed to tab component, applies active state/styles. */
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
/** Controls if tab can be edited. */
|
||||||
isEditable?: boolean;
|
isEditable?: boolean;
|
||||||
|
/** Can be passed to control editing state externally. */
|
||||||
|
isEditing?: boolean;
|
||||||
|
/** Shows loading indicator in place of an icon. */
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
/** The name of the tab. */
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
/** Callback when the tab is clicked. */
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
/** Callback when tab is closed. */
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
/** Callback when tab enters edit mode. */
|
||||||
|
onEnterEditMode?: () => void;
|
||||||
|
/** Callback when tab exits edit mode. */
|
||||||
|
onExitEditMode?: () => void;
|
||||||
|
/** Callback when tab name is saved. */
|
||||||
onNameSave: (name: string) => void;
|
onNameSave: (name: string) => void;
|
||||||
|
/** Function to validate the name. */
|
||||||
validateName: (name: string) => string | null;
|
validateName: (name: string) => string | null;
|
||||||
canEdit: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export const EditableEntityName = (props: EditableEntityNameProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return icon;
|
return icon;
|
||||||
}, [isLoading, icon]);
|
}, [isLoading, icon, size]);
|
||||||
|
|
||||||
const inputProps = useMemo(
|
const inputProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,26 @@
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
export interface EditableEntityNameProps {
|
export interface EditableEntityNameProps {
|
||||||
icon: React.ReactNode;
|
/** Controls if name can be edited. */
|
||||||
inputTestId?: string;
|
|
||||||
isEditing: boolean;
|
|
||||||
isLoading?: boolean;
|
|
||||||
name: string;
|
|
||||||
onExitEditing: () => void;
|
|
||||||
onNameSave: (name: string) => void;
|
|
||||||
validateName: (name: string) => string | null;
|
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
/** Icon component. */
|
||||||
|
icon: React.ReactNode;
|
||||||
|
/** Used for passing data-testid to input. */
|
||||||
|
inputTestId?: string;
|
||||||
|
/** Toggles editing mode. */
|
||||||
|
isEditing: boolean;
|
||||||
|
/** Controls if name is fixed width. */
|
||||||
isFixedWidth?: boolean;
|
isFixedWidth?: boolean;
|
||||||
|
/** Shows loading indicator in place of an icon. */
|
||||||
|
isLoading?: boolean;
|
||||||
|
/** The name of the entity. */
|
||||||
|
name: string;
|
||||||
|
/** Size of the icon & input. */
|
||||||
size?: "small" | "medium";
|
size?: "small" | "medium";
|
||||||
|
/** Callback when editing is exited. */
|
||||||
|
onExitEditing: () => void;
|
||||||
|
/** Callback when name is saved. */
|
||||||
|
onNameSave: (name: string) => void;
|
||||||
|
/** Function to validate the name. */
|
||||||
|
validateName: (name: string) => string | null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,14 @@ import React from "react";
|
||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
import { useBoolean } from "usehooks-ts";
|
import { useBoolean } from "usehooks-ts";
|
||||||
|
|
||||||
import { DismissibleTab, DismissibleTabBar } from "../..";
|
import { DismissibleTab } from "../../DismissibleTab";
|
||||||
|
|
||||||
import { EntityTabsHeader, EntityListButton, ToggleScreenModeButton } from ".";
|
import {
|
||||||
|
EntityTabBar,
|
||||||
|
EntityTabsHeader,
|
||||||
|
EntityListButton,
|
||||||
|
ToggleScreenModeButton,
|
||||||
|
} from "./EntityTabsHeader";
|
||||||
|
|
||||||
const meta: Meta<typeof EntityTabsHeader> = {
|
const meta: Meta<typeof EntityTabsHeader> = {
|
||||||
title: "ADS/Templates/Entity Tabs Header",
|
title: "ADS/Templates/Entity Tabs Header",
|
||||||
|
|
@ -25,7 +30,7 @@ const Template = ({ width }: Args) => {
|
||||||
<div style={{ width }}>
|
<div style={{ width }}>
|
||||||
<EntityTabsHeader>
|
<EntityTabsHeader>
|
||||||
<EntityListButton onClick={console.log} />
|
<EntityListButton onClick={console.log} />
|
||||||
<DismissibleTabBar onTabAdd={console.log}>
|
<EntityTabBar onTabAdd={console.log}>
|
||||||
<DismissibleTab onClick={console.log} onClose={console.log}>
|
<DismissibleTab onClick={console.log} onClose={console.log}>
|
||||||
One
|
One
|
||||||
</DismissibleTab>
|
</DismissibleTab>
|
||||||
|
|
@ -41,7 +46,7 @@ const Template = ({ width }: Args) => {
|
||||||
<DismissibleTab onClick={console.log} onClose={console.log}>
|
<DismissibleTab onClick={console.log} onClose={console.log}>
|
||||||
Five
|
Five
|
||||||
</DismissibleTab>
|
</DismissibleTab>
|
||||||
</DismissibleTabBar>
|
</EntityTabBar>
|
||||||
<ToggleScreenModeButton
|
<ToggleScreenModeButton
|
||||||
isInSplitScreenMode={isInSplitScreenMode}
|
isInSplitScreenMode={isInSplitScreenMode}
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Button } from "../..";
|
import { Button } from "../../Button";
|
||||||
|
import { DismissibleTabBar } from "../../DismissibleTab";
|
||||||
|
|
||||||
export const Root = styled.div`
|
export const Root = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -11,7 +12,6 @@ export const Root = styled.div`
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
padding: 0 var(--ads-v2-spaces-2);
|
padding: 0 var(--ads-v2-spaces-2);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const IconButton = styled(Button)`
|
export const IconButton = styled(Button)`
|
||||||
|
|
@ -20,3 +20,7 @@ export const IconButton = styled(Button)`
|
||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const TabBar = styled(DismissibleTabBar)`
|
||||||
|
margin-bottom: -1px;
|
||||||
|
`;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ import type {
|
||||||
ToggleScreenModeButtonProps,
|
ToggleScreenModeButtonProps,
|
||||||
} from "./EntityTabsHeader.types";
|
} from "./EntityTabsHeader.types";
|
||||||
|
|
||||||
import { ToggleButton } from "../..";
|
import { ToggleButton } from "../../ToggleButton";
|
||||||
|
import { type DismissibleTabBarProps } from "../../DismissibleTab/DismissibleTabBar.types";
|
||||||
|
|
||||||
export const EntityListButton = (props: EntityListButtonProps) => {
|
export const EntityListButton = (props: EntityListButtonProps) => {
|
||||||
return <ToggleButton {...props} icon="hamburger" size="md" />;
|
return <ToggleButton {...props} icon="hamburger" size="md" />;
|
||||||
|
|
@ -28,6 +29,10 @@ export const ToggleScreenModeButton = (props: ToggleScreenModeButtonProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EntityTabBar = (props: DismissibleTabBarProps) => {
|
||||||
|
return <Styled.TabBar {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
export function EntityTabsHeader({ children }: EntityTabsHeaderProps) {
|
export function EntityTabsHeader({ children }: EntityTabsHeaderProps) {
|
||||||
return <Styled.Root>{children}</Styled.Root>;
|
return <Styled.Root>{children}</Styled.Root>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
import type {
|
|
||||||
ToggleButtonProps,
|
import type { DismissibleTabBarProps } from "../../DismissibleTab";
|
||||||
ButtonProps,
|
import type { ToggleButtonProps } from "../../ToggleButton";
|
||||||
DismissibleTabBarProps,
|
import type { ButtonProps } from "../../Button";
|
||||||
} from "../..";
|
|
||||||
|
|
||||||
export type EntityListButtonProps = Omit<ToggleButtonProps, "icon" | "size">;
|
export type EntityListButtonProps = Omit<ToggleButtonProps, "icon" | "size">;
|
||||||
|
|
||||||
|
|
@ -17,11 +16,15 @@ export type ToggleScreenModeButtonProps = Omit<
|
||||||
type DismissibleTabBarType = ReactElement<DismissibleTabBarProps>;
|
type DismissibleTabBarType = ReactElement<DismissibleTabBarProps>;
|
||||||
type EntityListButtonType = ReactElement<EntityListButtonProps>;
|
type EntityListButtonType = ReactElement<EntityListButtonProps>;
|
||||||
|
|
||||||
|
/** Required for optional/conditional children. */
|
||||||
|
type OptionalChild<T> = T | null | false;
|
||||||
|
|
||||||
export interface EntityTabsHeaderProps {
|
export interface EntityTabsHeaderProps {
|
||||||
children:
|
children:
|
||||||
| DismissibleTabBarType
|
| DismissibleTabBarType
|
||||||
| [DismissibleTabBarType]
|
| [
|
||||||
| [EntityListButtonType, DismissibleTabBarType]
|
OptionalChild<EntityListButtonType>,
|
||||||
| [DismissibleTabBarType, ReactNode]
|
DismissibleTabBarType,
|
||||||
| [EntityListButtonType, DismissibleTabBarType, ReactNode];
|
OptionalChild<ReactNode>,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export {
|
export {
|
||||||
EntityTabsHeader,
|
EntityTabsHeader,
|
||||||
EntityListButton,
|
EntityListButton,
|
||||||
|
EntityTabBar,
|
||||||
ToggleScreenModeButton,
|
ToggleScreenModeButton,
|
||||||
} from "./EntityTabsHeader";
|
} from "./EntityTabsHeader";
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@ export * from "./EntityExplorer";
|
||||||
export * from "./Sidebar";
|
export * from "./Sidebar";
|
||||||
export * from "./EditableEntityName";
|
export * from "./EditableEntityName";
|
||||||
export * from "./EditableDismissibleTab";
|
export * from "./EditableDismissibleTab";
|
||||||
|
export * from "./EntityTabsHeader";
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
/* eslint-disable react-perf/jsx-no-new-object-as-prop */
|
|
||||||
/* eslint-disable react-perf/jsx-no-jsx-as-prop */
|
|
||||||
import "@testing-library/jest-dom";
|
|
||||||
import React from "react";
|
|
||||||
import { render, fireEvent } from "@testing-library/react";
|
|
||||||
|
|
||||||
import { Icon } from "@appsmith/ads";
|
|
||||||
import { FileTab } from "./FileTab";
|
|
||||||
import { DATA_TEST_ID } from "./constants";
|
|
||||||
|
|
||||||
describe("FileTab", () => {
|
|
||||||
const mockOnClick = jest.fn();
|
|
||||||
const mockOnClose = jest.fn();
|
|
||||||
const mockOnDoubleClick = jest.fn();
|
|
||||||
|
|
||||||
const TITLE = "test_file";
|
|
||||||
const TabIcon = () => <Icon name="js" />;
|
|
||||||
|
|
||||||
const setup = () => {
|
|
||||||
const utils = render(
|
|
||||||
<FileTab
|
|
||||||
isActive
|
|
||||||
onClick={mockOnClick}
|
|
||||||
onClose={mockOnClose}
|
|
||||||
onDoubleClick={mockOnDoubleClick}
|
|
||||||
title={TITLE}
|
|
||||||
>
|
|
||||||
<TabIcon />
|
|
||||||
{TITLE}
|
|
||||||
</FileTab>,
|
|
||||||
);
|
|
||||||
const tabElement = utils.getByText(TITLE);
|
|
||||||
|
|
||||||
return {
|
|
||||||
tabElement,
|
|
||||||
...utils,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
test("renders component", () => {
|
|
||||||
const { getByTestId, tabElement } = setup();
|
|
||||||
|
|
||||||
fireEvent.click(tabElement);
|
|
||||||
expect(mockOnClick).toHaveBeenCalled();
|
|
||||||
|
|
||||||
const closeButton = getByTestId(DATA_TEST_ID.CLOSE_BUTTON);
|
|
||||||
|
|
||||||
fireEvent.click(closeButton);
|
|
||||||
expect(mockOnClose).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("double click event is fired", () => {
|
|
||||||
const { getByTestId, tabElement } = setup();
|
|
||||||
|
|
||||||
fireEvent.doubleClick(tabElement);
|
|
||||||
|
|
||||||
expect(mockOnDoubleClick).toHaveBeenCalled();
|
|
||||||
|
|
||||||
const closeButton = getByTestId(DATA_TEST_ID.CLOSE_BUTTON);
|
|
||||||
|
|
||||||
fireEvent.click(closeButton);
|
|
||||||
expect(mockOnClose).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import clsx from "classnames";
|
|
||||||
|
|
||||||
import { Icon } from "@appsmith/ads";
|
|
||||||
import { sanitizeString } from "utils/URLUtils";
|
|
||||||
|
|
||||||
import * as Styled from "./styles";
|
|
||||||
import { DATA_TEST_ID } from "./constants";
|
|
||||||
|
|
||||||
export interface FileTabProps {
|
|
||||||
isActive: boolean;
|
|
||||||
title: string;
|
|
||||||
onClick: () => void;
|
|
||||||
onClose: (e: React.MouseEvent) => void;
|
|
||||||
children: React.ReactNode;
|
|
||||||
onDoubleClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FileTab = ({
|
|
||||||
children,
|
|
||||||
isActive,
|
|
||||||
onClick,
|
|
||||||
onClose,
|
|
||||||
onDoubleClick,
|
|
||||||
title,
|
|
||||||
}: FileTabProps) => {
|
|
||||||
return (
|
|
||||||
<Styled.Tab
|
|
||||||
className={clsx("editor-tab", isActive && "active")}
|
|
||||||
data-testid={`t--ide-tab-${sanitizeString(title)}`}
|
|
||||||
onClick={onClick}
|
|
||||||
onDoubleClick={onDoubleClick}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<Styled.CloseButton
|
|
||||||
aria-label="Close tab"
|
|
||||||
className="tab-close"
|
|
||||||
data-testid={DATA_TEST_ID.CLOSE_BUTTON}
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<Icon name="close-line" />
|
|
||||||
</Styled.CloseButton>
|
|
||||||
</Styled.Tab>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export const DATA_TEST_ID = {
|
|
||||||
INPUT: "t--ide-tab-editable-input",
|
|
||||||
CLOSE_BUTTON: "t--tab-close-btn",
|
|
||||||
SPINNER: "t--ide-tab-spinner",
|
|
||||||
};
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export { FileTab } from "./FileTab";
|
|
||||||
export type { FileTabProps } from "./FileTab";
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
import { Text as ADSText } from "@appsmith/ads";
|
|
||||||
|
|
||||||
export const Tab = styled.div`
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--ads-v2-colors-text-default);
|
|
||||||
cursor: pointer;
|
|
||||||
gap: var(--ads-v2-spaces-2);
|
|
||||||
border-top-left-radius: var(--ads-v2-border-radius);
|
|
||||||
border-top-right-radius: var(--ads-v2-border-radius);
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: var(--ads-v2-spaces-3);
|
|
||||||
padding-top: 6px; // to accommodate border and make icons align correctly
|
|
||||||
border-left: 1px solid transparent;
|
|
||||||
border-right: 1px solid transparent;
|
|
||||||
border-top: 3px solid transparent;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: var(--ads-v2-colors-control-field-default-bg);
|
|
||||||
border-top-color: var(--ads-v2-color-bg-brand);
|
|
||||||
border-left-color: var(--ads-v2-color-border-muted);
|
|
||||||
border-right-color: var(--ads-v2-color-border-muted);
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-weight: var(--ads-v2-font-weight-bold);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .tab-close {
|
|
||||||
position: relative;
|
|
||||||
right: -2px;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover > .tab-close,
|
|
||||||
&.active > .tab-close {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const IconContainer = styled.div`
|
|
||||||
height: 12px;
|
|
||||||
width: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 12px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Text = styled(ADSText)`
|
|
||||||
min-width: 3ch;
|
|
||||||
padding: 0 var(--ads-v2-spaces-1);
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const CloseButton = styled.button`
|
|
||||||
border-radius: var(--ads-v2-border-radius);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: var(--ads-v2-spaces-1);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--ads-v2-colors-action-tertiary-surface-hover-bg);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Flex, Spinner, Button } from "@appsmith/ads";
|
|
||||||
import { useCurrentEditorState, useIDETabClickHandlers } from "../hooks";
|
|
||||||
import { useIsJSAddLoading } from "ee/pages/Editor/IDE/EditorPane/JS/hooks";
|
|
||||||
import { EditorEntityTabState } from "ee/entities/IDE/constants";
|
|
||||||
|
|
||||||
const AddButton = () => {
|
|
||||||
const { addClickHandler } = useIDETabClickHandlers();
|
|
||||||
const isJSLoading = useIsJSAddLoading();
|
|
||||||
const { segmentMode } = useCurrentEditorState();
|
|
||||||
|
|
||||||
if (segmentMode === EditorEntityTabState.Add) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isJSLoading) {
|
|
||||||
return (
|
|
||||||
<Flex px="spaces-2">
|
|
||||||
<Spinner size="md" />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className="!min-w-[24px]"
|
|
||||||
data-testid="t--ide-tabs-add-button"
|
|
||||||
id="tabs-add-toggle"
|
|
||||||
isIconButton
|
|
||||||
kind="tertiary"
|
|
||||||
onClick={addClickHandler}
|
|
||||||
size="sm"
|
|
||||||
startIcon="add-line"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { AddButton };
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useEventCallback } from "usehooks-ts";
|
||||||
|
|
||||||
|
import { DismissibleTab, Text } from "@appsmith/ads";
|
||||||
|
|
||||||
import { FileTab } from "IDE/Components/FileTab";
|
|
||||||
import { useCurrentEditorState } from "../hooks";
|
|
||||||
import {
|
import {
|
||||||
EditorEntityTab,
|
EditorEntityTab,
|
||||||
EditorEntityTabState,
|
EditorEntityTabState,
|
||||||
} from "ee/entities/IDE/constants";
|
} from "ee/entities/IDE/constants";
|
||||||
import { Text } from "@appsmith/ads";
|
|
||||||
|
import { useCurrentEditorState } from "../hooks";
|
||||||
|
|
||||||
const AddTab = ({
|
const AddTab = ({
|
||||||
isListActive,
|
isListActive,
|
||||||
|
|
@ -19,24 +21,26 @@ const AddTab = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { segment, segmentMode } = useCurrentEditorState();
|
const { segment, segmentMode } = useCurrentEditorState();
|
||||||
|
|
||||||
if (segmentMode !== EditorEntityTabState.Add) return null;
|
const onCloseClick = useEventCallback((e: React.MouseEvent) => {
|
||||||
|
|
||||||
const onCloseClick = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onClose();
|
onClose();
|
||||||
};
|
});
|
||||||
|
|
||||||
const content = `New ${segment === EditorEntityTab.JS ? "JS" : "Query"}`;
|
if (segmentMode !== EditorEntityTabState.Add) return null;
|
||||||
|
|
||||||
|
const segmentName = segment === EditorEntityTab.JS ? "JS" : "Query";
|
||||||
|
const content = `New ${segmentName}`;
|
||||||
|
const dataTestId = `t--ide-tab-new_${segmentName.toLowerCase()}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileTab
|
<DismissibleTab
|
||||||
|
dataTestId={dataTestId}
|
||||||
isActive={segmentMode === EditorEntityTabState.Add && !isListActive}
|
isActive={segmentMode === EditorEntityTabState.Add && !isListActive}
|
||||||
onClick={newTabClickCallback}
|
onClick={newTabClickCallback}
|
||||||
onClose={(e) => onCloseClick(e)}
|
onClose={onCloseClick}
|
||||||
title={content}
|
|
||||||
>
|
>
|
||||||
<Text kind="body-s">{content}</Text>
|
<Text kind="body-s">{content}</Text>
|
||||||
</FileTab>
|
</DismissibleTab>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import React from "react";
|
|
||||||
import { Flex } from "@appsmith/ads";
|
|
||||||
import { EDITOR_TABS_HEIGHT } from "../EditorPane/constants";
|
|
||||||
|
|
||||||
const Container = (props: { children: ReactNode }) => {
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
alignItems="center"
|
|
||||||
backgroundColor="#FFFFFF"
|
|
||||||
borderBottom="1px solid var(--ads-v2-color-border-muted)"
|
|
||||||
gap="spaces-2"
|
|
||||||
id="ide-tabs-container"
|
|
||||||
maxHeight={EDITOR_TABS_HEIGHT}
|
|
||||||
minHeight={EDITOR_TABS_HEIGHT}
|
|
||||||
px="spaces-2"
|
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Container;
|
|
||||||
|
|
@ -1,21 +1,23 @@
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
|
|
||||||
import { FileTab } from "IDE/Components/FileTab";
|
|
||||||
import { type EntityItem } from "ee/entities/IDE/constants";
|
|
||||||
import { useCurrentEditorState } from "../hooks";
|
|
||||||
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useEventCallback } from "usehooks-ts";
|
import { useEventCallback } from "usehooks-ts";
|
||||||
import { getIsSavingEntityName } from "ee/selectors/entitiesSelector";
|
|
||||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
import { EditableDismissibleTab } from "@appsmith/ads";
|
||||||
|
|
||||||
|
import { type EntityItem } from "ee/entities/IDE/constants";
|
||||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||||
|
import { getIsSavingEntityName } from "ee/selectors/entitiesSelector";
|
||||||
|
|
||||||
|
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||||
|
import { sanitizeString } from "utils/URLUtils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getEditableTabPermissions,
|
getEditableTabPermissions,
|
||||||
saveEntityName,
|
saveEntityName,
|
||||||
} from "ee/entities/IDE/utils";
|
} from "ee/entities/IDE/utils";
|
||||||
import { noop } from "lodash";
|
import { useIsRenaming, useValidateEntityName } from "IDE";
|
||||||
import { EditableName, useIsRenaming } from "IDE";
|
|
||||||
import { IconContainer } from "IDE/Components/FileTab/styles";
|
import { useCurrentEditorState } from "../hooks";
|
||||||
|
|
||||||
interface EditableTabProps {
|
interface EditableTabProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -30,6 +32,7 @@ interface EditableTabProps {
|
||||||
export function EditableTab(props: EditableTabProps) {
|
export function EditableTab(props: EditableTabProps) {
|
||||||
const { entity, icon, id, isActive, onClick, onClose, title } = props;
|
const { entity, icon, id, isActive, onClick, onClose, title } = props;
|
||||||
const { segment } = useCurrentEditorState();
|
const { segment } = useCurrentEditorState();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
||||||
const isChangePermitted = getEditableTabPermissions({
|
const isChangePermitted = getEditableTabPermissions({
|
||||||
|
|
@ -43,39 +46,36 @@ export function EditableTab(props: EditableTabProps) {
|
||||||
getIsSavingEntityName(state, { id, segment, entity }),
|
getIsSavingEntityName(state, { id, segment, entity }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClose = useEventCallback((e: React.MouseEvent) => {
|
const validateName = useValidateEntityName({
|
||||||
e.stopPropagation();
|
entityName: title,
|
||||||
onClose(id);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDoubleClick = isChangePermitted ? enterEditMode : noop;
|
const handleClose = useEventCallback(() => {
|
||||||
|
onClose(id);
|
||||||
const dispatch = useDispatch();
|
});
|
||||||
|
|
||||||
const handleNameSave = useCallback(
|
const handleNameSave = useCallback(
|
||||||
(name: string) => {
|
(name: string) => {
|
||||||
dispatch(saveEntityName({ params: { id, name }, segment, entity }));
|
dispatch(saveEntityName({ params: { id, name }, segment, entity }));
|
||||||
exitEditMode();
|
|
||||||
},
|
},
|
||||||
[dispatch, entity, exitEditMode, id, segment],
|
[dispatch, entity, id, segment],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileTab
|
<EditableDismissibleTab
|
||||||
|
dataTestId={`t--ide-tab-${sanitizeString(title)}`}
|
||||||
|
icon={icon}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
onClick={onClick}
|
isEditable={isChangePermitted}
|
||||||
onClose={handleClose}
|
|
||||||
onDoubleClick={handleDoubleClick}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<EditableName
|
|
||||||
exitEditing={exitEditMode}
|
|
||||||
icon={<IconContainer>{icon}</IconContainer>}
|
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
name={title}
|
name={title}
|
||||||
|
onClick={onClick}
|
||||||
|
onClose={handleClose}
|
||||||
|
onEnterEditMode={enterEditMode}
|
||||||
|
onExitEditMode={exitEditMode}
|
||||||
onNameSave={handleNameSave}
|
onNameSave={handleNameSave}
|
||||||
|
validateName={validateName}
|
||||||
/>
|
/>
|
||||||
</FileTab>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,13 @@ describe("EditorTabs render checks", () => {
|
||||||
|
|
||||||
it("Renders correctly in split view", () => {
|
it("Renders correctly in split view", () => {
|
||||||
const state = getIDETestState({ ideView: EditorViewMode.SplitScreen });
|
const state = getIDETestState({ ideView: EditorViewMode.SplitScreen });
|
||||||
const { getByTestId, queryByTestId } = renderComponent(
|
const { getByTestId, queryAllByRole, queryByTestId } = renderComponent(
|
||||||
`/app/applicationSlug/pageSlug-${page.basePageId}/edit/queries`,
|
`/app/applicationSlug/pageSlug-${page.basePageId}/edit/queries`,
|
||||||
state,
|
state,
|
||||||
);
|
);
|
||||||
// check tabs is empty
|
|
||||||
const tabsContainer = getByTestId("t--tabs-container");
|
|
||||||
|
|
||||||
expect(tabsContainer.firstChild).toBeNull();
|
// check that tabs are empty
|
||||||
|
expect(queryAllByRole("tab").length).toBe(0);
|
||||||
|
|
||||||
//check add button is not present
|
//check add button is not present
|
||||||
expect(queryByTestId("t--ide-tabs-add-button")).toBeNull();
|
expect(queryByTestId("t--ide-tabs-add-button")).toBeNull();
|
||||||
|
|
@ -58,7 +57,7 @@ describe("EditorTabs render checks", () => {
|
||||||
|
|
||||||
it("Renders correctly in fullscreen view", () => {
|
it("Renders correctly in fullscreen view", () => {
|
||||||
const state = getIDETestState({ ideView: EditorViewMode.FullScreen });
|
const state = getIDETestState({ ideView: EditorViewMode.FullScreen });
|
||||||
const { getByTestId, queryByTestId } = renderComponent(
|
const { getByTestId, queryAllByRole, queryByTestId } = renderComponent(
|
||||||
`/app/applicationSlug/pageSlug-${page.basePageId}/edit/queries`,
|
`/app/applicationSlug/pageSlug-${page.basePageId}/edit/queries`,
|
||||||
state,
|
state,
|
||||||
);
|
);
|
||||||
|
|
@ -66,10 +65,8 @@ describe("EditorTabs render checks", () => {
|
||||||
// check toggle
|
// check toggle
|
||||||
expect(queryByTestId("t--list-toggle")).toBeNull();
|
expect(queryByTestId("t--list-toggle")).toBeNull();
|
||||||
|
|
||||||
// check tabs is empty
|
// check that tabs are empty
|
||||||
const tabsContainer = getByTestId("t--tabs-container");
|
expect(queryAllByRole("tab").length).toBe(0);
|
||||||
|
|
||||||
expect(tabsContainer.firstChild).toBeNull();
|
|
||||||
|
|
||||||
//check add button is not present
|
//check add button is not present
|
||||||
expect(queryByTestId("t--ide-tabs-add-button")).toBeNull();
|
expect(queryByTestId("t--ide-tabs-add-button")).toBeNull();
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,3 @@ export const TabSelectors: Record<
|
||||||
itemUrlSelector: () => "",
|
itemUrlSelector: () => "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SCROLL_AREA_OPTIONS = {
|
|
||||||
overflow: {
|
|
||||||
x: "scroll",
|
|
||||||
y: "hidden",
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
import React, { useCallback, useEffect } from "react";
|
import React, { useCallback, useEffect } from "react";
|
||||||
import { shallowEqual, useDispatch, useSelector } from "react-redux";
|
import { shallowEqual, useDispatch, useSelector } from "react-redux";
|
||||||
import { Flex, ScrollArea, ToggleButton } from "@appsmith/ads";
|
import { useEventCallback } from "usehooks-ts";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EntityTabsHeader,
|
||||||
|
EntityListButton,
|
||||||
|
EntityTabBar,
|
||||||
|
} from "@appsmith/ads";
|
||||||
|
|
||||||
import { getIDEViewMode, getListViewActiveState } from "selectors/ideSelectors";
|
import { getIDEViewMode, getListViewActiveState } from "selectors/ideSelectors";
|
||||||
import type { EntityItem } from "ee/entities/IDE/constants";
|
import type { EntityItem } from "ee/entities/IDE/constants";
|
||||||
import {
|
import {
|
||||||
|
|
@ -8,27 +16,25 @@ import {
|
||||||
EditorEntityTabState,
|
EditorEntityTabState,
|
||||||
EditorViewMode,
|
EditorViewMode,
|
||||||
} from "ee/entities/IDE/constants";
|
} from "ee/entities/IDE/constants";
|
||||||
|
import { useIsJSAddLoading } from "ee/pages/Editor/IDE/EditorPane/JS/hooks";
|
||||||
|
|
||||||
|
import { identifyEntityFromPath } from "navigation/FocusEntity";
|
||||||
|
import { setListViewActiveState } from "actions/ideActions";
|
||||||
|
|
||||||
import Container from "./Container";
|
|
||||||
import {
|
import {
|
||||||
useCurrentEditorState,
|
useCurrentEditorState,
|
||||||
useIDETabClickHandlers,
|
useIDETabClickHandlers,
|
||||||
useShowSideBySideNudge,
|
useShowSideBySideNudge,
|
||||||
} from "../hooks";
|
} from "../hooks";
|
||||||
import { SCROLL_AREA_OPTIONS, TabSelectors } from "./constants";
|
|
||||||
import { AddButton } from "./AddButton";
|
|
||||||
import { useLocation } from "react-router";
|
|
||||||
import { identifyEntityFromPath } from "navigation/FocusEntity";
|
|
||||||
import { List } from "./List";
|
import { List } from "./List";
|
||||||
import { ScreenModeToggle } from "./ScreenModeToggle";
|
import { ScreenModeToggle } from "./ScreenModeToggle";
|
||||||
import { AddTab } from "./AddTab";
|
|
||||||
import { setListViewActiveState } from "actions/ideActions";
|
|
||||||
|
|
||||||
import { useEventCallback } from "usehooks-ts";
|
|
||||||
|
|
||||||
import { EditableTab } from "./EditableTab";
|
import { EditableTab } from "./EditableTab";
|
||||||
|
import { TabSelectors } from "./constants";
|
||||||
|
import { AddTab } from "./AddTab";
|
||||||
|
|
||||||
const EditorTabs = () => {
|
const EditorTabs = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
const ideViewMode = useSelector(getIDEViewMode);
|
const ideViewMode = useSelector(getIDEViewMode);
|
||||||
const { segment, segmentMode } = useCurrentEditorState();
|
const { segment, segmentMode } = useCurrentEditorState();
|
||||||
const { closeClickHandler, tabClickHandler } = useIDETabClickHandlers();
|
const { closeClickHandler, tabClickHandler } = useIDETabClickHandlers();
|
||||||
|
|
@ -37,47 +43,30 @@ const EditorTabs = () => {
|
||||||
const files = useSelector(tabsConfig.tabsSelector, shallowEqual);
|
const files = useSelector(tabsConfig.tabsSelector, shallowEqual);
|
||||||
const isListViewActive = useSelector(getListViewActiveState);
|
const isListViewActive = useSelector(getListViewActiveState);
|
||||||
const [showNudge, dismissNudge] = useShowSideBySideNudge();
|
const [showNudge, dismissNudge] = useShowSideBySideNudge();
|
||||||
|
const { addClickHandler } = useIDETabClickHandlers();
|
||||||
|
const isJSLoading = useIsJSAddLoading();
|
||||||
|
const hideAdd = segmentMode === EditorEntityTabState.Add || !files.length;
|
||||||
|
|
||||||
const location = useLocation();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const currentEntity = identifyEntityFromPath(location.pathname);
|
const currentEntity = identifyEntityFromPath(location.pathname);
|
||||||
|
const showEntityListButton =
|
||||||
|
ideViewMode === EditorViewMode.SplitScreen && files.length > 0;
|
||||||
|
|
||||||
// Turn off list view while changing segment, files
|
useEffect(
|
||||||
useEffect(() => {
|
function turnOffListViewWhileChangingSegmentFiles() {
|
||||||
dispatch(setListViewActiveState(false));
|
dispatch(setListViewActiveState(false));
|
||||||
}, [currentEntity.id, currentEntity.entity, files, segmentMode, dispatch]);
|
},
|
||||||
|
[currentEntity.id, currentEntity.entity, files, segmentMode, dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
// Show list view if all tabs is closed
|
useEffect(
|
||||||
useEffect(() => {
|
function showListViewIfAllTabsAreClosed() {
|
||||||
if (files.length === 0 && segmentMode !== EditorEntityTabState.Add) {
|
if (files.length === 0 && segmentMode !== EditorEntityTabState.Add) {
|
||||||
dispatch(setListViewActiveState(true));
|
dispatch(setListViewActiveState(true));
|
||||||
}
|
}
|
||||||
}, [files, segmentMode, currentEntity.entity, dispatch]);
|
},
|
||||||
|
[files, segmentMode, currentEntity.entity, dispatch],
|
||||||
// scroll to the active tab
|
|
||||||
useEffect(() => {
|
|
||||||
const activeTab = document.querySelector(".editor-tab.active");
|
|
||||||
|
|
||||||
if (activeTab) {
|
|
||||||
activeTab.scrollIntoView({
|
|
||||||
inline: "nearest",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [files, segmentMode]);
|
|
||||||
|
|
||||||
// show border if add button is sticky
|
|
||||||
useEffect(() => {
|
|
||||||
const ele = document.querySelector<HTMLElement>(
|
|
||||||
'[data-testid="t--editor-tabs"] > [data-overlayscrollbars-viewport]',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ele && ele.scrollWidth > ele.clientWidth) {
|
|
||||||
ele.style.borderRight = "1px solid var(--ads-v2-color-border)";
|
|
||||||
} else if (ele) {
|
|
||||||
ele.style.borderRight = "unset";
|
|
||||||
}
|
|
||||||
}, [files]);
|
|
||||||
|
|
||||||
const handleHamburgerClick = useEventCallback(() => {
|
const handleHamburgerClick = useEventCallback(() => {
|
||||||
if (files.length === 0 && segmentMode !== EditorEntityTabState.Add) return;
|
if (files.length === 0 && segmentMode !== EditorEntityTabState.Add) return;
|
||||||
|
|
||||||
|
|
@ -101,27 +90,19 @@ const EditorTabs = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Container>
|
<EntityTabsHeader>
|
||||||
{ideViewMode === EditorViewMode.SplitScreen && files.length > 0 ? (
|
{showEntityListButton && (
|
||||||
<ToggleButton
|
<EntityListButton
|
||||||
data-testid="t--list-toggle"
|
data-testid="t--list-toggle"
|
||||||
icon="hamburger"
|
|
||||||
isSelected={isListViewActive}
|
isSelected={isListViewActive}
|
||||||
onClick={handleHamburgerClick}
|
onClick={handleHamburgerClick}
|
||||||
size="md"
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
<ScrollArea
|
|
||||||
className="h-[32px] top-[0.5px]"
|
<EntityTabBar
|
||||||
data-testid="t--editor-tabs"
|
hideAdd={hideAdd}
|
||||||
options={SCROLL_AREA_OPTIONS}
|
isAddingNewTab={isJSLoading}
|
||||||
size="sm"
|
onTabAdd={addClickHandler}
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
className="items-center"
|
|
||||||
data-testid="t--tabs-container"
|
|
||||||
gap="spaces-2"
|
|
||||||
height="100%"
|
|
||||||
>
|
>
|
||||||
{files.map((tab) => {
|
{files.map((tab) => {
|
||||||
const entity = entities.find((entity) => entity.key === tab.key);
|
const entity = entities.find((entity) => entity.key === tab.key);
|
||||||
|
|
@ -148,14 +129,10 @@ const EditorTabs = () => {
|
||||||
newTabClickCallback={handleNewTabClick}
|
newTabClickCallback={handleNewTabClick}
|
||||||
onClose={closeClickHandler}
|
onClose={closeClickHandler}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</EntityTabBar>
|
||||||
</ScrollArea>
|
|
||||||
{files.length > 0 ? <AddButton /> : null}
|
|
||||||
{/* Switch screen mode button */}
|
|
||||||
<ScreenModeToggle dismissNudge={dismissNudge} showNudge={showNudge} />
|
<ScreenModeToggle dismissNudge={dismissNudge} showNudge={showNudge} />
|
||||||
</Container>
|
</EntityTabsHeader>
|
||||||
|
|
||||||
{/* Overflow list */}
|
|
||||||
{isListViewActive && ideViewMode === EditorViewMode.SplitScreen && (
|
{isListViewActive && ideViewMode === EditorViewMode.SplitScreen && (
|
||||||
<List />
|
<List />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user