Skip to content

Commit 76199dd

Browse files
Separate the Copy Link to Event button into a dedicated component (#927)
- Create new WorkflowHistoryEventLinkButton component that copies the link to a history event - Remove resulting dead code
1 parent 26e508a commit 76199dd

File tree

6 files changed

+185
-99
lines changed

6 files changed

+185
-99
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import copy from 'copy-to-clipboard';
2+
3+
import { render, screen, userEvent, waitFor } from '@/test-utils/rtl';
4+
5+
import WorkflowHistoryEventLinkButton from '../workflow-history-event-link-button';
6+
7+
jest.mock('copy-to-clipboard', jest.fn);
8+
9+
describe('WorkflowHistoryEventLinkButton', () => {
10+
let originalWindow: Window & typeof globalThis;
11+
12+
beforeEach(() => {
13+
// Mock window.location
14+
originalWindow = window;
15+
window = Object.create(window);
16+
Object.defineProperty(window, 'location', {
17+
value: {
18+
...window.location,
19+
origin: 'http://localhost',
20+
pathname:
21+
'/domains/test-domain/workflows/test-workflow/test-run/history',
22+
},
23+
writable: true,
24+
});
25+
});
26+
27+
afterEach(() => {
28+
window = originalWindow;
29+
jest.clearAllMocks();
30+
});
31+
32+
it('renders correctly', () => {
33+
render(<WorkflowHistoryEventLinkButton historyEventId="123" />);
34+
const button = screen.getByRole('button');
35+
expect(button).toBeInTheDocument();
36+
expect(button).toHaveAccessibleName('Copy link to event');
37+
});
38+
39+
it('shows tooltip with "Copy link to event" by default', async () => {
40+
const user = userEvent.setup();
41+
render(<WorkflowHistoryEventLinkButton historyEventId="123" />);
42+
const button = screen.getByRole('button');
43+
await user.hover(button);
44+
expect(await screen.findByText('Copy link to event')).toBeInTheDocument();
45+
});
46+
47+
it('copies the current URL with event ID when clicked', async () => {
48+
const user = userEvent.setup();
49+
render(<WorkflowHistoryEventLinkButton historyEventId="123" />);
50+
const button = screen.getByRole('button');
51+
await user.click(button);
52+
expect(copy).toHaveBeenCalledWith(
53+
'http://localhost/domains/test-domain/workflows/test-workflow/test-run/history?he=123'
54+
);
55+
});
56+
57+
it('shows "Copied link to event" tooltip after clicking', async () => {
58+
const user = userEvent.setup();
59+
render(<WorkflowHistoryEventLinkButton historyEventId="123" />);
60+
const button = screen.getByRole('button');
61+
await user.click(button);
62+
expect(await screen.findByText('Copied link to event')).toBeInTheDocument();
63+
});
64+
65+
it('resets tooltip text when mouse leaves', async () => {
66+
const user = userEvent.setup();
67+
render(<WorkflowHistoryEventLinkButton historyEventId="123" />);
68+
const button = screen.getByRole('button');
69+
await user.click(button);
70+
71+
const copiedText = await screen.findByText('Copied link to event');
72+
expect(copiedText).toBeInTheDocument();
73+
74+
await user.unhover(button);
75+
await waitFor(() => {
76+
expect(copiedText).not.toBeInTheDocument();
77+
});
78+
79+
await user.hover(button);
80+
expect(await screen.findByText('Copy link to event')).toBeInTheDocument();
81+
});
82+
83+
it('adds ungrouped view parameter when isUngroupedView is true', async () => {
84+
const user = userEvent.setup();
85+
render(
86+
<WorkflowHistoryEventLinkButton historyEventId="123" isUngroupedView />
87+
);
88+
const button = screen.getByRole('button');
89+
await user.click(button);
90+
expect(copy).toHaveBeenCalledWith(
91+
'http://localhost/domains/test-domain/workflows/test-workflow/test-run/history?he=123&u=true'
92+
);
93+
});
94+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { type Theme } from 'baseui';
2+
import { type ButtonOverrides } from 'baseui/button';
3+
import { type StyleObject } from 'styletron-react';
4+
5+
export const overrides = {
6+
copyButton: {
7+
BaseButton: {
8+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
9+
height: $theme.sizing.scale600,
10+
width: $theme.sizing.scale600,
11+
}),
12+
},
13+
} satisfies ButtonOverrides,
14+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useState } from 'react';
2+
3+
import { Button } from 'baseui/button';
4+
import { StatefulTooltip } from 'baseui/tooltip';
5+
import copy from 'copy-to-clipboard';
6+
import queryString from 'query-string';
7+
import { MdLink } from 'react-icons/md';
8+
9+
import { overrides } from './workflow-history-event-link-button.styles';
10+
import { type Props } from './workflow-history-event-link-button.types';
11+
12+
export default function WorkflowHistoryEventLinkButton({
13+
historyEventId,
14+
isUngroupedView,
15+
}: Props) {
16+
const [isEventLinkCopied, setIsEventLinkCopied] = useState(false);
17+
18+
return (
19+
<StatefulTooltip
20+
showArrow
21+
placement="right"
22+
popoverMargin={8}
23+
accessibilityType="tooltip"
24+
content={() =>
25+
isEventLinkCopied ? 'Copied link to event' : 'Copy link to event'
26+
}
27+
onMouseLeave={() => setIsEventLinkCopied(false)}
28+
returnFocus
29+
autoFocus
30+
>
31+
<Button
32+
aria-label="Copy link to event"
33+
size="mini"
34+
shape="circle"
35+
kind="tertiary"
36+
overrides={overrides.copyButton}
37+
onClick={(e) => {
38+
e.stopPropagation();
39+
copy(
40+
queryString.stringifyUrl({
41+
url: window.location.origin + window.location.pathname,
42+
query: {
43+
he: historyEventId,
44+
u: isUngroupedView ? 'true' : undefined,
45+
},
46+
})
47+
);
48+
setIsEventLinkCopied(true);
49+
}}
50+
>
51+
<MdLink />
52+
</Button>
53+
</StatefulTooltip>
54+
);
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type Props = {
2+
historyEventId: string;
3+
isUngroupedView?: boolean;
4+
};

src/views/workflow-history/workflow-history-events-card/__tests__/workflow-history-events-card.test.tsx

Lines changed: 15 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import copy from 'copy-to-clipboard';
2-
31
import { render, screen, userEvent } from '@/test-utils/rtl';
42

53
import {
@@ -19,7 +17,15 @@ jest.mock(
1917
() => jest.fn(({ event }) => <div>Details eventId: {event.eventId}</div>)
2018
);
2119

22-
jest.mock('copy-to-clipboard', jest.fn);
20+
jest.mock(
21+
'../../workflow-history-event-link-button/workflow-history-event-link-button',
22+
() =>
23+
jest.fn(({ historyEventId }) => (
24+
<div data-testid="event-link-button" data-event-id={historyEventId}>
25+
Link to Event {historyEventId}
26+
</div>
27+
))
28+
);
2329

2430
describe('WorkflowHistoryEventsCard', () => {
2531
it('shows events label and status correctly', () => {
@@ -68,6 +74,7 @@ describe('WorkflowHistoryEventsCard', () => {
6874
`Details eventId: ${scheduleActivityTaskEvent.eventId}`
6975
)
7076
).not.toBeInTheDocument();
77+
expect(screen.queryByTestId('event-link-button')).not.toBeInTheDocument();
7178
});
7279

7380
it('render accordion expanded when get getIsEventExpanded returns true', async () => {
@@ -89,6 +96,11 @@ describe('WorkflowHistoryEventsCard', () => {
8996
`Details eventId: ${scheduleActivityTaskEvent.eventId}`
9097
)
9198
).toBeInTheDocument();
99+
expect(screen.getByTestId('event-link-button')).toBeInTheDocument();
100+
expect(screen.getByTestId('event-link-button')).toHaveAttribute(
101+
'data-event-id',
102+
scheduleActivityTaskEvent.eventId
103+
);
92104
});
93105

94106
it('should call onEventToggle callback on click', async () => {
@@ -121,56 +133,6 @@ describe('WorkflowHistoryEventsCard', () => {
121133
expect(mockedOnEventToggle).toHaveBeenCalledWith('9');
122134
});
123135

124-
it('should show copy event button when the accordion is expanded', async () => {
125-
// TODO: this is a bit hacky, see if there is a better way to mock window properties
126-
const originalWindow = window;
127-
window = Object.create(window);
128-
Object.defineProperty(window, 'location', {
129-
value: {
130-
...window.location,
131-
origin: 'http://localhost',
132-
pathname: '/domains/mock-domain/workflows/wfid/runid/history',
133-
},
134-
writable: true,
135-
});
136-
137-
const events: Props['events'] = [
138-
scheduleActivityTaskEvent,
139-
startActivityTaskEvent,
140-
];
141-
142-
const eventsMetadata: Props['eventsMetadata'] = [
143-
{
144-
label: 'First event',
145-
status: 'COMPLETED',
146-
},
147-
{
148-
label: 'Second event',
149-
status: 'ONGOING',
150-
},
151-
];
152-
153-
const { user } = setup({
154-
events,
155-
eventsMetadata,
156-
getIsEventExpanded: jest.fn().mockReturnValue(true),
157-
});
158-
159-
const shareButtons = await screen.findAllByTestId('share-button');
160-
expect(shareButtons).toHaveLength(2);
161-
162-
await user.hover(shareButtons[0]);
163-
expect(await screen.findByText('Copy link to event')).toBeInTheDocument();
164-
165-
await user.click(shareButtons[0]);
166-
expect(copy).toHaveBeenCalledWith(
167-
'http://localhost/domains/mock-domain/workflows/wfid/runid/history?he=7'
168-
);
169-
expect(await screen.findByText('Copied link to event')).toBeInTheDocument();
170-
171-
window = originalWindow;
172-
});
173-
174136
it('should add placeholder event when showMissingEventPlaceholder is set to true', async () => {
175137
const events: Props['events'] = [scheduleActivityTaskEvent];
176138
const eventsMetadata: Props['eventsMetadata'] = [

src/views/workflow-history/workflow-history-events-card/workflow-history-events-card.tsx

Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
'use client';
2-
import React, { useState } from 'react';
2+
import React from 'react';
33

44
import { StatelessAccordion, Panel } from 'baseui/accordion';
5-
import { Button } from 'baseui/button';
65
import { Skeleton } from 'baseui/skeleton';
7-
import { StatefulTooltip } from 'baseui/tooltip';
8-
import copy from 'copy-to-clipboard';
9-
import queryString from 'query-string';
10-
import { MdLink } from 'react-icons/md';
116

127
import useStyletronClasses from '@/hooks/use-styletron-classes';
138

149
import WorkflowHistoryEventDetails from '../workflow-history-event-details/workflow-history-event-details';
10+
import WorkflowHistoryEventLinkButton from '../workflow-history-event-link-button/workflow-history-event-link-button';
1511
import getBadgeContainerSize from '../workflow-history-event-status-badge/helpers/get-badge-container-size';
1612
import WorkflowHistoryEventStatusBadge from '../workflow-history-event-status-badge/workflow-history-event-status-badge';
1713

@@ -32,8 +28,6 @@ export default function WorkflowHistoryEventsCard({
3228
}: Props) {
3329
const { cls, theme } = useStyletronClasses(cssStyles);
3430

35-
const [isEventLinkCopied, setIsEventLinkCopied] = useState(false);
36-
3731
if (!eventsMetadata?.length && !showEventPlaceholder) return null;
3832
const expanded = events.reduce((result, event) => {
3933
const id = event.eventId === null ? event.computedEventId : event.eventId;
@@ -63,44 +57,7 @@ export default function WorkflowHistoryEventsCard({
6357
<div className={cls.eventLabel}>
6458
{eventMetadata.label}
6559
{isPanelExpanded && (
66-
<StatefulTooltip
67-
showArrow
68-
placement="right"
69-
popoverMargin={8}
70-
accessibilityType="tooltip"
71-
content={() =>
72-
isEventLinkCopied
73-
? 'Copied link to event'
74-
: 'Copy link to event'
75-
}
76-
onMouseLeave={() => setIsEventLinkCopied(false)}
77-
returnFocus
78-
autoFocus
79-
>
80-
<Button
81-
data-testid="share-button"
82-
size="mini"
83-
shape="circle"
84-
kind="tertiary"
85-
overrides={overrides.shareButton}
86-
onClick={(e) => {
87-
e.stopPropagation();
88-
copy(
89-
queryString.stringifyUrl({
90-
url:
91-
window.location.origin +
92-
window.location.pathname,
93-
query: {
94-
he: id,
95-
},
96-
})
97-
);
98-
setIsEventLinkCopied(true);
99-
}}
100-
>
101-
<MdLink />
102-
</Button>
103-
</StatefulTooltip>
60+
<WorkflowHistoryEventLinkButton historyEventId={id} />
10461
)}
10562
</div>
10663
</div>

0 commit comments

Comments
 (0)