diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-14 11:41:52 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-14 11:41:52 +0300 |
commit | 585826cb22ecea5998a2c2a4675735c94bdeedac (patch) | |
tree | 5b05f0b30d33cef48963609e8a18a4dff260eab3 /app/assets/javascripts/admin | |
parent | df221d036e5d0c6c0ee4d55b9c97f481ee05dee8 (diff) |
Add latest changes from gitlab-org/gitlab@16-6-stable-eev16.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/admin')
28 files changed, 618 insertions, 223 deletions
diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue index 3c46de7c2be..f0540ffa71e 100644 --- a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue +++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue @@ -7,6 +7,7 @@ import ReportDetails from './report_details.vue'; import ReportedContent from './reported_content.vue'; import ActivityEventsList from './activity_events_list.vue'; import ActivityHistoryItem from './activity_history_item.vue'; +import AbuseReportNotes from './abuse_report_notes.vue'; const alertDefaults = { visible: false, @@ -24,6 +25,7 @@ export default { ReportedContent, ActivityEventsList, ActivityHistoryItem, + AbuseReportNotes, }, mixins: [glFeatureFlagsMixin()], props: { @@ -96,5 +98,10 @@ export default { /> </template> </activity-events-list> + + <abuse-report-notes + v-if="glFeatures.abuseReportNotes" + :abuse-report-id="abuseReport.report.globalId" + /> </section> </template> diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue new file mode 100644 index 00000000000..80af7d7400a --- /dev/null +++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue @@ -0,0 +1,92 @@ +<script> +import { uniqueId } from 'lodash'; +import { __ } from '~/locale'; +import { createAlert } from '~/alert'; +import SkeletonLoadingContainer from '~/vue_shared/components/notes/skeleton_note.vue'; +import { SKELETON_NOTES_COUNT } from '~/admin/abuse_report/constants'; +import abuseReportNotesQuery from '../graphql/notes/abuse_report_notes.query.graphql'; +import AbuseReportDiscussion from './notes/abuse_report_discussion.vue'; + +export default { + name: 'AbuseReportNotes', + SKELETON_NOTES_COUNT, + i18n: { + fetchError: __('An error occurred while fetching comments, please try again.'), + }, + components: { + SkeletonLoadingContainer, + AbuseReportDiscussion, + }, + props: { + abuseReportId: { + type: String, + required: true, + }, + }, + data() { + return { + addNoteKey: uniqueId(`abuse-report-add-note-${this.abuseReportId}`), + }; + }, + apollo: { + abuseReportNotes: { + query: abuseReportNotesQuery, + variables() { + return { + id: this.abuseReportId, + }; + }, + update(data) { + return data.abuseReport?.discussions || []; + }, + skip() { + return !this.abuseReportId; + }, + error() { + createAlert({ message: this.$options.i18n.fetchError }); + }, + }, + }, + computed: { + initialLoading() { + return this.$apollo.queries.abuseReportNotes.loading; + }, + notesArray() { + return this.abuseReportNotes?.nodes || []; + }, + }, + methods: { + getDiscussionKey(discussion) { + const discussionId = discussion.notes.nodes[0].id; + return discussionId.split('/')[discussionId.split('/').length - 1]; + }, + }, +}; +</script> + +<template> + <div> + <div class="issuable-discussion gl-mb-5 gl-clearfix!"> + <template v-if="initialLoading"> + <ul class="notes main-notes-list timeline"> + <skeleton-loading-container + v-for="index in $options.SKELETON_NOTES_COUNT" + :key="index" + class="note-skeleton" + /> + </ul> + </template> + + <template v-else> + <ul class="notes main-notes-list timeline"> + <abuse-report-discussion + v-for="discussion in notesArray" + :key="getDiscussionKey(discussion)" + :discussion="discussion.notes.nodes" + :abuse-report-id="abuseReportId" + /> + </ul> + </template> + </div> + </div> +</template> diff --git a/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue b/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue index 8c4c1da28b8..2206e600543 100644 --- a/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue +++ b/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue @@ -11,7 +11,7 @@ export default { <!-- The styles `issuable-discussion`, `timeline`, `main-notes-list` and `notes` used below are declared in app/assets/stylesheets/pages/notes.scss --> <section class="gl-pt-6 issuable-discussion"> - <h2 class="gl-font-lg gl-mt-0 gl-mb-2">{{ $options.i18n.activity }}</h2> + <h2 class="gl-font-size-h1 gl-mt-0 gl-mb-4">{{ $options.i18n.activity }}</h2> <ul class="timeline main-notes-list notes"> <slot name="history-items"></slot> </ul> diff --git a/app/assets/javascripts/admin/abuse_report/components/labels_select.vue b/app/assets/javascripts/admin/abuse_report/components/labels_select.vue index 747c9a1a947..d2d143f0460 100644 --- a/app/assets/javascripts/admin/abuse_report/components/labels_select.vue +++ b/app/assets/javascripts/admin/abuse_report/components/labels_select.vue @@ -11,7 +11,7 @@ import DropdownContentsCreateView from '~/sidebar/components/labels/labels_selec import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue'; import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue'; import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue'; -import abuseReportLabelsQuery from './graphql/abuse_report_labels.query.graphql'; +import abuseReportLabelsQuery from '../graphql/abuse_report_labels.query.graphql'; export default { components: { diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue new file mode 100644 index 00000000000..4d24471fa43 --- /dev/null +++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue @@ -0,0 +1,104 @@ +<script> +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; +import DiscussionNotesRepliesWrapper from '~/notes/components/discussion_notes_replies_wrapper.vue'; +import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue'; +import AbuseReportNote from './abuse_report_note.vue'; + +export default { + name: 'AbuseReportDiscussion', + components: { + TimelineEntryItem, + DiscussionNotesRepliesWrapper, + ToggleRepliesWidget, + AbuseReportNote, + }, + props: { + abuseReportId: { + type: String, + required: true, + }, + discussion: { + type: Array, + required: true, + }, + }, + data() { + return { + isExpanded: true, + }; + }, + computed: { + note() { + return this.discussion[0]; + }, + noteId() { + return getIdFromGraphQLId(this.note.id); + }, + replies() { + if (this.discussion?.length > 1) { + return this.discussion.slice(1); + } + return null; + }, + hasReplies() { + return Boolean(this.replies?.length); + }, + discussionId() { + return this.discussion[0]?.discussion?.id || ''; + }, + }, + methods: { + toggleDiscussion() { + this.isExpanded = !this.isExpanded; + }, + }, +}; +</script> + +<template> + <abuse-report-note + v-if="!hasReplies" + :note="note" + :abuse-report-id="abuseReportId" + class="gl-mb-4" + /> + <timeline-entry-item v-else :data-note-id="noteId" class="note note-discussion gl-px-0"> + <div class="timeline-content"> + <div class="discussion"> + <div class="discussion-body"> + <div class="discussion-wrapper"> + <div class="discussion-notes"> + <ul class="notes"> + <abuse-report-note + :note="note" + :discussion-id="discussionId" + :abuse-report-id="abuseReportId" + class="gl-mb-4" + /> + <discussion-notes-replies-wrapper> + <toggle-replies-widget + v-if="hasReplies" + :collapsed="!isExpanded" + :replies="replies" + @toggle="toggleDiscussion({ discussionId })" + /> + <template v-if="isExpanded"> + <template v-for="reply in replies"> + <abuse-report-note + :key="reply.id" + :discussion-id="discussionId" + :note="reply" + :abuse-report-id="abuseReportId" + /> + </template> + </template> + </discussion-notes-replies-wrapper> + </ul> + </div> + </div> + </div> + </div> + </div> + </timeline-entry-item> +</template> diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue new file mode 100644 index 00000000000..6da3017e11e --- /dev/null +++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue @@ -0,0 +1,81 @@ +<script> +import { GlAvatarLink, GlAvatar } from '@gitlab/ui'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; +import NoteHeader from '~/notes/components/note_header.vue'; +import NoteBody from './abuse_report_note_body.vue'; + +export default { + name: 'AbuseReportNote', + directives: { + SafeHtml, + }, + components: { + GlAvatarLink, + GlAvatar, + TimelineEntryItem, + NoteHeader, + NoteBody, + }, + props: { + abuseReportId: { + type: String, + required: true, + }, + note: { + type: Object, + required: true, + }, + }, + computed: { + noteAnchorId() { + return `note_${getIdFromGraphQLId(this.note.id)}`; + }, + author() { + return this.note.author; + }, + authorId() { + return getIdFromGraphQLId(this.author.id); + }, + }, +}; +</script> + +<template> + <timeline-entry-item :id="noteAnchorId" class="note note-wrapper note-comment"> + <div :key="note.id" class="timeline-avatar gl-float-left"> + <gl-avatar-link + :href="author.webUrl" + :data-user-id="authorId" + :data-username="author.username" + class="js-user-link" + > + <gl-avatar + :src="author.avatarUrl" + :entity-name="author.username" + :alt="author.name" + :size="32" + /> + </gl-avatar-link> + </div> + <div class="timeline-content"> + <div data-testid="note-wrapper"> + <div class="note-header"> + <note-header + :author="author" + :created-at="note.createdAt" + :note-id="note.id" + :note-url="note.url" + > + <span v-if="note.createdAt" class="d-none d-sm-inline">·</span> + </note-header> + </div> + + <div class="timeline-discussion-body"> + <note-body ref="noteBody" :note="note" /> + </div> + </div> + </div> + </timeline-entry-item> +</template> diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue new file mode 100644 index 00000000000..ab3d7f5fa6c --- /dev/null +++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue @@ -0,0 +1,48 @@ +<script> +import SafeHtml from '~/vue_shared/directives/safe_html'; +import { renderGFM } from '~/behaviors/markdown/render_gfm'; + +export default { + name: 'AbuseReportNoteBody', + directives: { + SafeHtml, + }, + props: { + note: { + type: Object, + required: true, + }, + }, + watch: { + 'note.bodyHtml': { + immediate: true, + async handler(newVal, oldVal) { + if (newVal === oldVal) { + return; + } + await this.$nextTick(); + this.renderGFM(); + }, + }, + }, + methods: { + renderGFM() { + renderGFM(this.$refs['note-body']); + gl?.lazyLoader?.searchLazyImages(); + }, + }, + safeHtmlConfig: { + ADD_TAGS: ['use', 'gl-emoji', 'copy-code'], + }, +}; +</script> + +<template> + <div ref="note-body" class="note-body"> + <div + v-safe-html:[$options.safeHtmlConfig]="note.bodyHtml" + class="note-text md" + data-testid="abuse-report-note-body" + ></div> + </div> +</template> diff --git a/app/assets/javascripts/admin/abuse_report/components/report_details.vue b/app/assets/javascripts/admin/abuse_report/components/report_details.vue index 10e1dca7f91..89017e6cbd4 100644 --- a/app/assets/javascripts/admin/abuse_report/components/report_details.vue +++ b/app/assets/javascripts/admin/abuse_report/components/report_details.vue @@ -1,8 +1,8 @@ <script> import { __ } from '~/locale'; import { createAlert } from '~/alert'; +import abuseReportQuery from '../graphql/abuse_report.query.graphql'; import LabelsSelect from './labels_select.vue'; -import abuseReportQuery from './graphql/abuse_report.query.graphql'; export default { name: 'ReportDetails', diff --git a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue index 84d6f25ac05..99c8b3ece10 100644 --- a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue +++ b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue @@ -67,7 +67,7 @@ export default { <div class="gl-pb-3 gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column gl-align-items-center" > - <h2 class="gl-font-lg gl-mt-2 gl-mb-2"> + <h2 class="gl-font-size-h1 gl-mt-2 gl-mb-2"> {{ $options.i18n.reportTypes[reportType] }} </h2> @@ -128,7 +128,7 @@ export default { </gl-link> <time-ago-tooltip :time="report.reportedAt" - class="gl-ml-3 gl-text-secondary gl-xs-w-full" + class="gl-ml-3 gl-text-secondary gl-w-full gl-sm-w-auto" /> </div> </div> diff --git a/app/assets/javascripts/admin/abuse_report/constants.js b/app/assets/javascripts/admin/abuse_report/constants.js index f028408bed7..c56ea678b1d 100644 --- a/app/assets/javascripts/admin/abuse_report/constants.js +++ b/app/assets/javascripts/admin/abuse_report/constants.js @@ -111,3 +111,5 @@ export const HISTORY_ITEMS_I18N = { reportedByForCategory: s__('AbuseReport|Reported by %{name} for %{category}.'), deletedReporter: s__('AbuseReport|No user found'), }; + +export const SKELETON_NOTES_COUNT = 5; diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report.query.graphql index f5b075cb9af..640eec718f8 100644 --- a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql +++ b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report.query.graphql @@ -1,5 +1,6 @@ query abuseReportQuery($id: AbuseReportID!) { abuseReport(id: $id) { + id labels { nodes { id diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report_labels.query.graphql index 4e724b4db2c..4e724b4db2c 100644 --- a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql +++ b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report_labels.query.graphql diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql index 0781b8e634b..0781b8e634b 100644 --- a/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql +++ b/app/assets/javascripts/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql new file mode 100644 index 00000000000..84b57b4ed79 --- /dev/null +++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql @@ -0,0 +1,30 @@ +#import "~/graphql_shared/fragments/author.fragment.graphql" +#import "./abuse_report_note_permissions.fragment.graphql" + +fragment AbuseReportNote on Note { + id + body + bodyHtml + createdAt + lastEditedAt + url + resolved + author { + ...Author + } + lastEditedBy { + ...Author + webPath + } + userPermissions { + ...AbuseReportNotePermissions + } + discussion { + id + notes { + nodes { + id + } + } + } +} diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql new file mode 100644 index 00000000000..01436436b93 --- /dev/null +++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql @@ -0,0 +1,3 @@ +fragment AbuseReportNotePermissions on NotePermissions { + adminNote +} diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql new file mode 100644 index 00000000000..3a13ac1f37a --- /dev/null +++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql @@ -0,0 +1,18 @@ +#import "./abuse_report_note.fragment.graphql" + +query abuseReportNotes($id: AbuseReportID!) { + abuseReport(id: $id) { + id + discussions { + nodes { + id + replyId + notes { + nodes { + ...AbuseReportNote + } + } + } + } + } +} diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql new file mode 100644 index 00000000000..53ac9468e08 --- /dev/null +++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql @@ -0,0 +1,18 @@ +#import "./abuse_report_note.fragment.graphql" + +mutation createAbuseReportNote($input: CreateNoteInput!) { + createNote(input: $input) { + note { + id + discussion { + id + notes { + nodes { + ...AbuseReportNote + } + } + } + } + errors + } +} diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql new file mode 100644 index 00000000000..e8ff2933159 --- /dev/null +++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql @@ -0,0 +1,8 @@ +mutation deleteAbuseReportNote($input: DestroyNoteInput!) { + destroyNote(input: $input) { + errors + note { + id + } + } +} diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql new file mode 100644 index 00000000000..e11165074c9 --- /dev/null +++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql @@ -0,0 +1,10 @@ +#import "./abuse_report_note.fragment.graphql" + +mutation updateAbuseReportNote($input: UpdateNoteInput!) { + updateNote(input: $input) { + note { + ...AbuseReportNote + } + errors + } +} diff --git a/app/assets/javascripts/admin/background_migrations/index.js b/app/assets/javascripts/admin/background_migrations/index.js index 4ddd8f17c9a..890df17080d 100644 --- a/app/assets/javascripts/admin/background_migrations/index.js +++ b/app/assets/javascripts/admin/background_migrations/index.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import * as Sentry from '@sentry/browser'; +import * as Sentry from '~/sentry/sentry_browser_wrapper'; import Translate from '~/vue_shared/translate'; import BackgroundMigrationsDatabaseListbox from './components/database_listbox.vue'; diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue index 2c555aca3c0..753b1fb1819 100644 --- a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue +++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue @@ -107,7 +107,7 @@ export default { targetSelected: '', targetPath: this.broadcastMessage.targetPath, targetAccessLevels: this.broadcastMessage.targetAccessLevels, - targetAccessLevelOptions: this.targetAccessLevelOptions.map(([text, value]) => ({ + targetAccessLevelCheckBoxGroupOptions: this.targetAccessLevelOptions.map(([text, value]) => ({ text, value, })), @@ -324,7 +324,10 @@ export default { :state="!isValidated || targetRolesValid" data-testid="target-roles-checkboxes" > - <gl-form-checkbox-group v-model="targetAccessLevels" :options="targetAccessLevelOptions" /> + <gl-form-checkbox-group + v-model="targetAccessLevels" + :options="targetAccessLevelCheckBoxGroupOptions" + /> </gl-form-group> <gl-form-group diff --git a/app/assets/javascripts/admin/users/components/actions/index.js b/app/assets/javascripts/admin/users/components/actions/index.js index 4e63a85df89..633bc4d8b15 100644 --- a/app/assets/javascripts/admin/users/components/actions/index.js +++ b/app/assets/javascripts/admin/users/components/actions/index.js @@ -9,6 +9,8 @@ import Reject from './reject.vue'; import Unban from './unban.vue'; import Unblock from './unblock.vue'; import Unlock from './unlock.vue'; +import Trust from './trust_user.vue'; +import Untrust from './untrust_user.vue'; export default { Activate, @@ -22,4 +24,6 @@ export default { Unblock, Unlock, Reject, + Trust, + Untrust, }; diff --git a/app/assets/javascripts/admin/users/components/actions/trust_user.vue b/app/assets/javascripts/admin/users/components/actions/trust_user.vue new file mode 100644 index 00000000000..41ff8d4120d --- /dev/null +++ b/app/assets/javascripts/admin/users/components/actions/trust_user.vue @@ -0,0 +1,62 @@ +<script> +import { GlDisclosureDropdownItem } from '@gitlab/ui'; +import { sprintf, s__, __ } from '~/locale'; +import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub'; +import { I18N_USER_ACTIONS } from '../../constants'; + +// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922 +const messageHtml = ` + <p>${s__('AdminUsers|When not being monitored for spam:')}</p> + <ul> + <li>${s__( + 'AdminUsers|The user can create issues, notes, snippets, and merge requests that appear to be spam without being blocked.', + )}</li> + </ul> + <p>${s__('AdminUsers|You can untrust this user in the future.')}</p> +`; + +export default { + components: { + GlDisclosureDropdownItem, + }, + props: { + username: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + }, + methods: { + onClick() { + eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, { + path: this.path, + method: 'put', + modalAttributes: { + title: sprintf(s__('AdminUsers|Stop monitoring %{username} for possible spam?'), { + username: this.username, + }), + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.trust, + attributes: { variant: 'confirm' }, + }, + messageHtml, + }, + }); + }, + }, +}; +</script> + +<template> + <gl-disclosure-dropdown-item @action="onClick"> + <template #list-item> + <slot></slot> + </template> + </gl-disclosure-dropdown-item> +</template> diff --git a/app/assets/javascripts/admin/users/components/actions/untrust_user.vue b/app/assets/javascripts/admin/users/components/actions/untrust_user.vue new file mode 100644 index 00000000000..da59833af07 --- /dev/null +++ b/app/assets/javascripts/admin/users/components/actions/untrust_user.vue @@ -0,0 +1,56 @@ +<script> +import { GlDisclosureDropdownItem } from '@gitlab/ui'; +import { sprintf, s__, __ } from '~/locale'; +import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub'; +import { I18N_USER_ACTIONS } from '../../constants'; + +// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922 +const messageHtml = `<p>${s__( + 'AdminUsers|You can trust this user in the future if necessary.', +)}</p>`; + +export default { + components: { + GlDisclosureDropdownItem, + }, + props: { + username: { + type: String, + required: true, + }, + path: { + type: String, + required: true, + }, + }, + methods: { + onClick() { + eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, { + path: this.path, + method: 'put', + modalAttributes: { + title: sprintf(s__('AdminUsers|Re-enable spam monitoring for %{username}?'), { + username: this.username, + }), + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: I18N_USER_ACTIONS.untrust, + attributes: { variant: 'confirm' }, + }, + messageHtml, + }, + }); + }, + }, +}; +</script> + +<template> + <gl-disclosure-dropdown-item @action="onClick"> + <template #list-item> + <slot></slot> + </template> + </gl-disclosure-dropdown-item> +</template> diff --git a/app/assets/javascripts/admin/users/components/app.vue b/app/assets/javascripts/admin/users/components/app.vue index a3abd904a6b..b0caffb6ca6 100644 --- a/app/assets/javascripts/admin/users/components/app.vue +++ b/app/assets/javascripts/admin/users/components/app.vue @@ -1,9 +1,15 @@ <script> -import UsersTable from './users_table.vue'; +import { createAlert } from '~/alert'; +import { s__ } from '~/locale'; +import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils'; +import UsersTable from '~/vue_shared/components/users_table/users_table.vue'; +import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql'; +import UserActions from './user_actions.vue'; export default { components: { UsersTable, + UserActions, }, props: { users: { @@ -16,11 +22,64 @@ export default { required: true, }, }, + data() { + return { + groupCounts: {}, + }; + }, + apollo: { + groupCounts: { + query: getUsersGroupCountsQuery, + variables() { + return { + usernames: this.users.map((user) => user.username), + }; + }, + update(data) { + const nodes = data?.users?.nodes || []; + const parsedIds = convertNodeIdsFromGraphQLIds(nodes); + + return parsedIds.reduce((acc, { id, groupCount }) => { + acc[id] = groupCount || 0; + return acc; + }, {}); + }, + error(error) { + createAlert({ + message: this.$options.i18n.groupCountFetchError, + captureError: true, + error, + }); + }, + skip() { + return !this.users.length; + }, + }, + }, + computed: { + groupCountsLoading() { + return this.$apollo.queries.groupCounts.loading; + }, + }, + i18n: { + groupCountFetchError: s__( + 'AdminUsers|Could not load user group counts. Please refresh the page to try again.', + ), + }, }; </script> <template> <div> - <users-table :users="users" :paths="paths" /> + <users-table + :users="users" + :admin-user-path="paths.adminUser" + :group-counts="groupCounts" + :group-counts-loading="groupCountsLoading" + > + <template #user-actions="{ user }"> + <user-actions :user="user" :paths="paths" :show-button-labels="true" /> + </template> + </users-table> </div> </template> diff --git a/app/assets/javascripts/admin/users/components/user_avatar.vue b/app/assets/javascripts/admin/users/components/user_avatar.vue deleted file mode 100644 index dd354794cf3..00000000000 --- a/app/assets/javascripts/admin/users/components/user_avatar.vue +++ /dev/null @@ -1,67 +0,0 @@ -<script> -import { GlAvatarLabeled, GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui'; -import { truncate } from '~/lib/utils/text_utility'; -import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from '../constants'; - -export default { - directives: { - GlTooltip: GlTooltipDirective, - }, - components: { - GlAvatarLabeled, - GlBadge, - GlIcon, - }, - props: { - user: { - type: Object, - required: true, - }, - adminUserPath: { - type: String, - required: true, - }, - }, - computed: { - adminUserHref() { - return this.adminUserPath.replace('id', this.user.username); - }, - adminUserMailto() { - return `mailto:${this.user.email}`; - }, - userNoteShort() { - return truncate(this.user.note, LENGTH_OF_USER_NOTE_TOOLTIP); - }, - }, - USER_AVATAR_SIZE, -}; -</script> - -<template> - <div - v-if="user" - class="js-user-link gl-display-inline-block" - :data-user-id="user.id" - :data-username="user.username" - > - <gl-avatar-labeled - :size="$options.USER_AVATAR_SIZE" - :src="user.avatarUrl" - :label="user.name" - :sub-label="user.email" - :label-link="adminUserHref" - :sub-label-link="adminUserMailto" - > - <template #meta> - <div v-if="user.note" class="gl-text-gray-500 gl-p-1"> - <gl-icon v-gl-tooltip="userNoteShort" name="document" /> - </div> - <div v-for="(badge, idx) in user.badges" :key="idx" class="gl-p-1"> - <gl-badge class="gl-display-flex!" size="sm" :variant="badge.variant">{{ - badge.text - }}</gl-badge> - </div> - </template> - </gl-avatar-labeled> - </div> -</template> diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue deleted file mode 100644 index 65737be1e67..00000000000 --- a/app/assets/javascripts/admin/users/components/users_table.vue +++ /dev/null @@ -1,142 +0,0 @@ -<script> -import { GlSkeletonLoader, GlTable } from '@gitlab/ui'; -import { createAlert } from '~/alert'; -import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils'; -import { thWidthPercent } from '~/lib/utils/table_utility'; -import { s__, __ } from '~/locale'; -import UserDate from '~/vue_shared/components/user_date.vue'; -import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql'; -import UserActions from './user_actions.vue'; -import UserAvatar from './user_avatar.vue'; - -export default { - components: { - GlSkeletonLoader, - GlTable, - UserAvatar, - UserActions, - UserDate, - }, - props: { - users: { - type: Array, - required: true, - }, - paths: { - type: Object, - required: true, - }, - }, - data() { - return { - groupCounts: [], - }; - }, - apollo: { - groupCounts: { - query: getUsersGroupCountsQuery, - variables() { - return { - usernames: this.users.map((user) => user.username), - }; - }, - update(data) { - const nodes = data?.users?.nodes || []; - const parsedIds = convertNodeIdsFromGraphQLIds(nodes); - - return parsedIds.reduce((acc, { id, groupCount }) => { - acc[id] = groupCount || 0; - return acc; - }, {}); - }, - error(error) { - createAlert({ - message: this.$options.i18n.groupCountFetchError, - captureError: true, - error, - }); - }, - skip() { - return !this.users.length; - }, - }, - }, - i18n: { - groupCountFetchError: s__( - 'AdminUsers|Could not load user group counts. Please refresh the page to try again.', - ), - }, - fields: [ - { - key: 'name', - label: __('Name'), - thClass: thWidthPercent(40), - }, - { - key: 'projectsCount', - label: __('Projects'), - thClass: thWidthPercent(10), - }, - { - key: 'groupCount', - label: __('Groups'), - thClass: thWidthPercent(10), - }, - { - key: 'createdAt', - label: __('Created on'), - thClass: thWidthPercent(15), - }, - { - key: 'lastActivityOn', - label: __('Last activity'), - thClass: thWidthPercent(15), - }, - { - key: 'settings', - label: '', - thClass: thWidthPercent(10), - }, - ], -}; -</script> - -<template> - <div> - <gl-table - :items="users" - :fields="$options.fields" - :empty-text="s__('AdminUsers|No users found')" - show-empty - stacked="md" - :tbody-tr-attr="{ 'data-testid': 'user-row-content' }" - > - <template #cell(name)="{ item: user }"> - <user-avatar :user="user" :admin-user-path="paths.adminUser" /> - </template> - - <template #cell(createdAt)="{ item: { createdAt } }"> - <user-date :date="createdAt" /> - </template> - - <template #cell(lastActivityOn)="{ item: { lastActivityOn } }"> - <user-date :date="lastActivityOn" show-never /> - </template> - - <template #cell(groupCount)="{ item: { id } }"> - <div :data-testid="`user-group-count-${id}`"> - <gl-skeleton-loader v-if="$apollo.loading" :width="40" :lines="1" /> - <span v-else>{{ groupCounts[id] }}</span> - </div> - </template> - - <template #cell(projectsCount)="{ item: { id, projectsCount } }"> - <div :data-testid="`user-project-count-${id}`">{{ projectsCount }}</div> - </template> - - <template #cell(settings)="{ item: user }"> - <user-actions :user="user" :paths="paths" :show-button-labels="true" /> - </template> - </gl-table> - </div> -</template> diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js index 9cd61d6b1db..73383623aa2 100644 --- a/app/assets/javascripts/admin/users/constants.js +++ b/app/assets/javascripts/admin/users/constants.js @@ -1,9 +1,5 @@ import { s__, __ } from '~/locale'; -export const USER_AVATAR_SIZE = 32; - -export const LENGTH_OF_USER_NOTE_TOOLTIP = 100; - export const I18N_USER_ACTIONS = { edit: __('Edit'), userAdministration: s__('AdminUsers|User administration'), @@ -19,4 +15,6 @@ export const I18N_USER_ACTIONS = { deleteWithContributions: s__('AdminUsers|Delete user and contributions'), ban: s__('AdminUsers|Ban user'), unban: s__('AdminUsers|Unban user'), + trust: s__('AdminUsers|Trust user'), + untrust: s__('AdminUsers|Untrust user'), }; |