diff --git a/package-lock.json b/package-lock.json index d9ad3c053..6db04829c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.14.2-pre-1", + "version": "1.14.2-pre-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.14.2-pre-1", + "version": "1.14.2-pre-2", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index d4d2bb3e7..406232770 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.14.2-pre-1", + "version": "1.14.2-pre-2", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 502e9b36e..e0de61a42 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -19,9 +19,10 @@ export const Host = window?.__ORCHESTRATOR_ROOT__ ?? '/orchestrator' export const DOCUMENTATION_HOME_PAGE = 'https://docs.devtron.ai' export const DEVTRON_HOME_PAGE = 'https://devtron.ai/' -export const DOCUMENTATION_VERSION = '/v/v0.7' +export const DOCUMENTATION_VERSION = '/devtron/v0.7' export const DISCORD_LINK = 'https://discord.devtron.ai/' export const DEFAULT_JSON_SCHEMA_URI = 'https://json-schema.org/draft/2020-12/schema' +export const LICENSE_DASHBOARD_HOME_PAGE = 'https://license.devtron.ai/dashboard' export const PATTERNS = { STRING: /^[a-zA-Z0-9_]+$/, @@ -47,6 +48,7 @@ export const PATTERNS = { const GLOBAL_CONFIG_TEMPLATES_DEVTRON_APP = '/global-config/templates/devtron-apps' export const URLS = { + LOGIN: '/login', LOGIN_SSO: '/login/sso', PERMISSION_GROUPS: '/global-config/auth/groups', APP: '/app', diff --git a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx index 938301e16..8dd8d6f92 100644 --- a/src/Shared/Components/ActionMenu/ActionMenu.component.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenu.component.tsx @@ -1,10 +1,8 @@ -import { MutableRefObject } from 'react' - import { CustomInput } from '../CustomInput' import { Popover } from '../Popover' import { SelectPickerMenuListFooter } from '../SelectPicker/common' import { ActionMenuItem } from './ActionMenuItem' -import { ActionMenuItemType, ActionMenuProps } from './types' +import { ActionMenuItemProps, ActionMenuProps } from './types' import { useActionMenu } from './useActionMenu.hook' import './actionMenu.scss' @@ -51,8 +49,8 @@ export const ActionMenu = ({ // HANDLERS const handleOptionMouseEnter = (index: number) => () => setFocusedIndex(index) - const handleOptionOnClick = (item: ActionMenuItemType) => () => { - onClick(item) + const handleOptionOnClick: ActionMenuItemProps['onClick'] = (item, e) => { + onClick(item, e) closePopover() } @@ -82,7 +80,7 @@ export const ActionMenu = ({ )}
    } + ref={scrollableRef} role="menu" className="action-menu m-0 p-0 flex-grow-1 dc__overflow-auto dc__overscroll-none" > @@ -114,7 +112,7 @@ export const ActionMenu = ({ itemRef={itemsRef.current[index]} isFocused={index === focusedIndex} onMouseEnter={handleOptionMouseEnter(index)} - onClick={handleOptionOnClick(item)} + onClick={handleOptionOnClick} disableDescriptionEllipsis={disableDescriptionEllipsis} /> ) @@ -132,7 +130,7 @@ export const ActionMenu = ({ )}
{footerConfig && ( -
+
)} diff --git a/src/Shared/Components/ActionMenu/ActionMenuItem.tsx b/src/Shared/Components/ActionMenu/ActionMenuItem.tsx index 96300da47..b140129d1 100644 --- a/src/Shared/Components/ActionMenu/ActionMenuItem.tsx +++ b/src/Shared/Components/ActionMenu/ActionMenuItem.tsx @@ -1,13 +1,17 @@ -import { LegacyRef, Ref } from 'react' +import { LegacyRef, MouseEvent, Ref } from 'react' import { Link } from 'react-router-dom' import { Tooltip } from '@Common/Tooltip' +import { ComponentSizeType } from '@Shared/constants' +import { Button, ButtonProps, ButtonVariantType } from '../Button' import { Icon } from '../Icon' +import { NumbersCount } from '../NumbersCount' import { getTooltipProps } from '../SelectPicker/common' -import { ActionMenuItemProps } from './types' +import { DTSwitch, DTSwitchProps } from '../Switch' +import { ActionMenuItemProps, ActionMenuItemType } from './types' -const COMMON_ACTION_MENU_ITEM_CLASS = 'flex-grow-1 flex left top dc__gap-8 py-6 px-8' +const COMMON_ACTION_MENU_ITEM_CLASS = 'w-100 flex left top dc__gap-8 py-6 px-8' export const ActionMenuItem = ({ item, @@ -22,7 +26,7 @@ export const ActionMenuItem = ({ description, label, startIcon, - endIcon, + trailingItem, tooltipProps, type = 'neutral', isDisabled, @@ -40,22 +44,83 @@ export const ActionMenuItem = ({ const isNegativeType = type === 'negative' // HANDLERS - const handleClick = () => { - onClick(item) + const handleClick = (e: MouseEvent | MouseEvent) => { + onClick(item, e) } + const handleTrailingSwitchChange = + ({ type: trailingItemType, config }: ActionMenuItemType['trailingItem']): DTSwitchProps['onChange'] => + (e) => { + if (trailingItemType === 'switch') { + e.stopPropagation() + config.onChange(e) + } + } + + const handleTrailingButtonClick = + ({ type: trailingItemType, config }: ActionMenuItemType['trailingItem']): ButtonProps['onClick'] => + (e) => { + e.stopPropagation() + if (trailingItemType === 'button' && config.onClick) { + config.onClick(e) + } + } + // RENDERERS const renderIcon = (iconProps: typeof startIcon) => iconProps && ( -
+ -
+ ) + const renderTrailingItem = () => { + if (!trailingItem) { + return null + } + + const { type: trailingItemType, config } = trailingItem + + switch (trailingItemType) { + case 'icon': + return renderIcon(config) + case 'text': { + const { value, icon } = config + return ( + + {value} + {icon && } + + ) + } + case 'counter': + return + case 'switch': + return ( + + ) + case 'button': + return ( + @@ -124,7 +192,6 @@ export const ActionMenuItem = ({ tabIndex={-1} // Intentionally added margin to the left and right to have the gap on the edges of the options className={`action-menu__option br-4 mr-4 ml-4 ${isDisabled ? 'dc__disabled' : 'cursor'} ${isNegativeType ? 'dc__hover-r50' : 'dc__hover-n50'} ${isFocused ? `action-menu__option--focused${isNegativeType ? '-negative' : ''}` : ''}`} - onClick={!isDisabled ? handleClick : undefined} aria-disabled={isDisabled} > {renderComponent()} diff --git a/src/Shared/Components/ActionMenu/types.ts b/src/Shared/Components/ActionMenu/types.ts index 599212c4f..838a93f03 100644 --- a/src/Shared/Components/ActionMenu/types.ts +++ b/src/Shared/Components/ActionMenu/types.ts @@ -1,9 +1,14 @@ -import { LegacyRef, Ref } from 'react' +import { LegacyRef, MouseEvent, ReactElement, Ref } from 'react' import { LinkProps } from 'react-router-dom' +import { OmitNever } from '@Shared/types' + +import { ButtonProps } from '../Button' import { IconsProps } from '../Icon' +import { NumbersCountProps } from '../NumbersCount' import { PopoverProps, UsePopoverProps } from '../Popover' import { SelectPickerOptionType, SelectPickerProps } from '../SelectPicker' +import { DTSwitchProps } from '../Switch' type ConditionalActionMenuComponentType = | { @@ -32,6 +37,43 @@ type ActionMenuItemIconType = Pick & { color?: IconsProps['color'] } +type TrailingItemType = + | { + type: 'icon' + config: ActionMenuItemIconType + } + | { + type: 'text' + config: { + value: string + icon?: ActionMenuItemIconType + } + } + | { + type: 'counter' + config: { + value: NumbersCountProps['count'] + } + } + | { + type: 'switch' + config: Pick< + DTSwitchProps, + | 'ariaLabel' + | 'isChecked' + | 'indeterminate' + | 'isDisabled' + | 'isLoading' + | 'name' + | 'onChange' + | 'tooltipContent' + > + } + | { + type: 'button' + config: OmitNever, 'size' | 'variant'>> + } + export type ActionMenuItemType = Omit< SelectPickerOptionType, 'label' | 'value' | 'endIcon' | 'startIcon' @@ -49,8 +91,8 @@ export type ActionMenuItemType = Om type?: 'neutral' | 'negative' /** Defines the icon to be displayed at the start of the menu item. */ startIcon?: ActionMenuItemIconType - /** Defines the icon to be displayed at the end of the menu item. */ - endIcon?: ActionMenuItemIconType + /** Defines the item to be displayed at the end of the menu item. */ + trailingItem?: TrailingItemType } & ConditionalActionMenuComponentType export type ActionMenuOptionType = { @@ -85,7 +127,7 @@ export type ActionMenuProps = UseAc * Callback function triggered when an item is clicked. * @param item - The selected item. */ - onClick: (item: ActionMenuItemType) => void + onClick: (item: ActionMenuItemType, e: MouseEvent | MouseEvent) => void /** * Config for the footer at the bottom of action menu list. It is sticky by default */ diff --git a/src/Shared/Components/ActionMenu/useActionMenu.hook.ts b/src/Shared/Components/ActionMenu/useActionMenu.hook.ts index 4898d836d..6d78b2f5a 100644 --- a/src/Shared/Components/ActionMenu/useActionMenu.hook.ts +++ b/src/Shared/Components/ActionMenu/useActionMenu.hook.ts @@ -1,4 +1,4 @@ -import { ChangeEvent, createRef, RefObject, useEffect, useMemo, useRef, useState } from 'react' +import { ChangeEvent, createRef, RefObject, useEffect, useRef, useState } from 'react' import { usePopover, UsePopoverProps } from '../Popover' import { UseActionMenuProps } from './types' @@ -17,13 +17,9 @@ export const useActionMenu = ({ const [focusedIndex, setFocusedIndex] = useState(-1) const [searchTerm, setSearchTerm] = useState('') - // MEMOIZED CONSTANTS - const filteredOptions = useMemo( - () => (isSearchable ? filterActionMenuOptions(options, searchTerm) : options), - [isSearchable, JSON.stringify(options), searchTerm], - ) - - const flatOptions = useMemo(() => getActionMenuFlatOptions(filteredOptions), [filteredOptions]) + // CONSTANTS + const filteredOptions = isSearchable ? filterActionMenuOptions(options, searchTerm) : options + const flatOptions = getActionMenuFlatOptions(filteredOptions) // REFS const itemsRef = useRef[]>( diff --git a/src/Shared/Components/Button/Button.component.tsx b/src/Shared/Components/Button/Button.component.tsx index 525f96440..d8eb3a367 100644 --- a/src/Shared/Components/Button/Button.component.tsx +++ b/src/Shared/Components/Button/Button.component.tsx @@ -18,8 +18,7 @@ import { MutableRefObject, PropsWithChildren, useEffect, useRef, useState } from import { Link } from 'react-router-dom' import { Progressing } from '@Common/Progressing' -import { Tooltip } from '@Common/Tooltip' -import { TooltipProps } from '@Common/Tooltip/types' +import { Tooltip, TooltipProps } from '@Common/Tooltip' import { ComponentSizeType } from '@Shared/constants' import { ButtonComponentType, ButtonProps, ButtonStyleType, ButtonVariantType } from './types' @@ -170,6 +169,7 @@ const Button = ({ variant = ButtonVariantType.primary, size = ComponentSizeType.large, style = ButtonStyleType.default, + fontWeight = 'bold', startIcon = null, endIcon = null, disabled = false, @@ -231,23 +231,22 @@ const Button = ({ } if (Object.hasOwn(tooltipProps, 'shortcutKeyCombo') && 'shortcutKeyCombo' in tooltipProps) { - return tooltipProps + return tooltipProps as TooltipProps } return { - // TODO: using some typing somersaults here, clean it up later - alwaysShowTippyOnHover: showTooltip && !!(tooltipProps as Required>)?.content, - ...(tooltipProps as Required>), - } + alwaysShowTippyOnHover: showTooltip && !!tooltipProps?.content, + ...tooltipProps, + } as TooltipProps } return ( -
+
= { - [ComponentSizeType.xxs_small_icon]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xxs]} px-9 fw-6 dc__gap-6 mw-48`, - [ComponentSizeType.xxs]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xxs]} px-9 fw-6 dc__gap-6 mw-48`, - [ComponentSizeType.xs]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xs]} px-9 fw-6 dc__gap-6 mw-48`, - [ComponentSizeType.small]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.small]} px-9 fw-6 dc__gap-8 mw-48`, - [ComponentSizeType.medium]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.medium]} px-11 fw-6 dc__gap-8 mw-48`, - [ComponentSizeType.large]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.large]} px-13 fw-6 dc__gap-8 mw-64`, - [ComponentSizeType.xl]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xl]} px-15 fw-6 dc__gap-12 mw-64`, + [ComponentSizeType.xxs_small_icon]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xxs]} px-9 dc__gap-6 mw-48`, + [ComponentSizeType.xxs]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xxs]} px-9 dc__gap-6 mw-48`, + [ComponentSizeType.xs]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xs]} px-9 dc__gap-6 mw-48`, + [ComponentSizeType.small]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.small]} px-9 dc__gap-8 mw-48`, + [ComponentSizeType.medium]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.medium]} px-11 dc__gap-8 mw-48`, + [ComponentSizeType.large]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.large]} px-13 dc__gap-8 mw-64`, + [ComponentSizeType.xl]: `${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[ComponentSizeType.xl]} px-15 dc__gap-12 mw-64`, +} as const + +export const BUTTON_FONT_WEIGHT_TO_CLASS_NAME_MAP: Record = { + bold: 'fw-6', + normal: 'fw-4', } as const export const ICON_BUTTON_SIZE_TO_CLASS_NAME_MAP: Record = { diff --git a/src/Shared/Components/Button/types.ts b/src/Shared/Components/Button/types.ts index 5ed004be5..240bf10ff 100644 --- a/src/Shared/Components/Button/types.ts +++ b/src/Shared/Components/Button/types.ts @@ -150,11 +150,10 @@ export type ButtonProps - | (Omit & - Required>) + | Omit, 'alwaysShowTippyOnHover'> + | Omit, 'alwaysShowTippyOnHover'> + | Omit, 'alwaysShowTippyOnHover'> } | { showTooltip?: never @@ -178,6 +177,12 @@ export type ButtonProps & { + Pick & { isAutoTriggerActive: boolean } >) => - `button button__${variant}--${style} ${icon ? ICON_BUTTON_SIZE_TO_CLASS_NAME_MAP[size] : BUTTON_SIZE_TO_CLASS_NAME_MAP[size]} ${isAutoTriggerActive ? 'button--auto-click' : ''} ${isLoading ? 'button--loading' : ''}` + `button button__${variant}--${style} ${icon ? ICON_BUTTON_SIZE_TO_CLASS_NAME_MAP[size] : BUTTON_SIZE_TO_CLASS_NAME_MAP[size]} ${BUTTON_FONT_WEIGHT_TO_CLASS_NAME_MAP[fontWeight]} ${isAutoTriggerActive ? 'button--auto-click' : ''} ${isLoading ? 'button--loading' : ''}` diff --git a/src/Shared/Components/CICDHistory/CiPipelineSourceConfig.tsx b/src/Shared/Components/CICDHistory/CiPipelineSourceConfig.tsx index ccc7ee723..36e89ded4 100644 --- a/src/Shared/Components/CICDHistory/CiPipelineSourceConfig.tsx +++ b/src/Shared/Components/CICDHistory/CiPipelineSourceConfig.tsx @@ -162,7 +162,7 @@ export const CiPipelineSourceConfig = ({ {!baseText && ( <>
@@ -181,7 +181,7 @@ export const CiPipelineSourceConfig = ({
) : ( - {sourceValueAdv} + {sourceValueAdv} )}
)} diff --git a/src/Shared/Components/CodeEditor/codeEditor.scss b/src/Shared/Components/CodeEditor/codeEditor.scss index 900144ee4..bd1894d7e 100644 --- a/src/Shared/Components/CodeEditor/codeEditor.scss +++ b/src/Shared/Components/CodeEditor/codeEditor.scss @@ -133,7 +133,8 @@ border-bottom: none; &:has(.code-editor__search) { - z-index: 0; + width: fit-content; + margin-left: auto; } } diff --git a/src/Shared/Components/DocLink/DocLink.tsx b/src/Shared/Components/DocLink/DocLink.tsx index a37450ee6..bbb698abd 100644 --- a/src/Shared/Components/DocLink/DocLink.tsx +++ b/src/Shared/Components/DocLink/DocLink.tsx @@ -12,13 +12,15 @@ export const DocLink = ({ docLinkKey, text = 'Learn more', dataTestId, + startIcon, showExternalIcon, onClick, + fontWeight, size = ComponentSizeType.medium, variant = ButtonVariantType.text, isExternalLink, openInNewTab = false, - fullWidth = true, + fullWidth = false, }: DocLinkProps) => { // HOOKS const { isEnterprise, setSidePanelConfig } = useMainContext() @@ -34,7 +36,7 @@ export const DocLink = ({ const handleClick = (e: MouseEvent) => { if (!isExternalLink && !openInNewTab && !e.metaKey && documentationLink.startsWith(DOCUMENTATION_HOME_PAGE)) { e.preventDefault() - setSidePanelConfig((prev) => ({ ...prev, open: true, docLink: documentationLink })) + setSidePanelConfig((prev) => ({ ...prev, open: true, docLink: documentationLink, reinitialize: true })) } onClick?.(e) } @@ -50,8 +52,10 @@ export const DocLink = ({ text={text} variant={variant} size={size} + startIcon={startIcon} endIcon={showExternalIcon && } fullWidth={fullWidth} + fontWeight={fontWeight} /> ) } diff --git a/src/Shared/Components/DocLink/constants.ts b/src/Shared/Components/DocLink/constants.ts index 776298bde..f1a32de92 100644 --- a/src/Shared/Components/DocLink/constants.ts +++ b/src/Shared/Components/DocLink/constants.ts @@ -16,7 +16,7 @@ export const DOCUMENTATION = { APP_METRICS: 'usage/applications/app-details/app-metrics', APP_OVERVIEW_TAGS: 'usage/applications/overview#manage-tags', APP_ROLLOUT_DEPLOYMENT_TEMPLATE: 'usage/applications/creating-application/deployment-template/rollout-deployment', - BUILD_STAGE: 'usage/applications/creating-application/ci-pipeline#build-stage', + BUILD_STAGE: 'usage/applications/creating-application/workflow/ci-pipeline#build-stage', APP_TAGS: 'usage/applications/create-application#tags', BLOB_STORAGE: 'configurations-overview/installation-configuration#configuration-of-blob-storage', BULK_UPDATE: 'usage/bulk-update', @@ -45,7 +45,7 @@ export const DOCUMENTATION = { GLOBAL_CONFIG_CHART: 'getting-started/global-configurations/chart-repo', GLOBAL_CONFIG_CLUSTER: 'getting-started/global-configurations/cluster-and-environments', GLOBAL_CONFIG_CUSTOM_CHART: 'getting-started/global-configurations/custom-charts', - GLOBAL_CONFIG_CUSTOM_CHART_PRE_REQUISITES: 'getting-started/global-configurations/custom-charts#prerequisites', + GLOBAL_CONFIG_CUSTOM_CHART_PRE_REQUISITES: 'global-configurations/deployment-charts#preparing-a-deployment-chart', GLOBAL_CONFIG_DOCKER: 'getting-started/global-configurations/container-registries', GLOBAL_CONFIG_GIT: 'getting-started/global-configurations/git-accounts', GLOBAL_CONFIG_GITOPS: 'global-configurations/gitops', diff --git a/src/Shared/Components/DocLink/types.ts b/src/Shared/Components/DocLink/types.ts index c5526b098..4dfec7b99 100644 --- a/src/Shared/Components/DocLink/types.ts +++ b/src/Shared/Components/DocLink/types.ts @@ -10,9 +10,9 @@ export type BaseDocLink = { docLinkKey: T extends true ? string : keyof typeof DOCUMENTATION } -export type DocLinkProps = Pick< +export type DocLinkProps = Pick< ButtonProps, - 'dataTestId' | 'size' | 'variant' | 'fullWidth' + 'dataTestId' | 'size' | 'variant' | 'fullWidth' | 'fontWeight' | 'startIcon' > & Omit, 'isEnterprise'> & { text?: string diff --git a/src/Shared/Components/FeatureDescription/types.ts b/src/Shared/Components/FeatureDescription/types.ts index 2861952dc..3c1ce9bf2 100644 --- a/src/Shared/Components/FeatureDescription/types.ts +++ b/src/Shared/Components/FeatureDescription/types.ts @@ -22,7 +22,7 @@ import { DocLinkProps } from '../DocLink' interface BaseFeatureDescriptionModalProps { renderDescriptionContent?: () => ReactNode - docLink?: DocLinkProps['docLinkKey'] + docLink?: DocLinkProps['docLinkKey'] imageVariant?: ImageType SVGImage?: React.FunctionComponent> imageStyles?: React.CSSProperties diff --git a/src/Shared/Components/Header/HelpButton.tsx b/src/Shared/Components/Header/HelpButton.tsx index f5c952907..2815642b6 100644 --- a/src/Shared/Components/Header/HelpButton.tsx +++ b/src/Shared/Components/Header/HelpButton.tsx @@ -2,12 +2,12 @@ import { useRef, useState } from 'react' import ReactGA from 'react-ga4' import { SliderButton } from '@typeform/embed-react' -import { URLS } from '@Common/Constants' +import { DOCUMENTATION_HOME_PAGE, URLS } from '@Common/Constants' import { ComponentSizeType } from '@Shared/constants' import { useMainContext } from '@Shared/Providers' import { InstallationType } from '@Shared/types' -import { ActionMenu, ActionMenuItemType } from '../ActionMenu' +import { ActionMenu } from '../ActionMenu' import { Button, ButtonComponentType, ButtonVariantType } from '../Button' import { Icon } from '../Icon' import { HelpButtonActionMenuProps, HelpButtonProps, HelpMenuItems } from './types' @@ -17,7 +17,7 @@ const CheckForUpdates = ({ serverInfo, fetchingServerInfo, }: Pick) => ( -
+
{fetchingServerInfo ? (

Checking version

) : ( @@ -41,7 +41,8 @@ export const HelpButton = ({ serverInfo, fetchingServerInfo, onClick }: HelpButt const [isActionMenuOpen, setIsActionMenuOpen] = useState(false) // HOOKS - const { handleOpenLicenseInfoDialog, licenseData, setGettingStartedClicked, isEnterprise } = useMainContext() + const { handleOpenLicenseInfoDialog, licenseData, setGettingStartedClicked, isEnterprise, setSidePanelConfig } = + useMainContext() // REFS const typeFormSliderButtonRef = useRef(null) @@ -50,10 +51,10 @@ export const HelpButton = ({ serverInfo, fetchingServerInfo, onClick }: HelpButt const FEEDBACK_FORM_ID = `UheGN3KJ#source=${window.location.hostname}` // HANDLERS - const handleAnalytics = (option: ActionMenuItemType) => { + const handleAnalytics: HelpButtonActionMenuProps['onClick'] = (item) => { ReactGA.event({ category: 'Help Nav', - action: `${option.label} Clicked`, + action: `${item.label} Clicked`, }) } @@ -73,7 +74,21 @@ export const HelpButton = ({ serverInfo, fetchingServerInfo, onClick }: HelpButt setGettingStartedClicked(true) } - const handleActionMenuClick: HelpButtonActionMenuProps['onClick'] = (item) => { + const handleViewDocumentationClick: HelpButtonActionMenuProps['onClick'] = (item, e) => { + handleAnalytics(item, e) + // Opens documentation in side panel when clicked normally, or in a new tab when clicked with the meta/command key + if (!e.metaKey) { + e.preventDefault() + setSidePanelConfig((prev) => ({ + ...prev, + open: true, + docLink: DOCUMENTATION_HOME_PAGE, + reinitialize: true, + })) + } + } + + const handleActionMenuClick: HelpButtonActionMenuProps['onClick'] = (item, e) => { switch (item.id) { case HelpMenuItems.GETTING_STARTED: handleGettingStartedClick() @@ -85,12 +100,14 @@ export const HelpButton = ({ serverInfo, fetchingServerInfo, onClick }: HelpButt handleFeedbackClick() break case HelpMenuItems.JOIN_DISCORD_COMMUNITY: - case HelpMenuItems.VIEW_DOCUMENTATION: case HelpMenuItems.OPEN_NEW_TICKET: case HelpMenuItems.VIEW_ALL_TICKETS: case HelpMenuItems.CHAT_WITH_SUPPORT: case HelpMenuItems.RAISE_ISSUE_REQUEST: - handleAnalytics(item) + handleAnalytics(item, e) + break + case HelpMenuItems.VIEW_DOCUMENTATION: + handleViewDocumentationClick(item, e) break default: } @@ -110,7 +127,7 @@ export const HelpButton = ({ serverInfo, fetchingServerInfo, onClick }: HelpButt onOpen={setIsActionMenuOpen} {...(serverInfo?.installationType === InstallationType.OSS_HELM ? { - menuListFooterConfig: { + footerConfig: { type: 'customNode', value: ( diff --git a/src/Shared/Components/Header/PageHeader.tsx b/src/Shared/Components/Header/PageHeader.tsx index 858b9933d..28ef087ce 100644 --- a/src/Shared/Components/Header/PageHeader.tsx +++ b/src/Shared/Components/Header/PageHeader.tsx @@ -18,22 +18,21 @@ import { useEffect, useState } from 'react' import ReactGA from 'react-ga4' import Tippy from '@tippyjs/react' -import { ReactComponent as ICCaretDownSmall } from '@Icons/ic-caret-down-small.svg' import { ReactComponent as Close } from '@Icons/ic-close.svg' import { ReactComponent as ICMediumPaintBucket } from '@IconsV2/ic-medium-paintbucket.svg' import { InstallationType } from '@Shared/types' -import { getAlphabetIcon, TippyCustomized, TippyTheme } from '../../../Common' +import { TippyCustomized, TippyTheme } from '../../../Common' import { MAX_LOGIN_COUNT, POSTHOG_EVENT_ONBOARDING } from '../../../Common/Constants' import { useMainContext, useTheme, useUserEmail } from '../../Providers' import GettingStartedCard from '../GettingStartedCard/GettingStarted' import { InfoIconTippy } from '../InfoIconTippy' -import LogoutCard from '../LogoutCard' import { HelpButton } from './HelpButton' import { IframePromoButton } from './IframePromoButton' +import { ProfileMenu } from './ProfileMenu' import { getServerInfo } from './service' import { PageHeaderType, ServerInfo } from './types' -import { getIsShowingLicenseData, handlePostHogEventUpdate, setActionWithExpiry } from './utils' +import { handlePostHogEventUpdate, setActionWithExpiry } from './utils' import './pageHeader.scss' @@ -50,13 +49,11 @@ const PageHeader = ({ markAsBeta, tippyProps, }: PageHeaderType) => { - const { loginCount, setLoginCount, showGettingStartedCard, setShowGettingStartedCard, licenseData } = - useMainContext() + const { loginCount, setLoginCount, showGettingStartedCard, setShowGettingStartedCard } = useMainContext() const { showSwitchThemeLocationTippy, handleShowSwitchThemeLocationTippyChange } = useTheme() const { isTippyCustomized, tippyRedirectLink, TippyIcon, tippyMessage, onClickTippyButton, additionalContent } = tippyProps || {} - const [showLogOutCard, setShowLogOutCard] = useState(false) const { email } = useUserEmail() const [currentServerInfo, setCurrentServerInfo] = useState<{ serverInfo: ServerInfo; fetchingServerInfo: boolean }>( { @@ -98,25 +95,19 @@ const PageHeader = ({ handleShowSwitchThemeLocationTippyChange(false) } - const onClickLogoutButton = () => { + const handleProfileMenuButtonClick = () => { handleCloseSwitchThemeLocationTippyChange() - setShowLogOutCard(!showLogOutCard) setActionWithExpiry('clickedOkay', 1) hideGettingStartedCard() } - const onHelpButtonClick = async () => { + const handleHelpButtonClick = async () => { if ( !window._env_.K8S_CLIENT && currentServerInfo.serverInfo?.installationType !== InstallationType.ENTERPRISE ) { await getCurrentServerInfo() } - - if (showLogOutCard) { - setShowLogOutCard(false) - } - setActionWithExpiry('clickedOkay', 1) hideGettingStartedCard() await handlePostHogEventUpdate(POSTHOG_EVENT_ONBOARDING.HELP) @@ -138,7 +129,7 @@ const PageHeader = ({ {!window._env_.K8S_CLIENT && ( - +
+ +
)} @@ -183,8 +166,6 @@ const PageHeader = ({ Beta ) - const showingLicenseBar = getIsShowingLicenseData(licenseData) - const renderIframeButton = () => return ( @@ -251,7 +232,7 @@ const PageHeader = ({ {markAsBeta && renderBetaTag()}
{showTabs && ( -
+
{renderIframeButton()} {typeof renderActionButtons === 'function' && renderActionButtons()} {renderLogoutHelpSection()} @@ -271,16 +252,8 @@ const PageHeader = ({ loginCount={loginCount} /> )} - {showLogOutCard && ( - - )} {!showTabs && ( -
+
{typeof renderActionButtons === 'function' && renderActionButtons()} {renderIframeButton()} {renderLogoutHelpSection()} diff --git a/src/Shared/Components/Header/ProfileMenu.tsx b/src/Shared/Components/Header/ProfileMenu.tsx new file mode 100644 index 000000000..1b84c759e --- /dev/null +++ b/src/Shared/Components/Header/ProfileMenu.tsx @@ -0,0 +1,78 @@ +import { useMemo } from 'react' +import { Link } from 'react-router-dom' + +import { URLS } from '@Common/Constants' +import { getAlphabetIcon } from '@Common/Helper' +import { clearCookieOnLogout } from '@Shared/Helpers' +import { useMainContext } from '@Shared/Providers' + +import { Icon } from '../Icon' +import { Popover, usePopover } from '../Popover' +import { ThemeSwitcher } from '../ThemeSwitcher' +import { ProfileMenuProps } from './types' + +export const ProfileMenu = ({ user, onClick }: ProfileMenuProps) => { + // HOOKS + const { viewIsPipelineRBACConfiguredNode } = useMainContext() + + const { open, overlayProps, popoverProps, triggerProps, scrollableRef, closePopover } = usePopover({ + id: 'profile-menu', + alignment: 'end', + width: 250, + }) + + // ELEMENTS + const triggerElement = useMemo( + () => ( + + ), + [open], + ) + + // HANDLERS + const onLogout = () => { + closePopover() + clearCookieOnLogout() + } + + return ( + +
+
+
+
+

{user}

+

{user}

+
+ {getAlphabetIcon(user, 'dc__no-shrink m-0-imp fs-16 icon-dim-36')} +
+
+
+ + {viewIsPipelineRBACConfiguredNode} +
+
+ + Logout + + +
+
+
+ ) +} diff --git a/src/Shared/Components/Header/types.ts b/src/Shared/Components/Header/types.ts index 37313b42b..925965001 100644 --- a/src/Shared/Components/Header/types.ts +++ b/src/Shared/Components/Header/types.ts @@ -71,3 +71,8 @@ export enum HelpMenuItems { } export type HelpButtonActionMenuProps = ActionMenuProps + +export interface ProfileMenuProps { + user: string + onClick?: () => void +} diff --git a/src/Shared/Components/Header/utils.ts b/src/Shared/Components/Header/utils.ts index 769041b14..1896ea901 100644 --- a/src/Shared/Components/Header/utils.ts +++ b/src/Shared/Components/Header/utils.ts @@ -16,7 +16,6 @@ import { LOGIN_COUNT } from '@Common/Constants' -import { DevtronLicenseInfo, LicenseStatus } from '../License' import { COMMON_HELP_ACTION_MENU_ITEMS, ENTERPRISE_HELP_ACTION_MENU_ITEMS, @@ -43,9 +42,6 @@ export const setActionWithExpiry = (key: string, days: number): void => { localStorage.setItem(key, `${getDateInMilliseconds(days)}`) } -export const getIsShowingLicenseData = (licenseData: DevtronLicenseInfo) => - licenseData && (licenseData.licenseStatus !== LicenseStatus.ACTIVE || licenseData.isTrial) - export const getHelpActionMenuOptions = ({ isEnterprise, isTrial, diff --git a/src/Shared/Components/License/DevtronLicenseCard.tsx b/src/Shared/Components/License/DevtronLicenseCard.tsx index 25395dcc6..1b068b4f9 100644 --- a/src/Shared/Components/License/DevtronLicenseCard.tsx +++ b/src/Shared/Components/License/DevtronLicenseCard.tsx @@ -5,11 +5,10 @@ import { ReactComponent as ICChatSupport } from '@IconsV2/ic-chat-circle-dots.sv import { ReactComponent as TexturedBG } from '@Images/licenseCardBG.svg' import { ClipboardButton, getTTLInHumanReadableFormat } from '@Common/index' import { CONTACT_SUPPORT_LINK, ENTERPRISE_SUPPORT_LINK } from '@Shared/constants' -import { getHandleOpenURL } from '@Shared/Helpers' import { AppThemeType } from '@Shared/Providers' import { getThemeOppositeThemeClass } from '@Shared/Providers/ThemeProvider/utils' -import { Button, ButtonVariantType } from '../Button' +import { Button, ButtonComponentType, ButtonVariantType } from '../Button' import { Icon } from '../Icon' import { DevtronLicenseCardProps, LicenseStatus } from './types' import { getLicenseColorsAccordingToStatus } from './utils' @@ -147,7 +146,8 @@ export const DevtronLicenseCard = ({ startIcon={} text="Contact support" variant={ButtonVariantType.text} - onClick={getHandleOpenURL(CONTACT_SUPPORT_LINK)} + component={ButtonComponentType.anchor} + anchorProps={{ href: CONTACT_SUPPORT_LINK }} />
)} diff --git a/src/Shared/Components/LogoutCard.tsx b/src/Shared/Components/LogoutCard.tsx deleted file mode 100644 index 6f2e4cece..000000000 --- a/src/Shared/Components/LogoutCard.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react' -import { useHistory } from 'react-router-dom' - -import { clearCookieOnLogout } from '@Shared/Helpers' -import { useMainContext } from '@Shared/Providers' - -import { getRandomColor, stopPropagation } from '../../Common' -import { Icon } from './Icon' -import { ThemeSwitcher } from './ThemeSwitcher' - -interface LogoutCardType { - className: string - userFirstLetter: string - setShowLogOutCard: React.Dispatch> - showLogOutCard: boolean -} - -export const LOGOUT_CARD_BASE_BUTTON_CLASS = - 'dc__unset-button-styles dc__content-space px-12 py-8 fs-13 fw-4 lh-20 cursor w-100 flex left br-4' - -const LogoutCard = ({ className, userFirstLetter, setShowLogOutCard, showLogOutCard }: LogoutCardType) => { - const history = useHistory() - const { viewIsPipelineRBACConfiguredNode } = useMainContext() - - const onLogout = () => { - clearCookieOnLogout() - history.push('/login') - } - - const toggleLogoutCard = () => { - setShowLogOutCard(!showLogOutCard) - } - - return ( -
-
-
-
-

{userFirstLetter}

-

{userFirstLetter}

-
-

- {userFirstLetter[0]} -

-
-
- - - {viewIsPipelineRBACConfiguredNode} - - -
-
-
- ) -} - -export default LogoutCard diff --git a/src/Shared/Components/ModalSidebarPanel/types.ts b/src/Shared/Components/ModalSidebarPanel/types.ts index e6a6b4e4e..dbee95e50 100644 --- a/src/Shared/Components/ModalSidebarPanel/types.ts +++ b/src/Shared/Components/ModalSidebarPanel/types.ts @@ -23,5 +23,5 @@ export interface ModalSidebarPanelProps { heading: string | null icon?: JSX.Element children?: ReactNode - documentationLink: DocLinkProps['docLinkKey'] + documentationLink: DocLinkProps['docLinkKey'] } diff --git a/src/Shared/Components/NumbersCount/index.ts b/src/Shared/Components/NumbersCount/index.ts index b3b6ec1db..cdf8ca7ba 100644 --- a/src/Shared/Components/NumbersCount/index.ts +++ b/src/Shared/Components/NumbersCount/index.ts @@ -15,3 +15,4 @@ */ export { default as NumbersCount } from './NumbersCount.component' +export * from './types' diff --git a/src/Shared/Components/Popover/Popover.component.tsx b/src/Shared/Components/Popover/Popover.component.tsx index 68640c840..d332af886 100644 --- a/src/Shared/Components/Popover/Popover.component.tsx +++ b/src/Shared/Components/Popover/Popover.component.tsx @@ -15,24 +15,25 @@ export const Popover = ({ open, popoverProps, overlayProps, - triggerProps, + triggerProps: { bounds, ...triggerProps }, buttonProps, triggerElement, children, }: PopoverProps) => ( -
+ <>
{triggerElement ||
{open && ( - <> - {/* Overlay to block interactions with the background */} -
- - {children} - - +
+
+
+ + {children} + +
+
)} -
+ ) diff --git a/src/Shared/Components/Popover/popover.scss b/src/Shared/Components/Popover/popover.scss index f241ad180..26caeb6e0 100644 --- a/src/Shared/Components/Popover/popover.scss +++ b/src/Shared/Components/Popover/popover.scss @@ -6,7 +6,3 @@ left:0; z-index: var(--modal-index); } - -.popover-content { - z-index: var(--modal-index); -} diff --git a/src/Shared/Components/Popover/types.ts b/src/Shared/Components/Popover/types.ts index 5b1fdaac8..40ea7c343 100644 --- a/src/Shared/Components/Popover/types.ts +++ b/src/Shared/Components/Popover/types.ts @@ -1,4 +1,4 @@ -import { DetailedHTMLProps, KeyboardEvent, MutableRefObject, ReactElement } from 'react' +import { DetailedHTMLProps, KeyboardEvent, LegacyRef, MutableRefObject, ReactElement } from 'react' import { HTMLMotionProps } from 'framer-motion' import { ButtonProps } from '../Button' @@ -67,7 +67,9 @@ export interface UsePopoverReturnType { * Props to be spread onto the trigger element that opens the popover. * These props include standard HTML attributes for a `div` element. */ - triggerProps: DetailedHTMLProps, HTMLDivElement> + triggerProps: DetailedHTMLProps, HTMLDivElement> & { + bounds: Pick + } /** * Props to be spread onto the overlay element of the popover. * These props include standard HTML attributes for a `div` element. @@ -82,7 +84,7 @@ export interface UsePopoverReturnType { * A mutable reference to the scrollable element inside the popover. \ * This reference should be assigned to the element that is scrollable. */ - scrollableRef: MutableRefObject + scrollableRef: MutableRefObject | LegacyRef /** * A function to close the popover. */ diff --git a/src/Shared/Components/Popover/usePopover.hook.ts b/src/Shared/Components/Popover/usePopover.hook.ts index d95ff93b2..26a0b7a0e 100644 --- a/src/Shared/Components/Popover/usePopover.hook.ts +++ b/src/Shared/Components/Popover/usePopover.hook.ts @@ -1,4 +1,4 @@ -import { useLayoutEffect, useRef, useState } from 'react' +import { MouseEvent, useLayoutEffect, useRef, useState } from 'react' import { UsePopoverProps, UsePopoverReturnType } from './types' import { @@ -21,6 +21,7 @@ export const usePopover = ({ const [open, setOpen] = useState(false) const [actualPosition, setActualPosition] = useState(position) const [actualAlignment, setActualAlignment] = useState(alignment) + const [triggerBounds, setTriggerBounds] = useState(null) // CONSTANTS const isAutoWidth = width === 'auto' @@ -49,25 +50,42 @@ export const usePopover = ({ onTriggerKeyDown?.(e, open, closePopover) } - const handlePopoverKeyDown = (e: React.KeyboardEvent) => onPopoverKeyDown(e, open, closePopover) + const handlePopoverKeyDown = (e: React.KeyboardEvent) => onPopoverKeyDown?.(e, open, closePopover) + + const handleOverlayClick = (e: MouseEvent) => { + if (!popover.current?.contains(e.target as Node)) { + closePopover() + } + } useLayoutEffect(() => { if (!open || !triggerRef.current || !popover.current || !scrollableRef.current) { return } - const triggerRect = triggerRef.current.getBoundingClientRect() - const popoverRect = popover.current.getBoundingClientRect() - - const { fallbackPosition, fallbackAlignment } = getPopoverActualPositionAlignment({ - position, - alignment, - triggerRect, - popoverRect, - }) + const updatePopoverPosition = () => { + const triggerRect = triggerRef.current.getBoundingClientRect() + const popoverRect = popover.current.getBoundingClientRect() + + const { fallbackPosition, fallbackAlignment } = getPopoverActualPositionAlignment({ + position, + alignment, + triggerRect, + popoverRect, + }) + + setActualPosition(fallbackPosition) + setActualAlignment(fallbackAlignment) + setTriggerBounds({ + left: triggerRect.left, + top: triggerRect.top, + height: triggerRect.height, + width: triggerRect.width, + }) + } - setActualPosition(fallbackPosition) - setActualAlignment(fallbackAlignment) + // update position on open + updatePopoverPosition() // prevent scroll propagation unless scrollable const handleWheel = (e: WheelEvent) => { @@ -84,9 +102,12 @@ export const usePopover = ({ } scrollableRef.current.addEventListener('wheel', handleWheel, { passive: false }) + window.addEventListener('resize', updatePopoverPosition) + // eslint-disable-next-line consistent-return return () => { scrollableRef.current.removeEventListener('wheel', handleWheel) + window.removeEventListener('resize', updatePopoverPosition) } }, [open, position, alignment]) @@ -100,17 +121,18 @@ export const usePopover = ({ 'aria-haspopup': 'listbox', 'aria-expanded': open, tabIndex: 0, + bounds: triggerBounds ?? { left: 0, top: 0, height: 0, width: 0 }, }, overlayProps: { role: 'dialog', - onClick: closePopover, + onClick: handleOverlayClick, className: 'popover-overlay', }, popoverProps: { id, ref: popover, role: 'listbox', - className: `popover-content dc__position-abs bg__menu--primary shadow__menu border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''}`, + className: `dc__position-abs bg__menu--primary shadow__menu border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''}`, onKeyDown: handlePopoverKeyDown, style: { width: !isAutoWidth ? `${width}px` : undefined, diff --git a/src/Shared/Components/Switch/types.ts b/src/Shared/Components/Switch/types.ts index 9b8ff64a5..fa4c6e42c 100644 --- a/src/Shared/Components/Switch/types.ts +++ b/src/Shared/Components/Switch/types.ts @@ -1,3 +1,5 @@ +import { ButtonHTMLAttributes } from 'react' + import { ComponentSizeType } from '@Shared/constants' import { IconBaseColorType } from '@Shared/types' @@ -91,7 +93,7 @@ export type DTSwitchProps = { * Callback function that is called when the switch state changes. * This function should handle the logic for toggling the switch. */ - onChange: () => void + onChange: ButtonHTMLAttributes['onClick'] /** * Indicates whether the switch is disabled. diff --git a/src/Shared/Components/ThemeSwitcher/ThemeSwitcher.component.tsx b/src/Shared/Components/ThemeSwitcher/ThemeSwitcher.component.tsx index ec4b635e2..587de3b63 100644 --- a/src/Shared/Components/ThemeSwitcher/ThemeSwitcher.component.tsx +++ b/src/Shared/Components/ThemeSwitcher/ThemeSwitcher.component.tsx @@ -14,32 +14,31 @@ * limitations under the License. */ -import { ReactComponent as ICCaretLeftSmall } from '@Icons/ic-caret-left-small.svg' import { getThemePreferenceText, useTheme } from '@Shared/Providers' -import { LOGOUT_CARD_BASE_BUTTON_CLASS } from '../LogoutCard' +import { Icon } from '../Icon' import { ThemeSwitcherProps } from './types' -const ThemeSwitcher = ({ onChange }: ThemeSwitcherProps) => { +const ThemeSwitcher = ({ onClick }: ThemeSwitcherProps) => { const { handleThemeSwitcherDialogVisibilityChange, themePreference } = useTheme() const handleShowThemeSwitcherDialog = () => { handleThemeSwitcherDialogVisibilityChange(true) - onChange() + onClick?.() } return ( ) } diff --git a/src/Shared/Components/ThemeSwitcher/index.ts b/src/Shared/Components/ThemeSwitcher/index.ts index 5913dfa3f..9dba93c23 100644 --- a/src/Shared/Components/ThemeSwitcher/index.ts +++ b/src/Shared/Components/ThemeSwitcher/index.ts @@ -15,4 +15,3 @@ */ export { default as ThemeSwitcher } from './ThemeSwitcher.component' -export type { ThemeSwitcherProps } from './types' diff --git a/src/Shared/Components/ThemeSwitcher/types.ts b/src/Shared/Components/ThemeSwitcher/types.ts index a75045c9b..f86536088 100644 --- a/src/Shared/Components/ThemeSwitcher/types.ts +++ b/src/Shared/Components/ThemeSwitcher/types.ts @@ -15,5 +15,5 @@ */ export interface ThemeSwitcherProps { - onChange: () => void + onClick?: () => void } diff --git a/src/Shared/Providers/types.ts b/src/Shared/Providers/types.ts index fd2b10217..4911a42b9 100644 --- a/src/Shared/Providers/types.ts +++ b/src/Shared/Providers/types.ts @@ -31,8 +31,12 @@ export interface ReloadVersionConfigTypes { } export interface SidePanelConfig { + /** Determines whether the side panel is visible */ open: boolean - docLink?: string | null + /** Optional flag to reset/reinitialize the side panel state */ + reinitialize?: boolean + /** URL to documentation that should be displayed in the panel */ + docLink: string | null } export interface MainContext { diff --git a/src/Shared/types.ts b/src/Shared/types.ts index cced7b5cd..bd265c365 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -986,6 +986,27 @@ export type Never = { [K in keyof T]?: never } +/** + * A utility type that filters out properties from type `T` that are of type `never`. \ + * This is useful when you want to remove properties that have been marked as `never` from a type, + * effectively creating a new type without those properties. + * + * @template T - The input type from which to filter out `never` properties. + * @example + * ```typescript + * type User = { + * id: number; + * name: string; + * deleted: never; + * } + * + * type ActiveUser = OmitNever; // { id: number; name: string; } + * ``` + */ +export type OmitNever = { + [K in keyof T as T[K] extends never ? never : K]: T[K] +} + export interface TargetPlatformItemDTO { name: string }