Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-28 12:08:05 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-28 12:08:05 +0300
commit4c788f43cbcd70bcceb4e40891d329952aa016d0 (patch)
tree9cd741579d79355a207ab74d37f26af768281fa0 /app
parent616129d41caac06a1ea0b0a36d1b3018eabac833 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/boards/models/list.js8
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js9
-rw-r--r--app/assets/javascripts/logs/constants.js7
-rw-r--r--app/assets/javascripts/logs/logs_tracking_helper.js18
-rw-r--r--app/assets/javascripts/logs/stores/actions.js18
-rw-r--r--app/assets/javascripts/registry/explorer/components/image_list.vue92
-rw-r--r--app/assets/javascripts/registry/explorer/components/image_list_row.vue136
-rw-r--r--app/assets/stylesheets/framework/broadcast_messages.scss1
-rw-r--r--app/controllers/admin/users_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb11
-rw-r--r--app/controllers/projects/merge_requests/drafts_controller.rb129
-rw-r--r--app/helpers/notes_helper.rb4
-rw-r--r--app/policies/draft_note_policy.rb13
-rw-r--r--app/serializers/draft_note_entity.rb39
-rw-r--r--app/serializers/draft_note_serializer.rb4
-rw-r--r--app/services/users/build_service.rb3
-rw-r--r--app/views/admin/users/_admin_notes.html.haml7
-rw-r--r--app/views/admin/users/_form.html.haml2
-rw-r--r--app/views/admin/users/_user_detail.html.haml2
-rw-r--r--app/views/admin/users/_user_detail_note.html.haml7
-rw-r--r--app/views/admin/users/_user_listing_note.html.haml3
-rw-r--r--app/views/admin/users/show.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/pipelines/charts.html.haml2
-rw-r--r--app/workers/new_note_worker.rb7
26 files changed, 417 insertions, 116 deletions
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index c2166de351b..bf5aee2a2a4 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -83,13 +83,7 @@ class List {
}
destroy() {
- const index = boardsStore.state.lists.indexOf(this);
- boardsStore.state.lists.splice(index, 1);
- boardsStore.updateNewListDropdown(this.id);
-
- boardsStore.destroyList(this.id).catch(() => {
- // TODO: handle request error
- });
+ boardsStore.destroy(this);
}
update() {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 6f66bc5d9b7..cde7256a258 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -547,6 +547,15 @@ const boardsStore = {
destroyList(id) {
return axios.delete(`${this.state.endpoints.listsEndpoint}/${id}`);
},
+ destroy(list) {
+ const index = this.state.lists.indexOf(list);
+ this.state.lists.splice(index, 1);
+ this.updateNewListDropdown(list.id);
+
+ this.destroyList(list.id).catch(() => {
+ // TODO: handle request error
+ });
+ },
saveList(list) {
const entity = list.label || list.assignee || list.milestone;
diff --git a/app/assets/javascripts/logs/constants.js b/app/assets/javascripts/logs/constants.js
index 51770aa7a1c..2bca1ecfd38 100644
--- a/app/assets/javascripts/logs/constants.js
+++ b/app/assets/javascripts/logs/constants.js
@@ -1,3 +1,10 @@
export const dateFormatMask = 'mmm dd HH:MM:ss.l';
export const TOKEN_TYPE_POD_NAME = 'TOKEN_TYPE_POD_NAME';
+
+export const tracking = {
+ USED_SEARCH_BAR: 'used_search_bar',
+ POD_LOG_CHANGED: 'pod_log_changed',
+ TIME_RANGE_SET: 'time_range_set',
+ ENVIRONMENT_SELECTED: 'environment_selected',
+};
diff --git a/app/assets/javascripts/logs/logs_tracking_helper.js b/app/assets/javascripts/logs/logs_tracking_helper.js
new file mode 100644
index 00000000000..91b0392f71f
--- /dev/null
+++ b/app/assets/javascripts/logs/logs_tracking_helper.js
@@ -0,0 +1,18 @@
+import Tracking from '~/tracking';
+
+/**
+ * The value of 1 in count, means there was one action performed
+ * related to the tracked action, in either of the following categories
+ * 1. Refreshing the logs
+ * 2. Select an environment
+ * 3. Change the time range
+ * 4. Use the search bar
+ */
+const trackLogs = label =>
+ Tracking.event(document.body.dataset.page, 'logs_view', {
+ label,
+ property: 'count',
+ value: 1,
+ });
+
+export default trackLogs;
diff --git a/app/assets/javascripts/logs/stores/actions.js b/app/assets/javascripts/logs/stores/actions.js
index a86d3c775a9..79fde1c7f2b 100644
--- a/app/assets/javascripts/logs/stores/actions.js
+++ b/app/assets/javascripts/logs/stores/actions.js
@@ -2,7 +2,8 @@ import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
-import { TOKEN_TYPE_POD_NAME } from '../constants';
+import { TOKEN_TYPE_POD_NAME, tracking } from '../constants';
+import trackLogs from '../logs_tracking_helper';
import * as types from './mutation_types';
@@ -81,22 +82,22 @@ export const showFilteredLogs = ({ dispatch, commit }, filters = []) => {
commit(types.SET_CURRENT_POD_NAME, podName);
commit(types.SET_SEARCH, search);
- dispatch('fetchLogs');
+ dispatch('fetchLogs', tracking.USED_SEARCH_BAR);
};
export const showPodLogs = ({ dispatch, commit }, podName) => {
commit(types.SET_CURRENT_POD_NAME, podName);
- dispatch('fetchLogs');
+ dispatch('fetchLogs', tracking.POD_LOG_CHANGED);
};
export const setTimeRange = ({ dispatch, commit }, timeRange) => {
commit(types.SET_TIME_RANGE, timeRange);
- dispatch('fetchLogs');
+ dispatch('fetchLogs', tracking.TIME_RANGE_SET);
};
export const showEnvironment = ({ dispatch, commit }, environmentName) => {
commit(types.SET_PROJECT_ENVIRONMENT, environmentName);
- dispatch('fetchLogs');
+ dispatch('fetchLogs', tracking.ENVIRONMENT_SELECTED);
};
/**
@@ -111,19 +112,22 @@ export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => {
.get(environmentsPath)
.then(({ data }) => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, data.environments);
- dispatch('fetchLogs');
+ dispatch('fetchLogs', tracking.ENVIRONMENT_SELECTED);
})
.catch(() => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_ERROR);
});
};
-export const fetchLogs = ({ commit, state }) => {
+export const fetchLogs = ({ commit, state }, trackingLabel) => {
commit(types.REQUEST_LOGS_DATA);
return requestLogsUntilData({ commit, state })
.then(({ data }) => {
const { pod_name, pods, logs, cursor } = data;
+ if (logs && logs.length > 0) {
+ trackLogs(trackingLabel);
+ }
commit(types.RECEIVE_LOGS_DATA_SUCCESS, { logs, cursor });
commit(types.SET_CURRENT_POD_NAME, pod_name);
commit(types.RECEIVE_PODS_DATA_SUCCESS, pods);
diff --git a/app/assets/javascripts/registry/explorer/components/image_list.vue b/app/assets/javascripts/registry/explorer/components/image_list.vue
index bc209b12738..9d48769cbad 100644
--- a/app/assets/javascripts/registry/explorer/components/image_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/image_list.vue
@@ -1,24 +1,12 @@
<script>
-import { GlPagination, GlTooltipDirective, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-
-import {
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
- LIST_DELETE_BUTTON_DISABLED,
- REMOVE_REPOSITORY_LABEL,
- ROW_SCHEDULED_FOR_DELETION,
-} from '../constants';
+import { GlPagination } from '@gitlab/ui';
+import ImageListRow from './image_list_row.vue';
export default {
name: 'ImageList',
components: {
GlPagination,
- ClipboardButton,
- GlDeprecatedButton,
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
+ ImageListRow,
},
props: {
images: {
@@ -30,12 +18,6 @@ export default {
required: true,
},
},
- i18n: {
- LIST_DELETE_BUTTON_DISABLED,
- REMOVE_REPOSITORY_LABEL,
- ROW_SCHEDULED_FOR_DELETION,
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
- },
computed: {
currentPage: {
get() {
@@ -46,79 +28,25 @@ export default {
},
},
},
- methods: {
- encodeListItem(item) {
- const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id });
- return window.btoa(params);
- },
- },
};
</script>
<template>
<div class="gl-display-flex gl-flex-direction-column">
- <div
+ <image-list-row
v-for="(listItem, index) in images"
:key="index"
- v-gl-tooltip="{
- placement: 'left',
- disabled: !listItem.deleting,
- title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
- }"
- data-testid="rowItem"
- >
- <div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 border-bottom"
- :class="{ 'border-top': index === 0, 'disabled-content': listItem.deleting }"
- >
- <div class="gl-display-flex gl-align-items-center">
- <router-link
- data-testid="detailsLink"
- :to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
- >
- {{ listItem.path }}
- </router-link>
- <clipboard-button
- v-if="listItem.location"
- :disabled="listItem.deleting"
- :text="listItem.location"
- :title="listItem.location"
- css-class="btn-default btn-transparent btn-clipboard"
- />
- <gl-icon
- v-if="listItem.failedDelete"
- v-gl-tooltip
- :title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
- name="warning"
- class="text-warning align-middle"
- />
- </div>
- <div
- v-gl-tooltip="{ disabled: listItem.destroy_path }"
- class="d-none d-sm-block"
- :title="$options.i18n.LIST_DELETE_BUTTON_DISABLED"
- >
- <gl-deprecated-button
- v-gl-tooltip
- data-testid="deleteImageButton"
- :disabled="!listItem.destroy_path || listItem.deleting"
- :title="$options.i18n.REMOVE_REPOSITORY_LABEL"
- :aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
- class="btn-inverted"
- variant="danger"
- @click="$emit('delete', listItem)"
- >
- <gl-icon name="remove" />
- </gl-deprecated-button>
- </div>
- </div>
- </div>
+ :item="listItem"
+ :show-top-border="index === 0"
+ @delete="$emit('delete', $event)"
+ />
+
<gl-pagination
v-model="currentPage"
:per-page="pagination.perPage"
:total-items="pagination.total"
align="center"
- class="w-100 gl-mt-2"
+ class="w-100 gl-mt-3"
/>
</div>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/image_list_row.vue
new file mode 100644
index 00000000000..1b631ca36de
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/image_list_row.vue
@@ -0,0 +1,136 @@
+<script>
+import { GlTooltipDirective, GlButton, GlIcon, GlSprintf } from '@gitlab/ui';
+import { n__ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+import {
+ ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ LIST_DELETE_BUTTON_DISABLED,
+ REMOVE_REPOSITORY_LABEL,
+ ROW_SCHEDULED_FOR_DELETION,
+} from '../constants';
+
+export default {
+ name: 'ImageListrow',
+ components: {
+ ClipboardButton,
+ GlButton,
+ GlSprintf,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ showTopBorder: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ i18n: {
+ LIST_DELETE_BUTTON_DISABLED,
+ REMOVE_REPOSITORY_LABEL,
+ ROW_SCHEDULED_FOR_DELETION,
+ ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ },
+ computed: {
+ encodedItem() {
+ const params = JSON.stringify({
+ name: this.item.path,
+ tags_path: this.item.tags_path,
+ id: this.item.id,
+ });
+ return window.btoa(params);
+ },
+ disabledDelete() {
+ return !this.item.destroy_path || this.item.deleting;
+ },
+ tagsCountText() {
+ return n__(
+ 'ContainerRegistry|%{count} Tag',
+ 'ContainerRegistry|%{count} Tags',
+ this.item.tags_count,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-gl-tooltip="{
+ placement: 'left',
+ disabled: !item.deleting,
+ title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
+ }"
+ >
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-py-4 "
+ :class="{
+ 'gl-border-t-solid gl-border-t-1': showTopBorder,
+ 'disabled-content': item.deleting,
+ }"
+ >
+ <div class="gl-display-flex gl-flex-direction-column">
+ <div class="gl-display-flex gl-align-items-center">
+ <router-link
+ class="gl-text-black-normal gl-font-weight-bold"
+ data-testid="detailsLink"
+ :to="{ name: 'details', params: { id: encodedItem } }"
+ >
+ {{ item.path }}
+ </router-link>
+ <clipboard-button
+ v-if="item.location"
+ :disabled="item.deleting"
+ :text="item.location"
+ :title="item.location"
+ css-class="btn-default btn-transparent btn-clipboard gl-text-gray-500"
+ />
+ <gl-icon
+ v-if="item.failedDelete"
+ v-gl-tooltip
+ :title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
+ name="warning"
+ class="text-warning"
+ />
+ </div>
+ <div class="gl-font-sm gl-text-gray-500">
+ <span class="gl-display-flex gl-align-items-center" data-testid="tagsCount">
+ <gl-icon name="tag" class="gl-mr-2" />
+ <gl-sprintf :message="tagsCountText">
+ <template #count>
+ {{ item.tags_count }}
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+ </div>
+ <div
+ v-gl-tooltip="{
+ disabled: item.destroy_path,
+ title: $options.i18n.LIST_DELETE_BUTTON_DISABLED,
+ }"
+ class="d-none d-sm-block"
+ data-testid="deleteButtonWrapper"
+ >
+ <gl-button
+ v-gl-tooltip
+ data-testid="deleteImageButton"
+ :disabled="disabledDelete"
+ :title="$options.i18n.REMOVE_REPOSITORY_LABEL"
+ :aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
+ class="btn-inverted"
+ variant="danger"
+ icon="remove"
+ @click="$emit('delete', item)"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/stylesheets/framework/broadcast_messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss
index 9903d10d27c..6b5d1794f9a 100644
--- a/app/assets/stylesheets/framework/broadcast_messages.scss
+++ b/app/assets/stylesheets/framework/broadcast_messages.scss
@@ -42,7 +42,6 @@
}
.broadcast-message-dismiss {
- height: 100%;
color: $gray-800;
}
}
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index ee42baa8326..fc0acd8f99a 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -241,7 +241,8 @@ class Admin::UsersController < Admin::ApplicationController
:theme_id,
:twitter,
:username,
- :website_url
+ :website_url,
+ :note
]
end
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 2331674f42c..1bf143c9a91 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -162,8 +162,13 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def renderable_notes
define_diff_comment_vars unless @notes
- @notes
+ draft_notes =
+ if current_user
+ merge_request.draft_notes.authored_by(current_user)
+ else
+ []
+ end
+
+ @notes.concat(draft_notes)
end
end
-
-Projects::MergeRequests::DiffsController.prepend_if_ee('EE::Projects::MergeRequests::DiffsController')
diff --git a/app/controllers/projects/merge_requests/drafts_controller.rb b/app/controllers/projects/merge_requests/drafts_controller.rb
new file mode 100644
index 00000000000..f4846b1aa81
--- /dev/null
+++ b/app/controllers/projects/merge_requests/drafts_controller.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+class Projects::MergeRequests::DraftsController < Projects::MergeRequests::ApplicationController
+ include Gitlab::Utils::StrongMemoize
+
+ respond_to :json
+
+ before_action :authorize_create_note!, only: [:create, :publish]
+ before_action :authorize_admin_draft!, only: [:update, :destroy]
+ before_action :authorize_admin_draft!, if: -> { action_name == 'publish' && params[:id].present? }
+
+ def index
+ drafts = prepare_notes_for_rendering(draft_notes)
+ render json: DraftNoteSerializer.new(current_user: current_user).represent(drafts)
+ end
+
+ def create
+ create_params = draft_note_params.merge(in_reply_to_discussion_id: params[:in_reply_to_discussion_id])
+ create_service = DraftNotes::CreateService.new(merge_request, current_user, create_params)
+
+ draft_note = create_service.execute
+
+ prepare_notes_for_rendering(draft_note)
+
+ render json: DraftNoteSerializer.new(current_user: current_user).represent(draft_note)
+ end
+
+ def update
+ draft_note.update!(draft_note_params)
+
+ prepare_notes_for_rendering(draft_note)
+
+ render json: DraftNoteSerializer.new(current_user: current_user).represent(draft_note)
+ end
+
+ def destroy
+ DraftNotes::DestroyService.new(merge_request, current_user).execute(draft_note)
+
+ head :ok
+ end
+
+ def publish
+ result = DraftNotes::PublishService.new(merge_request, current_user).execute(draft_note(allow_nil: true))
+
+ if result[:status] == :success
+ head :ok
+ else
+ render json: { message: result[:message] }, status: result[:status]
+ end
+ end
+
+ def discard
+ DraftNotes::DestroyService.new(merge_request, current_user).execute
+
+ head :ok
+ end
+
+ private
+
+ def draft_note(allow_nil: false)
+ strong_memoize(:draft_note) do
+ draft_notes.find(params[:id])
+ end
+ rescue ActiveRecord::RecordNotFound => ex
+ # draft_note is allowed to be nil in #publish
+ raise ex unless allow_nil
+ end
+
+ def draft_notes
+ return unless current_user
+
+ strong_memoize(:draft_notes) do
+ merge_request.draft_notes.authored_by(current_user)
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def merge_request
+ @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def draft_note_params
+ params.require(:draft_note).permit(
+ :commit_id,
+ :note,
+ :position,
+ :resolve_discussion
+ ).tap do |h|
+ # Old FE version will still be sending `draft_note[commit_id]` as 'undefined'.
+ # That can result to having a note linked to a commit with 'undefined' ID
+ # which is non-existent.
+ h[:commit_id] = nil if h[:commit_id] == 'undefined'
+ end
+ end
+
+ def prepare_notes_for_rendering(notes)
+ return [] unless notes
+
+ notes = Array.wrap(notes)
+
+ # Preload author and access-level information
+ DraftNote.preload_author(notes)
+ user_ids = notes.map(&:author_id)
+ project.team.max_member_access_for_user_ids(user_ids)
+
+ notes.map(&method(:render_draft_note))
+ end
+
+ def render_draft_note(note)
+ params = { target_id: merge_request.id, target_type: 'MergeRequest', text: note.note }
+ result = PreviewMarkdownService.new(@project, current_user, params).execute
+ markdown_params = { markdown_engine: result[:markdown_engine], issuable_state_filter_enabled: true }
+
+ note.rendered_note = view_context.markdown(result[:text], markdown_params)
+ note.users_referenced = result[:users]
+ note.commands_changes = view_context.markdown(result[:commands])
+
+ note
+ end
+
+ def authorize_admin_draft!
+ access_denied! unless can?(current_user, :admin_note, draft_note)
+ end
+
+ def authorize_create_note!
+ access_denied! unless can?(current_user, :create_note, merge_request)
+ end
+end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index bb9892ad596..3a752521e4b 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -61,8 +61,8 @@ module NotesHelper
class: 'add-diff-note js-add-diff-note-button',
type: 'submit', name: 'button',
data: diff_view_line_data(line_code, position, line_type),
- title: 'Add a comment to this line' do
- icon('comment-o')
+ title: _('Add a comment to this line') do
+ sprite_icon('comment', size: 12)
end
end
diff --git a/app/policies/draft_note_policy.rb b/app/policies/draft_note_policy.rb
new file mode 100644
index 00000000000..be99d12c5f8
--- /dev/null
+++ b/app/policies/draft_note_policy.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class DraftNotePolicy < BasePolicy
+ delegate { @subject.merge_request }
+
+ condition(:is_author) { @user && @subject.author == @user }
+
+ rule { is_author }.policy do
+ enable :read_note
+ enable :admin_note
+ enable :resolve_note
+ end
+end
diff --git a/app/serializers/draft_note_entity.rb b/app/serializers/draft_note_entity.rb
new file mode 100644
index 00000000000..cab4849ebc9
--- /dev/null
+++ b/app/serializers/draft_note_entity.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+class DraftNoteEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id
+ expose :author, using: NoteUserEntity
+ expose :merge_request_id
+ expose :position, if: -> (note, _) { note.on_diff? }
+ expose :line_code
+ expose :file_identifier_hash
+ expose :file_hash
+ expose :file_path
+ expose :note
+ expose :rendered_note, as: :note_html
+ expose :references
+ expose :discussion_id
+ expose :resolve_discussion
+ expose :noteable_type
+
+ expose :current_user do
+ expose :can_edit do |note|
+ can?(current_user, :admin_note, note)
+ end
+
+ expose :can_award_emoji do |note|
+ note.emoji_awardable?
+ end
+
+ expose :can_resolve do |note|
+ note.resolvable? && can?(current_user, :resolve_note, note)
+ end
+ end
+
+ private
+
+ def current_user
+ request.current_user
+ end
+end
diff --git a/app/serializers/draft_note_serializer.rb b/app/serializers/draft_note_serializer.rb
new file mode 100644
index 00000000000..282d7f9bdda
--- /dev/null
+++ b/app/serializers/draft_note_serializer.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+class DraftNoteSerializer < BaseSerializer
+ entity DraftNoteEntity
+end
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index 3938d675596..f06f00a5c3f 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -82,7 +82,8 @@ module Users
:organization,
:location,
:public_email,
- :user_type
+ :user_type,
+ :note
]
end
diff --git a/app/views/admin/users/_admin_notes.html.haml b/app/views/admin/users/_admin_notes.html.haml
new file mode 100644
index 00000000000..5d91ba1a1ca
--- /dev/null
+++ b/app/views/admin/users/_admin_notes.html.haml
@@ -0,0 +1,7 @@
+%fieldset
+ %legend= _('Admin notes')
+ .form-group.row
+ .col-sm-2.col-form-label.text-right
+ = f.label :note, s_('AdminNote|Note')
+ .col-sm-10
+ = f.text_area :note, class: 'form-control'
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 3281718071c..38c6c8b2a62 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -83,7 +83,7 @@
.col-sm-10
= f.text_field :website_url, class: 'form-control'
- = render_if_exists 'admin/users/admin_notes', f: f
+ = render 'admin/users/admin_notes', f: f
.form-actions
- if @user.new_record?
diff --git a/app/views/admin/users/_user_detail.html.haml b/app/views/admin/users/_user_detail.html.haml
index a29f369b9de..3839231cb95 100644
--- a/app/views/admin/users/_user_detail.html.haml
+++ b/app/views/admin/users/_user_detail.html.haml
@@ -6,7 +6,7 @@
= image_tag avatar_icon_for_user(user), class: 'avatar s16 d-xs-flex d-md-none mr-1 gl-mt-2', alt: _('Avatar for %{name}') % { name: sanitize_name(user.name) }
= link_to user.name, admin_user_path(user), class: 'text-plain js-user-link', data: { user_id: user.id, qa_selector: 'username_link' }
- = render_if_exists 'admin/users/user_listing_note', user: user
+ = render 'admin/users/user_listing_note', user: user
- user_badges_in_admin_section(user).each do |badge|
- css_badge = "badge badge-#{badge[:variant]}" if badge[:variant].present?
diff --git a/app/views/admin/users/_user_detail_note.html.haml b/app/views/admin/users/_user_detail_note.html.haml
new file mode 100644
index 00000000000..4f2a682c5ca
--- /dev/null
+++ b/app/views/admin/users/_user_detail_note.html.haml
@@ -0,0 +1,7 @@
+- if @user.note.present?
+ - text = @user.note
+ .card.border-info
+ .card-header.bg-info.text-white
+ = _('Admin Note')
+ .card-body
+ %p= text
diff --git a/app/views/admin/users/_user_listing_note.html.haml b/app/views/admin/users/_user_listing_note.html.haml
new file mode 100644
index 00000000000..df4af009c5c
--- /dev/null
+++ b/app/views/admin/users/_user_listing_note.html.haml
@@ -0,0 +1,3 @@
+- if user.note.present?
+ %span.has-tooltip.user-note{ title: user.note }
+ = icon("sticky-note-o cgrey")
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index cd07fee8e59..fa707b73d3e 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -154,7 +154,7 @@
%br
= link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?', qa_selector: 'confirm_user_button' }
- = render_if_exists 'admin/users/user_detail_note'
+ = render 'admin/users/user_detail_note'
- if @user.deactivated?
.card.border-info
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 88d1ec54cb0..4442bdcdf1d 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -22,8 +22,8 @@
.header-action-buttons
- if defined?(@notes_count) && @notes_count > 0
- %span.btn.disabled.btn-grouped.d-none.d-sm-block.append-right-10
- = icon('comment')
+ %span.btn.disabled.btn-grouped.d-none.d-sm-block.append-right-10.has-tooltip{ title: n_("%d comment on this commit", "%d comments on this commit", @notes_count) % @notes_count }
+ = sprite_icon('comment')
= @notes_count
= link_to project_tree_path(@project, @commit), class: "btn btn-default append-right-10 d-none d-sm-none d-md-inline" do
#{ _('Browse files') }
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 855b719dc45..7395c16c38b 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -14,7 +14,7 @@
.file-actions.d-none.d-sm-block
- if blob&.readable_text?
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: _("Toggle comments for this file"), disabled: @diff_notes_disabled do
- = icon('comment')
+ = sprite_icon('comment', size: 16)
\
- if editable_diff?(diff_file)
- link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {}
diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml
index 7496ca97d56..55f1b9098c3 100644
--- a/app/views/projects/pipelines/charts.html.haml
+++ b/app/views/projects/pipelines/charts.html.haml
@@ -1,4 +1,4 @@
-- page_title _('CI / CD Charts')
+- page_title _('CI / CD Analytics')
#js-project-pipelines-charts-app{ data: { counts: @counts, success_ratio: success_ratio(@counts),
times_chart: { labels: @charts[:pipeline_times].labels, values: @charts[:pipeline_times].pipeline_times },
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index ee1d2237001..b31311b0e44 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -19,14 +19,11 @@ class NewNoteWorker # rubocop:disable Scalability/IdempotentWorker
Gitlab::AppLogger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
end
end
+ # rubocop: enable CodeReuse/ActiveRecord
private
- # EE-only method
def skip_notification?(note)
- false
+ note.review.present?
end
- # rubocop: enable CodeReuse/ActiveRecord
end
-
-NewNoteWorker.prepend_if_ee('EE::NewNoteWorker')