diff options
Diffstat (limited to 'app')
73 files changed, 574 insertions, 400 deletions
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index f01c67e9474..b05138ac1ac 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -41,6 +41,7 @@ #= require shortcuts_issuable #= require shortcuts_network #= require jquery.nicescroll +#= require date.format #= require_tree . #= require fuzzaldrin-plus #= require cropper @@ -163,7 +164,7 @@ $ -> $('.trigger-submit').on 'change', -> $(@).parents('form').submit() - $('abbr.timeago, .js-timeago').timeago() + gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), false) # Flash if (flash = $(".flash-container")).length > 0 diff --git a/app/assets/javascripts/behaviors/requires_input.js.coffee b/app/assets/javascripts/behaviors/requires_input.js.coffee index 79d750d1847..0faa570ce13 100644 --- a/app/assets/javascripts/behaviors/requires_input.js.coffee +++ b/app/assets/javascripts/behaviors/requires_input.js.coffee @@ -35,4 +35,18 @@ $.fn.requiresInput = -> $form.on 'change input', fieldSelector, requireInput $ -> - $('form.js-requires-input').requiresInput() + $form = $('form.js-requires-input') + $form.requiresInput() + + # Hide or Show the help block when creating a new project + # based on the option selected + hideOrShowHelpBlock = (form) -> + selected = $('.js-select-namespace option:selected') + if selected.length and selected.data('options-parent') is 'groups' + return form.find('.help-block').hide() + else if selected.length + form.find('.help-block').show() + + hideOrShowHelpBlock($form) + + $('.select2.js-select-namespace').change -> hideOrShowHelpBlock($form) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index ee1d0fad289..2dc37257e22 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -122,7 +122,9 @@ class GitLabDropdown FILTER_INPUT = '.dropdown-input .dropdown-input-field' constructor: (@el, @options) -> - @dropdown = $(@el).parent() + self = @ + selector = $(@el).data "target" + @dropdown = if selector? then $(selector) else $(@el).parent() # Set Defaults { diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index d1fe116397a..90385621879 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -218,7 +218,7 @@ class @LabelsSelect selectable: true toggleLabel: (selected) -> - if selected and selected.title isnt 'Any Label' + if selected and selected.title? selected.title else defaultLabel diff --git a/app/assets/javascripts/lib/datetime_utility.js.coffee b/app/assets/javascripts/lib/datetime_utility.js.coffee new file mode 100644 index 00000000000..ad1d1c70481 --- /dev/null +++ b/app/assets/javascripts/lib/datetime_utility.js.coffee @@ -0,0 +1,17 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + w.gl.utils.formatDate = (datetime) -> + dateFormat(datetime, 'mmm d, yyyy h:MMtt Z') + + w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) -> + $timeagoEls.each( -> + $el = $(@) + $el.attr('title', gl.utils.formatDate($el.attr('datetime'))) + ) + + $timeagoEls.timeago() if setTimeago + +) window diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee index 3f9ca39912c..9e28353ac34 100644 --- a/app/assets/javascripts/lib/notify.js.coffee +++ b/app/assets/javascripts/lib/notify.js.coffee @@ -2,6 +2,11 @@ notificationGranted = (message, opts, onclick) -> notification = new Notification(message, opts) + # Hide the notification after X amount of seconds + setTimeout -> + notification.close() + , 8000 + if onclick notification.onclick = onclick diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 9946249adbf..ef0b534a709 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -142,7 +142,7 @@ class @MergeRequestTabs url: "#{source}.json" success: (data) => document.querySelector("div#commits").innerHTML = data.html - $('.js-timeago').timeago() + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')) @commitsLoaded = true @scrollToElement("#commits") @@ -153,7 +153,7 @@ class @MergeRequestTabs url: "#{source}.json" + @_location.search success: (data) => document.querySelector("div#diffs").innerHTML = data.html - $('.js-timeago').timeago() + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) $('div#diffs .js-syntax-highlight').syntaxHighlight() @expandViewContainer() if @diffViewType() is 'parallel' @diffsLoaded = true @@ -166,7 +166,7 @@ class @MergeRequestTabs url: "#{source}.json" success: (data) => document.querySelector("div#builds").innerHTML = data.html - $('.js-timeago').timeago() + gl.utils.localTimeAgo($('.js-timeago', 'div#builds')) @buildsLoaded = true @scrollToElement("#builds") diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 84a8887fbce..065626beeb8 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -12,10 +12,19 @@ class @MergeRequestWidget @readyForCICheck = true clearInterval @fetchBuildStatusInterval + @clearEventListeners() + @addEventListeners() @pollCIStatus() notifyPermissions() - setOpts: (@opts) -> + clearEventListeners: -> + $(document).off 'page:change.merge_request' + + addEventListeners: -> + $(document).on 'page:change.merge_request', => + if $('body').data('page') isnt 'projects:merge_requests:show' + clearInterval @fetchBuildStatusInterval + @clearEventListeners() mergeInProgress: (deleteSourceBranch = false)-> $.ajax @@ -38,7 +47,7 @@ class @MergeRequestWidget $('.mr-state-widget').replaceWith(data) ciLabelForStatus: (status) -> - if status == 'success' + if status is 'success' 'passed' else status @@ -67,18 +76,28 @@ class @MergeRequestWidget @opts.ci_status = data.status return - if data.status isnt @opts.ci_status + if data.status isnt @opts.ci_status and data.status? @showCIStatus data.status if data.coverage @showCICoverage data.coverage if showNotification - message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status)) + status = @ciLabelForStatus(data.status) + + if status is "preparing" + title = @opts.ci_title.preparing + status = status.charAt(0).toUpperCase() + status.slice(1); + message = @opts.ci_message.preparing.replace('{{status}}', status) + else + title = @opts.ci_title.normal + message = @opts.ci_message.normal.replace('{{status}}', status) + + title = title.replace('{{status}}', status) message = message.replace('{{sha}}', data.sha) message = message.replace('{{title}}', data.title) notify( - "Build #{@ciLabelForStatus(data.status)}", + title, message, @opts.gitlab_icon, -> @@ -98,6 +117,8 @@ class @MergeRequestWidget @setMergeButtonClass('btn-danger') when "running", "pending" @setMergeButtonClass('btn-warning') + when "success" + @setMergeButtonClass('btn-create') else $('.ci_widget.ci-error').show() @setMergeButtonClass('btn-danger') @@ -107,4 +128,6 @@ class @MergeRequestWidget $('.ci_widget:visible .ci-coverage').text(text) setMergeButtonClass: (css_class) -> - $('.accept_merge_request').removeClass("btn-create").addClass(css_class) + $('.accept_merge_request') + .removeClass('btn-danger btn-warning btn-create') + .addClass(css_class) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 86e3b860fcb..a67890200dd 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -163,9 +163,15 @@ class @Notes else if @isNewNote(note) @note_ids.push(note.id) - $('ul.main-notes-list') + $notesList = $('ul.main-notes-list') + + $notesList .append(note.html) .syntaxHighlight() + + # Update datetime format on the recent note + gl.utils.localTimeAgo($notesList.find("#note_#{note.id} .js-timeago"), false) + @initTaskList() @updateNotesCount(1) @@ -217,6 +223,8 @@ class @Notes # append new note to all matching discussions discussionContainer.append note_html + gl.utils.localTimeAgo($('.js-timeago', note_html), false) + @updateNotesCount(1) ### @@ -345,7 +353,9 @@ class @Notes updateNote: (_xhr, note, _status) => # Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html) - $('.js-timeago', $html).timeago() + + gl.utils.localTimeAgo($('.js-timeago', $html)) + $html.syntaxHighlight() $html.find('.js-task-list-container').taskList('enable') diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index ae87c6c4e40..f4a2562885d 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -18,8 +18,11 @@ class @Profile $(this).find('.btn-save').enable() $(this).find('.loading-gif').hide() - $('.update-notifications').on 'ajax:complete', -> - $(this).find('.btn-save').enable() + $('.update-notifications').on 'ajax:success', (e, data) -> + if data.saved + new Flash("Notification settings saved", "notice") + else + new Flash("Failed to save new settings", "alert") @bindEvents() diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 87d313ed67c..07be85a32a5 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -37,19 +37,20 @@ class @Project $('.update-notification').on 'click', (e) -> e.preventDefault() notification_level = $(@).data 'notification-level' - $('#notification_level').val(notification_level) + label = $(@).data 'notification-title' + $('#notification_setting_level').val(notification_level) $('#notification-form').submit() - label = null - switch notification_level - when 0 then label = ' Disabled ' - when 1 then label = ' Participating ' - when 2 then label = ' Watching ' - when 3 then label = ' Global ' - when 4 then label = ' On Mention ' $('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>") $(@).parents('ul').find('li.active').removeClass 'active' $(@).parent().addClass 'active' + $('#notification-form').on 'ajax:success', (e, data) -> + if data.saved + new Flash("Notification settings saved", "notice") + else + new Flash("Failed to save new settings", "alert") + + @projectSelectDropdown() projectSelectDropdown: -> diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee index be8ab9b428d..704bd8dee53 100644 --- a/app/assets/javascripts/project_select.js.coffee +++ b/app/assets/javascripts/project_select.js.coffee @@ -1,5 +1,37 @@ class @ProjectSelect constructor: -> + $('.js-projects-dropdown-toggle').each (i, dropdown) -> + $dropdown = $(dropdown) + + $dropdown.glDropdown( + filterable: true + filterRemote: true + search: + fields: ['name_with_namespace'] + data: (term, callback) -> + finalCallback = (projects) -> + callback projects + + if @includeGroups + projectsCallback = (projects) -> + groupsCallback = (groups) -> + data = groups.concat(projects) + finalCallback(data) + + Api.groups term, false, groupsCallback + else + projectsCallback = finalCallback + + if @groupId + Api.groupProjects @groupId, term, projectsCallback + else + Api.projects term, @orderBy, projectsCallback + url: (project) -> + project.web_url + text: (project) -> + project.name_with_namespace + ) + $('.ajax-project-select').each (i, select) -> @groupId = $(select).data('group-id') @includeGroups = $(select).data('include-groups') diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee index 886da72e261..00d2b641723 100644 --- a/app/assets/javascripts/todos.js.coffee +++ b/app/assets/javascripts/todos.js.coffee @@ -59,6 +59,8 @@ class @Todos goToTodoUrl: (e)-> todoLink = $(this).data('url') + return unless todoLink + if e.metaKey e.preventDefault() window.open(todoLink,'_blank') diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index b3397d16016..3f015427d07 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -69,6 +69,7 @@ header { } .header-content { + position: relative; height: $header-height; padding-right: 20px; @@ -76,6 +77,10 @@ header { padding-right: 0; } + .dropdown-menu { + margin-top: -5px; + } + .title { margin: 0; font-size: 19px; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 94f5a12ff6a..192d53b048a 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -58,12 +58,12 @@ .nav-search { display: inline-block; - width: 50%; + width: 100%; padding: 11px 0; /* Small devices (phones, tablets, 768px and lower) */ - @media (max-width: $screen-sm-min) { - width: 100%; + @media (min-width: $screen-sm-min) { + width: 50%; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 1ebbd9b0e57..0b6be86ce6a 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -28,6 +28,7 @@ $gl-link-color: #3084bb; $gl-dark-link-color: #333; $gl-placeholder-color: #8f8f8f; $gl-icon-color: $gl-placeholder-color; +$gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-header-color: $gl-title-color; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index c66efe978cd..6fe57c737b3 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -41,8 +41,17 @@ word-wrap: break-word; .md { - color: #7f8fa4; + color: $gl-grayish-blue; font-size: $gl-font-size; + + .label { + color: $gl-text-color; + font-size: inherit; + } + + iframe.twitter-share-button { + vertical-align: bottom; + } } pre { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 88c1b614c74..8b6f37f21b5 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -263,6 +263,12 @@ } } + .dropdown-content { + a:hover { + color: inherit; + } + } + .dropdown-menu-toggle { width: 100%; padding-top: 6px; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index b79335eab91..4ef548ffbe7 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -142,6 +142,7 @@ overflow: hidden; font-size: 90%; margin: 0 3px; + word-break: break-all; } .mr-list { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 4d4d508396d..f4da17fadaa 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -71,12 +71,24 @@ border-color: $focus-border-color; } } + + p { + code { + white-space: normal; + } + + pre { + code { + white-space: pre; + } + } + } } } .discussion-form { padding: $gl-padding-top $gl-padding; - background-color: #fff; + background-color: $white-light; } .note-edit-form { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 7295fe51121..e421a31549a 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -81,9 +81,15 @@ ul.notes { @include md-typography; // On diffs code should wrap nicely and not overflow - pre { + p { code { - white-space: pre; + white-space: normal; + } + + pre { + code { + white-space: pre; + } } } @@ -112,6 +118,10 @@ ul.notes { margin: 10px 0; } } + + a { + word-break: break-all; + } } .note-header { @@ -127,7 +137,7 @@ ul.notes { margin-right: 10px; } .line_content { - white-space: pre-wrap; + white-space: pre; } } @@ -145,19 +155,27 @@ ul.notes { background: $background-color; color: $text-color; } + &.notes_line2 { text-align: center; padding: 10px 0; border-left: 1px solid #ddd !important; } + &.notes_content { - background-color: #fff; + background-color: $background-color; border-width: 1px 0; padding: 0; vertical-align: top; + white-space: normal; + &.parallel { border-width: 1px; } + + .notes { + background-color: $white-light; + } } } } diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index e83fa9e3d52..75f78569e3c 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -34,6 +34,11 @@ color: #7f8fa4; font-size: $gl-font-size; + .label { + color: $gl-text-color; + font-size: inherit; + } + p { color: #5c5d5e; } diff --git a/app/controllers/groups/notification_settings_controller.rb b/app/controllers/groups/notification_settings_controller.rb new file mode 100644 index 00000000000..de13b16ccf2 --- /dev/null +++ b/app/controllers/groups/notification_settings_controller.rb @@ -0,0 +1,16 @@ +class Groups::NotificationSettingsController < Groups::ApplicationController + before_action :authenticate_user! + + def update + notification_setting = current_user.notification_settings_for(group) + saved = notification_setting.update_attributes(notification_setting_params) + + render json: { saved: saved } + end + + private + + def notification_setting_params + params.require(:notification_setting).permit(:level) + end +end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index d28e96c3f18..df98f56a1cd 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -60,6 +60,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController continue_login_process end + rescue Gitlab::OAuth::SignupDisabledError + handle_signup_error end def omniauth_error @@ -92,16 +94,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController continue_login_process end rescue Gitlab::OAuth::SignupDisabledError - label = Gitlab::OAuth::Provider.label_for(oauth['provider']) - message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." - - if current_application_settings.signup_enabled? - message << " Create a GitLab account first, and then connect it to your #{label} account." - end - - flash[:notice] = message - - redirect_to new_user_session_path + handle_signup_error end def handle_service_ticket provider, ticket @@ -122,6 +115,19 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end end + def handle_signup_error + label = Gitlab::OAuth::Provider.label_for(oauth['provider']) + message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." + + if current_application_settings.signup_enabled? + message << " Create a GitLab account first, and then connect it to your #{label} account." + end + + flash[:notice] = message + + redirect_to new_user_session_path + end + def oauth @oauth ||= request.env['omniauth.auth'] end diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 1fd1d6882df..18ee55c839a 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -1,39 +1,18 @@ class Profiles::NotificationsController < Profiles::ApplicationController def show @user = current_user - @notification = current_user.notification - @project_members = current_user.project_members - @group_members = current_user.group_members + @group_notifications = current_user.notification_settings.for_groups + @project_notifications = current_user.notification_settings.for_projects end def update - type = params[:notification_type] - - @saved = if type == 'global' - current_user.update_attributes(user_params) - elsif type == 'group' - group_member = current_user.group_members.find(params[:notification_id]) - group_member.notification_level = params[:notification_level] - group_member.save - else - project_member = current_user.project_members.find(params[:notification_id]) - project_member.notification_level = params[:notification_level] - project_member.save - end - - respond_to do |format| - format.html do - if @saved - flash[:notice] = "Notification settings saved" - else - flash[:alert] = "Failed to save new settings" - end - - redirect_back_or_default(default: profile_notifications_path) - end - - format.js + if current_user.update_attributes(user_params) + flash[:notice] = "Notification settings saved" + else + flash[:alert] = "Failed to save new settings" end + + redirect_back_or_default(default: profile_notifications_path) end def user_params diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index da3a32a9f8e..4b8fc049047 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -118,7 +118,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare @ci_commit = @merge_request.ci_commit - @ci_commits = [@ci_commit].compact @statuses = @ci_commit.statuses if @ci_commit @note_counts = Note.where(commit_id: @commits.map(&:id)). @@ -238,6 +237,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + status = "preparing" if status.nil? + response = { title: merge_request.title, sha: merge_request.last_commit_short_sha, @@ -309,7 +310,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = @merge_request.merge_request_diff @ci_commit = @merge_request.ci_commit - @ci_commits = [@ci_commit].compact @statuses = @ci_commit.statuses if @ci_commit if @merge_request.locked_long_ago? diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 1b9dd568043..707a0d0e5c6 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -39,8 +39,7 @@ class Projects::NotesController < Projects::ApplicationController def destroy if note.editable? - note.destroy - note.reset_events_cache + Notes::DeleteService.new(project, current_user).execute(note) end respond_to do |format| diff --git a/app/controllers/projects/notification_settings_controller.rb b/app/controllers/projects/notification_settings_controller.rb new file mode 100644 index 00000000000..7d81cc03c73 --- /dev/null +++ b/app/controllers/projects/notification_settings_controller.rb @@ -0,0 +1,16 @@ +class Projects::NotificationSettingsController < Projects::ApplicationController + before_action :authenticate_user! + + def update + notification_setting = current_user.notification_settings_for(project) + saved = notification_setting.update_attributes(notification_setting_params) + + render json: { saved: saved } + end + + private + + def notification_setting_params + params.require(:notification_setting).permit(:level) + end +end diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 5c7614cfbaf..bb7a6b6a5ab 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -11,7 +11,6 @@ class Projects::RepositoriesController < Projects::ApplicationController end def archive - RepositoryArchiveCacheWorker.perform_async headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format])) head :ok rescue => ex diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3cc37e59855..3768efe142a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -101,14 +101,18 @@ class ProjectsController < Projects::ApplicationController respond_to do |format| format.html do + if current_user + @membership = @project.team.find_member(current_user.id) + + if @membership + @notification_setting = current_user.notification_settings_for(@project) + end + end + if @project.repository_exists? if @project.empty_repo? render 'projects/empty' else - if current_user - @membership = @project.team.find_member(current_user.id) - end - render :show end else diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e6ceb213532..16e5b8ac223 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -184,7 +184,7 @@ module ApplicationHelper element = content_tag :time, time.to_s, class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}", datetime: time.to_time.getutc.iso8601, - title: time.in_time_zone.to_s(:medium), + title: time.to_time.in_time_zone.to_s(:medium), data: { toggle: 'tooltip', placement: placement, container: 'body' } unless skip_js diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 3f7282d0c6c..417050b4132 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,4 +1,9 @@ module CiStatusHelper + def ci_status_path(ci_commit) + project = ci_commit.project + builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha) + end + def ci_status_with_icon(status, target = nil) content = ci_icon_for_status(status) + ' '.html_safe + ci_label_for_status(status) klass = "ci-status ci-#{status}" @@ -34,10 +39,11 @@ module CiStatusHelper end def render_ci_status(ci_commit, tooltip_placement: 'auto left') - return unless ci_commit.is_a?(Commit) || ci_commit.is_a?(Ci::Commit) - + # TODO: split this method into + # - render_commit_status + # - render_pipeline_status link_to ci_icon_for_status(ci_commit.status), - project_pipeline_path(ci_commit.project, ci_commit), + ci_status_path(ci_commit), class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", title: "Build #{ci_label_for_status(ci_commit.status)}", data: { toggle: 'tooltip', placement: tooltip_placement } diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index ed0db04e069..f07eff3fb57 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -33,14 +33,6 @@ module GitlabRoutingHelper namespace_project_builds_path(project.namespace, project, *args) end - def project_commit_path(project, commit) - builds_namespace_project_commit_path(project.namespace, project, commit.id) - end - - def project_pipeline_path(project, ci_commit) - builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha) - end - def activity_project_path(project, *args) activity_namespace_project_path(project.namespace, project, *args) end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index bcf8639c829..4cb8adcebad 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -52,6 +52,7 @@ module IssuesHelper def milestone_options(object) milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a + milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed? milestones.unshift(Milestone::None) options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index faba418c4db..94c6b548ecd 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -3,8 +3,16 @@ module NamespacesHelper groups = current_user.owned_groups + current_user.masters_groups users = [current_user.namespace] - group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [display_path ? g.path : g.human_name, g.id]} ] - users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [display_path ? u.path : u.human_name, u.id]} ] + data_attr_group = { 'data-options-parent' => 'groups' } + data_attr_users = { 'data-options-parent' => 'users' } + + group_opts = [ + "Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.path : g.human_name, g.id, data_attr_group] } + ] + + users_opts = [ + "Users", users.sort_by(&:human_name).map { |u| [display_path ? u.path : u.human_name, u.id, data_attr_users] } + ] options = [] options << group_opts diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 499c655d2bf..54ab9179efc 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,48 +1,48 @@ module NotificationsHelper include IconsHelper - def notification_icon(notification) - if notification.disabled? - icon('volume-off', class: 'ns-mute') - elsif notification.participating? - icon('volume-down', class: 'ns-part') - elsif notification.watch? - icon('volume-up', class: 'ns-watch') - else - icon('circle-o', class: 'ns-default') + def notification_icon_class(level) + case level.to_sym + when :disabled + 'microphone-slash' + when :participating + 'volume-up' + when :watch + 'eye' + when :mention + 'at' + when :global + 'globe' end end - def notification_list_item(notification_level, user_membership) - case notification_level - when Notification::N_DISABLED - update_notification_link(Notification::N_DISABLED, user_membership, 'Disabled', 'microphone-slash') - when Notification::N_PARTICIPATING - update_notification_link(Notification::N_PARTICIPATING, user_membership, 'Participate', 'volume-up') - when Notification::N_WATCH - update_notification_link(Notification::N_WATCH, user_membership, 'Watch', 'eye') - when Notification::N_MENTION - update_notification_link(Notification::N_MENTION, user_membership, 'On mention', 'at') - when Notification::N_GLOBAL - update_notification_link(Notification::N_GLOBAL, user_membership, 'Global', 'globe') - else - # do nothing - end + def notification_icon(level, text = nil) + icon("#{notification_icon_class(level)} fw", text: text) end - def update_notification_link(notification_level, user_membership, title, icon) - content_tag(:li, class: active_level_for(user_membership, notification_level)) do - link_to '#', class: 'update-notification', data: { notification_level: notification_level } do - icon("#{icon} fw", text: title) - end + def notification_title(level) + case level.to_sym + when :participating + 'Participate' + when :mention + 'On mention' + else + level.to_s.titlecase end end - def notification_label(user_membership) - Notification.new(user_membership).to_s - end + def notification_list_item(level, setting) + title = notification_title(level) + + data = { + notification_level: level, + notification_title: title + } - def active_level_for(user_membership, level) - 'active' if user_membership.notification_level == level + content_tag(:li, class: ('active' if setting.level == level)) do + link_to '#', class: 'update-notification', data: data do + notification_icon(level, title) + end + end end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 4e4c6e301d5..7e00aacceaa 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -65,21 +65,14 @@ module ProjectsHelper link_to(simple_sanitize(owner.name), user_path(owner)) end - project_link = link_to project_path(project), { class: "project-item-select-holder" } do - link_output = simple_sanitize(project.name) + project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } - if current_user - link_output += project_select_tag :project_path, - class: "project-item-select js-projects-dropdown", - data: { include_groups: false, order_by: 'last_activity_at' } - end - - link_output + if current_user + project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) end - project_link += icon "chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle" if current_user - full_title = namespace_link + ' / ' + project_link - full_title += ' · '.html_safe + link_to(simple_sanitize(name), url) if name + full_title = "#{namespace_link} / #{project_link}".html_safe + full_title << ' · '.html_safe << link_to(simple_sanitize(name), url) if name full_title end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index edc5686cf08..2f066682180 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -20,6 +20,8 @@ module TodosHelper end def todo_target_path(todo) + return unless todo.target.present? + anchor = dom_id(todo.note) if todo.note.present? if todo.for_commit? diff --git a/app/models/commit.rb b/app/models/commit.rb index a898f7ba337..9ffdcc59128 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -150,13 +150,11 @@ class Commit end def hook_attrs(with_changed_files: false) - path_with_namespace = project.path_with_namespace - data = { id: id, message: safe_message, timestamp: committed_date.xmlschema, - url: "#{Gitlab.config.gitlab.url}/#{path_with_namespace}/commit/#{id}", + url: commit_url, author: { name: author_name, email: author_email @@ -170,6 +168,10 @@ class Commit data end + def commit_url + project.present? ? "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{id}" : "" + end + # Discover issues should be closed when this commit is pushed to a project's # default branch. def closes_issues(current_user = self.committer) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 882595ca21e..66eb5dcecf9 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -51,8 +51,6 @@ class CommitStatus < ActiveRecord::Base scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } - AVAILABLE_STATUSES = ['pending', 'running', 'success', 'failed', 'canceled'] - state_machine :status, initial: :pending do event :run do transition pending: :running diff --git a/app/models/concerns/ci_status.rb b/app/models/concerns/ci_status.rb index 4c7089bc36c..67e15b2d55b 100644 --- a/app/models/concerns/ci_status.rb +++ b/app/models/concerns/ci_status.rb @@ -1,15 +1,17 @@ module CiStatus extend ActiveSupport::Concern - module ClassMethods + AVAILABLE_STATUSES = %w(pending running success failed canceled skipped) + + class_methods do def status_sql - builds = all.select('count(id)').to_sql - success = all.success.select('count(id)').to_sql - ignored = all.failed.where(allow_failure: true).select('count(id)').to_sql if all.try(:ignored) + builds = all.select('count(*)').to_sql + success = all.success.select('count(*)').to_sql + ignored = all.ignored.select('count(*)').to_sql if all.try(:ignored) ignored ||= '0' - pending = all.pending.select('count(id)').to_sql - running = all.running.select('count(id)').to_sql - canceled = all.canceled.select('count(id)').to_sql + pending = all.pending.select('count(*)').to_sql + running = all.running.select('count(*)').to_sql + canceled = all.canceled.select('count(*)').to_sql deduce_status = "(CASE WHEN (#{builds})=0 THEN 'skipped' @@ -24,7 +26,7 @@ module CiStatus end def status - pluck(self.status_sql).first + all.pluck(self.status_sql).first end def duration @@ -34,7 +36,7 @@ module CiStatus end included do - validates :status, inclusion: { in: %w(pending running failed success canceled skipped) } + validates :status, inclusion: { in: AVAILABLE_STATUSES } state_machine :status, initial: :pending do state :pending, value: 'pending' diff --git a/app/models/concerns/notifiable.rb b/app/models/concerns/notifiable.rb deleted file mode 100644 index d7dcd97911d..00000000000 --- a/app/models/concerns/notifiable.rb +++ /dev/null @@ -1,15 +0,0 @@ -# == Notifiable concern -# -# Contains notification functionality -# -module Notifiable - extend ActiveSupport::Concern - - included do - validates :notification_level, inclusion: { in: Notification.project_notification_levels }, presence: true - end - - def notification - @notification ||= Notification.new(self) - end -end diff --git a/app/models/group.rb b/app/models/group.rb index b332601c59b..9a04ac70d35 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -27,6 +27,7 @@ class Group < Namespace has_many :users, through: :group_members has_many :project_group_links, dependent: :destroy has_many :shared_projects, through: :project_group_links, source: :project + has_many :notification_settings, dependent: :destroy, as: :source validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :visibility_level_allowed_by_projects diff --git a/app/models/member.rb b/app/models/member.rb index ca08007b7eb..60efafef211 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -19,7 +19,6 @@ class Member < ActiveRecord::Base include Sortable - include Notifiable include Gitlab::Access attr_accessor :raw_invite_token @@ -56,12 +55,15 @@ class Member < ActiveRecord::Base before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } after_create :send_invite, if: :invite? + after_create :create_notification_setting, unless: :invite? after_create :post_create_hook, unless: :invite? after_update :post_update_hook, unless: :invite? after_destroy :post_destroy_hook, unless: :invite? delegate :name, :username, :email, to: :user, prefix: true + default_value_for :notification_level, NotificationSetting.levels[:global] + class << self def find_by_invite_token(invite_token) invite_token = Devise.token_generator.digest(self, :invite_token, invite_token) @@ -160,6 +162,14 @@ class Member < ActiveRecord::Base send_invite end + def create_notification_setting + user.notification_settings.find_or_create_for(source) + end + + def notification_setting + @notification_setting ||= user.notification_settings_for(source) + end + private def send_invite diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 65d2ea00570..9fb474a1a93 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -24,7 +24,6 @@ class GroupMember < Member # Make sure group member points only to group as it source default_value_for :source_type, SOURCE_TYPE - default_value_for :notification_level, Notification::N_GLOBAL validates_format_of :source_type, with: /\ANamespace\z/ default_scope { where(source_type: SOURCE_TYPE) } diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 560d1690e14..07ddb02ae9d 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -27,7 +27,6 @@ class ProjectMember < Member # Make sure project member points only to project as it source default_value_for :source_type, SOURCE_TYPE - default_value_for :notification_level, Notification::N_GLOBAL validates_format_of :source_type, with: /\AProject\z/ default_scope { where(source_type: SOURCE_TYPE) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 33869e215c9..6766e4d1afa 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -128,7 +128,7 @@ class MergeRequest < ActiveRecord::Base validates :target_project, presence: true validates :target_branch, presence: true validates :merge_user, presence: true, if: :merge_when_build_succeeds? - validate :validate_branches + validate :validate_branches, unless: :allow_broken validate :validate_fork scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } @@ -218,7 +218,7 @@ class MergeRequest < ActiveRecord::Base end if opened? || reopened? - similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.id).opened + similar_mrs = self.target_project.merge_requests.where(source_branch: source_branch, target_branch: target_branch, source_project_id: source_project.try(:id)).opened similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id if similar_mrs.any? errors.add :validate_branches, @@ -345,7 +345,7 @@ class MergeRequest < ActiveRecord::Base def hook_attrs attrs = { - source: source_project.hook_attrs, + source: source_project.try(:hook_attrs), target: target_project.hook_attrs, last_commit: nil, work_in_progress: work_in_progress? diff --git a/app/models/notification.rb b/app/models/notification.rb deleted file mode 100644 index 171b8df45c2..00000000000 --- a/app/models/notification.rb +++ /dev/null @@ -1,77 +0,0 @@ -class Notification - # - # Notification levels - # - N_DISABLED = 0 - N_PARTICIPATING = 1 - N_WATCH = 2 - N_GLOBAL = 3 - N_MENTION = 4 - - attr_accessor :target - - class << self - def notification_levels - [N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH] - end - - def options_with_labels - { - disabled: N_DISABLED, - participating: N_PARTICIPATING, - watch: N_WATCH, - mention: N_MENTION, - global: N_GLOBAL - } - end - - def project_notification_levels - [N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH, N_GLOBAL] - end - end - - def initialize(target) - @target = target - end - - def disabled? - target.notification_level == N_DISABLED - end - - def participating? - target.notification_level == N_PARTICIPATING - end - - def watch? - target.notification_level == N_WATCH - end - - def global? - target.notification_level == N_GLOBAL - end - - def mention? - target.notification_level == N_MENTION - end - - def level - target.notification_level - end - - def to_s - case level - when N_DISABLED - 'Disabled' - when N_PARTICIPATING - 'Participating' - when N_WATCH - 'Watching' - when N_MENTION - 'On mention' - when N_GLOBAL - 'Global' - else - # do nothing - end - end -end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb new file mode 100644 index 00000000000..5001738f411 --- /dev/null +++ b/app/models/notification_setting.rb @@ -0,0 +1,28 @@ +class NotificationSetting < ActiveRecord::Base + enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 } + + default_value_for :level, NotificationSetting.levels[:global] + + belongs_to :user + belongs_to :source, polymorphic: true + + validates :user, presence: true + validates :source, presence: true + validates :level, presence: true + validates :user_id, uniqueness: { scope: [:source_type, :source_id], + message: "already exists in source", + allow_nil: true } + + scope :for_groups, -> { where(source_type: 'Namespace') } + scope :for_projects, -> { where(source_type: 'Project') } + + def self.find_or_create_for(source) + setting = find_or_initialize_by(source: source) + + unless setting.persisted? + setting.save + end + + setting + end +end diff --git a/app/models/project.rb b/app/models/project.rb index b9d589a4594..e48830115bc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -154,6 +154,7 @@ class Project < ActiveRecord::Base has_many :project_group_links, dependent: :destroy has_many :invited_groups, through: :project_group_links, source: :group has_many :todos, dependent: :destroy + has_many :notification_settings, dependent: :destroy, as: :source has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" @@ -388,9 +389,15 @@ class Project < ActiveRecord::Base def add_import_job if forked? - RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) + job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) else - RepositoryImportWorker.perform_async(self.id) + job_id = RepositoryImportWorker.perform_async(self.id) + end + + if job_id + Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}" + else + Rails.logger.error "Import job failed to start for #{path_with_namespace}" end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 8dead3a5884..0b2289cfa39 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -253,6 +253,8 @@ class Repository # This ensures this particular cache is flushed after the first commit to a # new repository. expire_emptiness_caches if empty? + expire_branch_count_cache + expire_tag_count_cache end def expire_branch_cache(branch_name = nil) @@ -896,9 +898,9 @@ class Repository end def main_language - unless empty? - Linguist::Repository.new(rugged, rugged.head.target_id).language - end + return if empty? || rugged.head_unborn? + + Linguist::Repository.new(rugged, rugged.head.target_id).language end def avatar diff --git a/app/models/user.rb b/app/models/user.rb index 2b0bee2099f..031315debd7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -143,6 +143,7 @@ class User < ActiveRecord::Base has_many :spam_logs, dependent: :destroy has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :todos, dependent: :destroy + has_many :notification_settings, dependent: :destroy # # Validations @@ -157,7 +158,7 @@ class User < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true + validates :notification_level, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :unique_email, if: ->(user) { user.email_changed? } @@ -190,6 +191,13 @@ class User < ActiveRecord::Base # Note: When adding an option, it MUST go on the end of the array. enum project_view: [:readme, :activity, :files] + # Notification level + # Note: When adding an option, it MUST go on the end of the array. + # + # TODO: Add '_prefix: :notification' to enum when update to Rails 5. https://github.com/rails/rails/pull/19813 + # Because user.notification_disabled? is much better than user.disabled? + enum notification_level: [:disabled, :participating, :watch, :global, :mention] + alias_attribute :private_token, :authentication_token delegate :path, to: :namespace, allow_nil: true, prefix: true @@ -349,10 +357,6 @@ class User < ActiveRecord::Base "#{self.class.reference_prefix}#{username}" end - def notification - @notification ||= Notification.new(self) - end - def generate_password if self.force_random_password self.password = self.password_confirmation = Devise.friendly_token.first(8) @@ -827,6 +831,10 @@ class User < ActiveRecord::Base end end + def notification_settings_for(source) + notification_settings.find_or_initialize_by(source: source) + end + private def projects_union diff --git a/app/services/notes/delete_service.rb b/app/services/notes/delete_service.rb new file mode 100644 index 00000000000..7f1b30ec84e --- /dev/null +++ b/app/services/notes/delete_service.rb @@ -0,0 +1,8 @@ +module Notes + class DeleteService < BaseService + def execute(note) + note.destroy + note.reset_events_cache + end + end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index eff0d96f93d..42ec1ac9e1a 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -253,8 +253,8 @@ class NotificationService def project_watchers(project) project_members = project_member_notification(project) - users_with_project_level_global = project_member_notification(project, Notification::N_GLOBAL) - users_with_group_level_global = group_member_notification(project, Notification::N_GLOBAL) + users_with_project_level_global = project_member_notification(project, :global) + users_with_group_level_global = group_member_notification(project, :global) users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq) users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users) @@ -264,18 +264,16 @@ class NotificationService end def project_member_notification(project, notification_level=nil) - project_members = project.project_members - if notification_level - project_members.where(notification_level: notification_level).pluck(:user_id) + project.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id) else - project_members.pluck(:user_id) + project.notification_settings.pluck(:user_id) end end def group_member_notification(project, notification_level) if project.group - project.group.group_members.where(notification_level: notification_level).pluck(:user_id) + project.group.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id) else [] end @@ -284,13 +282,13 @@ class NotificationService def users_with_global_level_watch(ids) User.where( id: ids, - notification_level: Notification::N_WATCH + notification_level: NotificationSetting.levels[:watch] ).pluck(:id) end # Build a list of users based on project notifcation settings def select_project_member_setting(project, global_setting, users_global_level_watch) - users = project_member_notification(project, Notification::N_WATCH) + users = project_member_notification(project, :watch) # If project setting is global, add to watch list if global setting is watch global_setting.each do |user_id| @@ -304,7 +302,7 @@ class NotificationService # Build a list of users based on group notification settings def select_group_member_setting(project, project_members, global_setting, users_global_level_watch) - uids = group_member_notification(project, Notification::N_WATCH) + uids = group_member_notification(project, :watch) # Group setting is watch, add to users list if user is not project member users = [] @@ -331,40 +329,46 @@ class NotificationService # Remove users with disabled notifications from array # Also remove duplications and nil recipients def reject_muted_users(users, project = nil) - reject_users(users, :disabled?, project) + reject_users(users, :disabled, project) end # Remove users with notification level 'Mentioned' def reject_mention_users(users, project = nil) - reject_users(users, :mention?, project) + reject_users(users, :mention, project) end - # Reject users which method_name from notification object returns true. + # Reject users which has certain notification level # # Example: - # reject_users(users, :watch?, project) + # reject_users(users, :watch, project) # - def reject_users(users, method_name, project = nil) + def reject_users(users, level, project = nil) + level = level.to_s + + unless NotificationSetting.levels.keys.include?(level) + raise 'Invalid notification level' + end + users = users.to_a.compact.uniq users = users.reject(&:blocked?) users.reject do |user| - next user.notification.send(method_name) unless project + next user.notification_level == level unless project - member = project.project_members.find_by(user_id: user.id) + setting = user.notification_settings_for(project) - if !member && project.group - member = project.group.group_members.find_by(user_id: user.id) + if !setting && project.group + setting = user.notification_settings_for(project.group) end - # reject users who globally set mention notification and has no membership - next user.notification.send(method_name) unless member + # reject users who globally set mention notification and has no setting per project/group + next user.notification_level == level unless setting # reject users who set mention notification in project - next true if member.notification.send(method_name) + next true if setting.level == level - # reject users who have N_MENTION in project and disabled in global settings - member.notification.global? && user.notification.send(method_name) + # reject users who have mention level in project and disabled in global settings + setting.global? && user.notification_level == level end end diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 50d00051409..8700b4820cd 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -117,7 +117,7 @@ %td.build-link - if project - = link_to builds_namespace_project_commit_path(project.namespace, project, build.sha) do + = link_to ci_status_path(build.commit) do %strong #{build.commit.short_sha} %td.timestamp diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 0f3b8119379..44339293095 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -45,6 +45,8 @@ %h1.title= title + = yield :header_content + = render 'shared/outdated_browser' - if @project && !@project.empty_repo? diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index a7ef31acd3d..6dfe7fbdae8 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -17,4 +17,12 @@ - content_for :scripts_body do = render "layouts/init_auto_complete" if current_user +- content_for :header_content do + .js-dropdown-menu-projects + .dropdown-menu.dropdown-select.dropdown-menu-projects + = dropdown_title("Go to a project") + = dropdown_filter("Search your projects") + = dropdown_content + = dropdown_loading + = render template: "layouts/application" diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml new file mode 100644 index 00000000000..89ae7ffda2b --- /dev/null +++ b/app/views/profiles/notifications/_group_settings.html.haml @@ -0,0 +1,13 @@ +%li.notification-list-item + %span.notification.fa.fa-holder.append-right-5 + - if setting.global? + = notification_icon(current_user.notification_level) + - else + = notification_icon(setting.level) + + %span.str-truncated + = link_to group.name, group_path(group) + + .pull-right + = form_for [group, setting], remote: true, html: { class: 'update-notifications' } do |f| + = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml new file mode 100644 index 00000000000..17c097154da --- /dev/null +++ b/app/views/profiles/notifications/_project_settings.html.haml @@ -0,0 +1,13 @@ +%li.notification-list-item + %span.notification.fa.fa-holder.append-right-5 + - if setting.global? + = notification_icon(current_user.notification_level) + - else + = notification_icon(setting.level) + + %span.str-truncated + = link_to_project(project) + + .pull-right + = form_for [project.namespace.becomes(Namespace), project, setting], remote: true, html: { class: 'update-notifications' } do |f| + = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml deleted file mode 100644 index d0d044136f6..00000000000 --- a/app/views/profiles/notifications/_settings.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -%li.notification-list-item - %span.notification.fa.fa-holder.append-right-5 - - if notification.global? - = notification_icon(@notification) - - else - = notification_icon(notification) - - %span.str-truncated - - if membership.kind_of? GroupMember - = link_to membership.group.name, membership.group - - else - = link_to_project(membership.project) - .pull-right - = form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do - = hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type') - = hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id') - = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'form-control trigger-submit' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 6609295a2a5..a2a505c082b 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,8 +1,12 @@ - page_title "Notifications" - header_title page_title, profile_notifications_path -= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| - = form_errors(@user) +%div + - if @user.errors.any? + %div.alert.alert-danger + %ul + - @user.errors.full_messages.each do |msg| + %li= msg = hidden_field_tag :notification_type, 'global' .row @@ -16,56 +20,55 @@ .col-lg-9 %h5 Global notification settings - .form-group - = f.label :notification_email, class: "label-light" - = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2" - .form-group - = f.label :notification_level, class: 'label-light' - .radio - = f.label :notification_level, value: Notification::N_DISABLED do - = f.radio_button :notification_level, Notification::N_DISABLED - .level-title - Disabled - %p You will not get any notifications via email - .radio - = f.label :notification_level, value: Notification::N_MENTION do - = f.radio_button :notification_level, Notification::N_MENTION - .level-title - On Mention - %p You will receive notifications only for comments in which you were @mentioned + = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| + .form-group + = f.label :notification_email, class: "label-light" + = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2" + .form-group + = f.label :notification_level, class: 'label-light' + .radio + = f.label :notification_level, value: :disabled do + = f.radio_button :notification_level, :disabled + .level-title + Disabled + %p You will not get any notifications via email - .radio - = f.label :notification_level, value: Notification::N_PARTICIPATING do - = f.radio_button :notification_level, Notification::N_PARTICIPATING - .level-title - Participating - %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) + .radio + = f.label :notification_level, value: :mention do + = f.radio_button :notification_level, :mention + .level-title + On Mention + %p You will receive notifications only for comments in which you were @mentioned - .radio - = f.label :notification_level, value: Notification::N_WATCH do - = f.radio_button :notification_level, Notification::N_WATCH - .level-title - Watch - %p You will receive notifications for any activity + .radio + = f.label :notification_level, value: :participating do + = f.radio_button :notification_level, :participating + .level-title + Participating + %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) - .prepend-top-default - = f.submit 'Update settings', class: "btn btn-create" + .radio + = f.label :notification_level, value: :watch do + = f.radio_button :notification_level, :watch + .level-title + Watch + %p You will receive notifications for any activity + + .prepend-top-default + = f.submit 'Update settings', class: "btn btn-create" %hr -.col-lg-9.col-lg-push-3 - %h5 - Groups (#{@group_members.count}) - %div - %ul.bordered-list - - @group_members.each do |group_member| - - notification = Notification.new(group_member) - = render 'settings', type: 'group', membership: group_member, notification: notification - %h5 - Projects (#{@project_members.count}) - %p.account-well - To specify the notification level per project of a group you belong to, you need to be a member of the project itself, not only its group. - .append-bottom-default - %ul.bordered-list - - @project_members.each do |project_member| - - notification = Notification.new(project_member) - = render 'settings', type: 'project', membership: project_member, notification: notification + %h5 + Groups (#{@group_notifications.count}) + %div + %ul.bordered-list + - @group_notifications.each do |setting| + = render 'group_settings', setting: setting, group: setting.source + %h5 + Projects (#{@project_notifications.count}) + %p.account-well + To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there. + .append-bottom-default + %ul.bordered-list + - @project_notifications.each do |setting| + = render 'project_settings', setting: setting, project: setting.source diff --git a/app/views/profiles/notifications/update.js.haml b/app/views/profiles/notifications/update.js.haml deleted file mode 100644 index 84c6ab25599..00000000000 --- a/app/views/profiles/notifications/update.js.haml +++ /dev/null @@ -1,6 +0,0 @@ -- if @saved - :plain - new Flash("Notification settings saved", "notice") -- else - :plain - new Flash("Failed to save new settings", "alert") diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml index 6eccbaa4bfa..66c30283c7a 100644 --- a/app/views/projects/_last_commit.html.haml +++ b/app/views/projects/_last_commit.html.haml @@ -1,6 +1,6 @@ .project-last-commit - if commit.status - = link_to project_commit_path(commit.project, commit), class: "ci-status ci-#{commit.status}" do + = link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{commit.status}" do = ci_icon_for_status(commit.status) = ci_label_for_status(commit.status) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 20160a718bc..b50b2cf3764 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -4,7 +4,7 @@ .build-page .gray-content-block.top-block Build ##{@build.id} for commit - %strong.monospace= link_to @build.commit.short_sha, builds_namespace_project_commit_path(@project.namespace, @project, @build.sha) + %strong.monospace= link_to @build.commit.short_sha, ci_status_path(@build.commit) from = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) - merge_request = @build.merge_request @@ -173,7 +173,7 @@ Commit .pull-right %small - = link_to @build.commit.short_sha, builds_namespace_project_commit_path(@project.namespace, @project, @build.sha), class: "monospace" + = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace" %p %span.attr-name Branch: = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) @@ -196,7 +196,7 @@ .build-widget %h4.title #{pluralize(@builds.count(:id), "other build")} for = succeed ":" do - = link_to @build.commit.short_sha, builds_namespace_project_commit_path(@project.namespace, @project, @build.sha), class: "monospace" + = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace" %table.table.builds - @builds.each_with_index do |build, i| %tr.build diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index a3786c35a1f..c1e3e5b73a2 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,20 +1,11 @@ -- case @membership -- when ProjectMember - = form_tag profile_notifications_path, method: :put, remote: true, class: 'inline', id: 'notification-form' do - = hidden_field_tag :notification_type, 'project' - = hidden_field_tag :notification_id, @membership.id - = hidden_field_tag :notification_level +- if @notification_setting + = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f| + = f.hidden_field :level %span.dropdown %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} = icon('bell') - = notification_label(@membership) + = notification_title(@notification_setting.level) = icon('angle-down') %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - - Notification.project_notification_levels.each do |level| - = notification_list_item(level, @membership) - -- when GroupMember - .btn.disabled.notifications-btn.has-tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."} - = icon('bell') - = notification_label(@membership) - = icon('angle-down') + - NotificationSetting.levels.each do |level| + = notification_list_item(level.first, @notification_setting) diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index 582ce61a64a..2ec3c809e1c 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -18,7 +18,7 @@ = ci_commit.ref - if defined?(link_to_commit) && link_to_commit for commit - = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace" + = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace" - if ci_commit.duration > 0 took = time_interval_in_words ci_commit.duration diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 3e4ab09c6d4..1e6724fc92b 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input js-quick-submit' } do |f| = render 'shared/issuable/form', f: f, issuable: @merge_request :javascript diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml index 307a75d02ca..a116ffe2e15 100644 --- a/app/views/projects/merge_requests/show/_builds.html.haml +++ b/app/views/projects/merge_requests/show/_builds.html.haml @@ -1 +1,2 @@ -= render "projects/commit/builds", link_to_commit: true += render "projects/commit/ci_commit", ci_commit: @ci_commit, link_to_commit: true + diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 92d95358937..3c68d61c4b5 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -8,20 +8,22 @@ = render 'projects/merge_requests/widget/locked' :javascript - var merge_request_widget; var opts = { merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", ci_status: "", - ci_message: "Build {{status}} for \"{{title}}\"", + ci_message: { + normal: "Build {{status}} for \"{{title}}\"", + preparing: "{{status}} build for \"{{title}}\"" + }, ci_enable: #{@project.ci_service ? "true" : "false"}, + ci_title: { + preparing: "{{status}} build", + normal: "Build {{status}}" + }, builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; - if(typeof merge_request_widget === 'undefined') { - merge_request_widget = new MergeRequestWidget(opts); - } else { - merge_request_widget.setOpts(opts); - } + merge_request_widget = new MergeRequestWidget(opts); diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 25233112132..a4c6094c69a 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -19,7 +19,7 @@ - if current_user.can_select_namespace? .input-group-addon = root_url - = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2', tabindex: 1} + = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1} .input-group-addon \/ - else diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 5c42423541e..03a44ca99c0 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -10,12 +10,12 @@ = "#{note.author.to_reference} commented" %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - - if note_editable?(note) - .note-actions - - access = note.project.team.human_max_access(note.author.id) - - if access - %span.note-role - = access + .note-actions + - access = note.project.team.human_max_access(note.author.id) + - if access + %span.note-role + = access + - if note_editable?(note) = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index 7afbaeddee8..0a38327baa2 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -6,7 +6,7 @@ .commit-message-container .max-width-marker = text_area_tag 'commit_message', - (params[:commit_message] || local_assigns[:text]), + (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]), class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder], required: true, rows: (local_assigns[:rows] || 3), id: "commit_message-#{nonce}" diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index fd5e58c1f1f..f722e61eeac 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,7 +1,7 @@ - if params[:label_name].present? = hidden_field_tag(:label_name, params[:label_name]) .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} + %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} %span.dropdown-toggle-text = h(params[:label_name].presence || "Label") = icon('chevron-down') |