Commit fdf3bd55 authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch '336009-report-abuse-snippets' into 'master'

Fix mark snippet as spam button

See merge request gitlab-org/gitlab!71070
parents 8bc60d24 71a692d6
......@@ -11,15 +11,26 @@ import {
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
import { isEmpty } from 'lodash';
import CanCreateProjectSnippet from 'shared_queries/snippet/project_permissions.query.graphql';
import CanCreatePersonalSnippet from 'shared_queries/snippet/user_permissions.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
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 createFlash, { FLASH_TYPES } from '~/flash';
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 {
components: {
GlAvatar,
......@@ -54,7 +65,7 @@ export default {
},
},
},
inject: ['reportAbusePath'],
inject: ['reportAbusePath', 'canReportSpam'],
props: {
snippet: {
type: Object,
......@@ -63,7 +74,8 @@ export default {
},
data() {
return {
isDeleting: false,
isLoading: false,
isSubmittingSpam: false,
errorMessage: '',
canCreateSnippet: false,
};
......@@ -105,10 +117,11 @@ export default {
category: 'secondary',
},
{
condition: this.reportAbusePath,
condition: this.canReportSpam && !isEmpty(this.reportAbusePath),
text: __('Submit as spam'),
href: this.reportAbusePath,
click: this.submitAsSpam,
title: __('Submit as spam'),
loading: this.isSubmittingSpam,
},
];
},
......@@ -157,7 +170,7 @@ export default {
this.$refs.deleteModal.show();
},
deleteSnippet() {
this.isDeleting = true;
this.isLoading = true;
this.$apollo
.mutate({
mutation: DeleteSnippetMutation,
......@@ -167,17 +180,34 @@ export default {
if (data?.destroySnippet?.errors.length) {
throw new Error(data?.destroySnippet?.errors[0]);
}
this.isDeleting = false;
this.errorMessage = undefined;
this.closeDeleteModal();
this.redirectToSnippets();
})
.catch((err) => {
this.isDeleting = false;
this.isLoading = false;
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>
<template>
......@@ -189,9 +219,7 @@ export default {
:title="snippetVisibilityLevelDescription"
data-container="body"
>
<span class="sr-only">
{{ s__(`VisibilityLevel|${visibility}`) }}
</span>
<span class="sr-only">{{ s__(`VisibilityLevel|${visibility}`) }}</span>
<gl-icon :name="visibilityLevelIcon" :size="14" />
</div>
<div class="creator" data-testid="authored-message">
......@@ -233,6 +261,7 @@ export default {
>
<gl-button
:disabled="action.disabled"
:loading="action.loading"
:variant="action.variant"
:category="action.category"
:class="action.cssClass"
......@@ -240,9 +269,8 @@ export default {
data-qa-selector="snippet_action_button"
:data-qa-action="action.text"
@click="action.click ? action.click() : undefined"
>{{ action.text }}</gl-button
>
{{ action.text }}
</gl-button>
</div>
</template>
</div>
......@@ -266,14 +294,14 @@ export default {
<gl-modal ref="deleteModal" modal-id="delete-modal" title="Example title">
<template #modal-title>{{ __('Delete snippet?') }}</template>
<gl-alert v-if="errorMessage" variant="danger" class="mb-2" @dismiss="errorMessage = ''">{{
errorMessage
}}</gl-alert>
<gl-alert v-if="errorMessage" variant="danger" class="mb-2" @dismiss="errorMessage = ''">
{{ errorMessage }}
</gl-alert>
<gl-sprintf :message="__('Are you sure you want to delete %{name}?')">
<template #name
><strong>{{ snippet.title }}</strong></template
>
<template #name>
<strong>{{ snippet.title }}</strong>
</template>
</gl-sprintf>
<template #modal-footer>
......@@ -281,11 +309,11 @@ export default {
<gl-button
variant="danger"
category="primary"
:disabled="isDeleting"
:disabled="isLoading"
data-qa-selector="delete_snippet_button"
@click="deleteSnippet"
>
<gl-loading-icon v-if="isDeleting" size="sm" inline />
<gl-loading-icon v-if="isLoading" size="sm" inline />
{{ __('Delete snippet') }}
</gl-button>
</template>
......
......@@ -27,6 +27,7 @@ export default function appFactory(el, Component) {
visibilityLevels = '[]',
selectedLevel,
multipleLevelsRestricted,
canReportSpam,
reportAbusePath,
...restDataset
} = el.dataset;
......@@ -39,6 +40,7 @@ export default function appFactory(el, Component) {
selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE,
multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset,
reportAbusePath,
canReportSpam,
},
render(createElement) {
return createElement(Component, {
......
......@@ -3,7 +3,7 @@
- breadcrumb_title @snippet.to_reference
- 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
= render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet)
......
......@@ -12,7 +12,7 @@
- content_for :prefetch_asset_tags do
- 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
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
......
......@@ -31607,6 +31607,9 @@ msgstr ""
msgid "Smartcard authentication failed: client certificate header is missing."
msgstr ""
msgid "Snippet"
msgstr ""
msgid "Snippets"
msgstr ""
......@@ -31631,6 +31634,9 @@ msgstr ""
msgid "SnippetsEmptyState|There are no snippets to show."
msgstr ""
msgid "Snippets|%{spammable_titlecase} was submitted to Akismet successfully."
msgstr ""
msgid "Snippets|Add another file %{num}/%{total}"
msgstr ""
......@@ -31640,6 +31646,9 @@ msgstr ""
msgid "Snippets|Description (optional)"
msgstr ""
msgid "Snippets|Error with Akismet. Please check the logs for more info."
msgstr ""
msgid "Snippets|Files"
msgstr ""
......
......@@ -41,19 +41,23 @@ describe('Snippet view app', () => {
},
});
}
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEmbedDropdown = () => wrapper.findComponent(EmbedDropdown);
afterEach(() => {
wrapper.destroy();
});
it('renders loader while the query is in flight', () => {
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();
expect(wrapper.find(SnippetHeader).exists()).toBe(true);
expect(wrapper.find(SnippetTitle).exists()).toBe(true);
expect(wrapper.findComponent(SnippetHeader).exists()).toBe(true);
expect(wrapper.findComponent(SnippetTitle).exists()).toBe(true);
});
it('renders embed dropdown component if visibility allows', () => {
......@@ -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', () => {
......@@ -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', () => {
},
},
});
expect(wrapper.find(CloneDropdownButton).exists()).toBe(isRendered);
expect(wrapper.findComponent(CloneDropdownButton).exists()).toBe(isRendered);
},
);
});
......
import { GlButton, GlModal, GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
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 axios from '~/lib/utils/axios_utils';
import createFlash, { FLASH_TYPES } from '~/flash';
jest.mock('~/flash');
describe('Snippet header component', () => {
let wrapper;
let snippet;
let mutationTypes;
let mutationVariables;
let mock;
let errorMsg;
let err;
const originalRelativeUrlRoot = gon.relative_url_root;
const reportAbusePath = '/-/snippets/42/mark_as_spam';
const canReportSpam = true;
const GlEmoji = { template: '<img/>' };
......@@ -47,6 +54,7 @@ describe('Snippet header component', () => {
mocks: { $apollo },
provide: {
reportAbusePath,
canReportSpam,
...provide,
},
propsData: {
......@@ -118,10 +126,13 @@ describe('Snippet header component', () => {
RESOLVE: jest.fn(() => Promise.resolve({ data: { destroySnippet: { errors: [] } } })),
REJECT: jest.fn(() => Promise.reject(err)),
};
mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
mock.restore();
gon.relative_url_root = originalRelativeUrlRoot;
});
......@@ -186,7 +197,6 @@ describe('Snippet header component', () => {
{
category: 'primary',
disabled: false,
href: reportAbusePath,
text: 'Submit as spam',
variant: 'default',
},
......@@ -205,7 +215,6 @@ describe('Snippet header component', () => {
text: 'Delete',
},
{
href: reportAbusePath,
text: 'Submit as spam',
title: 'Submit as spam',
},
......@@ -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', () => {
beforeEach(() => {
createComponent({
......@@ -258,6 +292,7 @@ describe('Snippet header component', () => {
},
provide: {
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