Commit 71a692d6 authored by David O'Regan's avatar David O'Regan Committed by Vitaly Slobodin

Fix mark snippet as spam button

parent 2697d4ae
...@@ -11,15 +11,26 @@ import { ...@@ -11,15 +11,26 @@ import {
GlButton, GlButton,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import CanCreateProjectSnippet from 'shared_queries/snippet/project_permissions.query.graphql'; import CanCreateProjectSnippet from 'shared_queries/snippet/project_permissions.query.graphql';
import CanCreatePersonalSnippet from 'shared_queries/snippet/user_permissions.query.graphql'; import CanCreatePersonalSnippet from 'shared_queries/snippet/user_permissions.query.graphql';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility'; import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import createFlash, { FLASH_TYPES } from '~/flash';
import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql'; import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
export const i18n = {
snippetSpamSuccess: sprintf(
s__('Snippets|%{spammable_titlecase} was submitted to Akismet successfully.'),
{ spammable_titlecase: __('Snippet') },
),
snippetSpamFailure: s__('Snippets|Error with Akismet. Please check the logs for more info.'),
};
export default { export default {
components: { components: {
GlAvatar, GlAvatar,
...@@ -54,7 +65,7 @@ export default { ...@@ -54,7 +65,7 @@ export default {
}, },
}, },
}, },
inject: ['reportAbusePath'], inject: ['reportAbusePath', 'canReportSpam'],
props: { props: {
snippet: { snippet: {
type: Object, type: Object,
...@@ -63,7 +74,8 @@ export default { ...@@ -63,7 +74,8 @@ export default {
}, },
data() { data() {
return { return {
isDeleting: false, isLoading: false,
isSubmittingSpam: false,
errorMessage: '', errorMessage: '',
canCreateSnippet: false, canCreateSnippet: false,
}; };
...@@ -105,10 +117,11 @@ export default { ...@@ -105,10 +117,11 @@ export default {
category: 'secondary', category: 'secondary',
}, },
{ {
condition: this.reportAbusePath, condition: this.canReportSpam && !isEmpty(this.reportAbusePath),
text: __('Submit as spam'), text: __('Submit as spam'),
href: this.reportAbusePath, click: this.submitAsSpam,
title: __('Submit as spam'), title: __('Submit as spam'),
loading: this.isSubmittingSpam,
}, },
]; ];
}, },
...@@ -157,7 +170,7 @@ export default { ...@@ -157,7 +170,7 @@ export default {
this.$refs.deleteModal.show(); this.$refs.deleteModal.show();
}, },
deleteSnippet() { deleteSnippet() {
this.isDeleting = true; this.isLoading = true;
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: DeleteSnippetMutation, mutation: DeleteSnippetMutation,
...@@ -167,17 +180,34 @@ export default { ...@@ -167,17 +180,34 @@ export default {
if (data?.destroySnippet?.errors.length) { if (data?.destroySnippet?.errors.length) {
throw new Error(data?.destroySnippet?.errors[0]); throw new Error(data?.destroySnippet?.errors[0]);
} }
this.isDeleting = false;
this.errorMessage = undefined; this.errorMessage = undefined;
this.closeDeleteModal(); this.closeDeleteModal();
this.redirectToSnippets(); this.redirectToSnippets();
}) })
.catch((err) => { .catch((err) => {
this.isDeleting = false; this.isLoading = false;
this.errorMessage = err.message; this.errorMessage = err.message;
})
.finally(() => {
this.isLoading = false;
});
},
async submitAsSpam() {
try {
this.isSubmittingSpam = true;
await axios.post(this.reportAbusePath);
createFlash({
message: this.$options.i18n.snippetSpamSuccess,
type: FLASH_TYPES.SUCCESS,
}); });
} catch (error) {
createFlash({ message: this.$options.i18n.snippetSpamFailure });
} finally {
this.isSubmittingSpam = false;
}
}, },
}, },
i18n,
}; };
</script> </script>
<template> <template>
...@@ -189,9 +219,7 @@ export default { ...@@ -189,9 +219,7 @@ export default {
:title="snippetVisibilityLevelDescription" :title="snippetVisibilityLevelDescription"
data-container="body" data-container="body"
> >
<span class="sr-only"> <span class="sr-only">{{ s__(`VisibilityLevel|${visibility}`) }}</span>
{{ s__(`VisibilityLevel|${visibility}`) }}
</span>
<gl-icon :name="visibilityLevelIcon" :size="14" /> <gl-icon :name="visibilityLevelIcon" :size="14" />
</div> </div>
<div class="creator" data-testid="authored-message"> <div class="creator" data-testid="authored-message">
...@@ -233,6 +261,7 @@ export default { ...@@ -233,6 +261,7 @@ export default {
> >
<gl-button <gl-button
:disabled="action.disabled" :disabled="action.disabled"
:loading="action.loading"
:variant="action.variant" :variant="action.variant"
:category="action.category" :category="action.category"
:class="action.cssClass" :class="action.cssClass"
...@@ -240,9 +269,8 @@ export default { ...@@ -240,9 +269,8 @@ export default {
data-qa-selector="snippet_action_button" data-qa-selector="snippet_action_button"
:data-qa-action="action.text" :data-qa-action="action.text"
@click="action.click ? action.click() : undefined" @click="action.click ? action.click() : undefined"
>{{ action.text }}</gl-button
> >
{{ action.text }}
</gl-button>
</div> </div>
</template> </template>
</div> </div>
...@@ -266,14 +294,14 @@ export default { ...@@ -266,14 +294,14 @@ export default {
<gl-modal ref="deleteModal" modal-id="delete-modal" title="Example title"> <gl-modal ref="deleteModal" modal-id="delete-modal" title="Example title">
<template #modal-title>{{ __('Delete snippet?') }}</template> <template #modal-title>{{ __('Delete snippet?') }}</template>
<gl-alert v-if="errorMessage" variant="danger" class="mb-2" @dismiss="errorMessage = ''">{{ <gl-alert v-if="errorMessage" variant="danger" class="mb-2" @dismiss="errorMessage = ''">
errorMessage {{ errorMessage }}
}}</gl-alert> </gl-alert>
<gl-sprintf :message="__('Are you sure you want to delete %{name}?')"> <gl-sprintf :message="__('Are you sure you want to delete %{name}?')">
<template #name <template #name>
><strong>{{ snippet.title }}</strong></template <strong>{{ snippet.title }}</strong>
> </template>
</gl-sprintf> </gl-sprintf>
<template #modal-footer> <template #modal-footer>
...@@ -281,11 +309,11 @@ export default { ...@@ -281,11 +309,11 @@ export default {
<gl-button <gl-button
variant="danger" variant="danger"
category="primary" category="primary"
:disabled="isDeleting" :disabled="isLoading"
data-qa-selector="delete_snippet_button" data-qa-selector="delete_snippet_button"
@click="deleteSnippet" @click="deleteSnippet"
> >
<gl-loading-icon v-if="isDeleting" size="sm" inline /> <gl-loading-icon v-if="isLoading" size="sm" inline />
{{ __('Delete snippet') }} {{ __('Delete snippet') }}
</gl-button> </gl-button>
</template> </template>
......
...@@ -27,6 +27,7 @@ export default function appFactory(el, Component) { ...@@ -27,6 +27,7 @@ export default function appFactory(el, Component) {
visibilityLevels = '[]', visibilityLevels = '[]',
selectedLevel, selectedLevel,
multipleLevelsRestricted, multipleLevelsRestricted,
canReportSpam,
reportAbusePath, reportAbusePath,
...restDataset ...restDataset
} = el.dataset; } = el.dataset;
...@@ -39,6 +40,7 @@ export default function appFactory(el, Component) { ...@@ -39,6 +40,7 @@ export default function appFactory(el, Component) {
selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE, selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE,
multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset, multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset,
reportAbusePath, reportAbusePath,
canReportSpam,
}, },
render(createElement) { render(createElement) {
return createElement(Component, { return createElement(Component, {
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- breadcrumb_title @snippet.to_reference - breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets") - page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet) } } #js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
.row-content-block.top-block.content-component-block .row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet) = render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet)
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
- content_for :prefetch_asset_tags do - content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco', prefetch: true) - webpack_preload_asset_tag('monaco', prefetch: true)
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet) } } #js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
.row-content-block.top-block.content-component-block .row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true = render 'award_emoji/awards_block', awardable: @snippet, inline: true
......
...@@ -31565,6 +31565,9 @@ msgstr "" ...@@ -31565,6 +31565,9 @@ msgstr ""
msgid "Smartcard authentication failed: client certificate header is missing." msgid "Smartcard authentication failed: client certificate header is missing."
msgstr "" msgstr ""
msgid "Snippet"
msgstr ""
msgid "Snippets" msgid "Snippets"
msgstr "" msgstr ""
...@@ -31589,6 +31592,9 @@ msgstr "" ...@@ -31589,6 +31592,9 @@ msgstr ""
msgid "SnippetsEmptyState|There are no snippets to show." msgid "SnippetsEmptyState|There are no snippets to show."
msgstr "" msgstr ""
msgid "Snippets|%{spammable_titlecase} was submitted to Akismet successfully."
msgstr ""
msgid "Snippets|Add another file %{num}/%{total}" msgid "Snippets|Add another file %{num}/%{total}"
msgstr "" msgstr ""
...@@ -31598,6 +31604,9 @@ msgstr "" ...@@ -31598,6 +31604,9 @@ msgstr ""
msgid "Snippets|Description (optional)" msgid "Snippets|Description (optional)"
msgstr "" msgstr ""
msgid "Snippets|Error with Akismet. Please check the logs for more info."
msgstr ""
msgid "Snippets|Files" msgid "Snippets|Files"
msgstr "" msgstr ""
......
...@@ -41,19 +41,23 @@ describe('Snippet view app', () => { ...@@ -41,19 +41,23 @@ describe('Snippet view app', () => {
}, },
}); });
} }
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEmbedDropdown = () => wrapper.findComponent(EmbedDropdown);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders loader while the query is in flight', () => { it('renders loader while the query is in flight', () => {
createComponent({ loading: true }); createComponent({ loading: true });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); expect(findLoadingIcon().exists()).toBe(true);
}); });
it('renders all simple components after the query is finished', () => { it('renders all simple components required after the query is finished', () => {
createComponent(); createComponent();
expect(wrapper.find(SnippetHeader).exists()).toBe(true); expect(wrapper.findComponent(SnippetHeader).exists()).toBe(true);
expect(wrapper.find(SnippetTitle).exists()).toBe(true); expect(wrapper.findComponent(SnippetTitle).exists()).toBe(true);
}); });
it('renders embed dropdown component if visibility allows', () => { it('renders embed dropdown component if visibility allows', () => {
...@@ -65,7 +69,7 @@ describe('Snippet view app', () => { ...@@ -65,7 +69,7 @@ describe('Snippet view app', () => {
}, },
}, },
}); });
expect(wrapper.find(EmbedDropdown).exists()).toBe(true); expect(findEmbedDropdown().exists()).toBe(true);
}); });
it('renders correct snippet-blob components', () => { it('renders correct snippet-blob components', () => {
...@@ -98,7 +102,7 @@ describe('Snippet view app', () => { ...@@ -98,7 +102,7 @@ describe('Snippet view app', () => {
}, },
}, },
}); });
expect(wrapper.find(EmbedDropdown).exists()).toBe(isRendered); expect(findEmbedDropdown().exists()).toBe(isRendered);
}); });
}); });
...@@ -120,7 +124,7 @@ describe('Snippet view app', () => { ...@@ -120,7 +124,7 @@ describe('Snippet view app', () => {
}, },
}, },
}); });
expect(wrapper.find(CloneDropdownButton).exists()).toBe(isRendered); expect(wrapper.findComponent(CloneDropdownButton).exists()).toBe(isRendered);
}, },
); );
}); });
......
import { GlButton, GlModal, GlDropdown } from '@gitlab/ui'; import { GlButton, GlModal, GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo'; import { ApolloMutation } from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data'; import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility'; import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import SnippetHeader from '~/snippets/components/snippet_header.vue'; import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue';
import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql'; import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
import axios from '~/lib/utils/axios_utils';
import createFlash, { FLASH_TYPES } from '~/flash';
jest.mock('~/flash');
describe('Snippet header component', () => { describe('Snippet header component', () => {
let wrapper; let wrapper;
let snippet; let snippet;
let mutationTypes; let mutationTypes;
let mutationVariables; let mutationVariables;
let mock;
let errorMsg; let errorMsg;
let err; let err;
const originalRelativeUrlRoot = gon.relative_url_root; const originalRelativeUrlRoot = gon.relative_url_root;
const reportAbusePath = '/-/snippets/42/mark_as_spam'; const reportAbusePath = '/-/snippets/42/mark_as_spam';
const canReportSpam = true;
const GlEmoji = { template: '<img/>' }; const GlEmoji = { template: '<img/>' };
...@@ -47,6 +54,7 @@ describe('Snippet header component', () => { ...@@ -47,6 +54,7 @@ describe('Snippet header component', () => {
mocks: { $apollo }, mocks: { $apollo },
provide: { provide: {
reportAbusePath, reportAbusePath,
canReportSpam,
...provide, ...provide,
}, },
propsData: { propsData: {
...@@ -118,10 +126,13 @@ describe('Snippet header component', () => { ...@@ -118,10 +126,13 @@ describe('Snippet header component', () => {
RESOLVE: jest.fn(() => Promise.resolve({ data: { destroySnippet: { errors: [] } } })), RESOLVE: jest.fn(() => Promise.resolve({ data: { destroySnippet: { errors: [] } } })),
REJECT: jest.fn(() => Promise.reject(err)), REJECT: jest.fn(() => Promise.reject(err)),
}; };
mock = new MockAdapter(axios);
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
mock.restore();
gon.relative_url_root = originalRelativeUrlRoot; gon.relative_url_root = originalRelativeUrlRoot;
}); });
...@@ -186,7 +197,6 @@ describe('Snippet header component', () => { ...@@ -186,7 +197,6 @@ describe('Snippet header component', () => {
{ {
category: 'primary', category: 'primary',
disabled: false, disabled: false,
href: reportAbusePath,
text: 'Submit as spam', text: 'Submit as spam',
variant: 'default', variant: 'default',
}, },
...@@ -205,7 +215,6 @@ describe('Snippet header component', () => { ...@@ -205,7 +215,6 @@ describe('Snippet header component', () => {
text: 'Delete', text: 'Delete',
}, },
{ {
href: reportAbusePath,
text: 'Submit as spam', text: 'Submit as spam',
title: 'Submit as spam', title: 'Submit as spam',
}, },
...@@ -249,6 +258,31 @@ describe('Snippet header component', () => { ...@@ -249,6 +258,31 @@ describe('Snippet header component', () => {
); );
}); });
describe('submit snippet as spam', () => {
beforeEach(async () => {
createComponent();
});
it.each`
request | variant | text
${200} | ${'SUCCESS'} | ${i18n.snippetSpamSuccess}
${500} | ${'DANGER'} | ${i18n.snippetSpamFailure}
`(
'renders a "$variant" flash message with "$text" message for a request with a "$request" response',
async ({ request, variant, text }) => {
const submitAsSpamBtn = findButtons().at(2);
mock.onPost(reportAbusePath).reply(request);
submitAsSpamBtn.trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
message: expect.stringContaining(text),
type: FLASH_TYPES[variant],
});
},
);
});
describe('with guest user', () => { describe('with guest user', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
...@@ -258,6 +292,7 @@ describe('Snippet header component', () => { ...@@ -258,6 +292,7 @@ describe('Snippet header component', () => {
}, },
provide: { provide: {
reportAbusePath: null, reportAbusePath: null,
canReportSpam: false,
}, },
}); });
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment