Skip to content

Commit b3b474d

Browse files
authored
ref(issue-views): Implement FE packaging logic for issue views (#92053)
Adds disabled states and upsells to issue views that reflect our packaging logic. Controlled by the `organizations:issue-views` flag. Exhaustive list of changes if feature is not enabled: * All Views: The `+ Create View` button in the header has been disabled * All Views: The `+ Create View` button in the "My Views" empty state has been disabled * All Views: The rename and duplicate options in the issue view tables have been removed * Taxonomy and Feed Views: The `Save As` button has been disabled * Issue View: the `Save` button (and its dropdown) has been disabled Still waiting on upsell copy and designs.
1 parent da7e1d0 commit b3b474d

File tree

7 files changed

+206
-78
lines changed

7 files changed

+206
-78
lines changed

static/app/views/issueList/issueViews/issueViewSaveButton.spec.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import PageFiltersStore from 'sentry/stores/pageFiltersStore';
1616
import {IssueViewSaveButton} from 'sentry/views/issueList/issueViews/issueViewSaveButton';
1717
import {IssueSortOptions} from 'sentry/views/issueList/utils';
1818

19+
const organization = OrganizationFixture({
20+
features: ['issue-views'],
21+
});
22+
1923
const defaultProps = {
2024
query: 'is:unresolved',
2125
sort: IssueSortOptions.DATE,
@@ -73,6 +77,7 @@ describe('IssueViewSaveButton', function () {
7377
</Fragment>,
7478
{
7579
initialRouterConfig: initialRouterConfigFeed,
80+
organization,
7681
}
7782
);
7883

@@ -124,6 +129,7 @@ describe('IssueViewSaveButton', function () {
124129
</Fragment>,
125130
{
126131
initialRouterConfig: initialRouterConfigView,
132+
organization,
127133
}
128134
);
129135

@@ -183,6 +189,7 @@ describe('IssueViewSaveButton', function () {
183189
},
184190
},
185191
},
192+
organization,
186193
});
187194

188195
// Should show unsaved changes
@@ -231,8 +238,8 @@ describe('IssueViewSaveButton', function () {
231238
{
232239
organization: OrganizationFixture({
233240
access: ['org:read'],
241+
features: ['issue-views'],
234242
}),
235-
236243
initialRouterConfig: initialRouterConfigView,
237244
}
238245
);
@@ -288,6 +295,7 @@ describe('IssueViewSaveButton', function () {
288295
},
289296
},
290297
},
298+
organization,
291299
});
292300

293301
await screen.findByTestId('save-button-unsaved');
@@ -311,4 +319,13 @@ describe('IssueViewSaveButton', function () {
311319
// The save button should no longer show unsaved changes
312320
expect(screen.getByTestId('save-button')).toBeInTheDocument();
313321
});
322+
323+
it('shows a feature disabled hovercard when the feature is disabled', async function () {
324+
render(<IssueViewSaveButton {...defaultProps} />, {
325+
organization: OrganizationFixture({
326+
features: [],
327+
}),
328+
});
329+
expect(await screen.findByRole('button', {name: 'Save As'})).toBeDisabled();
330+
});
314331
});

static/app/views/issueList/issueViews/issueViewSaveButton.tsx

Lines changed: 95 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import styled from '@emotion/styled';
33

44
import {addSuccessMessage} from 'sentry/actionCreators/indicator';
55
import {openModal} from 'sentry/actionCreators/modal';
6+
import Feature from 'sentry/components/acl/feature';
7+
import FeatureDisabled from 'sentry/components/acl/featureDisabled';
68
import {Button} from 'sentry/components/core/button';
79
import {ButtonBar} from 'sentry/components/core/button/buttonBar';
810
import {DropdownMenu} from 'sentry/components/dropdownMenu';
11+
import {Hovercard} from 'sentry/components/hovercard';
912
import {IconChevron} from 'sentry/icons';
1013
import {t} from 'sentry/locale';
1114
import {space} from 'sentry/styles/space';
@@ -47,7 +50,6 @@ function SegmentedIssueViewSaveButton({
4750
const canEdit = view
4851
? canEditIssueView({user, groupSearchView: view, organization})
4952
: false;
50-
5153
const discardUnsavedChanges = () => {
5254
if (view) {
5355
trackAnalytics('issue_views.reset.clicked', {organization});
@@ -77,52 +79,72 @@ function SegmentedIssueViewSaveButton({
7779
};
7880

7981
return (
80-
<ButtonBar merged>
81-
<PrimarySaveButton
82-
priority={buttonPriority}
83-
data-test-id={hasUnsavedChanges ? 'save-button-unsaved' : 'save-button'}
84-
onClick={() => {
85-
if (canEdit) {
86-
saveView();
87-
} else {
88-
openCreateIssueViewModal();
82+
<Feature
83+
features={'organizations:issue-views'}
84+
hookName="feature-disabled:issue-views"
85+
renderDisabled={props => (
86+
<Hovercard
87+
body={
88+
<FeatureDisabled
89+
features={props.features}
90+
hideHelpToggle
91+
featureName={t('Issue Views')}
92+
/>
8993
}
90-
}}
91-
disabled={isSaving}
92-
>
93-
{canEdit ? t('Save') : t('Save As')}
94-
</PrimarySaveButton>
95-
<DropdownMenu
96-
items={[
97-
{
98-
key: 'reset',
99-
label: t('Reset'),
100-
disabled: !hasUnsavedChanges,
101-
onAction: () => {
102-
discardUnsavedChanges();
103-
},
104-
},
105-
{
106-
key: 'save-as',
107-
label: t('Save as new view'),
108-
onAction: () => {
109-
openCreateIssueViewModal();
110-
},
111-
hidden: !canEdit,
112-
},
113-
]}
114-
trigger={props => (
115-
<DropdownTrigger
116-
{...props}
117-
disabled={isSaving}
118-
icon={<IconChevron direction="down" />}
119-
aria-label={t('More save options')}
94+
>
95+
{typeof props.children === 'function' ? props.children(props) : props.children}
96+
</Hovercard>
97+
)}
98+
>
99+
{({hasFeature}) => (
100+
<ButtonBar merged>
101+
<PrimarySaveButton
120102
priority={buttonPriority}
103+
data-test-id={hasUnsavedChanges ? 'save-button-unsaved' : 'save-button'}
104+
onClick={() => {
105+
if (canEdit) {
106+
saveView();
107+
} else {
108+
openCreateIssueViewModal();
109+
}
110+
}}
111+
disabled={isSaving || !hasFeature}
112+
>
113+
{canEdit ? t('Save') : t('Save As')}
114+
</PrimarySaveButton>
115+
<DropdownMenu
116+
items={[
117+
{
118+
key: 'reset',
119+
label: t('Reset'),
120+
disabled: !hasUnsavedChanges,
121+
onAction: () => {
122+
discardUnsavedChanges();
123+
},
124+
},
125+
{
126+
key: 'save-as',
127+
label: t('Save as new view'),
128+
onAction: () => {
129+
openCreateIssueViewModal();
130+
},
131+
hidden: !canEdit,
132+
},
133+
]}
134+
trigger={props => (
135+
<DropdownTrigger
136+
{...props}
137+
disabled={!hasFeature || isSaving}
138+
icon={<IconChevron direction="down" />}
139+
aria-label={t('More save options')}
140+
priority={buttonPriority}
141+
/>
142+
)}
143+
position="bottom-end"
121144
/>
122-
)}
123-
position="bottom-end"
124-
/>
125-
</ButtonBar>
145+
</ButtonBar>
146+
)}
147+
</Feature>
126148
);
127149
}
128150

@@ -150,9 +172,35 @@ export function IssueViewSaveButton({query, sort}: IssueViewSaveButtonProps) {
150172

151173
if (!viewId) {
152174
return (
153-
<Button priority="primary" onClick={openCreateIssueViewModal}>
154-
{t('Save As')}
155-
</Button>
175+
<Feature
176+
features={'organizations:issue-views'}
177+
hookName="feature-disabled:issue-views"
178+
renderDisabled={props => (
179+
<Hovercard
180+
body={
181+
<FeatureDisabled
182+
features={props.features}
183+
hideHelpToggle
184+
featureName={t('Issue Views')}
185+
/>
186+
}
187+
>
188+
{typeof props.children === 'function'
189+
? props.children(props)
190+
: props.children}
191+
</Hovercard>
192+
)}
193+
>
194+
{({hasFeature}) => (
195+
<Button
196+
priority="primary"
197+
onClick={openCreateIssueViewModal}
198+
disabled={!hasFeature}
199+
>
200+
{t('Save As')}
201+
</Button>
202+
)}
203+
</Feature>
156204
);
157205
}
158206

static/app/views/issueList/issueViews/issueViewsList/issueViewsList.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import ConfigStore from 'sentry/stores/configStore';
1515
import IssueViewsList from 'sentry/views/issueList/issueViews/issueViewsList/issueViewsList';
1616

1717
const organization = OrganizationFixture({
18-
features: ['enforce-stacked-navigation'],
18+
features: ['enforce-stacked-navigation', 'issue-views'],
1919
});
2020

2121
describe('IssueViewsList', function () {

static/app/views/issueList/issueViews/issueViewsList/issueViewsList.tsx

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import {Fragment} from 'react';
22
import styled from '@emotion/styled';
33

4+
import Feature from 'sentry/components/acl/feature';
5+
import FeatureDisabled from 'sentry/components/acl/featureDisabled';
46
import {Button} from 'sentry/components/core/button';
57
import {ButtonBar} from 'sentry/components/core/button/buttonBar';
68
import {CompactSelect} from 'sentry/components/core/compactSelect';
9+
import {Hovercard} from 'sentry/components/hovercard';
710
import * as Layout from 'sentry/components/layouts/thirds';
811
import Pagination from 'sentry/components/pagination';
912
import Redirect from 'sentry/components/redirect';
@@ -229,20 +232,42 @@ function NoViewsBanner({
229232
'Your haven’t saved any issue views yet — saving views makes it easier to return to your most frequent search queries, like high priority, assigned to you, or most recent.'
230233
)}
231234
</BannerText>
232-
<BannerAddViewButton
233-
priority="primary"
234-
icon={<IconAdd />}
235-
size="sm"
236-
onClick={() => {
237-
trackAnalytics('issue_views.table.banner_create_view_clicked', {
238-
organization,
239-
});
240-
handleCreateView();
241-
}}
242-
disabled={isCreatingView}
235+
<Feature
236+
features={'organizations:issue-views'}
237+
hookName="feature-disabled:issue-views"
238+
renderDisabled={props => (
239+
<Hovercard
240+
body={
241+
<FeatureDisabled
242+
features={props.features}
243+
hideHelpToggle
244+
featureName={t('Issue Views')}
245+
/>
246+
}
247+
>
248+
{typeof props.children === 'function'
249+
? props.children(props)
250+
: props.children}
251+
</Hovercard>
252+
)}
243253
>
244-
{t('Create View')}
245-
</BannerAddViewButton>
254+
{({hasFeature}) => (
255+
<BannerAddViewButton
256+
priority="primary"
257+
icon={<IconAdd />}
258+
size="sm"
259+
onClick={() => {
260+
trackAnalytics('issue_views.table.banner_create_view_clicked', {
261+
organization,
262+
});
263+
handleCreateView();
264+
}}
265+
disabled={!hasFeature || isCreatingView}
266+
>
267+
{t('Create View')}
268+
</BannerAddViewButton>
269+
)}
270+
</Feature>
246271
</Banner>
247272
);
248273
}
@@ -370,21 +395,44 @@ export default function IssueViewsList() {
370395
{t('Give Feedback')}
371396
</Button>
372397
) : null}
373-
<Button
374-
priority="primary"
375-
icon={<IconAdd />}
376-
size="sm"
377-
disabled={isCreatingView}
378-
busy={isCreatingView}
379-
onClick={() => {
380-
trackAnalytics('issue_views.table.create_view_clicked', {
381-
organization,
382-
});
383-
handleCreateView();
384-
}}
398+
399+
<Feature
400+
features={'organizations:issue-views'}
401+
hookName="feature-disabled:issue-views"
402+
renderDisabled={props => (
403+
<Hovercard
404+
body={
405+
<FeatureDisabled
406+
features={props.features}
407+
hideHelpToggle
408+
featureName={t('Issue Views')}
409+
/>
410+
}
411+
>
412+
{typeof props.children === 'function'
413+
? props.children(props)
414+
: props.children}
415+
</Hovercard>
416+
)}
385417
>
386-
{t('Create View')}
387-
</Button>
418+
{({hasFeature}) => (
419+
<Button
420+
priority="primary"
421+
icon={<IconAdd />}
422+
size="sm"
423+
disabled={!hasFeature || isCreatingView}
424+
busy={isCreatingView}
425+
onClick={() => {
426+
trackAnalytics('issue_views.table.create_view_clicked', {
427+
organization,
428+
});
429+
handleCreateView();
430+
}}
431+
>
432+
{t('Create View')}
433+
</Button>
434+
)}
435+
</Feature>
388436
</ButtonBar>
389437
</Layout.HeaderActions>
390438
</Layout.Header>

0 commit comments

Comments
 (0)