Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									2b339d4e89
								
							
						
					
					
						commit
						cd4cb29b2c
					
				|  | @ -1,5 +1,16 @@ | ||||||
| Please view this file on the master branch, on stable branches it's out of date. | Please view this file on the master branch, on stable branches it's out of date. | ||||||
| 
 | 
 | ||||||
|  | ## 12.5.3 | ||||||
|  | 
 | ||||||
|  | ### Performance (1 change) | ||||||
|  | 
 | ||||||
|  | - Geo - Improve query performance to determine job artifacts to sync when selective sync is enabled. !19583 | ||||||
|  | 
 | ||||||
|  | ### Other (1 change) | ||||||
|  | 
 | ||||||
|  | - Geo - Does not schedule duplicated jobs while backfilling uploads, LFS objects and job artifacts. !20324 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| ## 12.5.1 | ## 12.5.1 | ||||||
| 
 | 
 | ||||||
| ### Security (6 changes) | ### Security (6 changes) | ||||||
|  |  | ||||||
|  | @ -16,13 +16,6 @@ entry. | ||||||
| - Flatten exception details in API and controller logs. !20434 | - Flatten exception details in API and controller logs. !20434 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| ## 12.5.2 |  | ||||||
| 
 |  | ||||||
| ### Security (1 change) |  | ||||||
| 
 |  | ||||||
| - Fix 500 error caused by invalid byte sequences in links. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ## 12.5.1 | ## 12.5.1 | ||||||
| 
 | 
 | ||||||
| ### Security (11 changes) | ### Security (11 changes) | ||||||
|  |  | ||||||
|  | @ -1 +1 @@ | ||||||
| 1.72.1 | 1.75.0 | ||||||
|  |  | ||||||
|  | @ -3,11 +3,44 @@ import 'at.js'; | ||||||
| import _ from 'underscore'; | import _ from 'underscore'; | ||||||
| import glRegexp from './lib/utils/regexp'; | import glRegexp from './lib/utils/regexp'; | ||||||
| import AjaxCache from './lib/utils/ajax_cache'; | import AjaxCache from './lib/utils/ajax_cache'; | ||||||
|  | import { spriteIcon } from './lib/utils/common_utils'; | ||||||
| 
 | 
 | ||||||
| function sanitize(str) { | function sanitize(str) { | ||||||
|   return str.replace(/<(?:.|\n)*?>/gm, ''); |   return str.replace(/<(?:.|\n)*?>/gm, ''); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function membersBeforeSave(members) { | ||||||
|  |   return _.map(members, member => { | ||||||
|  |     const GROUP_TYPE = 'Group'; | ||||||
|  | 
 | ||||||
|  |     let title = ''; | ||||||
|  |     if (member.username == null) { | ||||||
|  |       return member; | ||||||
|  |     } | ||||||
|  |     title = member.name; | ||||||
|  |     if (member.count && !member.mentionsDisabled) { | ||||||
|  |       title += ` (${member.count})`; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const autoCompleteAvatar = member.avatar_url || member.username.charAt(0).toUpperCase(); | ||||||
|  | 
 | ||||||
|  |     const rectAvatarClass = member.type === GROUP_TYPE ? 'rect-avatar' : ''; | ||||||
|  |     const imgAvatar = `<img src="${member.avatar_url}" alt="${member.username}" class="avatar ${rectAvatarClass} avatar-inline center s26"/>`; | ||||||
|  |     const txtAvatar = `<div class="avatar ${rectAvatarClass} center avatar-inline s26">${autoCompleteAvatar}</div>`; | ||||||
|  |     const avatarIcon = member.mentionsDisabled | ||||||
|  |       ? spriteIcon('notifications-off', 's16 vertical-align-middle prepend-left-5') | ||||||
|  |       : ''; | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       username: member.username, | ||||||
|  |       avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, | ||||||
|  |       title: sanitize(title), | ||||||
|  |       search: sanitize(`${member.username} ${member.name}`), | ||||||
|  |       icon: avatarIcon, | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export const defaultAutocompleteConfig = { | export const defaultAutocompleteConfig = { | ||||||
|   emojis: true, |   emojis: true, | ||||||
|   members: true, |   members: true, | ||||||
|  | @ -167,12 +200,13 @@ class GfmAutoComplete { | ||||||
|       alias: 'users', |       alias: 'users', | ||||||
|       displayTpl(value) { |       displayTpl(value) { | ||||||
|         let tmpl = GfmAutoComplete.Loading.template; |         let tmpl = GfmAutoComplete.Loading.template; | ||||||
|         const { avatarTag, username, title } = value; |         const { avatarTag, username, title, icon } = value; | ||||||
|         if (username != null) { |         if (username != null) { | ||||||
|           tmpl = GfmAutoComplete.Members.templateFunction({ |           tmpl = GfmAutoComplete.Members.templateFunction({ | ||||||
|             avatarTag, |             avatarTag, | ||||||
|             username, |             username, | ||||||
|             title, |             title, | ||||||
|  |             icon, | ||||||
|           }); |           }); | ||||||
|         } |         } | ||||||
|         return tmpl; |         return tmpl; | ||||||
|  | @ -185,33 +219,7 @@ class GfmAutoComplete { | ||||||
|       data: GfmAutoComplete.defaultLoadingData, |       data: GfmAutoComplete.defaultLoadingData, | ||||||
|       callbacks: { |       callbacks: { | ||||||
|         ...this.getDefaultCallbacks(), |         ...this.getDefaultCallbacks(), | ||||||
|         beforeSave(members) { |         beforeSave: membersBeforeSave, | ||||||
|           return $.map(members, m => { |  | ||||||
|             let title = ''; |  | ||||||
|             if (m.username == null) { |  | ||||||
|               return m; |  | ||||||
|             } |  | ||||||
|             title = m.name; |  | ||||||
|             if (m.count) { |  | ||||||
|               title += ` (${m.count})`; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             const GROUP_TYPE = 'Group'; |  | ||||||
| 
 |  | ||||||
|             const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); |  | ||||||
| 
 |  | ||||||
|             const rectAvatarClass = m.type === GROUP_TYPE ? 'rect-avatar' : ''; |  | ||||||
|             const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar ${rectAvatarClass} avatar-inline center s26"/>`; |  | ||||||
|             const txtAvatar = `<div class="avatar ${rectAvatarClass} center avatar-inline s26">${autoCompleteAvatar}</div>`; |  | ||||||
| 
 |  | ||||||
|             return { |  | ||||||
|               username: m.username, |  | ||||||
|               avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, |  | ||||||
|               title: sanitize(title), |  | ||||||
|               search: sanitize(`${m.username} ${m.name}`), |  | ||||||
|             }; |  | ||||||
|           }); |  | ||||||
|         }, |  | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | @ -624,8 +632,8 @@ GfmAutoComplete.Emoji = { | ||||||
| }; | }; | ||||||
| // Team Members
 | // Team Members
 | ||||||
| GfmAutoComplete.Members = { | GfmAutoComplete.Members = { | ||||||
|   templateFunction({ avatarTag, username, title }) { |   templateFunction({ avatarTag, username, title, icon }) { | ||||||
|     return `<li>${avatarTag} ${username} <small>${_.escape(title)}</small></li>`; |     return `<li>${avatarTag} ${username} <small>${_.escape(title)}</small> ${icon}</li>`; | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| GfmAutoComplete.Labels = { | GfmAutoComplete.Labels = { | ||||||
|  |  | ||||||
|  | @ -182,6 +182,10 @@ const bindEvents = () => { | ||||||
|         text: s__('ProjectTemplates|Netlify/Hexo'), |         text: s__('ProjectTemplates|Netlify/Hexo'), | ||||||
|         icon: '.template-option .icon-netlify', |         icon: '.template-option .icon-netlify', | ||||||
|       }, |       }, | ||||||
|  |       salesforcedx: { | ||||||
|  |         text: s__('ProjectTemplates|SalesforceDX'), | ||||||
|  |         icon: '.template-option svg.icon-gitlab', | ||||||
|  |       }, | ||||||
|       serverless_framework: { |       serverless_framework: { | ||||||
|         text: s__('ProjectTemplates|Serverless Framework/JS'), |         text: s__('ProjectTemplates|Serverless Framework/JS'), | ||||||
|         icon: '.template-option .icon-serverless_framework', |         icon: '.template-option .icon-serverless_framework', | ||||||
|  |  | ||||||
|  | @ -181,6 +181,7 @@ class GroupsController < Groups::ApplicationController | ||||||
|       :avatar, |       :avatar, | ||||||
|       :description, |       :description, | ||||||
|       :emails_disabled, |       :emails_disabled, | ||||||
|  |       :mentions_disabled, | ||||||
|       :lfs_enabled, |       :lfs_enabled, | ||||||
|       :name, |       :name, | ||||||
|       :path, |       :path, | ||||||
|  |  | ||||||
|  | @ -55,7 +55,8 @@ module Users | ||||||
|         username: group.full_path, |         username: group.full_path, | ||||||
|         name: group.full_name, |         name: group.full_name, | ||||||
|         avatar_url: group.avatar_url, |         avatar_url: group.avatar_url, | ||||||
|         count: group_counts.fetch(group.id, 0) |         count: group_counts.fetch(group.id, 0), | ||||||
|  |         mentionsDisabled: group.mentions_disabled | ||||||
|       } |       } | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -25,3 +25,5 @@ module Issues | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | 
 | ||||||
|  | Issues::DuplicateService.prepend_if_ee('EE::Issues::DuplicateService') | ||||||
|  |  | ||||||
|  | @ -23,6 +23,13 @@ | ||||||
|           %span.d-block= s_('GroupSettings|Disable email notifications') |           %span.d-block= s_('GroupSettings|Disable email notifications') | ||||||
|           %span.text-muted= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.') |           %span.text-muted= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.') | ||||||
| 
 | 
 | ||||||
|  |     .form-group.append-bottom-default | ||||||
|  |       .form-check | ||||||
|  |         = f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'form-check-input' | ||||||
|  |         = f.label :mentions_disabled, class: 'form-check-label' do | ||||||
|  |           %span.d-block= s_('GroupSettings|Disable group mentions') | ||||||
|  |           %span.text-muted= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.') | ||||||
|  | 
 | ||||||
|     = render_if_exists 'groups/settings/ip_restriction', f: f, group: @group |     = render_if_exists 'groups/settings/ip_restriction', f: f, group: @group | ||||||
|     = render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group |     = render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group | ||||||
|     = render 'groups/settings/lfs', f: f |     = render 'groups/settings/lfs', f: f | ||||||
|  |  | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Relate issues when they are marked as duplicated | ||||||
|  | merge_request: 20161 | ||||||
|  | author: minghuan lei | ||||||
|  | type: added | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Allow groups to disable mentioning their members, if the group is mentioned | ||||||
|  | merge_request: 20184 | ||||||
|  | author: Fabio Huser | ||||||
|  | type: added | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Upgrade to Gitaly v1.75.0 | ||||||
|  | merge_request: 21045 | ||||||
|  | author: | ||||||
|  | type: changed | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | --- | ||||||
|  | title: Add SalesforceDX project template | ||||||
|  | merge_request: 20831 | ||||||
|  | author: | ||||||
|  | type: added | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class AddMentionsDisabledToNamespaces < ActiveRecord::Migration[5.2] | ||||||
|  |   DOWNTIME = false | ||||||
|  | 
 | ||||||
|  |   def change | ||||||
|  |     add_column :namespaces, :mentions_disabled, :boolean | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -349,6 +349,7 @@ ActiveRecord::Schema.define(version: 2019_11_25_140458) do | ||||||
|     t.boolean "sourcegraph_enabled", default: false, null: false |     t.boolean "sourcegraph_enabled", default: false, null: false | ||||||
|     t.string "sourcegraph_url", limit: 255 |     t.string "sourcegraph_url", limit: 255 | ||||||
|     t.boolean "sourcegraph_public_only", default: true, null: false |     t.boolean "sourcegraph_public_only", default: true, null: false | ||||||
|  |     t.bigint "snippet_size_limit", default: 52428800, null: false | ||||||
|     t.text "encrypted_akismet_api_key" |     t.text "encrypted_akismet_api_key" | ||||||
|     t.string "encrypted_akismet_api_key_iv", limit: 255 |     t.string "encrypted_akismet_api_key_iv", limit: 255 | ||||||
|     t.text "encrypted_elasticsearch_aws_secret_access_key" |     t.text "encrypted_elasticsearch_aws_secret_access_key" | ||||||
|  | @ -361,7 +362,6 @@ ActiveRecord::Schema.define(version: 2019_11_25_140458) do | ||||||
|     t.string "encrypted_slack_app_secret_iv", limit: 255 |     t.string "encrypted_slack_app_secret_iv", limit: 255 | ||||||
|     t.text "encrypted_slack_app_verification_token" |     t.text "encrypted_slack_app_verification_token" | ||||||
|     t.string "encrypted_slack_app_verification_token_iv", limit: 255 |     t.string "encrypted_slack_app_verification_token_iv", limit: 255 | ||||||
|     t.bigint "snippet_size_limit", default: 52428800, null: false |  | ||||||
|     t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id" |     t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id" | ||||||
|     t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id" |     t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id" | ||||||
|     t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id" |     t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id" | ||||||
|  | @ -2603,6 +2603,7 @@ ActiveRecord::Schema.define(version: 2019_11_25_140458) do | ||||||
|     t.boolean "emails_disabled" |     t.boolean "emails_disabled" | ||||||
|     t.integer "max_pages_size" |     t.integer "max_pages_size" | ||||||
|     t.integer "max_artifacts_size" |     t.integer "max_artifacts_size" | ||||||
|  |     t.boolean "mentions_disabled" | ||||||
|     t.index ["created_at"], name: "index_namespaces_on_created_at" |     t.index ["created_at"], name: "index_namespaces_on_created_at" | ||||||
|     t.index ["custom_project_templates_group_id", "type"], name: "index_namespaces_on_custom_project_templates_group_id_and_type", where: "(custom_project_templates_group_id IS NOT NULL)" |     t.index ["custom_project_templates_group_id", "type"], name: "index_namespaces_on_custom_project_templates_group_id_and_type", where: "(custom_project_templates_group_id IS NOT NULL)" | ||||||
|     t.index ["file_template_project_id"], name: "index_namespaces_on_file_template_project_id" |     t.index ["file_template_project_id"], name: "index_namespaces_on_file_template_project_id" | ||||||
|  |  | ||||||
|  | @ -431,6 +431,23 @@ To enable this feature: | ||||||
| 1. Expand the **Permissions, LFS, 2FA** section, and select **Disable email notifications**. | 1. Expand the **Permissions, LFS, 2FA** section, and select **Disable email notifications**. | ||||||
| 1. Click **Save changes**. | 1. Click **Save changes**. | ||||||
| 
 | 
 | ||||||
|  | #### Disabling group mentions | ||||||
|  | 
 | ||||||
|  | > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/21301) in GitLab 12.6. | ||||||
|  | 
 | ||||||
|  | You can prevent users from being added to a conversation and getting notified when | ||||||
|  | anyone mentions a group in which those users are members. | ||||||
|  | 
 | ||||||
|  | Groups with disabled mentions are visualized accordingly in the autocompletion dropdown. | ||||||
|  | 
 | ||||||
|  | This is particularly helpful for groups with a large number of users. | ||||||
|  | 
 | ||||||
|  | To enable this feature: | ||||||
|  | 
 | ||||||
|  | 1. Navigate to the group's **Settings > General** page. | ||||||
|  | 1. Expand the **Permissions, LFS, 2FA** section, and select **Disable group mentions**. | ||||||
|  | 1. Click **Save changes**. | ||||||
|  | 
 | ||||||
| ### Advanced settings | ### Advanced settings | ||||||
| 
 | 
 | ||||||
| - **Projects**: View all projects within that group, add members to each project, | - **Projects**: View all projects within that group, add members to each project, | ||||||
|  |  | ||||||
|  | @ -60,7 +60,7 @@ The following quick actions are applicable to descriptions, discussions and thre | ||||||
| | `/remove_epic`                        | ✓     |               |      | Remove from epic **(ULTIMATE)** | | | `/remove_epic`                        | ✓     |               |      | Remove from epic **(ULTIMATE)** | | ||||||
| | `/promote`                            | ✓     |               |      | Promote issue to epic **(ULTIMATE)** | | | `/promote`                            | ✓     |               |      | Promote issue to epic **(ULTIMATE)** | | ||||||
| | `/confidential`                       | ✓     |               |      | Make confidential | | | `/confidential`                       | ✓     |               |      | Make confidential | | ||||||
| | `/duplicate <#issue>`                 | ✓     |               |      | Mark this issue as a duplicate of another issue | | | `/duplicate <#issue>`                 | ✓     |               |      | Mark this issue as a duplicate of another issue and relate them for **(STARTER)** | | ||||||
| | `/create_merge_request <branch name>` | ✓     |               |      | Create a new merge request starting from the current issue | | | `/create_merge_request <branch name>` | ✓     |               |      | Create a new merge request starting from the current issue | | ||||||
| | `/relate #issue1 #issue2`             | ✓     |               |      | Mark issues as related **(STARTER)** | | | `/relate #issue1 #issue2`             | ✓     |               |      | Mark issues as related **(STARTER)** | | ||||||
| | `/move <path/to/project>`             | ✓     |               |      | Move this issue to another project | | | `/move <path/to/project>`             | ✓     |               |      | Move this issue to another project | | ||||||
|  |  | ||||||
|  | @ -97,7 +97,9 @@ module Banzai | ||||||
|       def find_users_for_groups(ids) |       def find_users_for_groups(ids) | ||||||
|         return [] if ids.empty? |         return [] if ids.empty? | ||||||
| 
 | 
 | ||||||
|         User.joins(:group_members).where(members: { source_id: ids }).to_a |         User.joins(:group_members).where(members: { | ||||||
|  |           source_id: Namespace.where(id: ids).where('mentions_disabled IS NOT TRUE').select(:id) | ||||||
|  |         }).to_a | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def find_users_for_projects(ids) |       def find_users_for_projects(ids) | ||||||
|  |  | ||||||
|  | @ -54,6 +54,7 @@ module Gitlab | ||||||
|       ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'), |       ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'), | ||||||
|       ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'), |       ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'), | ||||||
|       ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'), |       ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'), | ||||||
|  |       ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'), | ||||||
|       ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg') |       ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg') | ||||||
|     ].freeze |     ].freeze | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -29,14 +29,14 @@ module Gitlab | ||||||
|       def event(category, action, label: nil, property: nil, value: nil, context: nil) |       def event(category, action, label: nil, property: nil, value: nil, context: nil) | ||||||
|         return unless enabled? |         return unless enabled? | ||||||
| 
 | 
 | ||||||
|         snowplow.track_struct_event(category, action, label, property, value, context, Time.now.to_i) |         snowplow.track_struct_event(category, action, label, property, value, context, (Time.now.to_f * 1000).to_i) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def self_describing_event(schema_url, event_data_json, context: nil) |       def self_describing_event(schema_url, event_data_json, context: nil) | ||||||
|         return unless enabled? |         return unless enabled? | ||||||
| 
 | 
 | ||||||
|         event_json = SnowplowTracker::SelfDescribingJson.new(schema_url, event_data_json) |         event_json = SnowplowTracker::SelfDescribingJson.new(schema_url, event_data_json) | ||||||
|         snowplow.track_self_describing_event(event_json, context, Time.now.to_i) |         snowplow.track_self_describing_event(event_json, context, (Time.now.to_f * 1000).to_i) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       def snowplow_options(group) |       def snowplow_options(group) | ||||||
|  |  | ||||||
|  | @ -696,6 +696,9 @@ msgstr "" | ||||||
| msgid "A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." | msgid "A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "A project boilerplate for Salesforce App development with Salesforce Developer tools." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}." | msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -8860,6 +8863,9 @@ msgstr "" | ||||||
| msgid "GroupSettings|Disable email notifications" | msgid "GroupSettings|Disable email notifications" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "GroupSettings|Disable group mentions" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility." | msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -8908,6 +8914,9 @@ msgstr "" | ||||||
| msgid "GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects." | msgid "GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "GroupSettings|This setting will prevent group members from being notified if the group is mentioned." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "GroupSettings|Transfer group" | msgid "GroupSettings|Transfer group" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -13721,6 +13730,9 @@ msgstr "" | ||||||
| msgid "ProjectTemplates|Ruby on Rails" | msgid "ProjectTemplates|Ruby on Rails" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "ProjectTemplates|SalesforceDX" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "ProjectTemplates|Serverless Framework/JS" | msgid "ProjectTemplates|Serverless Framework/JS" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| /* eslint no-param-reassign: "off" */ | /* eslint no-param-reassign: "off" */ | ||||||
| 
 | 
 | ||||||
| import $ from 'jquery'; | import $ from 'jquery'; | ||||||
|  | import { membersBeforeSave } from '~/gfm_auto_complete'; | ||||||
| import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; | import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; | ||||||
| 
 | 
 | ||||||
| import 'jquery.caret'; | import 'jquery.caret'; | ||||||
|  | @ -262,6 +263,79 @@ describe('GfmAutoComplete', () => { | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   describe('membersBeforeSave', () => { | ||||||
|  |     const mockGroup = { | ||||||
|  |       username: 'my-group', | ||||||
|  |       name: 'My Group', | ||||||
|  |       count: 2, | ||||||
|  |       avatar_url: './group.jpg', | ||||||
|  |       type: 'Group', | ||||||
|  |       mentionsDisabled: false, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     it('should return the original object when username is null', () => { | ||||||
|  |       expect(membersBeforeSave([{ ...mockGroup, username: null }])).toEqual([ | ||||||
|  |         { ...mockGroup, username: null }, | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should set the text avatar if avatar_url is null', () => { | ||||||
|  |       expect(membersBeforeSave([{ ...mockGroup, avatar_url: null }])).toEqual([ | ||||||
|  |         { | ||||||
|  |           username: 'my-group', | ||||||
|  |           avatarTag: '<div class="avatar rect-avatar center avatar-inline s26">M</div>', | ||||||
|  |           title: 'My Group (2)', | ||||||
|  |           search: 'my-group My Group', | ||||||
|  |           icon: '', | ||||||
|  |         }, | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should set the image avatar if avatar_url is given', () => { | ||||||
|  |       expect(membersBeforeSave([mockGroup])).toEqual([ | ||||||
|  |         { | ||||||
|  |           username: 'my-group', | ||||||
|  |           avatarTag: | ||||||
|  |             '<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline center s26"/>', | ||||||
|  |           title: 'My Group (2)', | ||||||
|  |           search: 'my-group My Group', | ||||||
|  |           icon: '', | ||||||
|  |         }, | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should set mentions disabled icon if mentionsDisabled is set', () => { | ||||||
|  |       expect(membersBeforeSave([{ ...mockGroup, mentionsDisabled: true }])).toEqual([ | ||||||
|  |         { | ||||||
|  |           username: 'my-group', | ||||||
|  |           avatarTag: | ||||||
|  |             '<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline center s26"/>', | ||||||
|  |           title: 'My Group', | ||||||
|  |           search: 'my-group My Group', | ||||||
|  |           icon: | ||||||
|  |             '<svg class="s16 vertical-align-middle prepend-left-5"><use xlink:href="undefined#notifications-off" /></svg>', | ||||||
|  |         }, | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should set the right image classes for User type members', () => { | ||||||
|  |       expect( | ||||||
|  |         membersBeforeSave([ | ||||||
|  |           { username: 'my-user', name: 'My User', avatar_url: './users.jpg', type: 'User' }, | ||||||
|  |         ]), | ||||||
|  |       ).toEqual([ | ||||||
|  |         { | ||||||
|  |           username: 'my-user', | ||||||
|  |           avatarTag: | ||||||
|  |             '<img src="./users.jpg" alt="my-user" class="avatar  avatar-inline center s26"/>', | ||||||
|  |           title: 'My User', | ||||||
|  |           search: 'my-user My User', | ||||||
|  |           icon: '', | ||||||
|  |         }, | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   describe('Issues.insertTemplateFunction', () => { |   describe('Issues.insertTemplateFunction', () => { | ||||||
|     it('should return default template', () => { |     it('should return default template', () => { | ||||||
|       expect(GfmAutoComplete.Issues.insertTemplateFunction({ id: 5, title: 'Some Issue' })).toBe( |       expect(GfmAutoComplete.Issues.insertTemplateFunction({ id: 5, title: 'Some Issue' })).toBe( | ||||||
|  | @ -298,6 +372,41 @@ describe('GfmAutoComplete', () => { | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |   describe('Members.templateFunction', () => { | ||||||
|  |     it('should return html with avatarTag and username', () => { | ||||||
|  |       expect( | ||||||
|  |         GfmAutoComplete.Members.templateFunction({ | ||||||
|  |           avatarTag: 'IMG', | ||||||
|  |           username: 'my-group', | ||||||
|  |           title: '', | ||||||
|  |           icon: '', | ||||||
|  |         }), | ||||||
|  |       ).toBe('<li>IMG my-group <small></small> </li>'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should add icon if icon is set', () => { | ||||||
|  |       expect( | ||||||
|  |         GfmAutoComplete.Members.templateFunction({ | ||||||
|  |           avatarTag: 'IMG', | ||||||
|  |           username: 'my-group', | ||||||
|  |           title: '', | ||||||
|  |           icon: '<i class="icon"/>', | ||||||
|  |         }), | ||||||
|  |       ).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('should add escaped title if title is set', () => { | ||||||
|  |       expect( | ||||||
|  |         GfmAutoComplete.Members.templateFunction({ | ||||||
|  |           avatarTag: 'IMG', | ||||||
|  |           username: 'my-group', | ||||||
|  |           title: 'MyGroup+', | ||||||
|  |           icon: '<i class="icon"/>', | ||||||
|  |         }), | ||||||
|  |       ).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|   describe('labels', () => { |   describe('labels', () => { | ||||||
|     const dataSources = { |     const dataSources = { | ||||||
|       labels: `${TEST_HOST}/autocomplete_sources/labels`, |       labels: `${TEST_HOST}/autocomplete_sources/labels`, | ||||||
|  |  | ||||||
|  | @ -19,15 +19,23 @@ describe Banzai::ReferenceParser::UserParser do | ||||||
|           link['data-group'] = project.group.id.to_s |           link['data-group'] = project.group.id.to_s | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         it 'returns the users of the group' do |  | ||||||
|           create(:group_member, group: group, user: user) |  | ||||||
| 
 |  | ||||||
|           expect(subject.referenced_by([link])).to eq([user]) |  | ||||||
|         end |  | ||||||
| 
 |  | ||||||
|         it 'returns an empty Array when the group has no users' do |         it 'returns an empty Array when the group has no users' do | ||||||
|           expect(subject.referenced_by([link])).to eq([]) |           expect(subject.referenced_by([link])).to eq([]) | ||||||
|         end |         end | ||||||
|  | 
 | ||||||
|  |         context 'when group has members' do | ||||||
|  |           let!(:group_member) { create(:group_member, group: group, user: user) } | ||||||
|  | 
 | ||||||
|  |           it 'returns the users of the group' do | ||||||
|  |             expect(subject.referenced_by([link])).to eq([user]) | ||||||
|  |           end | ||||||
|  | 
 | ||||||
|  |           it 'returns an empty Array when the group has mentions disabled' do | ||||||
|  |             group.update!(mentions_disabled: true) | ||||||
|  | 
 | ||||||
|  |             expect(subject.referenced_by([link])).to eq([]) | ||||||
|  |           end | ||||||
|  |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       context 'using a non-existing group ID' do |       context 'using a non-existing group ID' do | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ describe Gitlab::ProjectTemplate do | ||||||
|         described_class.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html'), |         described_class.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html'), | ||||||
|         described_class.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook'), |         described_class.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook'), | ||||||
|         described_class.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo'), |         described_class.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo'), | ||||||
|  |         described_class.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'), | ||||||
|         described_class.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg') |         described_class.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg') | ||||||
|       ] |       ] | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -97,7 +97,7 @@ describe Gitlab::Tracking do | ||||||
|           '_property_', |           '_property_', | ||||||
|           '_value_', |           '_value_', | ||||||
|           nil, |           nil, | ||||||
|           timestamp.to_i |           (timestamp.to_f * 1000).to_i | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         track_event |         track_event | ||||||
|  | @ -130,7 +130,7 @@ describe Gitlab::Tracking do | ||||||
|         expect(tracker).to receive(:track_self_describing_event).with( |         expect(tracker).to receive(:track_self_describing_event).with( | ||||||
|           '_event_json_', |           '_event_json_', | ||||||
|           nil, |           nil, | ||||||
|           timestamp.to_i |           (timestamp.to_f * 1000).to_i | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         track_event |         track_event | ||||||
|  |  | ||||||
|  | @ -689,8 +689,9 @@ describe Issues::UpdateService, :mailer do | ||||||
| 
 | 
 | ||||||
|       context 'valid canonical_issue_id' do |       context 'valid canonical_issue_id' do | ||||||
|         it 'calls the duplicate service with both issues' do |         it 'calls the duplicate service with both issues' do | ||||||
|           expect_any_instance_of(Issues::DuplicateService) |           expect_next_instance_of(Issues::DuplicateService) do |service| | ||||||
|             .to receive(:execute).with(issue, canonical_issue) |             expect(service).to receive(:execute).with(issue, canonical_issue) | ||||||
|  |           end | ||||||
| 
 | 
 | ||||||
|           update_issue(canonical_issue_id: canonical_issue.id) |           update_issue(canonical_issue_id: canonical_issue.id) | ||||||
|         end |         end | ||||||
|  |  | ||||||
|  | @ -1,36 +0,0 @@ | ||||||
| # frozen_string_literal: true |  | ||||||
| 
 |  | ||||||
| shared_examples 'duplicate quick action' do |  | ||||||
|   context 'mark issue as duplicate' do |  | ||||||
|     let(:original_issue) { create(:issue, project: project) } |  | ||||||
| 
 |  | ||||||
|     context 'when the current user can update issues' do |  | ||||||
|       it 'does not create a note, and marks the issue as a duplicate' do |  | ||||||
|         add_note("/duplicate ##{original_issue.to_reference}") |  | ||||||
| 
 |  | ||||||
|         expect(page).not_to have_content "/duplicate #{original_issue.to_reference}" |  | ||||||
|         expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" |  | ||||||
| 
 |  | ||||||
|         expect(issue.reload).to be_closed |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     context 'when the current user cannot update the issue' do |  | ||||||
|       let(:guest) { create(:user) } |  | ||||||
|       before do |  | ||||||
|         project.add_guest(guest) |  | ||||||
|         gitlab_sign_out |  | ||||||
|         sign_in(guest) |  | ||||||
|         visit project_issue_path(project, issue) |  | ||||||
|       end |  | ||||||
| 
 |  | ||||||
|       it 'does not create a note, and does not mark the issue as a duplicate' do |  | ||||||
|         add_note("/duplicate ##{original_issue.to_reference}") |  | ||||||
| 
 |  | ||||||
|         expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" |  | ||||||
| 
 |  | ||||||
|         expect(issue.reload).to be_open |  | ||||||
|       end |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
| end |  | ||||||
										
											Binary file not shown.
										
									
								
							
		Loading…
	
		Reference in New Issue