From 4e4cf27495e6c5f59ae389774ad8c7e098bca6a6 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 4 Mar 2025 13:48:40 +0100 Subject: [PATCH 1/7] [Discover] Disable closing the last tab --- .../shared/kbn-unified-tabs/README.md | 2 +- .../src/components/tab/tab.tsx | 34 +++++++++++-------- .../tabbed_content/tabbed_content.tsx | 2 +- .../src/components/tabs_bar/tabs_bar.tsx | 2 +- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/platform/packages/shared/kbn-unified-tabs/README.md b/src/platform/packages/shared/kbn-unified-tabs/README.md index 461b6e0fe834f..f9006b60cbe59 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/README.md +++ b/src/platform/packages/shared/kbn-unified-tabs/README.md @@ -5,7 +5,7 @@ Tabs bar components. ## Storybook Run the following command: -`NODE_OPTIONS="--openssl-legacy-provider" node scripts/storybook unified_tabs`. +`yarn storybook unified_tabs`. ## Example plugin diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.tsx index 32418db39f1f4..2ed35b2b5d4f8 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.tsx +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { MouseEvent, useCallback } from 'react'; +import React, { MouseEvent, useCallback, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; import { @@ -26,11 +26,12 @@ export interface TabProps { isSelected: boolean; tabContentId: string; onSelect: (item: TabItem) => void; - onClose: (item: TabItem) => void; + onClose: ((item: TabItem) => void) | undefined; } export const Tab: React.FC = ({ item, isSelected, tabContentId, onSelect, onClose }) => { const { euiTheme } = useEuiTheme(); + const containerRef = useRef(); const tabContainerDataTestSubj = `unifiedTabs_tab_${item.id}`; const closeButtonLabel = i18n.translate('unifiedTabs.closeTabButton', { @@ -51,23 +52,24 @@ export const Tab: React.FC = ({ item, isSelected, tabContentId, onSele const onCloseEvent = useCallback( (event: MouseEvent) => { event.stopPropagation(); - onClose(item); + onClose?.(item); }, [onClose, item] ); const onClickEvent = useCallback( (event: MouseEvent) => { - if (event.currentTarget.getAttribute('data-test-subj') === tabContainerDataTestSubj) { + if (event.currentTarget === containerRef.current) { // if user presses on the space around the buttons, we should still trigger the onSelectEvent onSelectEvent(event); } }, - [onSelectEvent, tabContainerDataTestSubj] + [onSelectEvent] ); return ( = ({ item, isSelected, tabContentId, onSele {item.label} - - - + {Boolean(onClose) && ( + + + + )} ); }; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx index 5f35fbb2c753f..d42c229308f60 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx @@ -49,7 +49,7 @@ export const TabbedContent: React.FC = ({ (getNextState: (prevState: TabbedContentState) => TabbedContentState) => { _setState((prevState) => { const nextState = getNextState(prevState); - onChanged(nextState); + setTimeout(() => onChanged(nextState), 0); return nextState; }); }, diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.tsx index 0ba5439106aa1..fe36aad42973e 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.tsx +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.tsx @@ -57,7 +57,7 @@ export const TabsBar: React.FC = ({ isSelected={selectedItem?.id === item.id} tabContentId={tabContentId} onSelect={onSelect} - onClose={onClose} + onClose={items.length > 1 ? onClose : undefined} // prevents closing the last tab /> ))} From ef0a8aba3eb8ae8251cd4a69cac6b9715b6e7b43 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 4 Mar 2025 15:04:41 +0100 Subject: [PATCH 2/7] [Discover] Create tab menu component --- .../src/components/tab/tab.tsx | 59 ++++++++----- .../src/components/tab_menu/index.ts | 10 +++ .../src/components/tab_menu/tab_menu.tsx | 82 +++++++++++++++++++ .../tabbed_content/tabbed_content.tsx | 12 ++- .../src/components/tabs_bar/tabs_bar.tsx | 14 ++-- .../shared/kbn-unified-tabs/src/types.ts | 10 +++ .../src/utils/get_tab_menu_actions.tsx | 72 ++++++++++++++++ 7 files changed, 232 insertions(+), 27 deletions(-) create mode 100644 src/platform/packages/shared/kbn-unified-tabs/src/components/tab_menu/index.ts create mode 100644 src/platform/packages/shared/kbn-unified-tabs/src/components/tab_menu/tab_menu.tsx create mode 100644 src/platform/packages/shared/kbn-unified-tabs/src/utils/get_tab_menu_actions.tsx diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.tsx index 2ed35b2b5d4f8..01822dcd1ed4d 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.tsx +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.tsx @@ -18,18 +18,27 @@ import { EuiThemeComputed, useEuiTheme, } from '@elastic/eui'; +import { TabMenu } from '../tab_menu'; import { getTabAttributes } from '../../utils/get_tab_attributes'; -import type { TabItem } from '../../types'; +import type { TabItem, GetTabMenuItems } from '../../types'; export interface TabProps { item: TabItem; isSelected: boolean; tabContentId: string; + getTabMenuItems?: GetTabMenuItems; onSelect: (item: TabItem) => void; onClose: ((item: TabItem) => void) | undefined; } -export const Tab: React.FC = ({ item, isSelected, tabContentId, onSelect, onClose }) => { +export const Tab: React.FC = ({ + item, + isSelected, + tabContentId, + getTabMenuItems, + onSelect, + onClose, +}) => { const { euiTheme } = useEuiTheme(); const containerRef = useRef(); @@ -91,18 +100,27 @@ export const Tab: React.FC = ({ item, isSelected, tabContentId, onSele {item.label} - {Boolean(onClose) && ( - - - - )} + + + {!!getTabMenuItems && ( + + + + )} + {!!onClose && ( + + + + )} + + ); }; @@ -115,8 +133,7 @@ function getTabContainerCss(euiTheme: EuiThemeComputed, isSelected: boolean) { border-right: ${euiTheme.border.thin}; border-color: ${euiTheme.colors.lightShade}; height: ${euiTheme.size.xl}; - padding-left: ${euiTheme.size.m}; - padding-right: ${euiTheme.size.xs}; + padding-inline: ${euiTheme.size.xs}; min-width: 96px; max-width: 280px; @@ -124,13 +141,14 @@ function getTabContainerCss(euiTheme: EuiThemeComputed, isSelected: boolean) { color: ${isSelected ? euiTheme.colors.text : euiTheme.colors.subduedText}; transition: background-color ${euiTheme.animation.fast}; - .unifiedTabs__closeTabBtn { + .unifiedTabs__tabActions { opacity: 0; transition: opacity ${euiTheme.animation.fast}; } - &:hover { - .unifiedTabs__closeTabBtn { + &:hover, + &:focus-within { + .unifiedTabs__tabActions { opacity: 1; } } @@ -153,9 +171,10 @@ function getTabContainerCss(euiTheme: EuiThemeComputed, isSelected: boolean) { function getTabButtonCss(euiTheme: EuiThemeComputed) { return css` width: 100%; + min-height: 100%; min-width: 0; flex-grow: 1; - padding-right: ${euiTheme.size.xs}; + padding-inline: ${euiTheme.size.xs}; text-align: left; color: inherit; border: none; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tab_menu/index.ts b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab_menu/index.ts new file mode 100644 index 0000000000000..bb842a4444504 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab_menu/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export { TabMenu, type TabMenuProps } from './tab_menu'; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tab_menu/tab_menu.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab_menu/tab_menu.tsx new file mode 100644 index 0000000000000..c9fe45bcfc5e8 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab_menu/tab_menu.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiHorizontalRule, + EuiPopover, + useGeneratedHtmlId, +} from '@elastic/eui'; +import type { TabItem, GetTabMenuItems } from '../../types'; + +export interface TabMenuProps { + item: TabItem; + getTabMenuItems: GetTabMenuItems; +} + +export const TabMenu: React.FC = ({ item, getTabMenuItems }) => { + const [isPopoverOpen, setPopover] = useState(false); + const contextMenuPopoverId = useGeneratedHtmlId(); + + const menuButtonLabel = i18n.translate('unifiedTabs.tabMenuButton', { + defaultMessage: 'Actions', + }); + + const closePopover = useCallback(() => { + setPopover(false); + }, [setPopover]); + + const panelItems = useMemo(() => { + const itemConfigs = getTabMenuItems(item); + + return itemConfigs.map((itemConfig, index) => { + if (itemConfig === 'divider') { + return ; + } + + return ( + { + itemConfig.onClick(); + closePopover(); + }} + > + {itemConfig.label} + + ); + }); + }, [item, getTabMenuItems, closePopover]); + + return ( + setPopover((prev) => !prev)} + /> + } + > + + + ); +}; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx index d42c229308f60..5192681e45ba8 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx @@ -7,10 +7,11 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { TabsBar } from '../tabs_bar'; import { getTabAttributes } from '../../utils/get_tab_attributes'; +import { getTabMenuActions } from '../../utils/get_tab_menu_actions'; import { TabItem } from '../../types'; export interface TabbedContentProps { @@ -93,6 +94,14 @@ export const TabbedContent: React.FC = ({ }); }, [changeState, createItem]); + const getTabMenuItems = useMemo(() => { + return getTabMenuActions({ + onDuplicate: (item) => alert(`Duplicate ${item.id}`), + onCloseOtherTabs: (item) => alert(`Close other tabs ${item.id}`), + onCloseTabsToTheRight: (item) => alert(`Close tabs to the right ${item.id}`), + }); + }, []); + return ( = ({ items={items} selectedItem={selectedItem} tabContentId={tabContentId} + getTabMenuItems={getTabMenuItems} onAdd={onAdd} onSelect={onSelect} onClose={onClose} diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.tsx index fe36aad42973e..0448a43d204a1 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.tsx +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabs_bar/tabs_bar.tsx @@ -11,22 +11,23 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; -import { Tab } from '../tab'; +import { Tab, type TabProps } from '../tab'; import type { TabItem } from '../../types'; -export interface TabsBarProps { +export type TabsBarProps = Pick< + TabProps, + 'getTabMenuItems' | 'onSelect' | 'onClose' | 'tabContentId' +> & { items: TabItem[]; selectedItem: TabItem | null; - tabContentId: string; onAdd: () => void; - onSelect: (item: TabItem) => void; - onClose: (item: TabItem) => void; -} +}; export const TabsBar: React.FC = ({ items, selectedItem, tabContentId, + getTabMenuItems, onAdd, onSelect, onClose, @@ -56,6 +57,7 @@ export const TabsBar: React.FC = ({ item={item} isSelected={selectedItem?.id === item.id} tabContentId={tabContentId} + getTabMenuItems={getTabMenuItems} onSelect={onSelect} onClose={items.length > 1 ? onClose : undefined} // prevents closing the last tab /> diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/types.ts b/src/platform/packages/shared/kbn-unified-tabs/src/types.ts index 16c76b660efa0..f1d0d35fca5fa 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/types.ts +++ b/src/platform/packages/shared/kbn-unified-tabs/src/types.ts @@ -11,3 +11,13 @@ export interface TabItem { id: string; label: string; } + +export interface TabMenuItemWithClick { + 'data-test-subj': string; + name: string; + label: string; + onClick: () => void; +} +export type TabMenuItem = TabMenuItemWithClick | 'divider'; + +export type GetTabMenuItems = (item: TabItem) => TabMenuItem[]; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/utils/get_tab_menu_actions.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/utils/get_tab_menu_actions.tsx new file mode 100644 index 0000000000000..cc4c16ee112d9 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/utils/get_tab_menu_actions.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; +import { TabItem, GetTabMenuItems, TabMenuItemWithClick } from '../types'; + +const DividerMenuItem = 'divider'; + +interface TabMenuItemProps { + name: string; + label: string; + item: TabItem; + onClick: (item: TabItem) => void; +} + +const getTabMenuItem = ({ + name, + label, + item, + onClick, +}: TabMenuItemProps): TabMenuItemWithClick => ({ + 'data-test-subj': `unifiedTabs_tabMenuItem_${name}`, + name, + label, + onClick: () => onClick(item), +}); + +export interface TabMenuActions { + onDuplicate: (item: TabItem) => void; + onCloseOtherTabs: (item: TabItem) => void; + onCloseTabsToTheRight: (item: TabItem) => void; +} + +export const getTabMenuActions = ({ + onDuplicate, + onCloseOtherTabs, + onCloseTabsToTheRight, +}: TabMenuActions): GetTabMenuItems => { + return (item) => [ + getTabMenuItem({ + item, + name: 'duplicate', + label: i18n.translate('unifiedTabs.tabMenu.duplicateMenuItem', { + defaultMessage: 'Duplicate', + }), + onClick: onDuplicate, + }), + DividerMenuItem, + getTabMenuItem({ + item, + name: 'closeOtherTabs', + label: i18n.translate('unifiedTabs.tabMenu.closeOtherTabsMenuItem', { + defaultMessage: 'Close other tabs', + }), + onClick: onCloseOtherTabs, + }), + getTabMenuItem({ + item, + name: 'closeTabsToTheRight', + label: i18n.translate('unifiedTabs.tabMenu.closeTabsToTheRightMenuItem', { + defaultMessage: 'Close tabs to the right', + }), + onClick: onCloseTabsToTheRight, + }), + ]; +}; From b6ae8da13f5d5641798dd23a401539e12f19e701 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 4 Mar 2025 15:24:47 +0100 Subject: [PATCH 3/7] [Discover] Move actions to utils --- .../tabbed_content/tabbed_content.tsx | 25 ++------ .../kbn-unified-tabs/src/utils/manage_tabs.ts | 58 +++++++++++++++++++ 2 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.ts diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx index 5192681e45ba8..f4575c66695cd 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx @@ -12,6 +12,7 @@ import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { TabsBar } from '../tabs_bar'; import { getTabAttributes } from '../../utils/get_tab_attributes'; import { getTabMenuActions } from '../../utils/get_tab_menu_actions'; +import { addTab, removeTab, selectTab } from '../../utils/manage_tabs'; import { TabItem } from '../../types'; export interface TabbedContentProps { @@ -59,39 +60,21 @@ export const TabbedContent: React.FC = ({ const onSelect = useCallback( (item: TabItem) => { - changeState((prevState) => ({ - ...prevState, - selectedItem: item, - })); + changeState((prevState) => selectTab(prevState, item)); }, [changeState] ); const onClose = useCallback( (item: TabItem) => { - changeState((prevState) => { - const nextItems = prevState.items.filter((prevItem) => prevItem.id !== item.id); - // TODO: better selection logic - const nextSelectedItem = nextItems.length ? nextItems[nextItems.length - 1] : null; - - return { - items: nextItems, - selectedItem: - prevState.selectedItem?.id !== item.id ? prevState.selectedItem : nextSelectedItem, - }; - }); + changeState((prevState) => removeTab(prevState, item)); }, [changeState] ); const onAdd = useCallback(() => { const newItem = createItem(); - changeState((prevState) => { - return { - items: [...prevState.items, newItem], - selectedItem: newItem, - }; - }); + changeState((prevState) => addTab(prevState, newItem)); }, [changeState, createItem]); const getTabMenuItems = useMemo(() => { diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.ts b/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.ts new file mode 100644 index 0000000000000..d6144d754a2eb --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { TabItem } from '../types'; + +interface TabsState { + items: TabItem[]; + selectedItem: TabItem | null; +} + +export const addTab = ({ items }: TabsState, item: TabItem): TabsState => { + return { + items: [...items, item], + selectedItem: item, + }; +}; + +export const removeTab = ({ items, selectedItem }: TabsState, item: TabItem): TabsState => { + const itemIndex = items.findIndex((i) => i.id === item.id); + + if (itemIndex === -1) { + return { + items, + selectedItem, + }; + } + + const nextItems = [...items]; + nextItems.splice(itemIndex, 1); + + if (selectedItem?.id !== item.id) { + return { + items: nextItems, + selectedItem, + }; + } + + const nextSelectedIndex = itemIndex === items.length - 1 ? itemIndex - 1 : itemIndex; + const nextSelectedItem = nextItems[nextSelectedIndex] || null; + + return { + items: nextItems, + selectedItem: nextSelectedItem, + }; +}; + +export const selectTab = ({ items, selectedItem }: TabsState, item: TabItem): TabsState => { + return { + items, + selectedItem: items.find((i) => i.id === item.id) || selectedItem, + }; +}; From 321bf3feab9686994ba610533b2daec59a117537 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 4 Mar 2025 16:01:44 +0100 Subject: [PATCH 4/7] [Discover] Add close actions --- .../tabbed_content/tabbed_content.tsx | 31 ++++++++++------ .../kbn-unified-tabs/src/utils/manage_tabs.ts | 36 ++++++++++++++++--- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx index f4575c66695cd..87d5d5d58cba0 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx @@ -12,7 +12,13 @@ import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { TabsBar } from '../tabs_bar'; import { getTabAttributes } from '../../utils/get_tab_attributes'; import { getTabMenuActions } from '../../utils/get_tab_menu_actions'; -import { addTab, removeTab, selectTab } from '../../utils/manage_tabs'; +import { + addTab, + closeTab, + selectTab, + closeOtherTabs, + closeTabsToTheRight, +} from '../../utils/manage_tabs'; import { TabItem } from '../../types'; export interface TabbedContentProps { @@ -46,14 +52,18 @@ export const TabbedContent: React.FC = ({ }; }); const { items, selectedItem } = state; + const stateRef = React.useRef(); + stateRef.current = state; const changeState = useCallback( (getNextState: (prevState: TabbedContentState) => TabbedContentState) => { - _setState((prevState) => { - const nextState = getNextState(prevState); - setTimeout(() => onChanged(nextState), 0); - return nextState; - }); + if (!stateRef.current) { + return; + } + + const nextState = getNextState(stateRef.current); + _setState(nextState); + onChanged(nextState); }, [_setState, onChanged] ); @@ -67,7 +77,7 @@ export const TabbedContent: React.FC = ({ const onClose = useCallback( (item: TabItem) => { - changeState((prevState) => removeTab(prevState, item)); + changeState((prevState) => closeTab(prevState, item)); }, [changeState] ); @@ -80,10 +90,11 @@ export const TabbedContent: React.FC = ({ const getTabMenuItems = useMemo(() => { return getTabMenuActions({ onDuplicate: (item) => alert(`Duplicate ${item.id}`), - onCloseOtherTabs: (item) => alert(`Close other tabs ${item.id}`), - onCloseTabsToTheRight: (item) => alert(`Close tabs to the right ${item.id}`), + onCloseOtherTabs: (item) => changeState((prevState) => closeOtherTabs(prevState, item)), + onCloseTabsToTheRight: (item) => + changeState((prevState) => closeTabsToTheRight(prevState, item)), }); - }, []); + }, [changeState]); return ( { }; }; -export const removeTab = ({ items, selectedItem }: TabsState, item: TabItem): TabsState => { +export const selectTab = ({ items, selectedItem }: TabsState, item: TabItem): TabsState => { + return { + items, + selectedItem: items.find((i) => i.id === item.id) || selectedItem, + }; +}; + +export const closeTab = ({ items, selectedItem }: TabsState, item: TabItem): TabsState => { const itemIndex = items.findIndex((i) => i.id === item.id); if (itemIndex === -1) { @@ -50,9 +57,30 @@ export const removeTab = ({ items, selectedItem }: TabsState, item: TabItem): Ta }; }; -export const selectTab = ({ items, selectedItem }: TabsState, item: TabItem): TabsState => { +export const closeOtherTabs = (_: TabsState, item: TabItem): TabsState => { return { - items, - selectedItem: items.find((i) => i.id === item.id) || selectedItem, + items: [item], + selectedItem: item, + }; +}; + +export const closeTabsToTheRight = ( + { items, selectedItem }: TabsState, + item: TabItem +): TabsState => { + const itemIndex = items.findIndex((i) => i.id === item.id); + + if (itemIndex === -1 || itemIndex === items.length - 1) { + return { + items, + selectedItem, + }; + } + + const nextItems = items.slice(0, itemIndex + 1); + + return { + items: nextItems, + selectedItem, }; }; From aea681f705ed006c13d5e6aa863d8fcf051fa2cf Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 4 Mar 2025 16:30:27 +0100 Subject: [PATCH 5/7] [Discover] Add duplicate action --- .../tabbed_content/tabbed_content.tsx | 9 ++++-- .../kbn-unified-tabs/src/utils/manage_tabs.ts | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx index 87d5d5d58cba0..b0f8449ff5865 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx @@ -16,6 +16,7 @@ import { addTab, closeTab, selectTab, + insertTab, closeOtherTabs, closeTabsToTheRight, } from '../../utils/manage_tabs'; @@ -89,12 +90,16 @@ export const TabbedContent: React.FC = ({ const getTabMenuItems = useMemo(() => { return getTabMenuActions({ - onDuplicate: (item) => alert(`Duplicate ${item.id}`), + onDuplicate: (item) => { + const newItem = createItem(); + newItem.label = `${item.label} (copy)`; + changeState((prevState) => insertTab(prevState, newItem, item)); + }, onCloseOtherTabs: (item) => changeState((prevState) => closeOtherTabs(prevState, item)), onCloseTabsToTheRight: (item) => changeState((prevState) => closeTabsToTheRight(prevState, item)), }); - }, [changeState]); + }, [changeState, createItem]); return ( { + const insertAfterIndex = items.findIndex((i) => i.id === insertAfterItem.id); + + if (insertAfterIndex === -1) { + return { + items, + selectedItem, + }; + } + + const nextItems = [...items]; + const insertIndex = insertAfterIndex + 1; + + if (insertIndex === nextItems.length) { + nextItems.push(item); + } else { + nextItems.splice(insertIndex, 0, item); + } + + return { + items: nextItems, + selectedItem: item, + }; +}; + export const closeOtherTabs = (_: TabsState, item: TabItem): TabsState => { return { items: [item], From 7f2eae15277e307e938955ef1727b14f4317b100 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 4 Mar 2025 17:08:02 +0100 Subject: [PATCH 6/7] [Discover] Conditionally show menu items --- .../tabbed_content/tabbed_content.tsx | 7 +- .../src/utils/get_tab_menu_actions.tsx | 72 -------------- .../src/utils/get_tab_menu_items.tsx | 96 +++++++++++++++++++ .../kbn-unified-tabs/src/utils/manage_tabs.ts | 10 +- 4 files changed, 109 insertions(+), 76 deletions(-) delete mode 100644 src/platform/packages/shared/kbn-unified-tabs/src/utils/get_tab_menu_actions.tsx create mode 100644 src/platform/packages/shared/kbn-unified-tabs/src/utils/get_tab_menu_items.tsx diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx index b0f8449ff5865..f62b80cc37f1c 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx @@ -11,7 +11,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { TabsBar } from '../tabs_bar'; import { getTabAttributes } from '../../utils/get_tab_attributes'; -import { getTabMenuActions } from '../../utils/get_tab_menu_actions'; +import { getTabMenuItemsFn } from '../../utils/get_tab_menu_items'; import { addTab, closeTab, @@ -89,7 +89,8 @@ export const TabbedContent: React.FC = ({ }, [changeState, createItem]); const getTabMenuItems = useMemo(() => { - return getTabMenuActions({ + return getTabMenuItemsFn({ + tabsState: state, onDuplicate: (item) => { const newItem = createItem(); newItem.label = `${item.label} (copy)`; @@ -99,7 +100,7 @@ export const TabbedContent: React.FC = ({ onCloseTabsToTheRight: (item) => changeState((prevState) => closeTabsToTheRight(prevState, item)), }); - }, [changeState, createItem]); + }, [changeState, createItem, state]); return ( void; -} - -const getTabMenuItem = ({ - name, - label, - item, - onClick, -}: TabMenuItemProps): TabMenuItemWithClick => ({ - 'data-test-subj': `unifiedTabs_tabMenuItem_${name}`, - name, - label, - onClick: () => onClick(item), -}); - -export interface TabMenuActions { - onDuplicate: (item: TabItem) => void; - onCloseOtherTabs: (item: TabItem) => void; - onCloseTabsToTheRight: (item: TabItem) => void; -} - -export const getTabMenuActions = ({ - onDuplicate, - onCloseOtherTabs, - onCloseTabsToTheRight, -}: TabMenuActions): GetTabMenuItems => { - return (item) => [ - getTabMenuItem({ - item, - name: 'duplicate', - label: i18n.translate('unifiedTabs.tabMenu.duplicateMenuItem', { - defaultMessage: 'Duplicate', - }), - onClick: onDuplicate, - }), - DividerMenuItem, - getTabMenuItem({ - item, - name: 'closeOtherTabs', - label: i18n.translate('unifiedTabs.tabMenu.closeOtherTabsMenuItem', { - defaultMessage: 'Close other tabs', - }), - onClick: onCloseOtherTabs, - }), - getTabMenuItem({ - item, - name: 'closeTabsToTheRight', - label: i18n.translate('unifiedTabs.tabMenu.closeTabsToTheRightMenuItem', { - defaultMessage: 'Close tabs to the right', - }), - onClick: onCloseTabsToTheRight, - }), - ]; -}; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/utils/get_tab_menu_items.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/utils/get_tab_menu_items.tsx new file mode 100644 index 0000000000000..b89d6b463b564 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/utils/get_tab_menu_items.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; +import { TabItem, GetTabMenuItems, TabMenuItemWithClick, TabMenuItem } from '../types'; +import { isLastTab, hasSingleTab, type TabsState } from './manage_tabs'; + +const DividerMenuItem = 'divider'; + +interface TabMenuItemProps { + name: string; + label: string; + item: TabItem; + onClick: (item: TabItem) => void; +} + +const getTabMenuItem = ({ + name, + label, + item, + onClick, +}: TabMenuItemProps): TabMenuItemWithClick => ({ + 'data-test-subj': `unifiedTabs_tabMenuItem_${name}`, + name, + label, + onClick: () => onClick(item), +}); + +export interface GetTabMenuItemsFnProps { + tabsState: TabsState; + onDuplicate: (item: TabItem) => void; + onCloseOtherTabs: (item: TabItem) => void; + onCloseTabsToTheRight: (item: TabItem) => void; +} + +export const getTabMenuItemsFn = ({ + tabsState, + onDuplicate, + onCloseOtherTabs, + onCloseTabsToTheRight, +}: GetTabMenuItemsFnProps): GetTabMenuItems => { + return (item) => { + const closeOtherTabsItem = hasSingleTab(tabsState) + ? null + : getTabMenuItem({ + item, + name: 'closeOtherTabs', + label: i18n.translate('unifiedTabs.tabMenu.closeOtherTabsMenuItem', { + defaultMessage: 'Close other tabs', + }), + onClick: onCloseOtherTabs, + }); + + const closeTabsToTheRightItem = isLastTab(tabsState, item) + ? null + : getTabMenuItem({ + item, + name: 'closeTabsToTheRight', + label: i18n.translate('unifiedTabs.tabMenu.closeTabsToTheRightMenuItem', { + defaultMessage: 'Close tabs to the right', + }), + onClick: onCloseTabsToTheRight, + }); + + const items: TabMenuItem[] = [ + getTabMenuItem({ + item, + name: 'duplicate', + label: i18n.translate('unifiedTabs.tabMenu.duplicateMenuItem', { + defaultMessage: 'Duplicate', + }), + onClick: onDuplicate, + }), + ]; + + if (closeOtherTabsItem || closeTabsToTheRightItem) { + items.push(DividerMenuItem); + + if (closeOtherTabsItem) { + items.push(closeOtherTabsItem); + } + + if (closeTabsToTheRightItem) { + items.push(closeTabsToTheRightItem); + } + } + + return items; + }; +}; diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.ts b/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.ts index 1f00ab742d5e6..c7f6686729587 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.ts +++ b/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.ts @@ -9,11 +9,19 @@ import type { TabItem } from '../types'; -interface TabsState { +export interface TabsState { items: TabItem[]; selectedItem: TabItem | null; } +export const hasSingleTab = ({ items }: TabsState): boolean => { + return items.length === 1; +}; + +export const isLastTab = ({ items }: TabsState, item: TabItem): boolean => { + return items[items.length - 1].id === item.id; +}; + export const addTab = ({ items }: TabsState, item: TabItem): TabsState => { return { items: [...items, item], From a6b0c773d0273b30dae4a8cbb7d95dfbb4401877 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Tue, 4 Mar 2025 17:40:12 +0100 Subject: [PATCH 7/7] [Discover] Add unit tests --- .../src/components/tab/tab.test.tsx | 33 +++ .../tabbed_content/tabbed_content.tsx | 4 +- .../src/utils/manage_tabs.test.ts | 196 ++++++++++++++++++ .../kbn-unified-tabs/src/utils/manage_tabs.ts | 2 +- 4 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.test.ts diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.test.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.test.tsx index f344e8603a946..d7e62077cafa0 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.test.tsx +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tab/tab.test.tsx @@ -47,4 +47,37 @@ describe('Tab', () => { expect(onClose).toHaveBeenCalled(); expect(onSelect).toHaveBeenCalledTimes(1); }); + + it('can render tab menu items', async () => { + const mockClick = jest.fn(); + const getTabMenuItems = jest.fn(() => [ + { + 'data-test-subj': 'test-subj', + name: 'test-name', + label: 'test-label', + onClick: mockClick, + }, + ]); + + render( + + ); + + const tabMenuButton = screen.getByTestId(`unifiedTabs_tabMenuBtn_${tabItem.id}`); + tabMenuButton.click(); + + expect(getTabMenuItems).toHaveBeenCalledWith(tabItem); + + const menuItem = screen.getByTestId('test-subj'); + menuItem.click(); + expect(mockClick).toHaveBeenCalledTimes(1); + expect(getTabMenuItems).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx index f62b80cc37f1c..66d9aa1d19a2d 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx +++ b/src/platform/packages/shared/kbn-unified-tabs/src/components/tabbed_content/tabbed_content.tsx @@ -16,7 +16,7 @@ import { addTab, closeTab, selectTab, - insertTab, + insertTabAfter, closeOtherTabs, closeTabsToTheRight, } from '../../utils/manage_tabs'; @@ -94,7 +94,7 @@ export const TabbedContent: React.FC = ({ onDuplicate: (item) => { const newItem = createItem(); newItem.label = `${item.label} (copy)`; - changeState((prevState) => insertTab(prevState, newItem, item)); + changeState((prevState) => insertTabAfter(prevState, newItem, item)); }, onCloseOtherTabs: (item) => changeState((prevState) => closeOtherTabs(prevState, item)), onCloseTabsToTheRight: (item) => diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.test.ts b/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.test.ts new file mode 100644 index 0000000000000..422c3354ecbf5 --- /dev/null +++ b/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.test.ts @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + hasSingleTab, + isLastTab, + addTab, + selectTab, + insertTabAfter, + closeTab, + closeOtherTabs, + closeTabsToTheRight, +} from './manage_tabs'; + +const items = Array.from({ length: 5 }).map((_, i) => ({ + id: `tab-${i}`, + label: `Tab ${i}`, +})); + +describe('manage_tabs', () => { + describe('hasSingleTab', () => { + it('returns true if there is only one tab', () => { + expect(hasSingleTab({ items: [items[0]], selectedItem: null })).toBe(true); + }); + + it('returns false if there is more than one tab', () => { + expect(hasSingleTab({ items, selectedItem: null })).toBe(false); + }); + }); + + describe('isLastTab', () => { + it('returns true if the item is the last tab', () => { + expect(isLastTab({ items, selectedItem: null }, items[4])).toBe(true); + }); + + it('returns true if the item is the only tab', () => { + expect(isLastTab({ items: [items[0]], selectedItem: null }, items[0])).toBe(true); + }); + + it('returns false if the item is not the last tab', () => { + expect(isLastTab({ items, selectedItem: null }, items[3])).toBe(false); + }); + + it('returns false if the item is not from the list', () => { + expect(isLastTab({ items: [items[0], items[1]], selectedItem: null }, items[3])).toBe(false); + }); + }); + + describe('addTab', () => { + it('adds a tab', () => { + const newItem = { id: 'tab-5', label: 'Tab 5' }; + const prevState = { items, selectedItem: items[0] }; + const nextState = addTab(prevState, newItem); + + expect(nextState.items).not.toBe(items); + expect(nextState.items).toEqual([...items, newItem]); + expect(nextState.selectedItem).toBe(newItem); + }); + }); + + describe('selectTab', () => { + it('selects a tab', () => { + const prevState = { items, selectedItem: items[0] }; + const nextState = selectTab(prevState, items[1]); + + expect(nextState.items).toBe(items); + expect(nextState.selectedItem).toBe(items[1]); + }); + + it("skips selecting a tab if it's not from the list", () => { + const limitedItems = [items[0], items[1]]; + const prevState = { items: limitedItems, selectedItem: items[0] }; + const nextState = selectTab(prevState, items[2]); + + expect(nextState.items).toBe(limitedItems); + expect(nextState.selectedItem).toBe(items[0]); + }); + }); + + describe('insertTabAfter', () => { + it('inserts a tab after another tab', () => { + const newItem = { id: 'tab-5', label: 'Tab 5' }; + const prevState = { items, selectedItem: items[0] }; + const nextState = insertTabAfter(prevState, newItem, items[2]); + + expect(nextState.items).not.toBe(items); + expect(nextState.items).toEqual([items[0], items[1], items[2], newItem, items[3], items[4]]); + expect(nextState.selectedItem).toBe(newItem); + }); + + it('inserts a tab after the last tab', () => { + const newItem = { id: 'tab-5', label: 'Tab 5' }; + const prevState = { items, selectedItem: items[0] }; + const nextState = insertTabAfter(prevState, newItem, items[items.length - 1]); + + expect(nextState.items).not.toBe(items); + expect(nextState.items).toEqual([items[0], items[1], items[2], items[3], items[4], newItem]); + expect(nextState.selectedItem).toBe(newItem); + }); + }); + + describe('closeTab', () => { + it('closes a tab from the middle', () => { + const prevState = { items, selectedItem: items[0] }; + const nextState = closeTab(prevState, items[2]); + + expect(nextState.items).not.toBe(items); + expect(nextState.items).toEqual([items[0], items[1], items[3], items[4]]); + expect(nextState.selectedItem).toBe(items[0]); + }); + + it('closes the first tab', () => { + const prevState = { items, selectedItem: items[0] }; + const nextState = closeTab(prevState, items[0]); + + expect(nextState.items).not.toBe(items); + expect(nextState.items).toEqual([items[1], items[2], items[3], items[4]]); + expect(nextState.selectedItem).toBe(items[1]); + }); + + it('closes the last tab', () => { + const prevState = { items, selectedItem: items[4] }; + const nextState = closeTab(prevState, items[items.length - 1]); + + expect(nextState.items).not.toBe(items); + expect(nextState.items).toEqual([items[0], items[1], items[2], items[3]]); + expect(nextState.selectedItem).toBe(items[3]); + }); + + it('closes the selected tab', () => { + const prevState = { items, selectedItem: items[2] }; + const nextState = closeTab(prevState, items[2]); + + expect(nextState.items).not.toBe(items); + expect(nextState.items).toEqual([items[0], items[1], items[3], items[4]]); + expect(nextState.selectedItem).toBe(items[3]); + }); + + it("skips closing a tab if it's not from the list", () => { + const limitedItems = [items[0], items[1]]; + const prevState = { items: limitedItems, selectedItem: items[0] }; + const nextState = closeTab(prevState, items[2]); + + expect(nextState.items).toBe(limitedItems); + expect(nextState.selectedItem).toBe(items[0]); + }); + }); + + describe('closeOtherTabs', () => { + it('closes other tabs', () => { + const prevState = { items, selectedItem: items[1] }; + const nextState = closeOtherTabs(prevState, items[2]); + + expect(nextState.items).not.toBe(items); + expect(nextState.items).toEqual([items[2]]); + expect(nextState.selectedItem).toBe(items[2]); + }); + + it('closes other tabs and keeps the only tab', () => { + const limitedItems = [items[2]]; + const prevState = { items: limitedItems, selectedItem: items[2] }; + const nextState = closeOtherTabs(prevState, items[2]); + + expect(nextState.items).not.toBe(limitedItems); + expect(nextState.items).toEqual(limitedItems); + expect(nextState.selectedItem).toBe(items[2]); + }); + }); + + describe('closeTabsToTheRight', () => { + it('closes tabs to the right from the middle', () => { + const prevState = { items, selectedItem: items[1] }; + const nextState = closeTabsToTheRight(prevState, items[2]); + + expect(nextState.items).not.toBe(items); + expect(nextState.items).toEqual([items[0], items[1], items[2]]); + expect(nextState.selectedItem).toBe(items[1]); + }); + + it('closes tabs to the right from the beginning', () => { + const limitedItems = [items[0], items[1], items[2]]; + const prevState = { items: limitedItems, selectedItem: items[0] }; + const nextState = closeTabsToTheRight(prevState, items[0]); + + expect(nextState.items).not.toBe(limitedItems); + expect(nextState.items).toEqual([items[0]]); + expect(nextState.selectedItem).toBe(items[0]); + }); + }); +}); diff --git a/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.ts b/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.ts index c7f6686729587..b95ff4bfbfce8 100644 --- a/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.ts +++ b/src/platform/packages/shared/kbn-unified-tabs/src/utils/manage_tabs.ts @@ -65,7 +65,7 @@ export const closeTab = ({ items, selectedItem }: TabsState, item: TabItem): Tab }; }; -export const insertTab = ( +export const insertTabAfter = ( { items, selectedItem }: TabsState, item: TabItem, insertAfterItem: TabItem