Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 74 additions & 6 deletions code/core/src/manager/components/sidebar/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { ComponentProps, FC, SyntheticEvent } from 'react';
import React, { useMemo, useState } from 'react';
import React, { useContext, useMemo, useState } from 'react';

import { PopoverProvider, TooltipLinkList } from 'storybook/internal/components';
import {
type API_HashEntry,
type Addon_Collection,
type Addon_TestProviderType,
Addon_TypesEnum,
type StatusValue,
} from 'storybook/internal/types';

import { CopyIcon, EditorIcon, EllipsisIcon } from '@storybook/icons';
Expand All @@ -18,7 +19,10 @@ import { styled } from 'storybook/theming';

import type { Link } from '../../../components/components/tooltip/TooltipLinkList';
import { Shortcut } from '../../container/Menu';
import { getMostCriticalStatusValue } from '../../utils/status';
import { UseSymbol } from './IconSymbols';
import { StatusButton } from './StatusButton';
import { StatusContext } from './StatusContext';
import type { ExcludesNull } from './Tree';

const empty = {
Expand All @@ -41,6 +45,7 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API)
const [hoverCount, setHoverCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const [copyText, setCopyText] = React.useState('Copy story name');
const { allStatuses, groupStatus } = useContext(StatusContext);

const shortcutKeys = api.getShortcutKeys();
const enableShortcuts = !!shortcutKeys;
Expand Down Expand Up @@ -85,7 +90,7 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API)
}

return defaultLinks;
}, [context, copyText, enableShortcuts, shortcutKeys]);
}, [api, context, copyText, enableShortcuts, shortcutKeys]);

const handlers = useMemo(() => {
return {
Expand Down Expand Up @@ -118,6 +123,69 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API)
const shouldRender =
!context.refId && (providerLinks.length > 0 || links.length > 0 || topLinks.length > 0);

const isLeafNode = context.type === 'story' || context.type === 'docs';

const itemStatus = useMemo<StatusValue>(() => {
let status: StatusValue = 'status-value:unknown';
if (!context) {
return status;
}

if (isLeafNode) {
const values = Object.values(allStatuses?.[context.id] || {}).map((s) => s.value);
status = getMostCriticalStatusValue(values);
}

if (!isLeafNode) {
// On component/groups we only show non-ellipsis on hover on non-success status colors
const groupValue = groupStatus && groupStatus[context.id];
status =
groupValue === 'status-value:success' || groupValue === undefined
? 'status-value:unknown'
: groupValue;
}

return status;
}, [allStatuses, groupStatus, context, isLeafNode]);

const MenuIcon = useMemo(() => {
// On component/groups we only show non-ellipsis on hover on non-success statuses
if (context.type !== 'story' && context.type !== 'docs') {
if (itemStatus !== 'status-value:success' && itemStatus !== 'status-value:unknown') {
return (
<svg key="icon" viewBox="0 0 6 6" width="6" height="6">
<UseSymbol type="dot" />
</svg>
);
}

return <EllipsisIcon />;
}

if (itemStatus === 'status-value:error') {
return (
<svg key="icon" viewBox="0 0 14 14" width="14" height="14">
<UseSymbol type="error" />
</svg>
);
}
if (itemStatus === 'status-value:warning') {
return (
<svg key="icon" viewBox="0 0 14 14" width="14" height="14">
<UseSymbol type="warning" />
</svg>
);
}
if (itemStatus === 'status-value:success') {
return (
<svg key="icon" viewBox="0 0 14 14" width="14" height="14">
<UseSymbol type="success" />
</svg>
);
}
return <EllipsisIcon />;
}, [itemStatus, context.type]);

return useMemo(() => {
// Never show the SidebarContextMenu in production
if (globalThis.CONFIG_TYPE !== 'DEVELOPMENT') {
Expand All @@ -141,15 +209,15 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API)
data-testid="context-menu"
ariaLabel="Open context menu"
type="button"
status="status-value:pending"
status={itemStatus}
onClick={handlers.onOpen}
>
<EllipsisIcon />
{MenuIcon}
</FloatingStatusButton>
</PopoverProvider>
) : null,
};
}, [context, handlers, isOpen, shouldRender, links, topLinks]);
}, [context, handlers, isOpen, shouldRender, links, topLinks, itemStatus, MenuIcon]);
};

/**
Expand Down Expand Up @@ -200,5 +268,5 @@ export function generateTestProviderLinks(
content,
};
})
.filter(Boolean as any as ExcludesNull);
.filter(Boolean as unknown as ExcludesNull);
}
37 changes: 1 addition & 36 deletions code/core/src/manager/components/sidebar/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,6 @@ const Node = React.memo<NodeProps>(function Node(props) {
} = props;
const theme = useTheme();
const { isDesktop, isMobile, setMobileMenuOpen } = useLayout();
const { counts, statusesByValue } = useStatusSummary(item);

if (!isDisplayed) {
return null;
Expand All @@ -240,42 +239,8 @@ const Node = React.memo<NodeProps>(function Node(props) {
}));
}

// TODO should this be updated for stories with tests?
if (item.type === 'component' || item.type === 'group') {
const links: Link[] = [];
const errorCount = counts['status-value:error'];
const warningCount = counts['status-value:warning'];
if (errorCount) {
links.push({
id: 'errors',
icon: StatusIconMap['status-value:error'],
title: `${errorCount} ${errorCount === 1 ? 'story' : 'stories'} with errors`,
onClick: () => {
const [firstStoryId] = Object.entries(statusesByValue['status-value:error'])[0];
onSelectStoryId(firstStoryId);
const errorStatuses = Object.values(statusesByValue['status-value:error']).flat();
fullStatusStore.selectStatuses(errorStatuses);
},
});
}
if (warningCount) {
links.push({
id: 'warnings',
icon: StatusIconMap['status-value:warning'],
title: `${warningCount} ${warningCount === 1 ? 'story' : 'stories'} with warnings`,
onClick: () => {
const [firstStoryId] = Object.entries(statusesByValue['status-value:warning'])[0];
onSelectStoryId(firstStoryId);
const warningStatuses = Object.values(statusesByValue['status-value:warning']).flat();
fullStatusStore.selectStatuses(warningStatuses);
},
});
}
return links;
}

return [];
}, [counts, item.id, item.type, onSelectStoryId, statuses, statusesByValue]);
}, [item.id, item.type, onSelectStoryId, statuses]);

const id = createId(item.id, refId);
const contextMenu =
Expand Down
Loading