From 0af459cc5e9c1e2d7a7d12a70bf0b100170ac56e Mon Sep 17 00:00:00 2001 From: Sagar Khalasi Date: Wed, 29 Jan 2025 17:46:54 +0530 Subject: [PATCH 1/2] fix: Fix for url redirection (#38884) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description > [!TIP] > _Add a TL;DR when the description is longer than 500 words or extremely technical (helps the content, marketing, and DevRel team)._ > > _Please also include relevant motivation and context. List any dependencies that are required for this change. Add links to Notion, Figma or any other documents that might be relevant to the PR._ Fixes #`Issue Number` _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## Automation /ok-to-test tags="@tag.AppUrl" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: fafc24c72c7353410d21e483e945047f1a3b0814 > Cypress dashboard. > Tags: `@tag.AppUrl` > Spec: >
Wed, 29 Jan 2025 05:18:48 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No ## Summary by CodeRabbit - **Tests** - Updated test suite for Slug URLs with a new tag `@tag.AppUrl` - Improved URL handling and test code readability in application URL tests --- .../ClientSide/OtherUIFeatures/ApplicationURL_spec.js | 9 ++++----- app/client/cypress/tags.js | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/client/cypress/e2e/Regression/ClientSide/OtherUIFeatures/ApplicationURL_spec.js b/app/client/cypress/e2e/Regression/ClientSide/OtherUIFeatures/ApplicationURL_spec.js index b7116e288f4e..c0abbd8e1dce 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/OtherUIFeatures/ApplicationURL_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/OtherUIFeatures/ApplicationURL_spec.js @@ -9,7 +9,7 @@ import { } from "../../../../support/Objects/ObjectsCore"; import { EntityItems } from "../../../../support/Pages/AssertHelper"; -describe("Slug URLs", () => { +describe("Slug URLs", { tags: ["@tag.AppUrl"] }, () => { let applicationName; let applicationId; @@ -145,10 +145,9 @@ describe("Slug URLs", () => { it("4. Checks redirect url", () => { cy.url().then((url) => { homePage.Signout(true); - agHelper.VisitNAssert(url + "?embed=true&a=b"); //removing 'getConsolidatedData' api check due to its flakyness - agHelper.AssertURL( - `?redirectUrl=${encodeURIComponent(url + "?embed=true&a=b")}`, - ); + const redirectUrl = `${url}?embed=true&a=b`; + agHelper.VisitNAssert(redirectUrl); + agHelper.AssertURL(`?redirectUrl=${encodeURIComponent(redirectUrl)}`); }); }); }); diff --git a/app/client/cypress/tags.js b/app/client/cypress/tags.js index 09843e88d6db..cecb780bf32a 100644 --- a/app/client/cypress/tags.js +++ b/app/client/cypress/tags.js @@ -4,6 +4,7 @@ module.exports = { "@tag.Anvil", "@tag.Audio", "@tag.Auditlogs", + "@tag.AppUrl", "@tag.Authentication", "@tag.AutoHeight", "@tag.Binding", From 3ab237de2b525b83834841dc6360da40ed16ccac Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 29 Jan 2025 16:34:24 +0300 Subject: [PATCH 2/2] feat: scrollable tabs list (#38855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Adding a scrollable list template component for DismissibleTab. Fixes #37692 ## Automation /ok-to-test tags="@tag.Sanity" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: 2007e0891e29e60c82f36f090e1daca029808bf1 > Cypress dashboard. > Tags: `@tag.Sanity` > Spec: >
Wed, 29 Jan 2025 10:42:35 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **New Features** - Introduced a new `DismissibleTabBar` component with advanced tab management capabilities. - Added ability to dynamically add and close tabs. - Implemented scrollable tab bar with sticky add button functionality. - **Improvements** - Enhanced event handling for tab close actions. - Improved component flexibility with ref forwarding in `ScrollArea`. - **Design System Updates** - Created new styled components for tab bar layout. - Added TypeScript interfaces for better type safety. - New Storybook story for `DismissibleTabBar` to showcase functionality. --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../ads/src/DismissibleTab/DismissibleTab.tsx | 13 +- .../DismissibleTabBar.stories.tsx | 104 ++++++++++++++++ .../DismissibleTabBar.styles.ts | 55 +++++++++ .../src/DismissibleTab/DismissibleTabBar.tsx | 114 ++++++++++++++++++ .../DismissibleTab/DismissibleTabBar.types.ts | 10 ++ .../ads/src/DismissibleTab/index.ts | 2 + .../ads/src/ScrollArea/ScrollArea.tsx | 94 ++++++++------- 7 files changed, 347 insertions(+), 45 deletions(-) create mode 100644 app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.stories.tsx create mode 100644 app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.styles.ts create mode 100644 app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.tsx create mode 100644 app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.types.ts diff --git a/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.tsx b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.tsx index 78a05a887bbf..f424baddb8d1 100644 --- a/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.tsx +++ b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTab.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useCallback } from "react"; import clsx from "classnames"; @@ -17,6 +17,15 @@ export const DismissibleTab = ({ onClose, onDoubleClick, }: DismissibleTabProps) => { + const handleClose = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + onClose(e); + }, + [onClose], + ); + return ( = { + title: "ADS/Components/Dismissible Tab Bar", + component: DismissibleTabBar, +}; + +export default meta; + +const createTabs = (count: number) => { + const tabs = []; + + for (let i = 1; i <= count; i++) { + tabs.push({ id: `tab${i}`, label: `Tab ${i}` }); + } + + return tabs; +}; + +const INITIAL_TAB_COUNT = 5; +const initialTabs = createTabs(INITIAL_TAB_COUNT); + +interface StoryProps extends DismissibleTabBarProps { + containerWidth: number; +} + +const Template = (props: StoryProps) => { + const { containerWidth, disableAdd } = props; + + const tabCountRef = useRef(INITIAL_TAB_COUNT); + const [tabs, setTabs] = useState(initialTabs); + const [activeTabId, setActiveTabId] = useState(initialTabs[0].id); + + const handleClose = (tabId: string) => () => { + const closedTabIndex = tabs.findIndex((tab) => tab.id === tabId); + const filteredTabs = tabs.filter((tab) => tab.id !== tabId); + + setTabs(filteredTabs); + + if (activeTabId === tabId && filteredTabs.length) { + if (closedTabIndex >= filteredTabs.length) { + const nextIndex = Math.max(0, closedTabIndex - 1); + const nextTab = filteredTabs[nextIndex]; + + setActiveTabId(nextTab.id); + } else { + const nextTab = filteredTabs[closedTabIndex]; + + setActiveTabId(nextTab.id); + } + } + }; + + const handleClick = (tabId: string) => () => { + setActiveTabId(tabId); + }; + + const handleTabAdd = () => { + const tabNumber = ++tabCountRef.current; + const tabId = `tab${tabNumber}`; + const nextTabs = [...tabs, { id: tabId, label: `Tab ${tabNumber}` }]; + + setTabs(nextTabs); + setActiveTabId(tabId); + }; + + return ( +
+ + {tabs.map((tab) => ( + + {tab.label} + + ))} + +
+ ); +}; + +export const Basic = Template.bind({}) as StoryObj; + +Basic.argTypes = { + containerWidth: { + control: { type: "range", min: 200, max: 600, step: 10 }, + }, +}; + +Basic.args = { + disableAdd: false, + containerWidth: 450, +}; diff --git a/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.styles.ts b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.styles.ts new file mode 100644 index 000000000000..92109d5c3a86 --- /dev/null +++ b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.styles.ts @@ -0,0 +1,55 @@ +import styled, { css } from "styled-components"; + +import { Button } from ".."; + +export const animatedLeftBorder = (showLeftBorder: boolean) => css` + transition: border-color 0.5s ease; + border-left: 1px solid transparent; + + border-left-color: ${showLeftBorder + ? "var(--ads-v2-color-border-muted)" + : "transparent"}; +`; + +export const Root = styled.div<{ + $showLeftBorder?: boolean; +}>` + display: flex; + align-items: center; + overflow: hidden; + white-space: nowrap; + position: relative; + height: 32px; + + ${({ $showLeftBorder }) => animatedLeftBorder($showLeftBorder ?? false)}; +`; + +export const TabsContainer = styled.div` + display: flex; + flex: 1 0 auto; + align-items: center; + gap: var(--ads-v2-spaces-2); + height: 100%; +`; + +export const StickySentinel = styled.div` + width: 1px; + height: 100%; +`; + +export const PlusButtonContainer = styled.div<{ $showLeftBorder?: boolean }>` + position: sticky; + right: 0; + border: none; + min-width: 32px; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + + ${({ $showLeftBorder }) => animatedLeftBorder($showLeftBorder ?? false)}; +`; + +export const PlusButton = styled(Button)` + min-width: 24px; +`; diff --git a/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.tsx b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.tsx new file mode 100644 index 000000000000..08e1dfc145f3 --- /dev/null +++ b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useRef, useState } from "react"; +import { noop } from "lodash"; + +import { ScrollArea } from "../ScrollArea"; + +import * as Styled from "./DismissibleTabBar.styles"; +import type { DismissibleTabBarProps } from "./DismissibleTabBar.types"; + +export const SCROLL_AREA_OPTIONS = { + overflow: { + x: "scroll", + y: "hidden", + }, +} as const; + +const SCROLL_AREA_STYLE = { + height: 34, + top: 1, +}; + +export const DismissibleTabBar = ({ + children, + disableAdd = false, + onTabAdd, +}: DismissibleTabBarProps) => { + const [isLeftIntersecting, setIsLeftIntersecting] = useState(false); + const [isRightIntersecting, setIsRightIntersecting] = useState(false); + + const containerRef = useRef(null); + const sentinelLeftRef = useRef(null); + const sentinelRightRef = useRef(null); + + const handleAdd = disableAdd ? noop : onTabAdd; + + useEffect(function observeSticky() { + if ( + !containerRef.current || + !sentinelLeftRef.current || + !sentinelRightRef.current + ) + return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.target === sentinelLeftRef.current) { + setIsLeftIntersecting(!entry.isIntersecting); + } + + if (entry.target === sentinelRightRef.current) { + setIsRightIntersecting(!entry.isIntersecting); + } + }); + }, + { + root: containerRef.current, + threshold: 1.0, + }, + ); + + observer.observe(sentinelLeftRef.current); + observer.observe(sentinelRightRef.current); + + return () => observer.disconnect(); + }, []); + + useEffect( + function debouncedScrollActiveTabIntoView() { + const timerId = setTimeout(() => { + // accessing active tab with a document query is a bit hacky, but it's more performant than keeping a map of refs and cloning children + const activeTab = document.querySelector(".editor-tab.active"); + + if (activeTab) { + activeTab.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }); + } + }, 100); + + return () => clearTimeout(timerId); + }, + [children], + ); + + return ( + + + + + {children} + + + + + + + + ); +}; diff --git a/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.types.ts b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.types.ts new file mode 100644 index 000000000000..990400b59566 --- /dev/null +++ b/app/client/packages/design-system/ads/src/DismissibleTab/DismissibleTabBar.types.ts @@ -0,0 +1,10 @@ +import type React from "react"; +import type { DismissibleTabProps } from "./DismissibleTab.types"; + +export interface DismissibleTabBarProps { + children: + | React.ReactElement + | React.ReactElement[]; + onTabAdd: () => void; + disableAdd?: boolean; +} diff --git a/app/client/packages/design-system/ads/src/DismissibleTab/index.ts b/app/client/packages/design-system/ads/src/DismissibleTab/index.ts index f00320299ac8..5923acefd8f9 100644 --- a/app/client/packages/design-system/ads/src/DismissibleTab/index.ts +++ b/app/client/packages/design-system/ads/src/DismissibleTab/index.ts @@ -1,2 +1,4 @@ export { DismissibleTab } from "./DismissibleTab"; export type { DismissibleTabProps } from "./DismissibleTab.types"; +export { DismissibleTabBar } from "./DismissibleTabBar"; +export type { DismissibleTabBarProps } from "./DismissibleTabBar.types"; diff --git a/app/client/packages/design-system/ads/src/ScrollArea/ScrollArea.tsx b/app/client/packages/design-system/ads/src/ScrollArea/ScrollArea.tsx index d36926aa4ce6..ff959019d12a 100644 --- a/app/client/packages/design-system/ads/src/ScrollArea/ScrollArea.tsx +++ b/app/client/packages/design-system/ads/src/ScrollArea/ScrollArea.tsx @@ -8,49 +8,57 @@ import "./styles.css"; import type { ScrollAreaProps } from "./ScrollArea.types"; -function ScrollArea(props: ScrollAreaProps) { - const ref = useRef(null); - const { - children, - className, - defer, - events, - options, - size = "md", - ...rest - } = props; - const defaultOptions: UseOverlayScrollbarsParams["options"] = { - scrollbars: { - theme: "ads-v2-scroll-theme", - autoHide: "scroll", - }, - ...options, - }; - const [initialize] = useOverlayScrollbars({ - options: defaultOptions, - events, - defer, - }); - - useEffect(() => { - if (ref.current) initialize(ref.current); - }, [initialize]); - - return ( -
- {children} -
- ); -} +const ScrollArea = React.forwardRef( + (props, ref) => { + const localRef = useRef(null); + const { + children, + className, + defer, + events, + options, + size = "md", + ...rest + } = props; + const defaultOptions: UseOverlayScrollbarsParams["options"] = { + scrollbars: { + theme: "ads-v2-scroll-theme", + autoHide: "scroll", + }, + ...options, + }; + const [initialize] = useOverlayScrollbars({ + options: defaultOptions, + events, + defer, + }); + + useEffect( + function init() { + const currentRef = + (typeof ref === "function" ? null : ref?.current) || localRef.current; + + if (currentRef) initialize(currentRef); + }, + [initialize, ref], + ); + + return ( +
+ {children} +
+ ); + }, +); ScrollArea.displayName = "ScrollArea";