diff options
Diffstat (limited to 'app/helpers')
28 files changed, 753 insertions, 256 deletions
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index c5820bf4c50..e0abc3a2869 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -1,21 +1,33 @@ module AppearancesHelper - def brand_item - nil - end - def brand_title - 'GitLab Community Edition' + if brand_item && brand_item.title + brand_item.title + else + 'GitLab Community Edition' + end end def brand_image - nil + if brand_item.logo? + image_tag brand_item.logo + else + nil + end end def brand_text - nil + markdown(brand_item.description) + end + + def brand_item + @appearance ||= Appearance.first end def brand_header_logo - render 'shared/logo.svg' + if brand_item && brand_item.header_logo? + image_tag brand_item.header_logo + else + render 'shared/logo.svg' + end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f7f7a1a02d3..e6ceb213532 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -72,7 +72,7 @@ module ApplicationHelper if user_or_email.is_a?(User) user = user_or_email else - user = User.find_by(email: user_or_email.downcase) + user = User.find_by_any_email(user_or_email.try(:downcase)) end if user @@ -118,12 +118,6 @@ module ApplicationHelper grouped_options_for_select(options, @ref || @project.default_branch) end - def emoji_autocomplete_source - # should be an array of strings - # so to_s can be called, because it is sufficient and to_json is too slow - Emoji.names.to_s - end - # Define whenever show last push event # with suggestion to create MR def show_last_push_widget?(event) @@ -169,22 +163,6 @@ module ApplicationHelper Gitlab.config.extra end - def search_placeholder - if @project && @project.persisted? - 'Search in this project' - elsif @snippet || @snippets || @show_snippets - 'Search snippets' - elsif @group && @group.persisted? - 'Search in this group' - else - 'Search' - end - end - - def broadcast_message - BroadcastMessage.current - end - # Render a `time` element with Javascript-based relative date and tooltip # # time - Time object @@ -204,9 +182,9 @@ module ApplicationHelper # Returns an HTML-safe String def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false) element = content_tag :time, time.to_s, - class: "#{html_class} js-timeago js-timeago-pending", - datetime: time.getutc.iso8601, - title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'), + 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), data: { toggle: 'tooltip', placement: placement, container: 'body' } unless skip_js @@ -218,6 +196,22 @@ module ApplicationHelper element end + def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', include_author: false) + return if object.updated_at == object.created_at + + content_tag :small, class: "edited-text" do + output = content_tag(:span, "Edited ") + output << time_ago_with_tooltip(object.updated_at, placement: placement, html_class: html_class) + + if include_author && object.updated_by && object.updated_by != object.author + output << content_tag(:span, " by ") + output << link_to_member(object.project, object.updated_by, avatar: false, author_class: nil) + end + + output + end + end + def render_markup(file_name, file_content) if gitlab_markdown?(file_name) Haml::Helpers.preserve(markdown(file_content)) @@ -228,8 +222,7 @@ module ApplicationHelper file_content end else - GitHub::Markup.render(file_name, file_content). - force_encoding(file_content.encoding).html_safe + other_markup(file_name, file_content) end rescue RuntimeError simple_format(file_content) @@ -266,7 +259,7 @@ module ApplicationHelper state: params[:state], scope: params[:scope], label_name: params[:label_name], - milestone_id: params[:milestone_id], + milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], author_id: params[:author_id], sort: params[:sort], @@ -308,7 +301,7 @@ module ApplicationHelper if project.nil? nil elsif current_controller?(:issues) - project.issues.send(entity).count + project.issues.visible_to_user(current_user).send(entity).count elsif current_controller?(:merge_requests) project.merge_requests.send(entity).count end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 7d6b58ee21a..23693629a4c 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -23,6 +23,10 @@ module ApplicationSettingsHelper current_application_settings.user_oauth_applications end + def askimet_enabled? + current_application_settings.akismet_enabled? + end + # Return a group of checkboxes that use Bootstrap's button plugin for a # toggle button effect. def restricted_level_checkboxes(help_block_id) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 0cfc0565e84..b4f80fd9b3e 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,11 +1,15 @@ module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze def ldap_enabled? Gitlab.config.ldap.enabled end + def omniauth_enabled? + Gitlab.config.omniauth.enabled + end + def provider_has_icon?(name) PROVIDERS_WITH_ICONS.include?(name.to_s) end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index d31d4cde08f..0f77b3b299a 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,21 +1,10 @@ module BlobHelper - def highlight(blob_name, blob_content, nowrap: false, continue: false) - @formatter ||= Rouge::Formatters::HTMLGitlab.new( - nowrap: nowrap, - cssclass: 'code highlight', - lineanchors: true, - lineanchorsid: 'LC' - ) - - begin - @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new - result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe - rescue - @lexer = Rouge::Lexers::PlainText - result = @formatter.format(@lexer.lex(blob_content)).html_safe - end + def highlighter(blob_name, blob_content, nowrap: false) + Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap) + end - result + def highlight(blob_name, blob_content, nowrap: false) + Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap) end def no_highlight_files @@ -37,20 +26,19 @@ module BlobHelper tree_join(ref, path), link_opts) - if !on_top_of_branch? + if !on_top_of_branch?(project, ref) button_tag "Edit", class: "btn btn-default disabled has_tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } - elsif can_edit_blob?(blob) - link_to "Edit", edit_path, class: 'btn btn-small' + elsif can_edit_blob?(blob, project, ref) + link_to "Edit", edit_path, class: 'btn' elsif can?(current_user, :fork_project, project) continue_params = { to: edit_path, notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } - fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id, - continue: continue_params) + fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) - link_to "Edit", fork_path, class: 'btn btn-small', method: :post + link_to "Edit", fork_path, class: 'btn', method: :post end end @@ -61,11 +49,11 @@ module BlobHelper return unless blob - if !on_top_of_branch? + if !on_top_of_branch?(project, ref) button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' } elsif blob.lfs_pointer? button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } - elsif can_edit_blob?(blob) + elsif can_edit_blob?(blob, project, ref) button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' elsif can?(current_user, :fork_project, project) continue_params = { @@ -73,8 +61,7 @@ module BlobHelper notice: edit_in_new_fork_notice + " Try to #{action} this file again.", notice_now: edit_in_new_fork_notice_now } - fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id, - continue: continue_params) + fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post end @@ -139,4 +126,51 @@ module BlobHelper blob.size end end + + # SVGs can contain malicious JavaScript; only include whitelisted + # elements and attributes. Note that this whitelist is by no means complete + # and may omit some elements. + def sanitize_svg(blob) + blob.data = Loofah.scrub_fragment(blob.data, :strip).to_xml + blob + end + + # If we blindly set the 'real' content type when serving a Git blob we + # are enabling XSS attacks. An attacker could upload e.g. a Javascript + # file to a Git repository, trick the browser of a victim into + # downloading the blob, and then the 'application/javascript' content + # type would tell the browser to execute the attacker's Javascript. By + # overriding the content type and setting it to 'text/plain' (in the + # example of Javascript) we tell the browser of the victim not to + # execute untrusted data. + def safe_content_type(blob) + if blob.text? + 'text/plain; charset=utf-8' + elsif blob.image? + blob.content_type + else + 'application/octet-stream' + end + end + + def cached_blob? + stale = stale?(etag: @blob.id) # The #stale? method sets cache headers. + + # Because we are opionated we set the cache headers ourselves. + response.cache_control[:public] = @project.public? + + if @ref && @commit && @ref == @commit.id + # This is a link to a commit by its commit SHA. That means that the blob + # is immutable. The only reason to invalidate the cache is if the commit + # was deleted or if the user lost access to the repository. + response.cache_control[:max_age] = Blob::CACHE_TIME_IMMUTABLE + else + # A branch or tag points at this blob. That means that the expected blob + # value may change over time. + response.cache_control[:max_age] = Blob::CACHE_TIME + end + + response.etag = @blob.id + !stale + end end diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index 6484dca6b55..43a29c96bca 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -1,16 +1,38 @@ module BroadcastMessagesHelper - def broadcast_styling(broadcast_message) - styling = '' + def broadcast_message(message = BroadcastMessage.current) + return unless message.present? + + content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do + icon('bullhorn') << ' ' << render_broadcast_message(message.message) + end + end + + def broadcast_message_style(broadcast_message) + style = '' if broadcast_message.color.present? - styling << "background-color: #{broadcast_message.color}" - styling << '; ' if broadcast_message.font.present? + style << "background-color: #{broadcast_message.color}" + style << '; ' if broadcast_message.font.present? end if broadcast_message.font.present? - styling << "color: #{broadcast_message.font}" + style << "color: #{broadcast_message.font}" end - styling + style + end + + def broadcast_message_status(broadcast_message) + if broadcast_message.active? + 'Active' + elsif broadcast_message.ended? + 'Expired' + else + 'Pending' + end + end + + def render_broadcast_message(message) + Banzai.render(message, pipeline: :broadcast_message).html_safe end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index ec0e3f409c1..d6c05843743 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -17,7 +17,7 @@ module ButtonHelper def clipboard_button(data = {}) content_tag :button, icon('clipboard'), - class: 'btn btn-xs btn-clipboard', + class: 'btn btn-clipboard', data: data, type: :button end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index d8bee21c82e..8b1575d5e0c 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -12,9 +12,13 @@ module CiStatusHelper ci_label_for_status(ci_commit.status) end - def ci_status_with_icon(status) - content_tag :span, class: "ci-status ci-#{status}" do - ci_icon_for_status(status) + ' '.html_safe + ci_label_for_status(status) + 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}" + if target + link_to content, target, class: klass + else + content_tag :span, content, class: klass end end @@ -42,12 +46,12 @@ module CiStatusHelper icon(icon_name + ' fw') end - def render_ci_status(ci_commit) + def render_ci_status(ci_commit, tooltip_placement: 'auto left') link_to ci_status_icon(ci_commit), ci_status_path(ci_commit), class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", title: "Build #{ci_status_label(ci_commit)}", - data: { toggle: 'tooltip', placement: 'left' } + data: { toggle: 'tooltip', placement: tooltip_placement } end def no_runners_for_project?(project) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 590d20ac7b3..f994c9e6170 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -123,6 +123,37 @@ module CommitsHelper ) end + def revert_commit_link(commit, continue_to_path, btn_class: nil) + return unless current_user + + tooltip = "Revert this #{revert_commit_type(commit)} in a new merge request" + + if can_collaborate_with_project? + content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do + link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}" + end + elsif can?(current_user, :fork_project, @project) + continue_params = { + to: continue_to_path, + notice: edit_in_new_fork_notice + ' Try to revert this commit again.', + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_forks_path(@project.namespace, @project, + namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to 'Revert', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip + end + end + + def revert_commit_type(commit) + if commit.merged_merge_request + 'merge request' + else + 'commit' + end + end + protected # Private: Returns a link to a person. If the person has a matching user and @@ -152,7 +183,7 @@ module CommitsHelper options = { class: "commit-#{options[:source]}-link has_tooltip", - data: { :'original-title' => sanitize(source_email) } + data: { 'original-title'.to_sym => sanitize(source_email) } } if user.nil? @@ -166,7 +197,7 @@ module CommitsHelper link_to( namespace_project_blob_path(project.namespace, project, tree_join(commit_sha, diff.new_path)), - class: 'btn btn-small view-file js-view-file' + class: 'btn view-file js-view-file' ) do raw('View file @') + content_tag(:span, commit_sha[0..6], class: 'commit-short-id') @@ -180,4 +211,15 @@ module CommitsHelper def clean(string) Sanitize.clean(string, remove_contents: true) end + + def limited_commits(commits) + if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE + [ + commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), + commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE + ] + else + [commits, 0] + end + end end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 24134310fc5..ff32e834499 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -1,106 +1,37 @@ module DiffHelper + def mark_inline_diffs(old_line, new_line) + old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs + + marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs) + marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs) + + [marked_old_line, marked_new_line] + end + def diff_view params[:view] == 'parallel' ? 'parallel' : 'inline' end - def allowed_diff_size - if diff_hard_limit_enabled? - Commit::DIFF_HARD_LIMIT_FILES - else - Commit::DIFF_SAFE_FILES - end + def diff_hard_limit_enabled? + params[:force_show_diff].present? end - def allowed_diff_lines + def diff_options + options = { ignore_whitespace_change: params[:w] == '1' } if diff_hard_limit_enabled? - Commit::DIFF_HARD_LIMIT_LINES - else - Commit::DIFF_SAFE_LINES + options.merge!(Commit.max_diff_options) end + options end - def safe_diff_files(diffs) - lines = 0 - safe_files = [] - diffs.first(allowed_diff_size).each do |diff| - lines += diff.diff.lines.count - break if lines > allowed_diff_lines - safe_files << Gitlab::Diff::File.new(diff) - end - safe_files - end - - def diff_hard_limit_enabled? - # Enabling hard limit allows user to see more diff information - if params[:force_show_diff].present? - true - else - false - end + def safe_diff_files(diffs, diff_refs) + diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs) } end def generate_line_code(file_path, line) Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) end - def parallel_diff(diff_file, index) - lines = [] - skip_next = false - - # Building array of lines - # - # [ - # left_type, left_line_number, left_line_content, left_line_code, - # right_line_type, right_line_number, right_line_content, right_line_code - # ] - # - diff_file.diff_lines.each do |line| - - full_line = line.text - type = line.type - line_code = generate_line_code(diff_file.file_path, line) - line_new = line.new_pos - line_old = line.old_pos - - next_line = diff_file.next_line(line.index) - - if next_line - next_line_code = generate_line_code(diff_file.file_path, next_line) - next_type = next_line.type - next_line = next_line.text - end - - if type == 'match' || type.nil? - # line in the right panel is the same as in the left one - line = [type, line_old, full_line, line_code, type, line_new, full_line, line_code] - lines.push(line) - elsif type == 'old' - if next_type == 'new' - # Left side has text removed, right side has text added - line = [type, line_old, full_line, line_code, next_type, line_new, next_line, next_line_code] - lines.push(line) - skip_next = true - elsif next_type == 'old' || next_type.nil? - # Left side has text removed, right side doesn't have any change - # No next line code, no new line number, no new line text - line = [type, line_old, full_line, line_code, next_type, nil, " ", nil] - lines.push(line) - end - elsif type == 'new' - if skip_next - # Change has been already included in previous line so no need to do it again - skip_next = false - next - else - # Change is only on the right side, left side has no change - line = [nil, nil, " ", line_code, type, line_new, full_line, line_code] - lines.push(line) - end - end - end - lines - end - def unfold_bottom_class(bottom) (bottom) ? 'js-unfold-bottom' : '' end @@ -111,14 +42,14 @@ module DiffHelper def diff_line_content(line) if line.blank? - " " + " ".html_safe else line end end def line_comments - @line_comments ||= @line_notes.select(&:active?).group_by(&:line_code) + @line_comments ||= @line_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) end def organize_comments(type_left, type_right, line_code_left, line_code_right) @@ -160,8 +91,7 @@ module DiffHelper def commit_for_diff(diff) if diff.deleted_file - first_commit = @first_commit || @commit - first_commit.parent || @first_commit + @base_commit || @commit.parent || @commit else @commit end @@ -187,7 +117,7 @@ module DiffHelper # Always use HTML to handle case where JSON diff rendered this button params_copy.delete(:format) - link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn') do + link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn'), data: { view_type: name } do title end end diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb new file mode 100644 index 00000000000..74f326e0b83 --- /dev/null +++ b/app/helpers/dropdowns_helper.rb @@ -0,0 +1,100 @@ +module DropdownsHelper + def dropdown_tag(toggle_text, options: {}, &block) + content_tag :div, class: "dropdown" do + data_attr = { toggle: "dropdown" } + + if options.has_key?(:data) + data_attr = options[:data].merge(data_attr) + end + + dropdown_output = dropdown_toggle(toggle_text, data_attr, options) + + dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do + output = "" + + if options.has_key?(:title) + output << dropdown_title(options[:title]) + end + + if options.has_key?(:filter) + output << dropdown_filter(options[:placeholder]) + end + + output << content_tag(:div, class: "dropdown-content") do + capture(&block) if block && !options.has_key?(:footer_content) + end + + if block && options.has_key?(:footer_content) + output << content_tag(:div, class: "dropdown-footer") do + capture(&block) + end + end + + output << dropdown_loading + + output.html_safe + end + + dropdown_output.html_safe + end + end + + def dropdown_toggle(toggle_text, data_attr, options) + content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do + output = content_tag(:span, toggle_text, class: "dropdown-toggle-text") + output << icon('chevron-down') + output.html_safe + end + end + + def dropdown_title(title, back: false) + content_tag :div, class: "dropdown-title" do + title_output = "" + + if back + title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do + icon('arrow-left') + end + end + + title_output << content_tag(:span, title) + + title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do + icon('times') + end + + title_output.html_safe + end + end + + def dropdown_filter(placeholder) + content_tag :div, class: "dropdown-input" do + filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder + filter_output << icon('search') + + filter_output.html_safe + end + end + + def dropdown_content(&block) + content_tag(:div, class: "dropdown-content") do + if block + capture(&block) + end + end + end + + def dropdown_footer(&block) + content_tag(:div, class: "dropdown-footer") do + if block + capture(&block) + end + end + end + + def dropdown_loading + content_tag :div, class: "dropdown-loading" do + icon('spinner spin') + end + end +end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index dde83ff36b5..a67a6b208e2 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -3,7 +3,7 @@ module EventsHelper author = event.author if author - link_to author.name, user_path(author.username) + link_to author.name, user_path(author.username), title: h(author.name) else event.author_name end @@ -27,13 +27,15 @@ module EventsHelper key = key.to_s active = 'active' if @event_filter.active?(key) link_opts = { - class: "event-filter-link btn btn-default #{active}", + class: "event-filter-link", id: "#{key}_event_filter", title: "Filter by #{tooltip.downcase}", } - link_to request.path, link_opts do - content_tag(:span, ' ' + tooltip) + content_tag :li, class: active do + link_to request.path, link_opts do + content_tag(:span, ' ' + tooltip) + end end end @@ -157,7 +159,7 @@ module EventsHelper link_to( namespace_project_commit_path(event.project.namespace, event.project, event.note_commit_id, - anchor: dom_id(event.target)), + anchor: dom_id(event.target), title: h(event.target_title)), class: "commit_short_id" ) do "#{event.note_target_type} #{event.note_short_commit_id}" @@ -165,12 +167,12 @@ module EventsHelper elsif event.note_project_snippet? link_to(namespace_project_snippet_path(event.project.namespace, event.project, - event.note_target)) do - "#{event.note_target_type} ##{truncate event.note_target_id}" + event.note_target), title: h(event.project.name)) do + "#{event.note_target_type} #{truncate event.note_target.to_reference}" end else link_to event_note_target_path(event) do - "#{event.note_target_type} ##{truncate event.note_target_iid}" + "#{event.note_target_type} #{truncate event.note_target.to_reference}" end end else @@ -192,7 +194,7 @@ module EventsHelper end def event_to_atom(xml, event) - if event.proper? + if event.proper?(current_user) xml.entry do event_link = event_feed_url(event) event_title = event_feed_title(event) diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index 0d291f9a87e..337b0aacbb5 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -1,5 +1,5 @@ module ExploreHelper - def explore_projects_filter_path(options={}) + def filter_projects_path(options={}) exist_opts = { sort: params[:sort], scope: params[:scope], @@ -9,9 +9,12 @@ module ExploreHelper } options = exist_opts.merge(options) - - path = explore_projects_path + path = request.path path << "?#{options.to_param}" path end + + def explore_controller? + controller.class.name.split("::").first == "Explore" + end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index ca41657cec1..2f760af02fd 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -50,6 +50,8 @@ module GitlabMarkdownHelper context[:project] ||= @project + text = Banzai.pre_process(text, context) + html = Banzai.render(text, context) context.merge!( @@ -78,6 +80,21 @@ module GitlabMarkdownHelper ) end + def other_markup(file_name, text) + Gitlab::OtherMarkup.render( + file_name, + text, + project: @project, + current_user: (current_user if defined?(current_user)), + + # RelativeLinkFilter + project_wiki: @project_wiki, + requested_path: @path, + ref: @ref, + commit: @commit + ) + end + # Return the first line of +text+, up to +max_chars+, after parsing the line # as Markdown. HTML tags in the parsed output are not counted toward the # +max_chars+ limit. If the length limit falls within a tag's contents, then @@ -91,7 +108,7 @@ module GitlabMarkdownHelper def render_wiki_content(wiki_page) case wiki_page.format when :markdown - markdown(wiki_page.content) + markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki) when :asciidoc asciidoc(wiki_page.content) else diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 5724d3aabec..ab3ef454e1c 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -7,7 +7,16 @@ module IconsHelper # font-awesome-rails gem, but should we ever use a different icon pack in the # future we won't have to change hundreds of method calls. def icon(names, options = {}) - fa_icon(names, options) + options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) + end + + def audit_icon(names, options = {}) + case names + when "standard" + names = "key" + end + + options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) end def spinner(text = nil, visible = false) @@ -37,7 +46,7 @@ module IconsHelper else # Gitlab::VisibilityLevel::PUBLIC 'globe' end - + name << " fw" if fw icon(name) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb new file mode 100644 index 00000000000..81df2094392 --- /dev/null +++ b/app/helpers/issuables_helper.rb @@ -0,0 +1,58 @@ +module IssuablesHelper + + def sidebar_gutter_toggle_icon + sidebar_gutter_collapsed? ? icon('angle-double-left') : icon('angle-double-right') + end + + def sidebar_gutter_collapsed_class + "right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}" + end + + def issuables_count(issuable) + base_issuable_scope(issuable).maximum(:iid) + end + + def next_issuable_for(issuable) + base_issuable_scope(issuable).where('iid > ?', issuable.iid).last + end + + def prev_issuable_for(issuable) + base_issuable_scope(issuable).where('iid < ?', issuable.iid).first + end + + def user_dropdown_label(user_id, default_label) + return "Unassigned" if user_id == "0" + + if @project + member = @project.team.find_member(user_id) + user = member.user if member + else + user = User.find_by(id: user_id) + end + + if user + user.name + else + default_label + end + end + + private + + def sidebar_gutter_collapsed? + cookies[:collapsed_gutter] == 'true' + end + + def base_issuable_scope(issuable) + issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable)) + end + + def issuable_state_scope(issuable) + if issuable.respond_to?(:merged?) && issuable.merged? + :merged + else + issuable.open? ? :opened : :closed + end + end + +end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 80e2741b09a..e00d3204027 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -44,14 +44,14 @@ module IssuesHelper end def bulk_update_milestone_options - milestones = project_active_milestones.to_a + milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a milestones.unshift(Milestone::None) options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) end def milestone_options(object) - milestones = object.project.milestones.active.to_a + milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a milestones.unshift(Milestone::None) options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id) @@ -69,7 +69,7 @@ module IssuesHelper end end - def issue_button_visibility(issue, closed) + def issue_button_visibility(issue, closed) return 'hidden' if issue.closed? == closed end @@ -80,7 +80,7 @@ module IssuesHelper xml.link href: namespace_project_issue_url(issue.project.namespace, issue.project, issue) xml.title truncate(issue.title, length: 80) - xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") + xml.updated issue.created_at.xmlschema xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) xml.author do |author| xml.name issue.author_name @@ -98,14 +98,21 @@ module IssuesHelper end.sort.to_sentence(last_word_connector: ', or ') end + def confidential_icon(issue) + icon('eye-slash') if issue.confidential? + end + def emoji_icon(name, unicode = nil, aliases = []) - unicode ||= Emoji.emoji_filename(name) + unicode ||= Emoji.emoji_filename(name) rescue "" content_tag :div, "", class: "icon emoji-icon emoji-#{unicode}", - "data-emoji" => name, - "data-aliases" => aliases.join(" "), - "data-unicode-name" => unicode + title: name, + data: { + aliases: aliases.join(' '), + emoji: name, + unicode_name: unicode + } end def emoji_author_list(notes, current_user) diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index a2c3d4d2f32..4455dcd0e20 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -7,6 +7,8 @@ module LabelsHelper # project - Project object which will be used as the context for the label's # link. If omitted, defaults to `@project`, or the label's own # project. + # type - The type of item the link will point to (:issue or + # :merge_request). If omitted, defaults to :issue. # block - An optional block that will be passed to `link_to`, forming the # body of the link element. If omitted, defaults to # `render_colored_label`. @@ -23,14 +25,19 @@ module LabelsHelper # # Force the generated link to use a provided project # link_to_label(label, project: Project.last) # + # # Force the generated link to point to merge requests instead of issues + # link_to_label(label, type: :merge_request) + # # # Customize link body with a block # link_to_label(label) { "My Custom Label Text" } # # Returns a String - def link_to_label(label, project: nil, &block) + def link_to_label(label, project: nil, type: :issue, &block) project ||= @project || label.project - link = namespace_project_issues_path(project.namespace, project, - label_name: label.name) + link = send("namespace_project_#{type.to_s.pluralize}_path", + project.namespace, + project, + label_name: label.name) if block_given? link_to link, &block @@ -43,19 +50,25 @@ module LabelsHelper @project.labels.pluck(:title) end - def render_colored_label(label) + def render_colored_label(label, label_suffix = '') label_color = label.color || Label::DEFAULT_COLOR text_color = text_color_for_bg(label_color) # Intentionally not using content_tag here so that this method can be called # by LabelReferenceFilter span = %(<span class="label color-label") + - %( style="background-color: #{label_color}; color: #{text_color}">) + - escape_once(label.name) + '</span>' + %(style="background-color: #{label_color}; color: #{text_color}">) + + %(#{escape_once(label.name)}#{label_suffix}</span>) span.html_safe end + def render_colored_cross_project_label(label) + label_suffix = label.project.name_with_namespace + label_suffix = " <i>in #{escape_once(label_suffix)}</i>" + render_colored_label(label, label_suffix) + end + def suggested_colors [ '#0033CC', @@ -83,7 +96,11 @@ module LabelsHelper end def text_color_for_bg(bg_color) - r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex) + if bg_color.length == 4 + r, g, b = bg_color[1, 4].scan(/./).map { |v| (v * 2).hex } + else + r, g, b = bg_color[1, 7].scan(/.{2}/).map(&:hex) + end if (r + g + b) > 500 '#333333' @@ -107,6 +124,15 @@ module LabelsHelper options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name]) end + def label_subscription_status(label) + label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed' + end + + def label_subscription_toggle_button_text(label) + label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe' + end + # Required for Banzai::Filter::LabelReferenceFilter - module_function :render_colored_label, :text_color_for_bg, :escape_once + module_function :render_colored_label, :render_colored_cross_project_label, + :text_color_for_bg, :escape_once end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index a42cbcff182..92ed0891e92 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -9,10 +9,36 @@ module MilestonesHelper end end + def milestones_label_path(opts = {}) + if @project + namespace_project_issues_path(@project.namespace, @project, opts) + elsif @group + issues_group_path(@group, opts) + else + issues_dashboard_path(opts) + end + end + + def milestones_browse_issuables_path(milestone, type:) + opts = { milestone_title: milestone.title } + + if @project + polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts) + elsif @group + polymorphic_url([type, @group], opts) + else + polymorphic_url([type, :dashboard], opts) + end + end + + def milestone_issues_by_label_count(milestone, label, state:) + milestone.issues.with_label(label.title).send(state).size + end + def milestone_progress_bar(milestone) options = { class: 'progress-bar progress-bar-success', - style: "width: #{milestone.percent_complete}%;" + style: "width: #{milestone.percent_complete(current_user)}%;" } content_tag :div, class: 'progress' do @@ -33,7 +59,18 @@ module MilestonesHelper grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } grouped_milestones.unshift(Milestone::None) grouped_milestones.unshift(Milestone::Any) + grouped_milestones.unshift(Milestone::Upcoming) options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title]) end + + def milestone_remaining_days(milestone) + if milestone.expired? + content_tag(:strong, 'expired') + elsif milestone.due_date + days = milestone.remaining_days + content = content_tag(:strong, days) + content << " #{'day'.pluralize(days)} remaining" + end + end end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index e6fb8670e57..5d86bd490a8 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -19,6 +19,20 @@ module NavHelper end end + def page_gutter_class + if current_path?('merge_requests#show') || + current_path?('merge_requests#diffs') || + current_path?('merge_requests#commits') || + current_path?('merge_requests#builds') || + current_path?('issues#show') + if cookies[:collapsed_gutter] == 'true' + "page-gutter right-sidebar-collapsed" + else + "page-gutter right-sidebar-expanded" + end + end + end + def nav_header_class if nav_menu_collapsed? "header-collapsed" diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 5f0c921413a..53c543c28c5 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -67,7 +67,7 @@ module NotesHelper line_type: line_type } - button_tag class: 'btn reply-btn js-discussion-reply-button', + button_tag class: 'btn btn-nr reply-btn js-discussion-reply-button', data: data, title: 'Add a reply' do link_text = icon('comment') link_text << ' Reply' diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 791cb9e50bd..82f805fa444 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -27,35 +27,20 @@ module PageLayoutHelper # # Returns an HTML-safe String. def page_description(description = nil) - @page_description ||= page_description_default - if description.present? @page_description = description.squish - else + elsif @page_description.present? sanitize(@page_description, tags: []).truncate_words(30) end end - # Default value for page_description when one hasn't been defined manually by - # a view - def page_description_default - if @project - @project.description || brand_title - else - brand_title - end - end - def page_image default = image_url('gitlab_logo.png') - if @project - @project.avatar_url || default - elsif @user - avatar_icon(@user) - else - default - end + subject = @project || @user || @group + + image = subject.avatar_url if subject.present? + image || default end # Define or get attributes to be used as Twitter card metadata diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 77ba612548a..b5acb80b720 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -8,7 +8,7 @@ module ProjectsHelper end def link_to_project(project) - link_to [project.namespace.becomes(Namespace), project] do + link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do title = content_tag(:span, project.name, class: 'project-name') if project.namespace @@ -20,6 +20,12 @@ module ProjectsHelper end end + def link_to_member_avatar(author, opts = {}) + default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" } + opts = default_opts.merge(opts) + image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] + end + def link_to_member(project, author, opts = {}) default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" } opts = default_opts.merge(opts) @@ -32,15 +38,19 @@ module ProjectsHelper author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] # Build name span tag - author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name] + if opts[:by_username] + author_html << content_tag(:span, sanitize("@#{author.username}"), class: opts[:author_class]) if opts[:name] + else + author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name] + end author_html = author_html.html_safe if opts[:name] - link_to(author_html, user_path(author), class: "author_link").html_safe + link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe else title = opts[:title].sub(":name", sanitize(author.name)) - link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => title, container: 'body' } ).html_safe + link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe end end @@ -53,14 +63,23 @@ module ProjectsHelper link_to(simple_sanitize(owner.name), user_path(owner)) end - project_link = link_to(simple_sanitize(project.name), project_path(project)) + project_link = link_to project_path(project), { class: "project-item-select-holder" } do + link_output = simple_sanitize(project.name) + + 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 + 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 - content_tag :span do - full_title - end + full_title end def remove_project_message(project) @@ -83,10 +102,6 @@ module ProjectsHelper project_nav_tabs.include? name end - def project_active_milestones - @project.milestones.active.order("due_date, title ASC") - end - def project_for_deploy_key(deploy_key) if deploy_key.projects.include?(@project) @project @@ -116,7 +131,7 @@ module ProjectsHelper private def get_project_nav_tabs(project, current_user) - nav_tabs = [:home] + nav_tabs = [:home, :forks] if !project.empty_repo? && can?(current_user, :download_code, project) nav_tabs << [:files, :commits, :network, :graphs] @@ -126,7 +141,7 @@ module ProjectsHelper nav_tabs << :merge_requests end - if project.builds_enabled? && can?(current_user, :read_build, project) + if can?(current_user, :read_build, project) nav_tabs << :builds end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index a6ee6880247..494dad0b41e 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -40,7 +40,7 @@ module SearchHelper { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, - { label: "help: Web Hooks Help", url: help_page_path("web_hooks", "web_hooks") }, + { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, { label: "help: Workflow Help", url: help_page_path("workflow", "README") }, ] end @@ -70,7 +70,7 @@ module SearchHelper # Autocomplete results for the current user's groups def groups_autocomplete(term, limit = 5) - GroupsFinder.new.execute(current_user).search(term).limit(limit).map do |group| + current_user.authorized_groups.search(term).limit(limit).map do |group| { label: "group: #{search_result_sanitize(group.name)}", url: group_path(group) @@ -80,7 +80,7 @@ module SearchHelper # Autocomplete results for the current user's projects def projects_autocomplete(term, limit = 5) - ProjectsFinder.new.execute(current_user).search_by_title(term). + current_user.authorized_projects.search_by_title(term). sorted_by_stars.non_archived.limit(limit).map do |p| { label: "project: #{search_result_sanitize(p.name_with_namespace)}", diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 906cb12cd48..0a5a8eb5aee 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -1,14 +1,4 @@ module SnippetsHelper - def lifetime_select_options - options = [ - ['forever', nil], - ['1 day', "#{Date.current + 1.day}"], - ['1 week', "#{Date.current + 1.week}"], - ['1 month', "#{Date.current + 1.month}"] - ] - options_for_select(options) - end - def reliable_snippet_path(snippet) if snippet.project_id? namespace_project_snippet_path(snippet.project.namespace, @@ -17,4 +7,79 @@ module SnippetsHelper snippet_path(snippet) end end + + # Get an array of line numbers surrounding a matching + # line, bounded by min/max. + # + # @returns Array of line numbers + def bounded_line_numbers(line, min, max, surrounding_lines) + lower = line - surrounding_lines > min ? line - surrounding_lines : min + upper = line + surrounding_lines < max ? line + surrounding_lines : max + (lower..upper).to_a + end + + # Returns a sorted set of lines to be included in a snippet preview. + # This ensures matching adjacent lines do not display duplicated + # surrounding code. + # + # @returns Array, unique and sorted. + def matching_lines(lined_content, surrounding_lines, query) + used_lines = [] + lined_content.each_with_index do |line, line_number| + used_lines.concat bounded_line_numbers( + line_number, + 0, + lined_content.size, + surrounding_lines + ) if line.include?(query) + end + + used_lines.uniq.sort + end + + # 'Chunkify' entire snippet. Splits the snippet data into matching lines + + # surrounding_lines() worth of unmatching lines. + # + # @returns a hash with {snippet_object, snippet_chunks:{data,start_line}} + def chunk_snippet(snippet, query, surrounding_lines = 3) + lined_content = snippet.content.split("\n") + used_lines = matching_lines(lined_content, surrounding_lines, query) + + snippet_chunk = [] + snippet_chunks = [] + snippet_start_line = 0 + last_line = -1 + + # Go through each used line, and add consecutive lines as a single chunk + # to the snippet chunk array. + used_lines.each do |line_number| + if last_line < 0 + # Start a new chunk. + snippet_start_line = line_number + snippet_chunk << lined_content[line_number] + elsif last_line == line_number - 1 + # Consecutive line, continue chunk. + snippet_chunk << lined_content[line_number] + else + # Non-consecutive line, add chunk to chunk array. + snippet_chunks << { + data: snippet_chunk.join("\n"), + start_line: snippet_start_line + 1 + } + + # Start a new chunk. + snippet_chunk = [lined_content[line_number]] + snippet_start_line = line_number + end + last_line = line_number + end + # Add final chunk to chunk array + snippet_chunks << { + data: snippet_chunk.join("\n"), + start_line: snippet_start_line + 1 + } + + # Return snippet with chunk array + { snippet_object: snippet, snippet_chunks: snippet_chunks } + end end diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index bb12d43f397..2f2d2721d6d 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -11,6 +11,18 @@ module SortingHelper sort_value_largest_repo => sort_title_largest_repo, sort_value_recently_signin => sort_title_recently_signin, sort_value_oldest_signin => sort_title_oldest_signin, + sort_value_downvotes => sort_title_downvotes, + sort_value_upvotes => sort_title_upvotes + } + end + + def projects_sort_options_hash + { + sort_value_name => sort_title_name, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_recently_created => sort_title_recently_created, + sort_value_oldest_created => sort_title_oldest_created, } end @@ -19,7 +31,7 @@ module SortingHelper end def sort_title_recently_updated - 'Recently updated' + 'Last updated' end def sort_title_oldest_created @@ -27,7 +39,7 @@ module SortingHelper end def sort_title_recently_created - 'Recently created' + 'Last created' end def sort_title_milestone_soon @@ -54,6 +66,14 @@ module SortingHelper 'Oldest sign in' end + def sort_title_downvotes + 'Least popular' + end + + def sort_title_upvotes + 'Most popular' + end + def sort_value_oldest_updated 'updated_asc' end @@ -63,11 +83,11 @@ module SortingHelper end def sort_value_oldest_created - 'created_asc' + 'id_asc' end def sort_value_recently_created - 'created_desc' + 'id_desc' end def sort_value_milestone_soon @@ -93,4 +113,12 @@ module SortingHelper def sort_value_oldest_signin 'oldest_sign_in' end + + def sort_value_downvotes + 'downvotes_desc' + end + + def sort_value_upvotes + 'upvotes_desc' + end end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb new file mode 100644 index 00000000000..07ddc691d85 --- /dev/null +++ b/app/helpers/todos_helper.rb @@ -0,0 +1,87 @@ +module TodosHelper + def todos_pending_count + current_user.todos.pending.count + end + + def todos_done_count + current_user.todos.done.count + end + + def todo_action_name(todo) + case todo.action + when Todo::ASSIGNED then 'assigned you' + when Todo::MENTIONED then 'mentioned you on' + end + end + + def todo_target_link(todo) + target = todo.target_type.titleize.downcase + link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo), { title: h(todo.target.title) } + end + + def todo_target_path(todo) + anchor = dom_id(todo.note) if todo.note.present? + + polymorphic_path([todo.project.namespace.becomes(Namespace), + todo.project, todo.target], anchor: anchor) + end + + def todos_filter_params + { + state: params[:state], + project_id: params[:project_id], + author_id: params[:author_id], + type: params[:type], + action_id: params[:action_id], + } + end + + def todos_filter_path(options = {}) + without = options.delete(:without) + + options = todos_filter_params.merge(options) + + if without.present? + without.each do |key| + options.delete(key) + end + end + + path = request.path + path << "?#{options.to_param}" + path + end + + def todo_actions_options + actions = [ + OpenStruct.new(id: '', title: 'Any Action'), + OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'), + OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned') + ] + + options_from_collection_for_select(actions, 'id', 'title', params[:action_id]) + end + + def todo_projects_options + projects = current_user.authorized_projects.sorted_by_activity.non_archived + projects = projects.includes(:namespace) + + projects = projects.map do |project| + OpenStruct.new(id: project.id, title: project.name_with_namespace) + end + + projects.unshift(OpenStruct.new(id: '', title: 'Any Project')) + + options_from_collection_for_select(projects, 'id', 'title', params[:project_id]) + end + + def todo_types_options + types = [ + OpenStruct.new(title: 'Any Type', name: ''), + OpenStruct.new(title: 'Issue', name: 'Issue'), + OpenStruct.new(title: 'Merge Request', name: 'MergeRequest') + ] + + options_from_collection_for_select(types, 'name', 'title', params[:type]) + end +end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 2ad7c80dae0..4920ca5af6e 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -56,8 +56,7 @@ module TreeHelper return false unless on_top_of_branch?(project, ref) - can?(current_user, :push_code, project) || - (current_user && current_user.already_forked?(project)) + can_collaborate_with_project?(project) end def tree_edit_branch(project = @project, ref = @ref) |