Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									0ab17699c8
								
							
						
					
					
						commit
						14cb5b3d79
					
				|  | @ -32,6 +32,7 @@ export default { | |||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   inject: ['blobHash'], | ||||
|   computed: { | ||||
|     downloadUrl() { | ||||
|       return `${this.rawPath}?inline=false`; | ||||
|  | @ -39,6 +40,9 @@ export default { | |||
|     copyDisabled() { | ||||
|       return this.activeViewer === RICH_BLOB_VIEWER; | ||||
|     }, | ||||
|     getBlobHashTarget() { | ||||
|       return `[data-blob-hash="${this.blobHash}"]`; | ||||
|     }, | ||||
|   }, | ||||
|   BTN_COPY_CONTENTS_TITLE, | ||||
|   BTN_DOWNLOAD_TITLE, | ||||
|  | @ -53,7 +57,7 @@ export default { | |||
|       :aria-label="$options.BTN_COPY_CONTENTS_TITLE" | ||||
|       :title="$options.BTN_COPY_CONTENTS_TITLE" | ||||
|       :disabled="copyDisabled" | ||||
|       data-clipboard-target="#blob-code-content" | ||||
|       :data-clipboard-target="getBlobHashTarget" | ||||
|       data-testid="copyContentsButton" | ||||
|       icon="copy-to-clipboard" | ||||
|       category="primary" | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ export default { | |||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ issue: 'activeIssue' }), | ||||
|     ...mapGetters({ issue: 'activeIssue', projectPathForActiveIssue: 'projectPathForActiveIssue' }), | ||||
|     hasDueDate() { | ||||
|       return this.issue.dueDate != null; | ||||
|     }, | ||||
|  | @ -36,10 +36,6 @@ export default { | |||
| 
 | ||||
|       return dateInWords(this.parsedDueDate, true); | ||||
|     }, | ||||
|     projectPath() { | ||||
|       const referencePath = this.issue.referencePath || ''; | ||||
|       return referencePath.slice(0, referencePath.indexOf('#')); | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapActions(['setActiveIssueDueDate']), | ||||
|  | @ -53,7 +49,7 @@ export default { | |||
| 
 | ||||
|       try { | ||||
|         const dueDate = date ? formatDate(date, 'yyyy-mm-dd') : null; | ||||
|         await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPath }); | ||||
|         await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPathForActiveIssue }); | ||||
|       } catch (e) { | ||||
|         createFlash({ message: this.$options.i18n.updateDueDateError }); | ||||
|       } finally { | ||||
|  |  | |||
|  | @ -21,9 +21,9 @@ export default { | |||
|   }, | ||||
|   inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'], | ||||
|   computed: { | ||||
|     ...mapGetters({ issue: 'activeIssue' }), | ||||
|     ...mapGetters(['activeIssue', 'projectPathForActiveIssue']), | ||||
|     selectedLabels() { | ||||
|       const { labels = [] } = this.issue; | ||||
|       const { labels = [] } = this.activeIssue; | ||||
| 
 | ||||
|       return labels.map(label => ({ | ||||
|         ...label, | ||||
|  | @ -31,17 +31,13 @@ export default { | |||
|       })); | ||||
|     }, | ||||
|     issueLabels() { | ||||
|       const { labels = [] } = this.issue; | ||||
|       const { labels = [] } = this.activeIssue; | ||||
| 
 | ||||
|       return labels.map(label => ({ | ||||
|         ...label, | ||||
|         scoped: isScopedLabel(label), | ||||
|       })); | ||||
|     }, | ||||
|     projectPath() { | ||||
|       const { referencePath = '' } = this.issue; | ||||
|       return referencePath.slice(0, referencePath.indexOf('#')); | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapActions(['setActiveIssueLabels']), | ||||
|  | @ -55,7 +51,7 @@ export default { | |||
|           .filter(label => !payload.find(selected => selected.id === label.id)) | ||||
|           .map(label => label.id); | ||||
| 
 | ||||
|         const input = { addLabelIds, removeLabelIds, projectPath: this.projectPath }; | ||||
|         const input = { addLabelIds, removeLabelIds, projectPath: this.projectPathForActiveIssue }; | ||||
|         await this.setActiveIssueLabels(input); | ||||
|       } catch (e) { | ||||
|         createFlash({ message: __('An error occurred while updating labels.') }); | ||||
|  | @ -68,7 +64,7 @@ export default { | |||
| 
 | ||||
|       try { | ||||
|         const removeLabelIds = [getIdFromGraphQLId(id)]; | ||||
|         const input = { removeLabelIds, projectPath: this.projectPath }; | ||||
|         const input = { removeLabelIds, projectPath: this.projectPathForActiveIssue }; | ||||
|         await this.setActiveIssueLabels(input); | ||||
|       } catch (e) { | ||||
|         createFlash({ message: __('An error occurred when removing the label.') }); | ||||
|  |  | |||
|  | @ -0,0 +1,71 @@ | |||
| <script> | ||||
| import { mapGetters, mapActions } from 'vuex'; | ||||
| import { GlToggle } from '@gitlab/ui'; | ||||
| import createFlash from '~/flash'; | ||||
| import { __, s__ } from '~/locale'; | ||||
| 
 | ||||
| export default { | ||||
|   i18n: { | ||||
|     header: { | ||||
|       title: __('Notifications'), | ||||
|       /* Any change to subscribeDisabledDescription | ||||
|          must be reflected in app/helpers/notifications_helper.rb */ | ||||
|       subscribeDisabledDescription: __( | ||||
|         'Notifications have been disabled by the project or group owner', | ||||
|       ), | ||||
|     }, | ||||
|     updateSubscribedErrorMessage: s__( | ||||
|       'IssueBoards|An error occurred while setting notifications status.', | ||||
|     ), | ||||
|   }, | ||||
|   components: { | ||||
|     GlToggle, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       loading: false, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters(['activeIssue', 'projectPathForActiveIssue']), | ||||
|     notificationText() { | ||||
|       return this.activeIssue.emailsDisabled | ||||
|         ? this.$options.i18n.header.subscribeDisabledDescription | ||||
|         : this.$options.i18n.header.title; | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     ...mapActions(['setActiveIssueSubscribed']), | ||||
|     async handleToggleSubscription() { | ||||
|       this.loading = true; | ||||
| 
 | ||||
|       try { | ||||
|         await this.setActiveIssueSubscribed({ | ||||
|           subscribed: !this.activeIssue.subscribed, | ||||
|           projectPath: this.projectPathForActiveIssue, | ||||
|         }); | ||||
|       } catch (error) { | ||||
|         createFlash({ message: this.$options.i18n.updateSubscribedErrorMessage }); | ||||
|       } finally { | ||||
|         this.loading = false; | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div | ||||
|     class="gl-display-flex gl-align-items-center gl-justify-content-space-between" | ||||
|     data-testid="sidebar-notifications" | ||||
|   > | ||||
|     <span data-testid="notification-header-text"> {{ notificationText }} </span> | ||||
|     <gl-toggle | ||||
|       v-if="!activeIssue.emailsDisabled" | ||||
|       :value="activeIssue.subscribed" | ||||
|       :is-loading="loading" | ||||
|       data-testid="notification-subscribe-toggle" | ||||
|       @change="handleToggleSubscription" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
|  | @ -0,0 +1,8 @@ | |||
| mutation issueSetSubscription($input: IssueSetSubscriptionInput!) { | ||||
|   issueSetSubscription(input: $input) { | ||||
|     issue { | ||||
|       subscribed | ||||
|     } | ||||
|     errors | ||||
|   } | ||||
| } | ||||
|  | @ -24,6 +24,7 @@ import destroyBoardListMutation from '../queries/board_list_destroy.mutation.gra | |||
| import issueCreateMutation from '../queries/issue_create.mutation.graphql'; | ||||
| import issueSetLabels from '../queries/issue_set_labels.mutation.graphql'; | ||||
| import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql'; | ||||
| import issueSetSubscriptionMutation from '../graphql/mutations/issue_set_subscription.mutation.graphql'; | ||||
| 
 | ||||
| const notImplemented = () => { | ||||
|   /* eslint-disable-next-line @gitlab/require-i18n-strings */ | ||||
|  | @ -423,6 +424,29 @@ export default { | |||
|     }); | ||||
|   }, | ||||
| 
 | ||||
|   setActiveIssueSubscribed: async ({ commit, getters }, input) => { | ||||
|     const { data } = await gqlClient.mutate({ | ||||
|       mutation: issueSetSubscriptionMutation, | ||||
|       variables: { | ||||
|         input: { | ||||
|           iid: String(getters.activeIssue.iid), | ||||
|           projectPath: input.projectPath, | ||||
|           subscribedState: input.subscribed, | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     if (data.issueSetSubscription?.errors?.length > 0) { | ||||
|       throw new Error(data.issueSetSubscription.errors); | ||||
|     } | ||||
| 
 | ||||
|     commit(types.UPDATE_ISSUE_BY_ID, { | ||||
|       issueId: getters.activeIssue.id, | ||||
|       prop: 'subscribed', | ||||
|       value: data.issueSetSubscription.issue.subscribed, | ||||
|     }); | ||||
|   }, | ||||
| 
 | ||||
|   fetchBacklog: () => { | ||||
|     notImplemented(); | ||||
|   }, | ||||
|  |  | |||
|  | @ -24,6 +24,11 @@ export default { | |||
|     return state.issues[state.activeId] || {}; | ||||
|   }, | ||||
| 
 | ||||
|   projectPathForActiveIssue: (_, getters) => { | ||||
|     const referencePath = getters.activeIssue.referencePath || ''; | ||||
|     return referencePath.slice(0, referencePath.indexOf('#')); | ||||
|   }, | ||||
| 
 | ||||
|   getListByLabelId: state => labelId => { | ||||
|     return find(state.boardLists, l => l.label?.id === labelId); | ||||
|   }, | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ const PERSISTENT_USER_CALLOUTS = [ | |||
|   '.js-admin-licensed-user-count-threshold', | ||||
|   '.js-buy-pipeline-minutes-notification-callout', | ||||
|   '.js-token-expiry-callout', | ||||
|   '.js-registration-enabled-callout', | ||||
| ]; | ||||
| 
 | ||||
| const initCallouts = () => { | ||||
|  |  | |||
|  | @ -51,6 +51,13 @@ export default { | |||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   provide() { | ||||
|     return { | ||||
|       blobHash: Math.random() | ||||
|         .toString() | ||||
|         .split('.')[1], | ||||
|     }; | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       blobContent: '', | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ export default { | |||
|     GlIcon, | ||||
|   }, | ||||
|   mixins: [ViewerMixin], | ||||
|   inject: ['blobHash'], | ||||
|   data() { | ||||
|     return { | ||||
|       highlightedLine: null, | ||||
|  | @ -64,7 +65,7 @@ export default { | |||
|       </a> | ||||
|     </div> | ||||
|     <div class="blob-content"> | ||||
|       <pre class="code highlight"><code id="blob-code-content" v-html="content"></code></pre> | ||||
|       <pre class="code highlight"><code :data-blob-hash="blobHash" v-html="content"></code></pre> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  |  | |||
|  | @ -394,6 +394,10 @@ module ApplicationSettingsHelper | |||
|   def show_documentation_base_url_field? | ||||
|     Feature.enabled?(:help_page_documentation_redirect) | ||||
|   end | ||||
| 
 | ||||
|   def signup_enabled? | ||||
|     !!Gitlab::CurrentSettings.signup_enabled | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper') | ||||
|  |  | |||
|  | @ -67,6 +67,7 @@ module NotificationsHelper | |||
|     when :custom | ||||
|       _('You will only receive notifications for the events you choose') | ||||
|     when :owner_disabled | ||||
|       # Any change must be reflected in board_sidebar_subscription.vue | ||||
|       _('Notifications have been disabled by the project or group owner') | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ module UserCalloutsHelper | |||
|   WEBHOOKS_MOVED = 'webhooks_moved' | ||||
|   CUSTOMIZE_HOMEPAGE = 'customize_homepage' | ||||
|   FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version' | ||||
|   REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout' | ||||
| 
 | ||||
|   def show_admin_integrations_moved? | ||||
|     !user_dismissed?(ADMIN_INTEGRATIONS_MOVED) | ||||
|  | @ -55,6 +56,10 @@ module UserCalloutsHelper | |||
|     !user_dismissed?(FEATURE_FLAGS_NEW_VERSION) | ||||
|   end | ||||
| 
 | ||||
|   def show_registration_enabled_user_callout? | ||||
|     current_user&.admin? && signup_enabled? && !user_dismissed?(REGISTRATION_ENABLED_CALLOUT) | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil) | ||||
|  |  | |||
|  | @ -105,7 +105,7 @@ module Ci | |||
|       raise ArgumentError, 'Offset is out of range' if offset < 0 || offset > size | ||||
|       raise ArgumentError, 'Chunk size overflow' if CHUNK_SIZE < (offset + new_data.bytesize) | ||||
| 
 | ||||
|       in_lock(*lock_params) { unsafe_append_data!(new_data, offset) } | ||||
|       in_lock(lock_key, **lock_params) { unsafe_append_data!(new_data, offset) } | ||||
| 
 | ||||
|       schedule_to_persist! if full? | ||||
|     end | ||||
|  | @ -151,7 +151,7 @@ module Ci | |||
|     # acquired | ||||
|     # | ||||
|     def persist_data! | ||||
|       in_lock(*lock_params) do         # exclusive Redis lock is acquired first | ||||
|       in_lock(lock_key, **lock_params) do # exclusive Redis lock is acquired first | ||||
|         raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save? | ||||
| 
 | ||||
|         self.reset.then do |chunk|     # we ensure having latest lock_version | ||||
|  | @ -289,11 +289,16 @@ module Ci | |||
|       build.trace_chunks.maximum(:chunk_index).to_i | ||||
|     end | ||||
| 
 | ||||
|     def lock_key | ||||
|       "trace_write:#{build_id}:chunks:#{chunk_index}" | ||||
|     end | ||||
| 
 | ||||
|     def lock_params | ||||
|       ["trace_write:#{build_id}:chunks:#{chunk_index}", | ||||
|        { ttl: WRITE_LOCK_TTL, | ||||
|       { | ||||
|         ttl: WRITE_LOCK_TTL, | ||||
|         retries: WRITE_LOCK_RETRY, | ||||
|          sleep_sec: WRITE_LOCK_SLEEP }] | ||||
|         sleep_sec: WRITE_LOCK_SLEEP | ||||
|       } | ||||
|     end | ||||
| 
 | ||||
|     def metrics | ||||
|  |  | |||
|  | @ -25,7 +25,8 @@ class UserCallout < ApplicationRecord | |||
|     personal_access_token_expiry: 21,          # EE-only | ||||
|     suggest_pipeline: 22, | ||||
|     customize_homepage: 23, | ||||
|     feature_flags_new_version: 24 | ||||
|     feature_flags_new_version: 24, | ||||
|     registration_enabled_callout: 25 | ||||
|   } | ||||
| 
 | ||||
|   validates :user, presence: true | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ module MergeRequests | |||
|       return error('Failed to create keep around refs.') unless kept_around? | ||||
|       return error('Failed to cache merge ref sha.') unless cache_merge_ref_sha | ||||
| 
 | ||||
|       delete_refs | ||||
|       delete_refs if repository.exists? | ||||
| 
 | ||||
|       return error('Failed to update schedule.') unless update_schedule | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ module MergeRequests | |||
|         merge_request.update_project_counter_caches | ||||
|         merge_request.cache_merge_request_closes_issues!(current_user) | ||||
|         merge_request.cleanup_schedule&.destroy | ||||
|         merge_request.update_column(:merge_ref_sha, nil) | ||||
|       end | ||||
| 
 | ||||
|       merge_request | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ | |||
| - if @project.last_repository_check_failed? | ||||
|   .row | ||||
|     .col-md-12 | ||||
|       .gl-alert.gl-alert-danger.gl-mb-5 | ||||
|       .gl-alert.gl-alert-danger.gl-mb-5{ data: { testid: 'last-repository-check-failed-alert' } } | ||||
|         = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') | ||||
|         .gl-alert-body | ||||
|           - last_check_message = _("Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages.") | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ | |||
|       = render_if_exists "layouts/header/token_expiry_notification" | ||||
|       = render "layouts/broadcast" | ||||
|       = render "layouts/header/read_only_banner" | ||||
|       = render "layouts/header/registration_enabled_callout" | ||||
|       = render "layouts/nav/classification_level_banner" | ||||
|       = yield :flash_message | ||||
|       = render "shared/ping_consent" | ||||
|  |  | |||
|  | @ -0,0 +1,15 @@ | |||
| - return unless show_registration_enabled_user_callout? | ||||
| 
 | ||||
| %div{ class: [container_class, @content_class, 'gl-pt-5!'] } | ||||
|   .gl-alert.gl-alert-warning.js-registration-enabled-callout{ role: 'alert', data: { feature_id: UserCalloutsHelper::REGISTRATION_ENABLED_CALLOUT, dismiss_endpoint: user_callouts_path } } | ||||
|     = sprite_icon('warning', size: 16, css_class: 'gl-alert-icon') | ||||
|     %button.gl-alert-dismiss.js-close{ type: 'button', aria: { label: _('Close') }, data: { testid: 'close-registration-enabled-callout' } } | ||||
|       = sprite_icon('close', size: 16) | ||||
|     .gl-alert-title | ||||
|       = _('Open registration is enabled on your instance.') | ||||
|     .gl-alert-body | ||||
|       = html_escape(_('%{anchorOpen}Learn more%{anchorClose} about how you can customize / disable registration on your instance.')) % { anchorOpen: "<a href=\"#{help_page_path('user/admin_area/settings/sign_up_restrictions')}\">".html_safe, anchorClose: '</a>'.html_safe } | ||||
|     .gl-alert-actions | ||||
|       = link_to general_admin_application_settings_path(anchor: 'js-signup-settings'), class: 'btn gl-alert-action btn-info btn-md gl-button' do | ||||
|         %span.gl-button-text | ||||
|           = _('View setting') | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Add user callout to alert admins that registration is open by default | ||||
| merge_request: 47425 | ||||
| author: | ||||
| type: added | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Do not fail when cleaning up MR with no repository | ||||
| merge_request: 47744 | ||||
| author: | ||||
| type: fixed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Fixed copy contents functionality for snippets | ||||
| merge_request: 47646 | ||||
| author: | ||||
| type: fixed | ||||
|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| title: Clear cached merge_ref_sha on reopen | ||||
| merge_request: 47747 | ||||
| author: | ||||
| type: fixed | ||||
|  | @ -106,7 +106,8 @@ test its execution using `CREATE INDEX CONCURRENTLY` in the `#database-lab` Slac | |||
| 
 | ||||
| - Write the raw SQL in the MR description. Preferably formatted | ||||
|   nicely with [pgFormatter](https://sqlformat.darold.net) or | ||||
|   [paste.depesz.com](https://paste.depesz.com). | ||||
|   [paste.depesz.com](https://paste.depesz.com) and using regular quotes | ||||
|   (e.g. `"projects"."id"`) and avoiding smart quotes (e.g. `“projects”.“id”`). | ||||
| - Include the output of `EXPLAIN (ANALYZE, BUFFERS)` of the relevant | ||||
|   queries in the description. If the output is too long, wrap it in | ||||
|   `<details>` blocks, paste it in a GitLab Snippet, or provide the | ||||
|  |  | |||
|  | @ -4,34 +4,34 @@ group: Ecosystem | |||
| info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers | ||||
| --- | ||||
| 
 | ||||
| # Setting up a development environment | ||||
| # Set up a development environment | ||||
| 
 | ||||
| The following are required to install and test the app: | ||||
| 
 | ||||
| 1. A Jira Cloud instance | ||||
| - A Jira Cloud instance. Atlassian provides [free instances for development and testing](https://developer.atlassian.com/platform/marketplace/getting-started/#free-developer-instances-to-build-and-test-your-app). | ||||
| - A GitLab instance available over the internet. For the app to work, Jira Cloud should | ||||
|   be able to connect to the GitLab instance through the internet. To easily expose your | ||||
|   local development environment, you can use tools like: | ||||
|   - [serveo](https://medium.com/automationmaster/how-to-forward-my-local-port-to-public-using-serveo-4979f352a3bf) | ||||
|   - [ngrok](https://ngrok.com). | ||||
| 
 | ||||
|    Atlassian provides free instances for development and testing. [Click here to sign up](https://developer.atlassian.com/platform/marketplace/getting-started/#free-developer-instances-to-build-and-test-your-app). | ||||
|   These also take care of SSL for you because Jira requires all connections to the app | ||||
|   host to be over SSL. | ||||
| 
 | ||||
| 1. A GitLab instance available over the internet | ||||
| ## Install the app in Jira | ||||
| 
 | ||||
|    For the app to work, Jira Cloud should be able to connect to the GitLab instance through the internet. | ||||
| To install the app in Jira: | ||||
| 
 | ||||
|    To easily expose your local development environment, you can use tools like | ||||
|    [serveo](https://medium.com/automationmaster/how-to-forward-my-local-port-to-public-using-serveo-4979f352a3bf) | ||||
|    or [ngrok](https://ngrok.com). These also take care of SSL for you because Jira | ||||
|    requires all connections to the app host to be over SSL. | ||||
| 1. Enable Jira development mode to install apps that are not from the Atlassian | ||||
|    Marketplace: | ||||
| 
 | ||||
| ## Installing the app in Jira | ||||
| 
 | ||||
| 1. Enable Jira development mode to install apps that are not from the Atlassian Marketplace | ||||
| 
 | ||||
|    1. Navigate to **Jira settings** (cog icon) > **Apps** > **Manage apps**. | ||||
|    1. In Jira, navigate to **Jira settings > Apps > Manage apps**. | ||||
|    1. Scroll to the bottom of the **Manage apps** page and click **Settings**. | ||||
|    1. Select **Enable development mode** and click **Apply**. | ||||
| 
 | ||||
| 1. Install the app | ||||
| 1. Install the app: | ||||
| 
 | ||||
|    1. Navigate to Jira, then choose **Jira settings** (cog icon) > **Apps** > **Manage apps**. | ||||
|    1. In Jira, navigate to **Jira settings > Apps > Manage apps**. | ||||
|    1. Click **Upload app**. | ||||
|    1. In the **From this URL** field, provide a link to the app descriptor. The host and port must point to your GitLab instance. | ||||
| 
 | ||||
|  |  | |||
|  | @ -356,6 +356,9 @@ msgstr "" | |||
| msgid "%{address} is an invalid IP address range" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "%{anchorOpen}Learn more%{anchorClose} about how you can customize / disable registration on your instance." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "%{author_link} wrote:" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -8410,9 +8413,6 @@ msgstr "" | |||
| msgid "DastProfiles|Copy HTTP header to clipboard" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Could not create site validation token. Please refresh the page, or try again later." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Could not create the scanner profile. Please try again." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -8437,9 +8437,6 @@ msgstr "" | |||
| msgid "DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Could not retrieve site validation status. Please refresh the page, or try again later." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Could not update the scanner profile. Please try again." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -8554,9 +8551,6 @@ msgstr "" | |||
| msgid "DastProfiles|Site is not validated yet, please follow the steps." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Site must be validated to run an active scan." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Spider timeout" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -8602,27 +8596,12 @@ msgstr "" | |||
| msgid "DastProfiles|Validate" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Validate target site" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Validating..." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the chosen method." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Validation failed. Please try again." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Validation is in progress..." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Validation must be turned off to change the target URL" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "DastProfiles|Validation succeeded. Both active and passive scans can be run against the target site." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Data is still calculating..." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -15098,6 +15077,9 @@ msgstr "" | |||
| msgid "IssueAnalytics|Weight" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "IssueBoards|An error occurred while setting notifications status." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "IssueBoards|Board" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -19068,6 +19050,9 @@ msgstr "" | |||
| msgid "Open raw" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Open registration is enabled on your instance." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Open sidebar" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -29895,6 +29880,9 @@ msgstr "" | |||
| msgid "View replaced file @ " | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "View setting" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "View supported languages and frameworks" | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,10 +5,10 @@ module QA | |||
|     class File < Base | ||||
|       attr_accessor :author_email, | ||||
|                     :author_name, | ||||
|                     :branch, | ||||
|                     :content, | ||||
|                     :commit_message, | ||||
|                     :name | ||||
|       attr_writer :branch | ||||
| 
 | ||||
|       attribute :project do | ||||
|         Project.fabricate! do |resource| | ||||
|  | @ -29,6 +29,10 @@ module QA | |||
|         @commit_message = 'QA Test - Commit message' | ||||
|       end | ||||
| 
 | ||||
|       def branch | ||||
|         @branch ||= "master" | ||||
|       end | ||||
| 
 | ||||
|       def fabricate! | ||||
|         project.visit! | ||||
| 
 | ||||
|  | @ -42,12 +46,6 @@ module QA | |||
|         end | ||||
|       end | ||||
| 
 | ||||
|       def resource_web_url(resource) | ||||
|         super | ||||
|       rescue ResourceURLMissingError | ||||
|         # this particular resource does not expose a web_url property | ||||
|       end | ||||
| 
 | ||||
|       def api_get_path | ||||
|         "/projects/#{CGI.escape(project.path_with_namespace)}/repository/files/#{CGI.escape(@name)}" | ||||
|       end | ||||
|  | @ -58,13 +56,20 @@ module QA | |||
| 
 | ||||
|       def api_post_body | ||||
|         { | ||||
|           branch: @branch || "master", | ||||
|           branch: branch, | ||||
|           author_email: @author_email || Runtime::User.default_email, | ||||
|           author_name: @author_name || Runtime::User.username, | ||||
|           content: content, | ||||
|           commit_message: commit_message | ||||
|         } | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def transform_api_resource(api_resource) | ||||
|         api_resource[:web_url] = "#{Runtime::Scenario.gitlab_address}/#{project.full_path}/-/tree/#{branch}/#{api_resource[:file_path]}" | ||||
|         api_resource | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -1,68 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module QA | ||||
|   RSpec.describe 'Create' do | ||||
|     describe 'Files management' do | ||||
|       it 'user creates, edits and deletes a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/451' do | ||||
|         Flow::Login.sign_in | ||||
| 
 | ||||
|         # Create | ||||
|         file_name = 'QA Test - File name' | ||||
|         file_content = 'QA Test - File content' | ||||
|         commit_message_for_create = 'QA Test - Create new file' | ||||
| 
 | ||||
|         Resource::File.fabricate_via_browser_ui! do |file| | ||||
|           file.name = file_name | ||||
|           file.content = file_content | ||||
|           file.commit_message = commit_message_for_create | ||||
|         end | ||||
| 
 | ||||
|         Page::File::Show.perform do |file| | ||||
|           aggregate_failures 'file details' do | ||||
|             expect(file).to have_file(file_name) | ||||
|             expect(file).to have_file_content(file_content) | ||||
|             expect(file).to have_commit_message(commit_message_for_create) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         # Edit | ||||
|         updated_file_content = 'QA Test - Updated file content' | ||||
|         commit_message_for_update = 'QA Test - Update file' | ||||
| 
 | ||||
|         Page::File::Show.perform(&:click_edit) | ||||
| 
 | ||||
|         Page::File::Form.perform do |file| | ||||
|           file.remove_content | ||||
|           file.add_content(updated_file_content) | ||||
|           file.add_commit_message(commit_message_for_update) | ||||
|           file.commit_changes | ||||
|         end | ||||
| 
 | ||||
|         Page::File::Show.perform do |file| | ||||
|           aggregate_failures 'file details' do | ||||
|             expect(file).to have_notice('Your changes have been successfully committed.') | ||||
|             expect(file).to have_file_content(updated_file_content) | ||||
|             expect(file).to have_commit_message(commit_message_for_update) | ||||
|           end | ||||
|         end | ||||
| 
 | ||||
|         # Delete | ||||
|         commit_message_for_delete = 'QA Test - Delete file' | ||||
| 
 | ||||
|         Page::File::Show.perform do |file| | ||||
|           file.click_delete | ||||
|           file.add_commit_message(commit_message_for_delete) | ||||
|           file.click_delete_file | ||||
|         end | ||||
| 
 | ||||
|         Page::Project::Show.perform do |project| | ||||
|           aggregate_failures 'file details' do | ||||
|             expect(project).to have_notice('The file has been successfully deleted.') | ||||
|             expect(project).to have_commit_message(commit_message_for_delete) | ||||
|             expect(project).not_to have_file(file_name) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,31 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module QA | ||||
|   RSpec.describe 'Create' do | ||||
|     context 'File management' do | ||||
|       file_name = 'QA Test - File name' | ||||
|       file_content = 'QA Test - File content' | ||||
|       commit_message_for_create = 'QA Test - Create new file' | ||||
| 
 | ||||
|       before do | ||||
|         Flow::Login.sign_in | ||||
|       end | ||||
| 
 | ||||
|       it 'user creates a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1093' do | ||||
|         Resource::File.fabricate_via_browser_ui! do |file| | ||||
|           file.name = file_name | ||||
|           file.content = file_content | ||||
|           file.commit_message = commit_message_for_create | ||||
|         end | ||||
| 
 | ||||
|         Page::File::Show.perform do |file| | ||||
|           aggregate_failures 'file details' do | ||||
|             expect(file).to have_file(file_name) | ||||
|             expect(file).to have_file_content(file_content) | ||||
|             expect(file).to have_commit_message(commit_message_for_create) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,32 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module QA | ||||
|   RSpec.describe 'Create' do | ||||
|     context 'File management' do | ||||
|       let(:file) { Resource::File.fabricate_via_api! } | ||||
| 
 | ||||
|       commit_message_for_delete = 'QA Test - Delete file' | ||||
| 
 | ||||
|       before do | ||||
|         Flow::Login.sign_in | ||||
|         file.visit! | ||||
|       end | ||||
| 
 | ||||
|       it 'user deletes a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1095' do | ||||
|         Page::File::Show.perform do |file| | ||||
|           file.click_delete | ||||
|           file.add_commit_message(commit_message_for_delete) | ||||
|           file.click_delete_file | ||||
|         end | ||||
| 
 | ||||
|         Page::Project::Show.perform do |project| | ||||
|           aggregate_failures 'file details' do | ||||
|             expect(project).to have_notice('The file has been successfully deleted.') | ||||
|             expect(project).to have_commit_message(commit_message_for_delete) | ||||
|             expect(project).not_to have_file(file.name) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,36 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module QA | ||||
|   RSpec.describe 'Create' do | ||||
|     context 'File management' do | ||||
|       let(:file) { Resource::File.fabricate_via_api! } | ||||
| 
 | ||||
|       updated_file_content = 'QA Test - Updated file content' | ||||
|       commit_message_for_update = 'QA Test - Update file' | ||||
| 
 | ||||
|       before do | ||||
|         Flow::Login.sign_in | ||||
|         file.visit! | ||||
|       end | ||||
| 
 | ||||
|       it 'user edits a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1094' do | ||||
|         Page::File::Show.perform(&:click_edit) | ||||
| 
 | ||||
|         Page::File::Form.perform do |file| | ||||
|           file.remove_content | ||||
|           file.add_content(updated_file_content) | ||||
|           file.add_commit_message(commit_message_for_update) | ||||
|           file.commit_changes | ||||
|         end | ||||
| 
 | ||||
|         Page::File::Show.perform do |file| | ||||
|           aggregate_failures 'file details' do | ||||
|             expect(file).to have_notice('Your changes have been successfully committed.') | ||||
|             expect(file).to have_file_content(updated_file_content) | ||||
|             expect(file).to have_commit_message(commit_message_for_update) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -46,7 +46,7 @@ RSpec.describe 'Admin uses repository checks', :request_store, :clean_gitlab_red | |||
|       ) | ||||
|       visit_admin_project_page(project) | ||||
| 
 | ||||
|       page.within('.gl-alert') do | ||||
|       page.within('[data-testid="last-repository-check-failed-alert"]') do | ||||
|         expect(page.text).to match(/Last repository check \(just now\) failed/) | ||||
|       end | ||||
|     end | ||||
|  |  | |||
|  | @ -0,0 +1,49 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe 'Registration enabled callout' do | ||||
|   let_it_be(:admin) { create(:admin) } | ||||
|   let_it_be(:non_admin) { create(:user) } | ||||
| 
 | ||||
|   context 'when "Sign-up enabled" setting is `true`' do | ||||
|     before do | ||||
|       stub_application_setting(signup_enabled: true) | ||||
|     end | ||||
| 
 | ||||
|     context 'when an admin is logged in' do | ||||
|       before do | ||||
|         sign_in(admin) | ||||
|         visit root_dashboard_path | ||||
|       end | ||||
| 
 | ||||
|       it 'displays callout' do | ||||
|         expect(page).to have_content 'Open registration is enabled on your instance.' | ||||
|         expect(page).to have_link 'View setting', href: general_admin_application_settings_path(anchor: 'js-signup-settings') | ||||
|       end | ||||
| 
 | ||||
|       context 'when callout is dismissed', :js do | ||||
|         before do | ||||
|           find('[data-testid="close-registration-enabled-callout"]').click | ||||
| 
 | ||||
|           visit root_dashboard_path | ||||
|         end | ||||
| 
 | ||||
|         it 'does not display callout' do | ||||
|           expect(page).not_to have_content 'Open registration is enabled on your instance.' | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when a non-admin is logged in' do | ||||
|       before do | ||||
|         sign_in(non_admin) | ||||
|         visit root_dashboard_path | ||||
|       end | ||||
| 
 | ||||
|       it 'does not display callout' do | ||||
|         expect(page).not_to have_content 'Open registration is enabled on your instance.' | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -14,8 +14,13 @@ describe('Blob Header Default Actions', () => { | |||
|   let btnGroup; | ||||
|   let buttons; | ||||
| 
 | ||||
|   const blobHash = 'foo-bar'; | ||||
| 
 | ||||
|   function createComponent(propsData = {}) { | ||||
|     wrapper = mount(BlobHeaderActions, { | ||||
|       provide: { | ||||
|         blobHash, | ||||
|       }, | ||||
|       propsData: { | ||||
|         rawPath: Blob.rawPath, | ||||
|         ...propsData, | ||||
|  |  | |||
|  | @ -11,7 +11,11 @@ describe('Blob Header Default Actions', () => { | |||
| 
 | ||||
|   function createComponent(blobProps = {}, options = {}, propsData = {}, shouldMount = false) { | ||||
|     const method = shouldMount ? mount : shallowMount; | ||||
|     const blobHash = 'foo-bar'; | ||||
|     wrapper = method.call(this, BlobHeader, { | ||||
|       provide: { | ||||
|         blobHash, | ||||
|       }, | ||||
|       propsData: { | ||||
|         blob: { ...Blob, ...blobProps }, | ||||
|         ...propsData, | ||||
|  |  | |||
|  | @ -0,0 +1,157 @@ | |||
| import Vuex from 'vuex'; | ||||
| import { mount, createLocalVue } from '@vue/test-utils'; | ||||
| import { GlToggle, GlLoadingIcon } from '@gitlab/ui'; | ||||
| import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue'; | ||||
| import * as types from '~/boards/stores/mutation_types'; | ||||
| import { createStore } from '~/boards/stores'; | ||||
| import { mockActiveIssue } from '../../mock_data'; | ||||
| import createFlash from '~/flash'; | ||||
| 
 | ||||
| jest.mock('~/flash.js'); | ||||
| 
 | ||||
| const localVue = createLocalVue(); | ||||
| localVue.use(Vuex); | ||||
| 
 | ||||
| describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => { | ||||
|   let wrapper; | ||||
|   let store; | ||||
| 
 | ||||
|   const findNotificationHeader = () => wrapper.find("[data-testid='notification-header-text']"); | ||||
|   const findToggle = () => wrapper.find(GlToggle); | ||||
|   const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon); | ||||
| 
 | ||||
|   const createComponent = (activeIssue = { ...mockActiveIssue }) => { | ||||
|     store = createStore(); | ||||
|     store.state.issues = { [activeIssue.id]: activeIssue }; | ||||
|     store.state.activeId = activeIssue.id; | ||||
| 
 | ||||
|     wrapper = mount(BoardSidebarSubscription, { | ||||
|       localVue, | ||||
|       store, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|     wrapper = null; | ||||
|     store = null; | ||||
|     jest.clearAllMocks(); | ||||
|   }); | ||||
| 
 | ||||
|   describe('Board sidebar subscription component template', () => { | ||||
|     it('displays "notifications" heading', () => { | ||||
|       createComponent(); | ||||
| 
 | ||||
|       expect(findNotificationHeader().text()).toBe('Notifications'); | ||||
|     }); | ||||
| 
 | ||||
|     it('renders toggle as "off" when currently not subscribed', () => { | ||||
|       createComponent(); | ||||
| 
 | ||||
|       expect(findToggle().exists()).toBe(true); | ||||
|       expect(findToggle().props('value')).toBe(false); | ||||
|     }); | ||||
| 
 | ||||
|     it('renders toggle as "on" when currently subscribed', () => { | ||||
|       createComponent({ | ||||
|         ...mockActiveIssue, | ||||
|         subscribed: true, | ||||
|       }); | ||||
| 
 | ||||
|       expect(findToggle().exists()).toBe(true); | ||||
|       expect(findToggle().props('value')).toBe(true); | ||||
|     }); | ||||
| 
 | ||||
|     describe('when notification emails have been disabled', () => { | ||||
|       beforeEach(() => { | ||||
|         createComponent({ | ||||
|           ...mockActiveIssue, | ||||
|           emailsDisabled: true, | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|       it('displays a message that notification have been disabled', () => { | ||||
|         expect(findNotificationHeader().text()).toBe( | ||||
|           'Notifications have been disabled by the project or group owner', | ||||
|         ); | ||||
|       }); | ||||
| 
 | ||||
|       it('does not render the toggle button', () => { | ||||
|         expect(findToggle().exists()).toBe(false); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('Board sidebar subscription component `behavior`', () => { | ||||
|     const mockSetActiveIssueSubscribed = subscribedState => { | ||||
|       jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => { | ||||
|         store.commit(types.UPDATE_ISSUE_BY_ID, { | ||||
|           issueId: mockActiveIssue.id, | ||||
|           prop: 'subscribed', | ||||
|           value: subscribedState, | ||||
|         }); | ||||
|       }); | ||||
|     }; | ||||
| 
 | ||||
|     it('subscribing to notification', async () => { | ||||
|       createComponent(); | ||||
|       mockSetActiveIssueSubscribed(true); | ||||
| 
 | ||||
|       expect(findGlLoadingIcon().exists()).toBe(false); | ||||
| 
 | ||||
|       findToggle().trigger('click'); | ||||
| 
 | ||||
|       await wrapper.vm.$nextTick(); | ||||
| 
 | ||||
|       expect(findGlLoadingIcon().exists()).toBe(true); | ||||
|       expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({ | ||||
|         subscribed: true, | ||||
|         projectPath: 'gitlab-org/test-subgroup/gitlab-test', | ||||
|       }); | ||||
| 
 | ||||
|       await wrapper.vm.$nextTick(); | ||||
| 
 | ||||
|       expect(findGlLoadingIcon().exists()).toBe(false); | ||||
|       expect(findToggle().props('value')).toBe(true); | ||||
|     }); | ||||
| 
 | ||||
|     it('unsubscribing from notification', async () => { | ||||
|       createComponent({ | ||||
|         ...mockActiveIssue, | ||||
|         subscribed: true, | ||||
|       }); | ||||
|       mockSetActiveIssueSubscribed(false); | ||||
| 
 | ||||
|       expect(findGlLoadingIcon().exists()).toBe(false); | ||||
| 
 | ||||
|       findToggle().trigger('click'); | ||||
| 
 | ||||
|       await wrapper.vm.$nextTick(); | ||||
| 
 | ||||
|       expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({ | ||||
|         subscribed: false, | ||||
|         projectPath: 'gitlab-org/test-subgroup/gitlab-test', | ||||
|       }); | ||||
|       expect(findGlLoadingIcon().exists()).toBe(true); | ||||
| 
 | ||||
|       await wrapper.vm.$nextTick(); | ||||
| 
 | ||||
|       expect(findGlLoadingIcon().exists()).toBe(false); | ||||
|       expect(findToggle().props('value')).toBe(false); | ||||
|     }); | ||||
| 
 | ||||
|     it('flashes an error message when setting the subscribed state fails', async () => { | ||||
|       createComponent(); | ||||
|       jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => { | ||||
|         throw new Error(); | ||||
|       }); | ||||
| 
 | ||||
|       findToggle().trigger('click'); | ||||
| 
 | ||||
|       await wrapper.vm.$nextTick(); | ||||
|       expect(createFlash).toHaveBeenNthCalledWith(1, { | ||||
|         message: wrapper.vm.$options.i18n.updateSubscribedErrorMessage, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -176,6 +176,14 @@ export const mockIssue = { | |||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export const mockActiveIssue = { | ||||
|   ...mockIssue, | ||||
|   id: 436, | ||||
|   iid: '27', | ||||
|   subscribed: false, | ||||
|   emailsDisabled: false, | ||||
| }; | ||||
| 
 | ||||
| export const mockIssueWithModel = new ListIssue(mockIssue); | ||||
| 
 | ||||
| export const mockIssue2 = { | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import { | |||
|   rawIssue, | ||||
|   mockIssues, | ||||
|   labels, | ||||
|   mockActiveIssue, | ||||
| } from '../mock_data'; | ||||
| import actions, { gqlClient } from '~/boards/stores/actions'; | ||||
| import * as types from '~/boards/stores/mutation_types'; | ||||
|  | @ -833,6 +834,57 @@ describe('setActiveIssueDueDate', () => { | |||
|   }); | ||||
| }); | ||||
| 
 | ||||
| describe('setActiveIssueSubscribed', () => { | ||||
|   const state = { issues: { [mockActiveIssue.id]: mockActiveIssue } }; | ||||
|   const getters = { activeIssue: mockActiveIssue }; | ||||
|   const subscribedState = true; | ||||
|   const input = { | ||||
|     subscribedState, | ||||
|     projectPath: 'gitlab-org/gitlab-test', | ||||
|   }; | ||||
| 
 | ||||
|   it('should commit subscribed status', done => { | ||||
|     jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ | ||||
|       data: { | ||||
|         issueSetSubscription: { | ||||
|           issue: { | ||||
|             subscribed: subscribedState, | ||||
|           }, | ||||
|           errors: [], | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
| 
 | ||||
|     const payload = { | ||||
|       issueId: getters.activeIssue.id, | ||||
|       prop: 'subscribed', | ||||
|       value: subscribedState, | ||||
|     }; | ||||
| 
 | ||||
|     testAction( | ||||
|       actions.setActiveIssueSubscribed, | ||||
|       input, | ||||
|       { ...state, ...getters }, | ||||
|       [ | ||||
|         { | ||||
|           type: types.UPDATE_ISSUE_BY_ID, | ||||
|           payload, | ||||
|         }, | ||||
|       ], | ||||
|       [], | ||||
|       done, | ||||
|     ); | ||||
|   }); | ||||
| 
 | ||||
|   it('throws error if fails', async () => { | ||||
|     jest | ||||
|       .spyOn(gqlClient, 'mutate') | ||||
|       .mockResolvedValue({ data: { issueSetSubscription: { errors: ['failed mutation'] } } }); | ||||
| 
 | ||||
|     await expect(actions.setActiveIssueSubscribed({ getters }, input)).rejects.toThrow(Error); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| describe('fetchBacklog', () => { | ||||
|   expectNotImplemented(actions.fetchBacklog); | ||||
| }); | ||||
|  |  | |||
|  | @ -124,6 +124,22 @@ describe('Boards - Getters', () => { | |||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('projectPathByIssueId', () => { | ||||
|     it('returns project path for the active issue', () => { | ||||
|       const mockActiveIssue = { | ||||
|         referencePath: 'gitlab-org/gitlab-test#1', | ||||
|       }; | ||||
|       expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual( | ||||
|         'gitlab-org/gitlab-test', | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     it('returns empty string as project when active issue is an empty object', () => { | ||||
|       const mockActiveIssue = {}; | ||||
|       expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(''); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('getIssuesByList', () => { | ||||
|     const boardsState = { | ||||
|       issuesByListId: mockIssuesByListId, | ||||
|  |  | |||
|  | @ -59,7 +59,7 @@ exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = ` | |||
|       class="code highlight" | ||||
|     > | ||||
|       <code | ||||
|         id="blob-code-content" | ||||
|         data-blob-hash="foo-bar" | ||||
|       > | ||||
|         <span | ||||
|           id="LC1" | ||||
|  |  | |||
|  | @ -5,9 +5,13 @@ import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/const | |||
| describe('Blob Simple Viewer component', () => { | ||||
|   let wrapper; | ||||
|   const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`; | ||||
|   const blobHash = 'foo-bar'; | ||||
| 
 | ||||
|   function createComponent(content = contentMock) { | ||||
|     wrapper = shallowMount(SimpleViewer, { | ||||
|       provide: { | ||||
|         blobHash, | ||||
|       }, | ||||
|       propsData: { | ||||
|         content, | ||||
|         type: 'text', | ||||
|  |  | |||
|  | @ -166,4 +166,32 @@ RSpec.describe ApplicationSettingsHelper do | |||
|       it { is_expected.to eq(false) } | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.signup_enabled?' do | ||||
|     subject { helper.signup_enabled? } | ||||
| 
 | ||||
|     context 'when signup is enabled' do | ||||
|       before do | ||||
|         stub_application_setting(signup_enabled: true) | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be true } | ||||
|     end | ||||
| 
 | ||||
|     context 'when signup is disabled' do | ||||
|       before do | ||||
|         stub_application_setting(signup_enabled: false) | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be false } | ||||
|     end | ||||
| 
 | ||||
|     context 'when `signup_enabled` is nil' do | ||||
|       before do | ||||
|         stub_application_setting(signup_enabled: nil) | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be false } | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -161,4 +161,50 @@ RSpec.describe UserCalloutsHelper do | |||
|       it { is_expected.to be_falsy } | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.show_registration_enabled_user_callout?' do | ||||
|     let_it_be(:admin) { create(:user, :admin) } | ||||
| 
 | ||||
|     subject { helper.show_registration_enabled_user_callout? } | ||||
| 
 | ||||
|     context 'when `current_user` is not an admin' do | ||||
|       before do | ||||
|         allow(helper).to receive(:current_user).and_return(user) | ||||
|         stub_application_setting(signup_enabled: true) | ||||
|         allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false } | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be false } | ||||
|     end | ||||
| 
 | ||||
|     context 'when signup is disabled' do | ||||
|       before do | ||||
|         allow(helper).to receive(:current_user).and_return(admin) | ||||
|         stub_application_setting(signup_enabled: false) | ||||
|         allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false } | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be false } | ||||
|     end | ||||
| 
 | ||||
|     context 'when user has dismissed callout' do | ||||
|       before do | ||||
|         allow(helper).to receive(:current_user).and_return(admin) | ||||
|         stub_application_setting(signup_enabled: true) | ||||
|         allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { true } | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be false } | ||||
|     end | ||||
| 
 | ||||
|     context 'when `current_user` is an admin, signup is enabled, and user has not dismissed callout' do | ||||
|       before do | ||||
|         allow(helper).to receive(:current_user).and_return(admin) | ||||
|         stub_application_setting(signup_enabled: true) | ||||
|         allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false } | ||||
|       end | ||||
| 
 | ||||
|       it { is_expected.to be true } | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -115,6 +115,19 @@ RSpec.describe MergeRequests::CleanupRefsService do | |||
| 
 | ||||
|         it_behaves_like 'service that does not clean up merge request refs' | ||||
|       end | ||||
| 
 | ||||
|       context 'when repository no longer exists' do | ||||
|         before do | ||||
|           Repositories::DestroyService.new(merge_request.project.repository).execute | ||||
|         end | ||||
| 
 | ||||
|         it 'does not fail and still mark schedule as complete' do | ||||
|           aggregate_failures do | ||||
|             expect(result[:status]).to eq(:success) | ||||
|             expect(merge_request.cleanup_schedule.completed_at).to be_present | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     shared_examples_for 'service that does not clean up merge request refs' do | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ RSpec.describe MergeRequests::ReopenService do | |||
|       before do | ||||
|         allow(service).to receive(:execute_hooks) | ||||
|         merge_request.create_cleanup_schedule(scheduled_at: Time.current) | ||||
|         merge_request.update_column(:merge_ref_sha, 'abc123') | ||||
| 
 | ||||
|         perform_enqueued_jobs do | ||||
|           service.execute(merge_request) | ||||
|  | @ -48,6 +49,10 @@ RSpec.describe MergeRequests::ReopenService do | |||
|         expect(merge_request.reload.cleanup_schedule).to be_nil | ||||
|       end | ||||
| 
 | ||||
|       it 'clears the cached merge_ref_sha' do | ||||
|         expect(merge_request.reload.merge_ref_sha).to be_nil | ||||
|       end | ||||
| 
 | ||||
|       context 'note creation' do | ||||
|         it 'creates resource state event about merge_request reopen' do | ||||
|           event = merge_request.resource_state_events.last | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue