Skip to content

Commit 44a1767

Browse files
feat(dashboard): Add duplicate widget button in dashboard edit mode (#31776)
Adds a duplicate widget button to Dashboard Widgets in Edit mode
1 parent d6fe4d4 commit 44a1767

File tree

3 files changed

+75
-23
lines changed

3 files changed

+75
-23
lines changed

static/app/views/dashboardsV2/dashboard.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import withPageFilters from 'sentry/utils/withPageFilters';
3030
import AddWidget, {ADD_WIDGET_BUTTON_DRAG_ID} from './addWidget';
3131
import {
3232
assignDefaultLayout,
33+
assignTempId,
3334
calculateColumnDepths,
3435
constructGridItemKey,
3536
DEFAULT_WIDGET_WIDTH,
@@ -305,16 +306,17 @@ class Dashboard extends Component<Props, State> {
305306
};
306307

307308
handleDuplicateWidget = (widget: Widget, index: number) => () => {
308-
const {dashboard, isEditing, handleUpdateWidgetList} = this.props;
309+
const {dashboard, onUpdate, isEditing, handleUpdateWidgetList} = this.props;
309310

310-
const widgetCopy = cloneDeep(widget);
311-
widgetCopy.id = undefined;
312-
widgetCopy.tempId = undefined;
311+
const widgetCopy = cloneDeep(
312+
assignTempId({...widget, id: undefined, tempId: undefined})
313+
);
313314

314315
let nextList = [...dashboard.widgets];
315316
nextList.splice(index, 0, widgetCopy);
316317
nextList = generateWidgetsAfterCompaction(nextList);
317318

319+
onUpdate(nextList);
318320
if (!!!isEditing) {
319321
handleUpdateWidgetList(nextList);
320322
}

static/app/views/dashboardsV2/widgetCard/index.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import ErrorBoundary from 'sentry/components/errorBoundary';
1111
import {Panel} from 'sentry/components/panels';
1212
import Placeholder from 'sentry/components/placeholder';
1313
import Tooltip from 'sentry/components/tooltip';
14-
import {IconDelete, IconEdit, IconGrabbable} from 'sentry/icons';
14+
import {IconCopy, IconDelete, IconEdit, IconGrabbable} from 'sentry/icons';
1515
import {t} from 'sentry/locale';
1616
import overflowEllipsis from 'sentry/styles/overflowEllipsis';
1717
import space from 'sentry/styles/space';
@@ -54,8 +54,15 @@ type Props = WithRouterProps & {
5454

5555
class WidgetCard extends React.Component<Props> {
5656
renderToolbar() {
57-
const {onEdit, onDelete, draggableProps, hideToolbar, isEditing, isMobile} =
58-
this.props;
57+
const {
58+
onEdit,
59+
onDelete,
60+
onDuplicate,
61+
draggableProps,
62+
hideToolbar,
63+
isEditing,
64+
isMobile,
65+
} = this.props;
5966

6067
if (!isEditing) {
6168
return null;
@@ -77,6 +84,9 @@ class WidgetCard extends React.Component<Props> {
7784
<IconClick data-test-id="widget-edit" onClick={onEdit}>
7885
<IconEdit color="textColor" />
7986
</IconClick>
87+
<IconClick aria-label={t('Duplicate Widget')} onClick={onDuplicate}>
88+
<IconCopy color="textColor" />
89+
</IconClick>
8090
<IconClick data-test-id="widget-delete" onClick={onDelete}>
8191
<IconDelete color="textColor" />
8292
</IconClick>

tests/js/spec/views/dashboardsV2/gridLayout/dashboard.spec.tsx

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ describe('Dashboards > Dashboard', () => {
1414
const organization = TestStubs.Organization({
1515
features: ['dashboards-basic', 'dashboards-edit', 'dashboard-grid-layout'],
1616
});
17+
const organizationWithFlag = TestStubs.Organization({
18+
features: [
19+
'dashboards-basic',
20+
'dashboards-edit',
21+
'dashboard-grid-layout',
22+
'issues-in-dashboards',
23+
],
24+
});
1725
const mockDashboard = {
1826
dateCreated: '2021-08-10T21:20:46.798237Z',
1927
id: '1',
@@ -101,6 +109,11 @@ describe('Dashboards > Dashboard', () => {
101109
},
102110
],
103111
});
112+
MockApiClient.addMockResponse({
113+
url: '/organizations/org-slug/tags/',
114+
method: 'GET',
115+
body: TestStubs.Tags(),
116+
});
104117
});
105118

106119
it('dashboard adds new widget if component is mounted with newWidget prop', async () => {
@@ -186,14 +199,6 @@ describe('Dashboards > Dashboard', () => {
186199
});
187200

188201
it('dashboard displays issue widgets if the user has issue widgets feature flag', async () => {
189-
const organizationWithFlag = TestStubs.Organization({
190-
features: [
191-
'dashboards-basic',
192-
'dashboards-edit',
193-
'dashboard-grid-layout',
194-
'issues-in-dashboards',
195-
],
196-
});
197202
const mockDashboardWithIssueWidget = {
198203
...mockDashboard,
199204
widgets: [newWidget, issueWidget],
@@ -204,14 +209,6 @@ describe('Dashboards > Dashboard', () => {
204209
});
205210

206211
it('renders suggested assignees', async () => {
207-
const organizationWithFlag = TestStubs.Organization({
208-
features: [
209-
'dashboards-basic',
210-
'dashboards-edit',
211-
'dashboard-grid-layout',
212-
'issues-in-dashboards',
213-
],
214-
});
215212
const mockDashboardWithIssueWidget = {
216213
...mockDashboard,
217214
widgets: [{...issueWidget}],
@@ -225,4 +222,47 @@ describe('Dashboards > Dashboard', () => {
225222
expect(await screen.findByText('Matching Issue Owners Rule')).toBeInTheDocument();
226223
});
227224
});
225+
226+
describe('Edit mode', () => {
227+
let widgets: Widget[];
228+
const mount = dashboard => {
229+
const getDashboardComponent = () => (
230+
<Dashboard
231+
paramDashboardId="1"
232+
dashboard={dashboard}
233+
organization={initialData.organization}
234+
isEditing
235+
onUpdate={newWidgets => {
236+
widgets.splice(0, widgets.length, ...newWidgets);
237+
}}
238+
handleUpdateWidgetList={() => undefined}
239+
handleAddCustomWidget={() => undefined}
240+
onSetWidgetToBeUpdated={() => undefined}
241+
router={initialData.router}
242+
location={initialData.location}
243+
widgetLimitReached={false}
244+
/>
245+
);
246+
const {rerender} = rtlMountWithTheme(getDashboardComponent());
247+
return {rerender: () => rerender(getDashboardComponent())};
248+
};
249+
250+
beforeEach(() => {
251+
widgets = [newWidget];
252+
});
253+
254+
it('displays the copy widget button in edit mode', () => {
255+
const dashboardWithOneWidget = {...mockDashboard, widgets};
256+
mount(dashboardWithOneWidget);
257+
expect(screen.getByLabelText('Duplicate Widget')).toBeInTheDocument();
258+
});
259+
260+
it('duplicates the widget', async () => {
261+
const dashboardWithOneWidget = {...mockDashboard, widgets};
262+
const {rerender} = mount(dashboardWithOneWidget);
263+
userEvent.click(screen.getByLabelText('Duplicate Widget'));
264+
rerender();
265+
expect(screen.getAllByText('Test Discover Widget')).toHaveLength(2);
266+
});
267+
});
228268
});

0 commit comments

Comments
 (0)