Skip to content

Commit

Permalink
Merge pull request #18484 from storybookjs/ghengeveld/ap-1856-collaps…
Browse files Browse the repository at this point in the history
…ible-group-ui

Interactions: Collapse child interactions
  • Loading branch information
shilman authored Jun 16, 2022
2 parents d4efb62 + 7defcaf commit ef2bc81
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 16 deletions.
31 changes: 29 additions & 2 deletions addons/interactions/src/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ interface InteractionsPanelProps {
active: boolean;
controls: Controls;
controlStates: ControlStates;
interactions: (Call & { status?: CallStates })[];
interactions: (Call & {
status?: CallStates;
childCallIds: Call['id'][];
isCollapsed: boolean;
toggleCollapsed: () => void;
})[];
fileName?: string;
hasException?: boolean;
isPlaying?: boolean;
Expand Down Expand Up @@ -97,6 +102,9 @@ export const AddonPanelPure: React.FC<InteractionsPanelProps> = React.memo(
callsById={calls}
controls={controls}
controlStates={controlStates}
childCallIds={call.childCallIds}
isCollapsed={call.isCollapsed}
toggleCollapsed={call.toggleCollapsed}
pausedAt={pausedAt}
/>
))}
Expand Down Expand Up @@ -125,13 +133,32 @@ export const Panel: React.FC<AddonPanelProps> = (props) => {
const [isPlaying, setPlaying] = React.useState(false);
const [isRerunAnimating, setIsRerunAnimating] = React.useState(false);
const [scrollTarget, setScrollTarget] = React.useState<HTMLElement>();
const [collapsed, setCollapsed] = React.useState<Set<Call['id']>>(new Set());

// Calls are tracked in a ref so we don't needlessly rerender.
const calls = React.useRef<Map<Call['id'], Omit<Call, 'status'>>>(new Map());
const setCall = ({ status, ...call }: Call) => calls.current.set(call.id, call);

const [log, setLog] = React.useState<LogItem[]>([]);
const interactions = log.map(({ callId, status }) => ({ ...calls.current.get(callId), status }));
const childCallMap = new Map<Call['id'], Call['id'][]>();
const interactions = log
.filter((call) => {
if (!call.parentId) return true;
childCallMap.set(call.parentId, (childCallMap.get(call.parentId) || []).concat(call.callId));
return !collapsed.has(call.parentId);
})
.map(({ callId, status }) => ({
...calls.current.get(callId),
status,
childCallIds: childCallMap.get(callId),
isCollapsed: collapsed.has(callId),
toggleCollapsed: () =>
setCollapsed((ids) => {
if (ids.has(callId)) ids.delete(callId);
else ids.add(callId);
return new Set(ids);
}),
}));

const endRef = React.useRef();
React.useEffect(() => {
Expand Down
70 changes: 57 additions & 13 deletions addons/interactions/src/components/Interaction/Interaction.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { IconButton, Icons, TooltipNote, WithTooltip } from '@storybook/components';
import { Call, CallStates, ControlStates } from '@storybook/instrumenter';
import { styled, typography } from '@storybook/theming';
import { transparentize } from 'polished';
Expand Down Expand Up @@ -55,9 +56,15 @@ const RowContainer = styled('div', {
}
);

const RowHeader = styled.div<{ disabled: boolean }>(({ theme, disabled }) => ({
display: 'flex',
'&:hover': disabled ? {} : { background: theme.background.hoverable },
}));

const RowLabel = styled('button', { shouldForwardProp: (prop) => !['call'].includes(prop) })<
React.ButtonHTMLAttributes<HTMLButtonElement> & { call: Call }
>(({ theme, disabled, call }) => ({
flex: 1,
display: 'grid',
background: 'none',
border: 0,
Expand All @@ -68,7 +75,6 @@ const RowLabel = styled('button', { shouldForwardProp: (prop) => !['call'].inclu
padding: '8px 15px',
textAlign: 'start',
cursor: disabled || call.status === CallStates.ERROR ? 'default' : 'pointer',
'&:hover': disabled ? {} : { background: theme.background.hoverable },
'&:focus-visible': {
outline: 0,
boxShadow: `inset 3px 0 0 0 ${
Expand All @@ -81,6 +87,19 @@ const RowLabel = styled('button', { shouldForwardProp: (prop) => !['call'].inclu
},
}));

const RowActions = styled.div(({ theme }) => ({
padding: 6,
}));

export const StyledIconButton = styled(IconButton as any)(({ theme }) => ({
color: theme.color.mediumdark,
margin: '0 3px',
}));

const Note = styled(TooltipNote)(({ theme }) => ({
fontFamily: theme.typography.fonts.base,
}));

const RowMessage = styled('div')(({ theme }) => ({
padding: '8px 10px 8px 36px',
fontSize: typography.size.s1,
Expand Down Expand Up @@ -112,29 +131,54 @@ export const Interaction = ({
callsById,
controls,
controlStates,
childCallIds,
isCollapsed,
toggleCollapsed,
pausedAt,
}: {
call: Call;
callsById: Map<Call['id'], Call>;
controls: Controls;
controlStates: ControlStates;
childCallIds?: Call['id'][];
isCollapsed: boolean;
toggleCollapsed: () => void;
pausedAt?: Call['id'];
}) => {
const [isHovered, setIsHovered] = React.useState(false);
return (
<RowContainer call={call} pausedAt={pausedAt}>
<RowLabel
call={call}
onClick={() => controls.goto(call.id)}
disabled={!controlStates.goto || !call.interceptable || !!call.parentId}
onMouseEnter={() => controlStates.goto && setIsHovered(true)}
onMouseLeave={() => controlStates.goto && setIsHovered(false)}
>
<StatusIcon status={isHovered ? CallStates.ACTIVE : call.status} />
<MethodCallWrapper style={{ marginLeft: 6, marginBottom: 1 }}>
<MethodCall call={call} callsById={callsById} />
</MethodCallWrapper>
</RowLabel>
<RowHeader disabled={!controlStates.goto || !call.interceptable || !!call.parentId}>
<RowLabel
call={call}
onClick={() => controls.goto(call.id)}
disabled={!controlStates.goto || !call.interceptable || !!call.parentId}
onMouseEnter={() => controlStates.goto && setIsHovered(true)}
onMouseLeave={() => controlStates.goto && setIsHovered(false)}
>
<StatusIcon status={isHovered ? CallStates.ACTIVE : call.status} />
<MethodCallWrapper style={{ marginLeft: 6, marginBottom: 1 }}>
<MethodCall call={call} callsById={callsById} />
</MethodCallWrapper>
</RowLabel>
<RowActions>
{childCallIds?.length > 0 && (
<WithTooltip
hasChrome={false}
tooltip={
<Note
note={`${isCollapsed ? 'Show' : 'Hide'} interactions (${childCallIds.length})`}
/>
}
>
<StyledIconButton containsIcon onClick={toggleCollapsed}>
<Icons icon="listunordered" />
</StyledIconButton>
</WithTooltip>
)}
</RowActions>
</RowHeader>

{call.status === CallStates.ERROR && call.exception?.callId === call.id && (
<Exception exception={call.exception} />
)}
Expand Down
6 changes: 5 additions & 1 deletion addons/interactions/src/components/MethodCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,11 @@ export const ClassNode = ({ name }: { name: string }) => {

export const FunctionNode = ({ name }: { name: string }) => {
const colors = useThemeColors();
return <span style={{ color: colors.function }}>{name || <i>anonymous</i>}</span>;
return name ? (
<span style={{ color: colors.function }}>{name}</span>
) : (
<span style={{ color: colors.nullish, fontStyle: 'italic' }}>anonymous</span>
);
};

export const ElementNode = ({
Expand Down

0 comments on commit ef2bc81

Please sign in to comment.