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);
|
||||
|
||||
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();
|
||||
agHelper.GetElement(dataSources._usePreparedStatement).should("be.focused");
|
||||
EditorNavigation.SelectEntityByName("S3_Query", EntityType.Query);
|
||||
|
|
|
|||
|
|
@ -53,13 +53,13 @@ describe("Tabs Navigation", { tags: ["@tag.IDE"] }, () => {
|
|||
agHelper.GetNClick(editorTabSelector("page1_query1"));
|
||||
|
||||
agHelper
|
||||
.GetElement(locators._queryName)
|
||||
.GetElement(locators._activeEntityTab)
|
||||
.should("have.text", "Page1_Query1");
|
||||
|
||||
agHelper.GetNClick(editorTabSelector("page1_query2"));
|
||||
|
||||
agHelper
|
||||
.GetElement(locators._queryName)
|
||||
.GetElement(locators._activeEntityTab)
|
||||
.should("have.text", "Page1_Query2");
|
||||
});
|
||||
|
||||
|
|
@ -93,13 +93,13 @@ describe("Tabs Navigation", { tags: ["@tag.IDE"] }, () => {
|
|||
agHelper.GetNClick(editorTabSelector("page2_query1"));
|
||||
|
||||
agHelper
|
||||
.GetElement(locators._queryName)
|
||||
.GetElement(locators._activeEntityTab)
|
||||
.should("have.text", "Page2_Query1");
|
||||
|
||||
agHelper.GetNClick(editorTabSelector("page2_query2"));
|
||||
|
||||
agHelper
|
||||
.GetElement(locators._queryName)
|
||||
.GetElement(locators._activeEntityTab)
|
||||
.should("have.text", "Page2_Query2");
|
||||
});
|
||||
|
||||
|
|
@ -108,13 +108,13 @@ describe("Tabs Navigation", { tags: ["@tag.IDE"] }, () => {
|
|||
agHelper.GetNClick(editorTabSelector("page1_query1"));
|
||||
|
||||
agHelper
|
||||
.GetElement(locators._queryName)
|
||||
.GetElement(locators._activeEntityTab)
|
||||
.should("have.text", "Page1_Query1");
|
||||
|
||||
agHelper.GetNClick(editorTabSelector("page1_query2"));
|
||||
|
||||
agHelper
|
||||
.GetElement(locators._queryName)
|
||||
.GetElement(locators._activeEntityTab)
|
||||
.should("have.text", "Page1_Query2");
|
||||
|
||||
PageLeftPane.switchSegment(PagePaneSegment.JS);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ describe(
|
|||
jsEditor.RenameJSObjectFromContextMenu("ChangedName1");
|
||||
|
||||
// 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
|
||||
jsEditor.CreateJSObject("", { prettify: false, toRun: false });
|
||||
|
|
@ -24,7 +24,7 @@ describe(
|
|||
jsEditor.RenameJSObjectFromContextMenu("ChangedName3");
|
||||
|
||||
// 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",
|
||||
toastToValidate: "copied to page",
|
||||
});
|
||||
agHelper.GetNAssertContains(locators._queryName, "Query1Copy");
|
||||
agHelper.GetNAssertContains(locators._activeEntityTab, "Query1Copy");
|
||||
dataSources.runQueryAndVerifyResponseViews({ count: 2 });
|
||||
PageList.AddNewPage();
|
||||
EditorNavigation.SelectEntityByName("Page1", EntityType.Page);
|
||||
|
|
@ -456,7 +456,7 @@ describe(
|
|||
toastToValidate: "moved to page",
|
||||
});
|
||||
agHelper.WaitUntilAllToastsDisappear();
|
||||
agHelper.GetNAssertContains(locators._queryName, "Query1Copy");
|
||||
agHelper.GetNAssertContains(locators._activeEntityTab, "Query1Copy");
|
||||
dataSources.runQueryAndVerifyResponseViews({ count: 2 });
|
||||
agHelper.ActionContextMenuWithInPane({
|
||||
action: "Delete",
|
||||
|
|
|
|||
|
|
@ -181,9 +181,9 @@ Cypress.Commands.add("createAndFillApi", (url, parameters) => {
|
|||
dataSources.NavigateToDSCreateNew();
|
||||
cy.testCreateApiButton();
|
||||
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);
|
||||
cy.get(locator._queryName)
|
||||
cy.get(locator._activeEntityTab)
|
||||
.invoke("text")
|
||||
.then((text) => {
|
||||
const someText = text;
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ export class CommonLocators {
|
|||
_link = ".ads-v2-link";
|
||||
_btnSpinner = ".ads-v2-spinner";
|
||||
_sidebar = ".t--sidebar";
|
||||
_queryName = ".editor-tab.active > .ads-v2-text";
|
||||
_queryNameTxt = ".editor-tab.active > .ads-v2-text input";
|
||||
_activeEntityTab = ".editor-tab.active .ads-v2-text";
|
||||
_activeEntityTabInput = ".editor-tab.active .ads-v2-text input";
|
||||
_editIcon = ".t--action-name-edit-icon";
|
||||
_emptyCanvasCta = "[data-testid='canvas-ctas']";
|
||||
_dsName = ".t--edit-datasource-name span";
|
||||
|
|
|
|||
|
|
@ -270,8 +270,8 @@ export class AggregateHelper {
|
|||
|
||||
public RenameQuery(renameVal: string, willFailError?: string) {
|
||||
this.rename({
|
||||
nameLocator: this.locator._queryName,
|
||||
textInputLocator: this.locator._queryNameTxt,
|
||||
nameLocator: this.locator._activeEntityTab,
|
||||
textInputLocator: this.locator._activeEntityTabInput,
|
||||
renameVal,
|
||||
dblClick: true,
|
||||
willFailError,
|
||||
|
|
@ -1162,7 +1162,7 @@ export class AggregateHelper {
|
|||
|
||||
public GetObjectName() {
|
||||
//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) {
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export class ApiPage {
|
|||
public settingsTriggerLocator = "[data-testid='t--js-settings-trigger']";
|
||||
public splitPaneContextMenuTrigger = ".entity-context-menu";
|
||||
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";
|
||||
|
||||
CreateApi(
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ export class JSEditor {
|
|||
public settingsTriggerLocator = "[data-testid='t--js-settings-trigger']";
|
||||
public contextMenuTriggerLocator = "[data-testid='t--more-action-trigger']";
|
||||
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(
|
||||
this.runButtonLocator,
|
||||
|
|
@ -55,8 +56,8 @@ export class JSEditor {
|
|||
private _onPageLoadSwitchStatus = (functionName: string) =>
|
||||
`//div[contains(@class, '${functionName}-on-page-load-setting')]//label/input`;
|
||||
|
||||
private _jsObjName = ".editor-tab.active > .ads-v2-text";
|
||||
public _jsObjTxt = ".editor-tab.active > .ads-v2-text input";
|
||||
private _jsObjName = this.locator._activeEntityTab;
|
||||
public _jsObjTxt = this.locator._activeEntityTabInput;
|
||||
public _newJSobj = "span:contains('New JS object')";
|
||||
private _bindingsClose = ".t--entity-property-close";
|
||||
public _propertyList = ".binding";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable no-console */
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { DismissibleTab } from ".";
|
||||
import { DismissibleTab } from "./DismissibleTab";
|
||||
|
||||
const meta: Meta<typeof DismissibleTab> = {
|
||||
title: "ADS/Components/Dismissible Tab",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import styled from "styled-components";
|
||||
|
||||
import { Button as ADSButton } from "..";
|
||||
import { Button as ADSButton } from "../Button";
|
||||
|
||||
export const Tab = styled.div`
|
||||
position: relative;
|
||||
|
|
@ -9,7 +9,8 @@ export const Tab = styled.div`
|
|||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
gap: var(--ads-v2-spaces-2);
|
||||
height: 100%;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
font-size: 12px;
|
||||
color: var(--ads-v2-color-fg);
|
||||
cursor: pointer;
|
||||
|
|
@ -20,8 +21,7 @@ export const Tab = styled.div`
|
|||
border-right: 1px solid transparent;
|
||||
border-top: 3px solid transparent;
|
||||
|
||||
padding: var(--ads-v2-spaces-3);
|
||||
padding-top: 6px;
|
||||
padding: 0 var(--ads-v2-spaces-3) 2px var(--ads-v2-spaces-3);
|
||||
|
||||
&.active {
|
||||
background: var(--ads-v2-colors-control-field-default-bg);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useCallback } from "react";
|
|||
|
||||
import clsx from "classnames";
|
||||
|
||||
import { Icon } from "..";
|
||||
import { Icon } from "../Icon";
|
||||
|
||||
import * as Styled from "./DismissibleTab.styles";
|
||||
import { DATA_TEST_ID } from "./constants";
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
import type React from "react";
|
||||
|
||||
export interface DismissibleTabProps {
|
||||
/** The content of the tab. */
|
||||
children: React.ReactNode;
|
||||
/** Used for custom styling, necessary for styled-components. */
|
||||
className?: string;
|
||||
/** Used for passing data-testid. */
|
||||
dataTestId?: string;
|
||||
/** Applies active styling. */
|
||||
isActive?: boolean;
|
||||
/** Callback when the tab is clicked. */
|
||||
onClick: () => void;
|
||||
/** Callback when tab is closed. */
|
||||
onClose: (e: React.MouseEvent) => void;
|
||||
/** Callback when tab is double clicked. */
|
||||
onDoubleClick?: () => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,9 @@
|
|||
import React, { useRef, useState } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import {
|
||||
DismissibleTab,
|
||||
DismissibleTabBar,
|
||||
type DismissibleTabBarProps,
|
||||
} from ".";
|
||||
import { DismissibleTab } from "./DismissibleTab";
|
||||
import { DismissibleTabBar } from "./DismissibleTabBar";
|
||||
import { type DismissibleTabBarProps } from "./DismissibleTabBar.types";
|
||||
|
||||
const meta: Meta<typeof DismissibleTabBar> = {
|
||||
title: "ADS/Components/Dismissible Tab Bar",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import styled, { css } from "styled-components";
|
||||
|
||||
import { Button } from "..";
|
||||
import { Button } from "../Button";
|
||||
|
||||
export const animatedLeftBorder = (showLeftBorder: boolean) => css`
|
||||
transition: border-color 0.5s ease;
|
||||
|
|
@ -16,10 +16,11 @@ export const Root = styled.div<{
|
|||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
height: 32px;
|
||||
max-height: 32px;
|
||||
min-height: 32px;
|
||||
|
||||
${({ $showLeftBorder }) => animatedLeftBorder($showLeftBorder ?? false)};
|
||||
`;
|
||||
|
|
@ -28,7 +29,6 @@ export const TabsContainer = styled.div`
|
|||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
align-items: center;
|
||||
gap: var(--ads-v2-spaces-2);
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
|
|
@ -41,11 +41,11 @@ export const PlusButtonContainer = styled.div<{ $showLeftBorder?: boolean }>`
|
|||
position: sticky;
|
||||
right: 0;
|
||||
border: none;
|
||||
min-width: 32px;
|
||||
height: 100%;
|
||||
min-width: 28px;
|
||||
min-height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: end;
|
||||
|
||||
${({ $showLeftBorder }) => animatedLeftBorder($showLeftBorder ?? false)};
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { noop } from "lodash";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { Spinner } from "../Spinner";
|
||||
import { ScrollArea } from "../ScrollArea";
|
||||
|
||||
import * as Styled from "./DismissibleTabBar.styles";
|
||||
|
|
@ -14,13 +16,15 @@ export const SCROLL_AREA_OPTIONS = {
|
|||
} as const;
|
||||
|
||||
const SCROLL_AREA_STYLE = {
|
||||
height: 34,
|
||||
top: 1,
|
||||
height: 32,
|
||||
};
|
||||
|
||||
export const DismissibleTabBar = ({
|
||||
children,
|
||||
className,
|
||||
disableAdd = false,
|
||||
hideAdd = false,
|
||||
isAddingNewTab,
|
||||
onTabAdd,
|
||||
}: DismissibleTabBarProps) => {
|
||||
const [isLeftIntersecting, setIsLeftIntersecting] = useState(false);
|
||||
|
|
@ -85,7 +89,10 @@ export const DismissibleTabBar = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<Styled.Root $showLeftBorder={isLeftIntersecting}>
|
||||
<Styled.Root
|
||||
$showLeftBorder={isLeftIntersecting}
|
||||
className={clsx(className)}
|
||||
>
|
||||
<ScrollArea
|
||||
data-testid="t--editor-tabs"
|
||||
options={SCROLL_AREA_OPTIONS}
|
||||
|
|
@ -99,16 +106,23 @@ export const DismissibleTabBar = ({
|
|||
<Styled.StickySentinel ref={sentinelRightRef} />
|
||||
</Styled.TabsContainer>
|
||||
</ScrollArea>
|
||||
<Styled.PlusButtonContainer $showLeftBorder={isRightIntersecting}>
|
||||
<Styled.PlusButton
|
||||
isDisabled={disableAdd}
|
||||
isIconButton
|
||||
kind="tertiary"
|
||||
onClick={handleAdd}
|
||||
startIcon="add-line"
|
||||
title="Add new tab"
|
||||
/>
|
||||
</Styled.PlusButtonContainer>
|
||||
{!hideAdd && (
|
||||
<Styled.PlusButtonContainer $showLeftBorder={isRightIntersecting}>
|
||||
{isAddingNewTab ? (
|
||||
<Spinner size="md" />
|
||||
) : (
|
||||
<Styled.PlusButton
|
||||
data-testid="t--ide-tabs-add-button"
|
||||
isDisabled={disableAdd}
|
||||
isIconButton
|
||||
kind="tertiary"
|
||||
onClick={handleAdd}
|
||||
startIcon="add-line"
|
||||
title="Add new tab"
|
||||
/>
|
||||
)}
|
||||
</Styled.PlusButtonContainer>
|
||||
)}
|
||||
</Styled.Root>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,9 +2,19 @@ import type React from "react";
|
|||
import type { DismissibleTabProps } from "./DismissibleTab.types";
|
||||
|
||||
export interface DismissibleTabBarProps {
|
||||
/** The content of the tab bar. */
|
||||
children:
|
||||
| React.ReactElement<DismissibleTabProps>
|
||||
| React.ReactElement<DismissibleTabProps>[];
|
||||
onTabAdd: () => void;
|
||||
| React.ReactElement<DismissibleTabProps>[]
|
||||
| React.ReactNode;
|
||||
/** Used for custom styling, necessary for styled-components. */
|
||||
className?: string;
|
||||
/** Button is visible, but disabled & not clickable. */
|
||||
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 type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { EditableDismissibleTab } from ".";
|
||||
import { Icon } from "../..";
|
||||
import { EditableDismissibleTab } from "./EditableDismissibleTab";
|
||||
import { Icon } from "../../Icon";
|
||||
|
||||
const meta: Meta<typeof EditableDismissibleTab> = {
|
||||
title: "ADS/Templates/Editable Dismissible Tab",
|
||||
|
|
@ -24,7 +24,7 @@ export const Basic: Story = {
|
|||
dataTestId: "t--dismissible-tab",
|
||||
icon: JSIcon(),
|
||||
name: "Hello",
|
||||
canEdit: true,
|
||||
isEditable: true,
|
||||
|
||||
onNameSave: console.log,
|
||||
validateName: (name: string) =>
|
||||
|
|
|
|||
|
|
@ -2,33 +2,38 @@ import React from "react";
|
|||
import { noop } from "lodash";
|
||||
import { useBoolean } from "usehooks-ts";
|
||||
|
||||
import { DismissibleTab } from "../..";
|
||||
import { EditableEntityName } from "..";
|
||||
import { DismissibleTab } from "../../DismissibleTab";
|
||||
import { EditableEntityName } from "../EditableEntityName";
|
||||
|
||||
import type { EditableDismissibleTabProps } from "./EditableDismissibleTab.types";
|
||||
|
||||
export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
|
||||
const {
|
||||
canEdit,
|
||||
dataTestId,
|
||||
icon,
|
||||
isActive,
|
||||
isEditable = true,
|
||||
isEditing: propIsEditing,
|
||||
isLoading,
|
||||
name,
|
||||
onClick,
|
||||
onClose,
|
||||
onEnterEditMode: propOnEnterEditMode,
|
||||
onExitEditMode: propOnExitEditMode,
|
||||
onNameSave,
|
||||
validateName,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
setFalse: exitEditMode,
|
||||
setTrue: enterEditMode,
|
||||
value: isEditing,
|
||||
setFalse: localOnExitEditMode,
|
||||
setTrue: localOnEnterEditMode,
|
||||
value: localIsEditing,
|
||||
} = 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 (
|
||||
<DismissibleTab
|
||||
|
|
@ -39,12 +44,12 @@ export const EditableDismissibleTab = (props: EditableDismissibleTabProps) => {
|
|||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<EditableEntityName
|
||||
canEdit={canEdit}
|
||||
canEdit={isEditable}
|
||||
icon={icon}
|
||||
isEditing={isEditing}
|
||||
isLoading={isLoading}
|
||||
name={name}
|
||||
onExitEditing={exitEditMode}
|
||||
onExitEditing={handleExitEditMode}
|
||||
onNameSave={onNameSave}
|
||||
validateName={validateName}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,31 @@
|
|||
import type React from "react";
|
||||
|
||||
export interface EditableDismissibleTabProps {
|
||||
/** Used for passing data-testid. */
|
||||
dataTestId?: string;
|
||||
/** Icon component to be displayed in the tab. */
|
||||
icon: React.ReactNode;
|
||||
/** Passed to tab component, applies active state/styles. */
|
||||
isActive: boolean;
|
||||
/** Controls if tab can be edited. */
|
||||
isEditable?: boolean;
|
||||
/** Can be passed to control editing state externally. */
|
||||
isEditing?: boolean;
|
||||
/** Shows loading indicator in place of an icon. */
|
||||
isLoading: boolean;
|
||||
/** The name of the tab. */
|
||||
name: string;
|
||||
|
||||
/** Callback when the tab is clicked. */
|
||||
onClick: () => void;
|
||||
/** Callback when tab is closed. */
|
||||
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;
|
||||
/** Function to validate the name. */
|
||||
validateName: (name: string) => string | null;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export const EditableEntityName = (props: EditableEntityNameProps) => {
|
|||
}
|
||||
|
||||
return icon;
|
||||
}, [isLoading, icon]);
|
||||
}, [isLoading, icon, size]);
|
||||
|
||||
const inputProps = useMemo(
|
||||
() => ({
|
||||
|
|
|
|||
|
|
@ -1,15 +1,26 @@
|
|||
import type React from "react";
|
||||
|
||||
export interface EditableEntityNameProps {
|
||||
icon: React.ReactNode;
|
||||
inputTestId?: string;
|
||||
isEditing: boolean;
|
||||
isLoading?: boolean;
|
||||
name: string;
|
||||
onExitEditing: () => void;
|
||||
onNameSave: (name: string) => void;
|
||||
validateName: (name: string) => string | null;
|
||||
/** Controls if name can be edited. */
|
||||
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;
|
||||
/** 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";
|
||||
/** 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 { 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> = {
|
||||
title: "ADS/Templates/Entity Tabs Header",
|
||||
|
|
@ -25,7 +30,7 @@ const Template = ({ width }: Args) => {
|
|||
<div style={{ width }}>
|
||||
<EntityTabsHeader>
|
||||
<EntityListButton onClick={console.log} />
|
||||
<DismissibleTabBar onTabAdd={console.log}>
|
||||
<EntityTabBar onTabAdd={console.log}>
|
||||
<DismissibleTab onClick={console.log} onClose={console.log}>
|
||||
One
|
||||
</DismissibleTab>
|
||||
|
|
@ -41,7 +46,7 @@ const Template = ({ width }: Args) => {
|
|||
<DismissibleTab onClick={console.log} onClose={console.log}>
|
||||
Five
|
||||
</DismissibleTab>
|
||||
</DismissibleTabBar>
|
||||
</EntityTabBar>
|
||||
<ToggleScreenModeButton
|
||||
isInSplitScreenMode={isInSplitScreenMode}
|
||||
onClick={toggle}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import styled from "styled-components";
|
||||
import { Button } from "../..";
|
||||
import { Button } from "../../Button";
|
||||
import { DismissibleTabBar } from "../../DismissibleTab";
|
||||
|
||||
export const Root = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -11,7 +12,6 @@ export const Root = styled.div`
|
|||
min-height: 32px;
|
||||
padding: 0 var(--ads-v2-spaces-2);
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const IconButton = styled(Button)`
|
||||
|
|
@ -20,3 +20,7 @@ export const IconButton = styled(Button)`
|
|||
min-width: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TabBar = styled(DismissibleTabBar)`
|
||||
margin-bottom: -1px;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import type {
|
|||
ToggleScreenModeButtonProps,
|
||||
} from "./EntityTabsHeader.types";
|
||||
|
||||
import { ToggleButton } from "../..";
|
||||
import { ToggleButton } from "../../ToggleButton";
|
||||
import { type DismissibleTabBarProps } from "../../DismissibleTab/DismissibleTabBar.types";
|
||||
|
||||
export const EntityListButton = (props: EntityListButtonProps) => {
|
||||
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) {
|
||||
return <Styled.Root>{children}</Styled.Root>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import type { ReactElement, ReactNode } from "react";
|
||||
import type {
|
||||
ToggleButtonProps,
|
||||
ButtonProps,
|
||||
DismissibleTabBarProps,
|
||||
} from "../..";
|
||||
|
||||
import type { DismissibleTabBarProps } from "../../DismissibleTab";
|
||||
import type { ToggleButtonProps } from "../../ToggleButton";
|
||||
import type { ButtonProps } from "../../Button";
|
||||
|
||||
export type EntityListButtonProps = Omit<ToggleButtonProps, "icon" | "size">;
|
||||
|
||||
|
|
@ -17,11 +16,15 @@ export type ToggleScreenModeButtonProps = Omit<
|
|||
type DismissibleTabBarType = ReactElement<DismissibleTabBarProps>;
|
||||
type EntityListButtonType = ReactElement<EntityListButtonProps>;
|
||||
|
||||
/** Required for optional/conditional children. */
|
||||
type OptionalChild<T> = T | null | false;
|
||||
|
||||
export interface EntityTabsHeaderProps {
|
||||
children:
|
||||
| DismissibleTabBarType
|
||||
| [DismissibleTabBarType]
|
||||
| [EntityListButtonType, DismissibleTabBarType]
|
||||
| [DismissibleTabBarType, ReactNode]
|
||||
| [EntityListButtonType, DismissibleTabBarType, ReactNode];
|
||||
| [
|
||||
OptionalChild<EntityListButtonType>,
|
||||
DismissibleTabBarType,
|
||||
OptionalChild<ReactNode>,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export {
|
||||
EntityTabsHeader,
|
||||
EntityListButton,
|
||||
EntityTabBar,
|
||||
ToggleScreenModeButton,
|
||||
} from "./EntityTabsHeader";
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ export * from "./EntityExplorer";
|
|||
export * from "./Sidebar";
|
||||
export * from "./EditableEntityName";
|
||||
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 { useEventCallback } from "usehooks-ts";
|
||||
|
||||
import { DismissibleTab, Text } from "@appsmith/ads";
|
||||
|
||||
import { FileTab } from "IDE/Components/FileTab";
|
||||
import { useCurrentEditorState } from "../hooks";
|
||||
import {
|
||||
EditorEntityTab,
|
||||
EditorEntityTabState,
|
||||
} from "ee/entities/IDE/constants";
|
||||
import { Text } from "@appsmith/ads";
|
||||
|
||||
import { useCurrentEditorState } from "../hooks";
|
||||
|
||||
const AddTab = ({
|
||||
isListActive,
|
||||
|
|
@ -19,24 +21,26 @@ const AddTab = ({
|
|||
}) => {
|
||||
const { segment, segmentMode } = useCurrentEditorState();
|
||||
|
||||
if (segmentMode !== EditorEntityTabState.Add) return null;
|
||||
|
||||
const onCloseClick = (e: React.MouseEvent) => {
|
||||
const onCloseClick = useEventCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
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 (
|
||||
<FileTab
|
||||
<DismissibleTab
|
||||
dataTestId={dataTestId}
|
||||
isActive={segmentMode === EditorEntityTabState.Add && !isListActive}
|
||||
onClick={newTabClickCallback}
|
||||
onClose={(e) => onCloseClick(e)}
|
||||
title={content}
|
||||
onClose={onCloseClick}
|
||||
>
|
||||
<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 { FileTab } from "IDE/Components/FileTab";
|
||||
import { type EntityItem } from "ee/entities/IDE/constants";
|
||||
import { useCurrentEditorState } from "../hooks";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
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 { getIsSavingEntityName } from "ee/selectors/entitiesSelector";
|
||||
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { sanitizeString } from "utils/URLUtils";
|
||||
|
||||
import {
|
||||
getEditableTabPermissions,
|
||||
saveEntityName,
|
||||
} from "ee/entities/IDE/utils";
|
||||
import { noop } from "lodash";
|
||||
import { EditableName, useIsRenaming } from "IDE";
|
||||
import { IconContainer } from "IDE/Components/FileTab/styles";
|
||||
import { useIsRenaming, useValidateEntityName } from "IDE";
|
||||
|
||||
import { useCurrentEditorState } from "../hooks";
|
||||
|
||||
interface EditableTabProps {
|
||||
id: string;
|
||||
|
|
@ -30,6 +32,7 @@ interface EditableTabProps {
|
|||
export function EditableTab(props: EditableTabProps) {
|
||||
const { entity, icon, id, isActive, onClick, onClose, title } = props;
|
||||
const { segment } = useCurrentEditorState();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
||||
const isChangePermitted = getEditableTabPermissions({
|
||||
|
|
@ -43,39 +46,36 @@ export function EditableTab(props: EditableTabProps) {
|
|||
getIsSavingEntityName(state, { id, segment, entity }),
|
||||
);
|
||||
|
||||
const handleClose = useEventCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onClose(id);
|
||||
const validateName = useValidateEntityName({
|
||||
entityName: title,
|
||||
});
|
||||
|
||||
const handleDoubleClick = isChangePermitted ? enterEditMode : noop;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const handleClose = useEventCallback(() => {
|
||||
onClose(id);
|
||||
});
|
||||
|
||||
const handleNameSave = useCallback(
|
||||
(name: string) => {
|
||||
dispatch(saveEntityName({ params: { id, name }, segment, entity }));
|
||||
exitEditMode();
|
||||
},
|
||||
[dispatch, entity, exitEditMode, id, segment],
|
||||
[dispatch, entity, id, segment],
|
||||
);
|
||||
|
||||
return (
|
||||
<FileTab
|
||||
<EditableDismissibleTab
|
||||
dataTestId={`t--ide-tab-${sanitizeString(title)}`}
|
||||
icon={icon}
|
||||
isActive={isActive}
|
||||
isEditable={isChangePermitted}
|
||||
isEditing={isEditing}
|
||||
isLoading={isLoading}
|
||||
name={title}
|
||||
onClick={onClick}
|
||||
onClose={handleClose}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
title={title}
|
||||
>
|
||||
<EditableName
|
||||
exitEditing={exitEditMode}
|
||||
icon={<IconContainer>{icon}</IconContainer>}
|
||||
isEditing={isEditing}
|
||||
isLoading={isLoading}
|
||||
name={title}
|
||||
onNameSave={handleNameSave}
|
||||
/>
|
||||
</FileTab>
|
||||
onEnterEditMode={enterEditMode}
|
||||
onExitEditMode={exitEditMode}
|
||||
onNameSave={handleNameSave}
|
||||
validateName={validateName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,14 +37,13 @@ describe("EditorTabs render checks", () => {
|
|||
|
||||
it("Renders correctly in split view", () => {
|
||||
const state = getIDETestState({ ideView: EditorViewMode.SplitScreen });
|
||||
const { getByTestId, queryByTestId } = renderComponent(
|
||||
const { getByTestId, queryAllByRole, queryByTestId } = renderComponent(
|
||||
`/app/applicationSlug/pageSlug-${page.basePageId}/edit/queries`,
|
||||
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
|
||||
expect(queryByTestId("t--ide-tabs-add-button")).toBeNull();
|
||||
|
|
@ -58,7 +57,7 @@ describe("EditorTabs render checks", () => {
|
|||
|
||||
it("Renders correctly in fullscreen view", () => {
|
||||
const state = getIDETestState({ ideView: EditorViewMode.FullScreen });
|
||||
const { getByTestId, queryByTestId } = renderComponent(
|
||||
const { getByTestId, queryAllByRole, queryByTestId } = renderComponent(
|
||||
`/app/applicationSlug/pageSlug-${page.basePageId}/edit/queries`,
|
||||
state,
|
||||
);
|
||||
|
|
@ -66,10 +65,8 @@ describe("EditorTabs render checks", () => {
|
|||
// check toggle
|
||||
expect(queryByTestId("t--list-toggle")).toBeNull();
|
||||
|
||||
// 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
|
||||
expect(queryByTestId("t--ide-tabs-add-button")).toBeNull();
|
||||
|
|
|
|||
|
|
@ -37,10 +37,3 @@ export const TabSelectors: Record<
|
|||
itemUrlSelector: () => "",
|
||||
},
|
||||
};
|
||||
|
||||
export const SCROLL_AREA_OPTIONS = {
|
||||
overflow: {
|
||||
x: "scroll",
|
||||
y: "hidden",
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import React, { useCallback, useEffect } from "react";
|
||||
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 type { EntityItem } from "ee/entities/IDE/constants";
|
||||
import {
|
||||
|
|
@ -8,27 +16,25 @@ import {
|
|||
EditorEntityTabState,
|
||||
EditorViewMode,
|
||||
} 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 {
|
||||
useCurrentEditorState,
|
||||
useIDETabClickHandlers,
|
||||
useShowSideBySideNudge,
|
||||
} 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 { ScreenModeToggle } from "./ScreenModeToggle";
|
||||
import { AddTab } from "./AddTab";
|
||||
import { setListViewActiveState } from "actions/ideActions";
|
||||
|
||||
import { useEventCallback } from "usehooks-ts";
|
||||
|
||||
import { EditableTab } from "./EditableTab";
|
||||
import { TabSelectors } from "./constants";
|
||||
import { AddTab } from "./AddTab";
|
||||
|
||||
const EditorTabs = () => {
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const ideViewMode = useSelector(getIDEViewMode);
|
||||
const { segment, segmentMode } = useCurrentEditorState();
|
||||
const { closeClickHandler, tabClickHandler } = useIDETabClickHandlers();
|
||||
|
|
@ -37,46 +43,29 @@ const EditorTabs = () => {
|
|||
const files = useSelector(tabsConfig.tabsSelector, shallowEqual);
|
||||
const isListViewActive = useSelector(getListViewActiveState);
|
||||
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 showEntityListButton =
|
||||
ideViewMode === EditorViewMode.SplitScreen && files.length > 0;
|
||||
|
||||
// Turn off list view while changing segment, files
|
||||
useEffect(() => {
|
||||
dispatch(setListViewActiveState(false));
|
||||
}, [currentEntity.id, currentEntity.entity, files, segmentMode, dispatch]);
|
||||
useEffect(
|
||||
function turnOffListViewWhileChangingSegmentFiles() {
|
||||
dispatch(setListViewActiveState(false));
|
||||
},
|
||||
[currentEntity.id, currentEntity.entity, files, segmentMode, dispatch],
|
||||
);
|
||||
|
||||
// Show list view if all tabs is closed
|
||||
useEffect(() => {
|
||||
if (files.length === 0 && segmentMode !== EditorEntityTabState.Add) {
|
||||
dispatch(setListViewActiveState(true));
|
||||
}
|
||||
}, [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]);
|
||||
useEffect(
|
||||
function showListViewIfAllTabsAreClosed() {
|
||||
if (files.length === 0 && segmentMode !== EditorEntityTabState.Add) {
|
||||
dispatch(setListViewActiveState(true));
|
||||
}
|
||||
},
|
||||
[files, segmentMode, currentEntity.entity, dispatch],
|
||||
);
|
||||
|
||||
const handleHamburgerClick = useEventCallback(() => {
|
||||
if (files.length === 0 && segmentMode !== EditorEntityTabState.Add) return;
|
||||
|
|
@ -101,61 +90,49 @@ const EditorTabs = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
{ideViewMode === EditorViewMode.SplitScreen && files.length > 0 ? (
|
||||
<ToggleButton
|
||||
<EntityTabsHeader>
|
||||
{showEntityListButton && (
|
||||
<EntityListButton
|
||||
data-testid="t--list-toggle"
|
||||
icon="hamburger"
|
||||
isSelected={isListViewActive}
|
||||
onClick={handleHamburgerClick}
|
||||
size="md"
|
||||
/>
|
||||
) : null}
|
||||
<ScrollArea
|
||||
className="h-[32px] top-[0.5px]"
|
||||
data-testid="t--editor-tabs"
|
||||
options={SCROLL_AREA_OPTIONS}
|
||||
size="sm"
|
||||
)}
|
||||
|
||||
<EntityTabBar
|
||||
hideAdd={hideAdd}
|
||||
isAddingNewTab={isJSLoading}
|
||||
onTabAdd={addClickHandler}
|
||||
>
|
||||
<Flex
|
||||
className="items-center"
|
||||
data-testid="t--tabs-container"
|
||||
gap="spaces-2"
|
||||
height="100%"
|
||||
>
|
||||
{files.map((tab) => {
|
||||
const entity = entities.find((entity) => entity.key === tab.key);
|
||||
{files.map((tab) => {
|
||||
const entity = entities.find((entity) => entity.key === tab.key);
|
||||
|
||||
return (
|
||||
<EditableTab
|
||||
entity={entity}
|
||||
icon={tab.icon}
|
||||
id={tab.key}
|
||||
isActive={
|
||||
currentEntity.id === tab.key &&
|
||||
segmentMode !== EditorEntityTabState.Add &&
|
||||
!isListViewActive
|
||||
}
|
||||
key={tab.key}
|
||||
onClick={handleTabClick(tab)}
|
||||
onClose={closeClickHandler}
|
||||
title={tab.title}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<AddTab
|
||||
isListActive={isListViewActive}
|
||||
newTabClickCallback={handleNewTabClick}
|
||||
onClose={closeClickHandler}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollArea>
|
||||
{files.length > 0 ? <AddButton /> : null}
|
||||
{/* Switch screen mode button */}
|
||||
return (
|
||||
<EditableTab
|
||||
entity={entity}
|
||||
icon={tab.icon}
|
||||
id={tab.key}
|
||||
isActive={
|
||||
currentEntity.id === tab.key &&
|
||||
segmentMode !== EditorEntityTabState.Add &&
|
||||
!isListViewActive
|
||||
}
|
||||
key={tab.key}
|
||||
onClick={handleTabClick(tab)}
|
||||
onClose={closeClickHandler}
|
||||
title={tab.title}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<AddTab
|
||||
isListActive={isListViewActive}
|
||||
newTabClickCallback={handleNewTabClick}
|
||||
onClose={closeClickHandler}
|
||||
/>
|
||||
</EntityTabBar>
|
||||
<ScreenModeToggle dismissNudge={dismissNudge} showNudge={showNudge} />
|
||||
</Container>
|
||||
</EntityTabsHeader>
|
||||
|
||||
{/* Overflow list */}
|
||||
{isListViewActive && ideViewMode === EditorViewMode.SplitScreen && (
|
||||
<List />
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user