Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-21 15:07:30 +00:00
parent d4c968c95c
commit f1a1bd96b7
37 changed files with 393 additions and 156 deletions

View File

@ -146,7 +146,7 @@ review-stop:
.allure-report-base: .allure-report-base:
image: image:
name: ${GITLAB_DEPENDENCY_PROXY}andrcuns/allure-report-publisher:0.3.2 name: ${GITLAB_DEPENDENCY_PROXY}andrcuns/allure-report-publisher:0.3.4
entrypoint: [""] entrypoint: [""]
stage: post-qa stage: post-qa
variables: variables:

View File

@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 13.12.5 (2021-06-21)
### Fixed (3 changes)
- [Fix failing spec](gitlab-org/gitlab@7d1a9b0155195eb082f5b33ba1310deed742a7a4) ([merge request](gitlab-org/gitlab!64488))
- [Advanced Search Settings page does not load if the ES url is unreachable](gitlab-org/gitlab@80b262f0e79f02a89724ed4e3988e686f53c959c) ([merge request](gitlab-org/gitlab!64488)) **GitLab Enterprise Edition**
- [Fix Password expired error on git fetch via SSH for LDAP user](gitlab-org/gitlab@19a7d7a6d3cd43f1c7559c729532ad3b9dafb75c) ([merge request](gitlab-org/gitlab!64488))
## 13.12.4 (2021-06-14) ## 13.12.4 (2021-06-14)
### Fixed (3 changes) ### Fixed (3 changes)

View File

@ -1,5 +1,5 @@
<script> <script>
import { GlLabel, GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui'; import { GlLabel, GlTooltip, GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner'; import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
@ -16,6 +16,7 @@ import IssueTimeEstimate from './issue_time_estimate.vue';
export default { export default {
components: { components: {
GlTooltip,
GlLabel, GlLabel,
GlLoadingIcon, GlLoadingIcon,
GlIcon, GlIcon,
@ -55,7 +56,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['isShowingLabels', 'issuableType']), ...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
...mapGetters(['isEpicBoard']), ...mapGetters(['isEpicBoard']),
cappedAssignees() { cappedAssignees() {
// e.g. maxRender is 4, // e.g. maxRender is 4,
@ -99,6 +100,9 @@ export default {
} }
return false; return false;
}, },
shouldRenderEpicCountables() {
return this.isEpicBoard && this.item.hasIssues;
},
showLabelFooter() { showLabelFooter() {
return this.isShowingLabels && this.item.labels.find(this.showLabel); return this.isShowingLabels && this.item.labels.find(this.showLabel);
}, },
@ -115,6 +119,17 @@ export default {
} }
return __('Blocked issue'); return __('Blocked issue');
}, },
totalEpicsCount() {
return this.item.descendantCounts.openedEpics + this.item.descendantCounts.closedEpics;
},
totalIssuesCount() {
return this.item.descendantCounts.openedIssues + this.item.descendantCounts.closedIssues;
},
totalWeight() {
return (
this.item.descendantWeightSum.openedIssues + this.item.descendantWeightSum.closedIssues
);
},
}, },
methods: { methods: {
...mapActions(['performSearch', 'setError']), ...mapActions(['performSearch', 'setError']),
@ -227,6 +242,59 @@ export default {
{{ itemId }} {{ itemId }}
</span> </span>
<span class="board-info-items gl-mt-3 gl-display-inline-block"> <span class="board-info-items gl-mt-3 gl-display-inline-block">
<span v-if="shouldRenderEpicCountables" data-testid="epic-countables">
<gl-tooltip :target="() => $refs.countBadge" data-testid="epic-countables-tooltip">
<p v-if="allowSubEpics" class="gl-font-weight-bold gl-m-0">
{{ __('Epics') }} &#8226;
<span class="gl-font-weight-normal"
>{{
sprintf(__('%{openedEpics} open, %{closedEpics} closed'), {
openedEpics: item.descendantCounts.openedEpics,
closedEpics: item.descendantCounts.closedEpics,
})
}}
</span>
</p>
<p class="gl-font-weight-bold gl-m-0">
{{ __('Issues') }} &#8226;
<span class="gl-font-weight-normal"
>{{
sprintf(__('%{openedIssues} open, %{closedIssues} closed'), {
openedIssues: item.descendantCounts.openedIssues,
closedIssues: item.descendantCounts.closedIssues,
})
}}
</span>
</p>
<p class="gl-font-weight-bold gl-m-0">
{{ __('Weight') }} &#8226;
<span class="gl-font-weight-normal" data-testid="epic-countables-total-weight"
>{{
sprintf(__('%{closedWeight} complete, %{openWeight} incomplete'), {
openWeight: item.descendantWeightSum.openedIssues,
closedWeight: item.descendantWeightSum.closedIssues,
})
}}
</span>
</p>
</gl-tooltip>
<span ref="countBadge" class="issue-count-badge board-card-info">
<span v-if="allowSubEpics" class="gl-mr-3">
<gl-icon name="epic" />
{{ totalEpicsCount }}
</span>
<span class="gl-mr-3" data-testid="epic-countables-counts-issues">
<gl-icon name="issues" />
{{ totalIssuesCount }}
</span>
<span class="gl-mr-3" data-testid="epic-countables-weight-issues">
<gl-icon name="weight" />
{{ totalWeight }}
</span>
</span>
</span>
<span v-if="!isEpicBoard">
<issue-due-date <issue-due-date
v-if="item.dueDate" v-if="item.dueDate"
:date="item.dueDate" :date="item.dueDate"
@ -239,6 +307,7 @@ export default {
@click="filterByWeight(item.weight)" @click="filterByWeight(item.weight)"
/> />
</span> </span>
</span>
</div> </div>
<div class="board-card-assignee gl-display-flex"> <div class="board-card-assignee gl-display-flex">
<user-avatar-link <user-avatar-link

View File

@ -1,4 +1,4 @@
/* global Flash */ /* global createFlash */
import $ from 'jquery'; import $ from 'jquery';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
@ -71,5 +71,9 @@ export function fetchCommitMergeRequests() {
$container.html($content); $container.html($content);
}) })
.catch(() => Flash(s__('Commits|An error occurred while fetching merge requests data.'))); .catch(() =>
createFlash({
message: s__('Commits|An error occurred while fetching merge requests data.'),
}),
);
} }

View File

@ -1,7 +1,6 @@
<script> <script>
/* global Flash */
import { GlLoadingIcon, GlModal } from '@gitlab/ui'; import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import createFlash from '~/flash';
import { getParameterByName } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/common_utils';
import { HIDDEN_CLASS } from '~/lib/utils/constants'; import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
@ -116,7 +115,7 @@ export default {
this.isLoading = false; this.isLoading = false;
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
Flash(COMMON_STR.FAILURE); createFlash({ message: COMMON_STR.FAILURE });
}); });
}, },
fetchAllGroups() { fetchAllGroups() {
@ -202,7 +201,7 @@ export default {
if (err.status === 403) { if (err.status === 403) {
message = COMMON_STR.LEAVE_FORBIDDEN; message = COMMON_STR.LEAVE_FORBIDDEN;
} }
Flash(message); createFlash({ message });
this.targetGroup.isBeingRemoved = false; this.targetGroup.isBeingRemoved = false;
}); });
}, },

View File

@ -3,7 +3,7 @@ import $ from 'jquery';
import loadAwardsHandler from '~/awards_handler'; import loadAwardsHandler from '~/awards_handler';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff'; import Diff from '~/diff';
import flash from '~/flash'; import createFlash from '~/flash';
import initChangesDropdown from '~/init_changes_dropdown'; import initChangesDropdown from '~/init_changes_dropdown';
import initNotes from '~/init_notes'; import initNotes from '~/init_notes';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
@ -39,7 +39,7 @@ if (filesContainer.length) {
new Diff(); new Diff();
}) })
.catch(() => { .catch(() => {
flash({ message: __('An error occurred while retrieving diff files') }); createFlash({ message: __('An error occurred while retrieving diff files') });
}); });
} else { } else {
new Diff(); new Diff();

View File

@ -170,7 +170,7 @@ export default {
}) })
.catch(() => { .catch(() => {
this.loading = false; this.loading = false;
createFlash(ERROR_MESSAGE); createFlash({ message: ERROR_MESSAGE });
}); });
}, },
formData() { formData() {

View File

@ -8,7 +8,7 @@ class Admin::RunnersController < Admin::ApplicationController
push_frontend_feature_flag(:runner_list_view_vue_ui, current_user, default_enabled: :yaml) push_frontend_feature_flag(:runner_list_view_vue_ui, current_user, default_enabled: :yaml)
end end
feature_category :continuous_integration feature_category :runner
NUMBER_OF_RUNNERS_PER_PAGE = 30 NUMBER_OF_RUNNERS_PER_PAGE = 30

View File

@ -7,7 +7,7 @@ class Groups::RunnersController < Groups::ApplicationController
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show] before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
feature_category :continuous_integration feature_category :runner
def show def show
end end

View File

@ -6,7 +6,7 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings' layout 'project_settings'
feature_category :continuous_integration feature_category :runner
def index def index
redirect_to project_settings_ci_cd_path(@project, anchor: 'js-runners-settings') redirect_to project_settings_ci_cd_path(@project, anchor: 'js-runners-settings')

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CascadeDeleteFreezePeriods < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
OLD_PROJECT_FK = 'fk_rails_2e02bbd1a6'
NEW_PROJECT_FK = 'fk_2e02bbd1a6'
disable_ddl_transaction!
def up
add_concurrent_foreign_key :ci_freeze_periods, :projects, column: :project_id, on_delete: :cascade, name: NEW_PROJECT_FK
remove_foreign_key_if_exists :ci_freeze_periods, :projects, column: :project_id, name: OLD_PROJECT_FK
end
def down
add_concurrent_foreign_key :ci_freeze_periods, :projects, column: :project_id, on_delete: nil, name: OLD_PROJECT_FK
remove_foreign_key_if_exists :ci_freeze_periods, :projects, column: :project_id, name: NEW_PROJECT_FK
end
end

View File

@ -0,0 +1 @@
3f73aa7d2cff11d00b330d88e76daaa058f82b7012da3c244f246da6e538921c

View File

@ -25627,6 +25627,9 @@ ALTER TABLE ONLY geo_event_log
ALTER TABLE ONLY deployments ALTER TABLE ONLY deployments
ADD CONSTRAINT fk_289bba3222 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE SET NULL; ADD CONSTRAINT fk_289bba3222 FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE SET NULL;
ALTER TABLE ONLY ci_freeze_periods
ADD CONSTRAINT fk_2e02bbd1a6 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY notes ALTER TABLE ONLY notes
ADD CONSTRAINT fk_2e82291620 FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE SET NULL; ADD CONSTRAINT fk_2e82291620 FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE SET NULL;
@ -26575,9 +26578,6 @@ ALTER TABLE ONLY onboarding_progresses
ALTER TABLE ONLY protected_branch_unprotect_access_levels ALTER TABLE ONLY protected_branch_unprotect_access_levels
ADD CONSTRAINT fk_rails_2d2aba21ef FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_2d2aba21ef FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_freeze_periods
ADD CONSTRAINT fk_rails_2e02bbd1a6 FOREIGN KEY (project_id) REFERENCES projects(id);
ALTER TABLE ONLY issuable_severities ALTER TABLE ONLY issuable_severities
ADD CONSTRAINT fk_rails_2fbb74ad6d FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_2fbb74ad6d FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;

View File

@ -90,7 +90,9 @@ export default {
try { try {
await this.contentEditor.setSerializedContent(this.content); await this.contentEditor.setSerializedContent(this.content);
} catch (e) { } catch (e) {
createFlash(__('There was an error loading content in the editor'), e); createFlash({
message: __('There was an error loading content in the editor'), error: e
});
} }
}, },
methods: { methods: {

View File

@ -106,7 +106,7 @@ In this file, we write the actions that call mutations for handling a list of us
.then(({ data }) => commit(types.RECEIVE_USERS_SUCCESS, data)) .then(({ data }) => commit(types.RECEIVE_USERS_SUCCESS, data))
.catch((error) => { .catch((error) => {
commit(types.RECEIVE_USERS_ERROR, error) commit(types.RECEIVE_USERS_ERROR, error)
createFlash('There was an error') createFlash({ message: 'There was an error' })
}); });
} }

View File

@ -31,7 +31,7 @@ install GitLab:
| [Helm charts](https://docs.gitlab.com/charts/) | The cloud native Helm chart for installing GitLab and all of its components on Kubernetes. | When installing GitLab on Kubernetes, there are some trade-offs that you need to be aware of: <br/>- Administration and troubleshooting requires Kubernetes knowledge.<br/>- It can be more expensive for smaller installations. The default installation requires more resources than a single node Linux package deployment, as most services are deployed in a redundant fashion.<br/>- There are some feature [limitations to be aware of](https://docs.gitlab.com/charts/#limitations).<br/><br/> Use this method if your infrastructure is built on Kubernetes and you're familiar with how it works. The methods for management, observability, and some concepts are different than traditional deployments. | | [Helm charts](https://docs.gitlab.com/charts/) | The cloud native Helm chart for installing GitLab and all of its components on Kubernetes. | When installing GitLab on Kubernetes, there are some trade-offs that you need to be aware of: <br/>- Administration and troubleshooting requires Kubernetes knowledge.<br/>- It can be more expensive for smaller installations. The default installation requires more resources than a single node Linux package deployment, as most services are deployed in a redundant fashion.<br/>- There are some feature [limitations to be aware of](https://docs.gitlab.com/charts/#limitations).<br/><br/> Use this method if your infrastructure is built on Kubernetes and you're familiar with how it works. The methods for management, observability, and some concepts are different than traditional deployments. |
| [Docker](https://docs.gitlab.com/omnibus/docker/) | The GitLab packages, Dockerized. | Use this method if you're familiar with Docker. | | [Docker](https://docs.gitlab.com/omnibus/docker/) | The GitLab packages, Dockerized. | Use this method if you're familiar with Docker. |
| [Source](installation.md) | Install GitLab and all of its components from scratch. | Use this method if none of the previous methods are available for your platform. Useful for unsupported systems like \*BSD.| | [Source](installation.md) | Install GitLab and all of its components from scratch. | Use this method if none of the previous methods are available for your platform. Useful for unsupported systems like \*BSD.|
| [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/quality/gitlab-environment-toolkit#documentation) | The GitLab Environment toolkit provides a set of automation tools to deploy a [reference architecture](../administration/reference_architectures/index.md) on most major cloud providers. | Since GET is in beta and not yet recommended for production use, use this method if you want to test deploying GitLab in scalable environment. | | [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/quality/gitlab-environment-toolkit#documentation) | The GitLab Environment toolkit provides a set of automation tools to deploy a [reference architecture](../administration/reference_architectures/index.md) on most major cloud providers. | Customers are very welcome to trial and evaluate GET today, however be aware of [key limitations](https://gitlab.com/gitlab-org/quality/gitlab-environment-toolkit#missing-features-to-be-aware-of) of the current iteration. For production environments further manual setup will be required based on your specific requirements. |
## Install GitLab on cloud providers ## Install GitLab on cloud providers

View File

@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated
type: reference, howto type: reference, howto
--- ---
# Threads **(FREE)** # Comments and threads **(FREE)**
GitLab encourages communication through comments, threads, and GitLab encourages communication through comments, threads, and
[code suggestions](../project/merge_requests/reviews/suggestions.md). [code suggestions](../project/merge_requests/reviews/suggestions.md).

View File

@ -112,6 +112,12 @@ You can filter by the following:
- Author - Author
- Label - Label
### View count of issues and weight in an epic
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/331330) in GitLab 14.1.
Epics on the **Epic Boards** show a summary of their issues and weight. Hovering over the total counts will show the number of open and closed issues, as well as the completed and incomplete weight.
### Move epics and lists ### Move epics and lists
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5079) in GitLab 14.0. > [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5079) in GitLab 14.0.

View File

@ -147,7 +147,7 @@ To use a custom issue template with Service Desk, in your project:
1. Go to **Settings > General > Service Desk**. 1. Go to **Settings > General > Service Desk**.
1. From the dropdown **Template to append to all Service Desk issues**, select your template. 1. From the dropdown **Template to append to all Service Desk issues**, select your template.
### Using custom email display name ### Using a custom email display name
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7529) in GitLab 12.8. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7529) in GitLab 12.8.
@ -160,22 +160,29 @@ To edit the custom email display name:
1. Enter a new name in **Email display name**. 1. Enter a new name in **Email display name**.
1. Select **Save Changes**. 1. Select **Save Changes**.
### Using custom email address **(FREE SELF)** ### Using a custom email address **(FREE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2201) in GitLab Premium 13.0. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2201) in GitLab Premium 13.0.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/284656) in GitLab 13.8. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/284656) in GitLab 13.8.
If the `service_desk_email` is configured, then you can create Service Desk It is possible to customize the email address used by Service Desk. To do this, you must configure
issues by sending emails to the Service Desk email address. The default both a [custom mailbox](#configuring-a-custom-mailbox) and a
address has the following format: [custom suffix](#configuring-a-custom-email-address-suffix).
`project_contact+%{key}@example.com`.
The `%{key}` part is used to find the project where the issue should be created. The #### Configuring a custom mailbox
`%{key}` part combines the path to the project and configurable project name suffix:
`<project_full_path>-<project_name_suffix>`.
You can set the project name suffix in your project's Service Desk settings. NOTE:
It can contain only lowercase letters (`a-z`), numbers (`0-9`), or underscores (`_`). On GitLab.com a custom mailbox is already configured with `contact-project+%{key}@incoming.gitlab.com` as the email address, so you only have to configure the
[custom suffix](#configuring-a-custom-email-address-suffix) in project settings.
Using the `service_desk_email` configuration, you can customize the mailbox
used by Service Desk. This allows you to have a separate email address for
Service Desk by also configuring a [custom suffix](#configuring-a-custom-email-address-suffix)
in project settings.
The `address` must include the `+%{key}` placeholder within the 'user'
portion of the address, before the `@`. This is used to identify the project
where the issue should be created.
NOTE: NOTE:
The `service_desk_email` and `incoming_email` configurations should The `service_desk_email` and `incoming_email` configurations should
@ -183,7 +190,7 @@ always use separate mailboxes. This is important, because emails picked from
`service_desk_email` mailbox are processed by a different worker and it would `service_desk_email` mailbox are processed by a different worker and it would
not recognize `incoming_email` emails. not recognize `incoming_email` emails.
To configure a custom email address for Service Desk with IMAP, add the following snippets to your configuration file: To configure a custom mailbox for Service Desk with IMAP, add the following snippets to your configuration file in full:
- Example for installations from source: - Example for installations from source:
@ -207,36 +214,22 @@ To configure a custom email address for Service Desk with IMAP, add the followin
```ruby ```ruby
gitlab_rails['service_desk_email_enabled'] = true gitlab_rails['service_desk_email_enabled'] = true
gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@gmail.com" gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@gmail.com"
gitlab_rails['service_desk_email_email'] = "project_support@gmail.com" gitlab_rails['service_desk_email_email'] = "project_support@gmail.com"
gitlab_rails['service_desk_email_password'] = "[REDACTED]" gitlab_rails['service_desk_email_password'] = "[REDACTED]"
gitlab_rails['service_desk_email_mailbox_name'] = "inbox" gitlab_rails['service_desk_email_mailbox_name'] = "inbox"
gitlab_rails['service_desk_email_idle_timeout'] = 60 gitlab_rails['service_desk_email_idle_timeout'] = 60
gitlab_rails['service_desk_email_log_file'] = "/var/log/gitlab/mailroom/mail_room_json.log" gitlab_rails['service_desk_email_log_file'] = "/var/log/gitlab/mailroom/mail_room_json.log"
gitlab_rails['service_desk_email_host'] = "imap.gmail.com" gitlab_rails['service_desk_email_host'] = "imap.gmail.com"
gitlab_rails['service_desk_email_port'] = 993 gitlab_rails['service_desk_email_port'] = 993
gitlab_rails['service_desk_email_ssl'] = true gitlab_rails['service_desk_email_ssl'] = true
gitlab_rails['service_desk_email_start_tls'] = false gitlab_rails['service_desk_email_start_tls'] = false
``` ```
In this case, suppose the `mygroup/myproject` project Service Desk settings has the project name
suffix set to `support`, and a user sends an email to `project_contact+mygroup-myproject-support@example.com`.
As a result, a new Service Desk issue is created from this email in the `mygroup/myproject` project.
The configuration options are the same as for configuring The configuration options are the same as for configuring
[incoming email](../../administration/incoming_email.md#set-it-up). [incoming email](../../administration/incoming_email.md#set-it-up).
#### Microsoft Graph ##### Microsoft Graph
> Introduced in [GitLab 13.11](https://gitlab.com/gitlab-org/gitlab/-/issues/214900) > Introduced in [GitLab 13.11](https://gitlab.com/gitlab-org/gitlab/-/issues/214900)
@ -247,17 +240,11 @@ Graph API instead of IMAP. Follow the [documentation in the incoming e-mail sect
```ruby ```ruby
gitlab_rails['service_desk_email_enabled'] = true gitlab_rails['service_desk_email_enabled'] = true
gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@example.onmicrosoft.com" gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@example.onmicrosoft.com"
gitlab_rails['service_desk_email_email'] = "project_contact@example.onmicrosoft.com" gitlab_rails['service_desk_email_email'] = "project_contact@example.onmicrosoft.com"
gitlab_rails['service_desk_email_mailbox_name'] = "inbox" gitlab_rails['service_desk_email_mailbox_name'] = "inbox"
gitlab_rails['service_desk_email_log_file'] = "/var/log/gitlab/mailroom/mail_room_json.log" gitlab_rails['service_desk_email_log_file'] = "/var/log/gitlab/mailroom/mail_room_json.log"
gitlab_rails['service_desk_inbox_method'] = 'microsoft_graph' gitlab_rails['service_desk_inbox_method'] = 'microsoft_graph'
gitlab_rails['service_desk_inbox_options'] = { gitlab_rails['service_desk_inbox_options'] = {
'tenant_id': '<YOUR-TENANT-ID>', 'tenant_id': '<YOUR-TENANT-ID>',
'client_id': '<YOUR-CLIENT-ID>', 'client_id': '<YOUR-CLIENT-ID>',
@ -268,6 +255,22 @@ Graph API instead of IMAP. Follow the [documentation in the incoming e-mail sect
The Microsoft Graph API is not yet supported in source installations. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/326169) for more details. The Microsoft Graph API is not yet supported in source installations. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/326169) for more details.
#### Configuring a custom email address suffix
You can set a custom suffix in your project's Service Desk settings once you have configured a [custom mailbox](#configuring-a-custom-mailbox).
It can contain only lowercase letters (`a-z`), numbers (`0-9`), or underscores (`_`).
When configured, the custom suffix creates a new Service Desk email address, consisting of the
`service_desk_email_address` setting and a key of the format: `<project_full_path>-<custom_suffix>`
For example, suppose the `mygroup/myproject` project Service Desk settings has the following configured:
- Project name suffix is set to `support`.
- Service Desk email address is configured to `contact+%{key}@example.com`.
The Service Desk email address for this project is: `contact+mygroup-myproject-support@example.com`.
The [incoming email](../../administration/incoming_email.md) address still works.
## Using Service Desk ## Using Service Desk
You can use Service Desk to [create an issue](#as-an-end-user-issue-creator) or [respond to one](#as-a-responder-to-the-issue). You can use Service Desk to [create an issue](#as-an-end-user-issue-creator) or [respond to one](#as-a-responder-to-the-issue).

View File

@ -436,6 +436,9 @@ msgid_plural "%{bold_start}%{count}%{bold_end} opened merge requests"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%{closedWeight} complete, %{openWeight} incomplete"
msgstr ""
msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements." msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements."
msgstr "" msgstr ""
@ -6352,6 +6355,9 @@ msgstr ""
msgid "Checkout|Failed to load states. Please try again." msgid "Checkout|Failed to load states. Please try again."
msgstr "" msgstr ""
msgid "Checkout|Failed to load the payment form. Please try again."
msgstr ""
msgid "Checkout|Failed to register credit card. Please try again." msgid "Checkout|Failed to register credit card. Please try again."
msgstr "" msgstr ""
@ -31434,9 +31440,6 @@ msgstr ""
msgid "SuperSonics|Cloud license" msgid "SuperSonics|Cloud license"
msgstr "" msgstr ""
msgid "SuperSonics|Enter activation code"
msgstr ""
msgid "SuperSonics|Expires on" msgid "SuperSonics|Expires on"
msgstr "" msgstr ""

View File

@ -72,8 +72,14 @@ module QA
testcase = example.metadata[:testcase] testcase = example.metadata[:testcase]
example.tms('Testcase', testcase) if testcase example.tms('Testcase', testcase) if testcase
issue = example.metadata.dig(:quarantine, :issue) quarantine_issue = example.metadata.dig(:quarantine, :issue)
example.issue('Issue', issue) if issue example.issue('Quarantine issue', quarantine_issue) if quarantine_issue
spec_file = example.file_path.split('/').last
example.issue(
'Failure issues',
"https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=#{spec_file}"
)
example.add_link(name: "Job(#{Env.ci_job_name})", url: Env.ci_job_url) if Env.running_in_ci? example.add_link(name: "Job(#{Env.ci_job_name})", url: Env.ci_job_url) if Env.running_in_ci?
end end

View File

@ -14,20 +14,20 @@ module QA
let!(:api_client) { Runtime::API::Client.new(user: user) } let!(:api_client) { Runtime::API::Client.new(user: user) }
let!(:personal_access_token) { api_client.personal_access_token } let!(:personal_access_token) { api_client.personal_access_token }
let!(:sandbox) do let(:sandbox) do
Resource::Sandbox.fabricate_via_api! do |group| Resource::Sandbox.fabricate_via_api! do |group|
group.api_client = admin_api_client group.api_client = admin_api_client
end end
end end
let!(:source_group) do let(:source_group) do
Resource::Sandbox.fabricate_via_api! do |group| Resource::Sandbox.fabricate_via_api! do |group|
group.api_client = api_client group.api_client = api_client
group.path = "source-group-for-import-#{SecureRandom.hex(4)}" group.path = "source-group-for-import-#{SecureRandom.hex(4)}"
end end
end end
let!(:subgroup) do let(:subgroup) do
Resource::Group.fabricate_via_api! do |group| Resource::Group.fabricate_via_api! do |group|
group.api_client = api_client group.api_client = api_client
group.sandbox = source_group group.sandbox = source_group
@ -63,6 +63,10 @@ module QA
before do before do
sandbox.add_member(user, Resource::Members::AccessLevel::MAINTAINER) sandbox.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
# create groups explicitly before connecting gitlab instance
source_group
subgroup
Flow::Login.sign_in(as: user) Flow::Login.sign_in(as: user)
Page::Main::Menu.perform(&:go_to_create_group) Page::Main::Menu.perform(&:go_to_create_group)
Page::Group::New.perform do |group| Page::Group::New.perform do |group|
@ -73,6 +77,7 @@ module QA
# Non blocking issues: # Non blocking issues:
# https://gitlab.com/gitlab-org/gitlab/-/issues/331252 # https://gitlab.com/gitlab-org/gitlab/-/issues/331252
# https://gitlab.com/gitlab-org/gitlab/-/issues/333678 <- can cause 500 when creating user and group back to back
it( it(
'imports group with subgroups and labels', 'imports group with subgroups and labels',
testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1785', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1785',
@ -96,9 +101,9 @@ module QA
Page::Group::BulkImport.perform do |import_page| Page::Group::BulkImport.perform do |import_page|
import_page.import_group(source_group.path, sandbox.path) import_page.import_group(source_group.path, sandbox.path)
aggregate_failures do
expect(import_page).to have_imported_group(source_group.path, wait: 180) expect(import_page).to have_imported_group(source_group.path, wait: 180)
aggregate_failures do
expect { imported_group.reload! }.to eventually_eq(source_group).within(duration: 10) expect { imported_group.reload! }.to eventually_eq(source_group).within(duration: 10)
expect { imported_group.labels }.to eventually_include(*source_group.labels).within(duration: 10) expect { imported_group.labels }.to eventually_include(*source_group.labels).within(duration: 10)

View File

@ -39,9 +39,9 @@ module QA
end end
context 'when using attachments in comments', :object_storage do context 'when using attachments in comments', :object_storage do
let(:gif_file_name) { 'banana_sample.gif' } let(:png_file_name) { 'testfile.png' }
let(:file_to_attach) do let(:file_to_attach) do
File.absolute_path(File.join('qa', 'fixtures', 'designs', gif_file_name)) File.absolute_path(File.join('qa', 'fixtures', 'designs', png_file_name))
end end
before do before do
@ -50,9 +50,9 @@ module QA
it 'comments on an issue with an attachment', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1742' do it 'comments on an issue with an attachment', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1742' do
Page::Project::Issue::Show.perform do |show| Page::Project::Issue::Show.perform do |show|
show.comment('See attached banana for scale', attachment: file_to_attach) show.comment('See attached image for scale', attachment: file_to_attach)
expect(show.noteable_note_item.find("img[src$='#{gif_file_name}']")).to be_visible expect(show.noteable_note_item.find("img[src$='#{png_file_name}']")).to be_visible
end end
end end
end end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
FactoryBot.define do
factory :ci_pending_build, class: 'Ci::PendingBuild' do
build factory: :ci_build
project
protected { build.protected }
end
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
FactoryBot.define do
factory :ci_running_build, class: 'Ci::RunningBuild' do
build factory: :ci_build
project
runner factory: :ci_runner
runner_type { runner.runner_type }
end
end

View File

@ -1,7 +1,7 @@
import { GlLabel, GlLoadingIcon } from '@gitlab/ui'; import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { range } from 'lodash'; import { range } from 'lodash';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue'; import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue'; import BoardCardInner from '~/boards/components/board_card_inner.vue';
import { issuableTypes } from '~/boards/constants'; import { issuableTypes } from '~/boards/constants';
@ -35,8 +35,14 @@ describe('Board card component', () => {
let store; let store;
const findBoardBlockedIcon = () => wrapper.find(BoardBlockedIcon); const findBoardBlockedIcon = () => wrapper.find(BoardBlockedIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEpicCountablesTotalTooltip = () => wrapper.findComponent(GlTooltip);
const findEpicCountables = () => wrapper.findByTestId('epic-countables');
const findEpicCountablesBadgeIssues = () => wrapper.findByTestId('epic-countables-counts-issues');
const findEpicCountablesBadgeWeight = () => wrapper.findByTestId('epic-countables-weight-issues');
const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight');
const createStore = () => { const createStore = ({ isEpicBoard = false } = {}) => {
store = new Vuex.Store({ store = new Vuex.Store({
...defaultStore, ...defaultStore,
state: { state: {
@ -45,16 +51,14 @@ describe('Board card component', () => {
}, },
getters: { getters: {
isGroupBoard: () => true, isGroupBoard: () => true,
isEpicBoard: () => false, isEpicBoard: () => isEpicBoard,
isProjectBoard: () => false, isProjectBoard: () => false,
}, },
}); });
}; };
const createWrapper = (props = {}) => { const createWrapper = (props = {}) => {
createStore(); wrapper = mountExtended(BoardCardInner, {
wrapper = mount(BoardCardInner, {
store, store,
propsData: { propsData: {
list, list,
@ -88,6 +92,7 @@ describe('Board card component', () => {
weight: 1, weight: 1,
}; };
createStore();
createWrapper({ item: issue, list }); createWrapper({ item: issue, list });
}); });
@ -414,7 +419,90 @@ describe('Board card component', () => {
}, },
}); });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); expect(findLoadingIcon().exists()).toBe(true);
});
});
describe('is an epic board', () => {
const descendantCounts = {
closedEpics: 0,
closedIssues: 0,
openedEpics: 0,
openedIssues: 0,
};
const descendantWeightSum = {
closedIssues: 0,
openedIssues: 0,
};
beforeEach(() => {
createStore({ isEpicBoard: true });
});
it('should render if the item has issues', () => {
createWrapper({
item: {
...issue,
descendantCounts,
descendantWeightSum,
hasIssues: true,
},
});
expect(findEpicCountables().exists()).toBe(true);
});
it('should not render if the item does not have issues', () => {
createWrapper({
item: {
...issue,
descendantCounts,
descendantWeightSum,
hasIssues: false,
},
});
expect(findEpicCountablesBadgeIssues().exists()).toBe(false);
});
it('shows render item countBadge and weights correctly', () => {
createWrapper({
item: {
...issue,
descendantCounts: {
...descendantCounts,
openedIssues: 1,
},
descendantWeightSum: {
...descendantWeightSum,
openedIssues: 2,
},
hasIssues: true,
},
});
expect(findEpicCountablesBadgeIssues().text()).toBe('1');
expect(findEpicCountablesBadgeWeight().text()).toBe('2');
});
it('renders the tooltip with the correct data', () => {
createWrapper({
item: {
...issue,
descendantCounts,
descendantWeightSum: {
closedIssues: 10,
openedIssues: 5,
},
hasIssues: true,
},
});
const tooltip = findEpicCountablesTotalTooltip();
expect(tooltip).toBeDefined();
expect(findEpicCountablesTotalWeight().text()).toBe('10 complete, 5 incomplete');
}); });
}); });
}); });

View File

@ -1,33 +1,17 @@
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { Extension } from '@tiptap/core';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import ToolbarButton from '~/content_editor/components/toolbar_button.vue'; import ToolbarButton from '~/content_editor/components/toolbar_button.vue';
import { createContentEditor } from '~/content_editor/services/create_content_editor'; import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_button', () => { describe('content_editor/components/toolbar_button', () => {
let wrapper; let wrapper;
let tiptapEditor; let tiptapEditor;
let toggleFooSpy;
const CONTENT_TYPE = 'bold'; const CONTENT_TYPE = 'bold';
const ICON_NAME = 'bold'; const ICON_NAME = 'bold';
const LABEL = 'Bold'; const LABEL = 'Bold';
const buildEditor = () => { const buildEditor = () => {
toggleFooSpy = jest.fn(); tiptapEditor = createTestEditor();
tiptapEditor = createContentEditor({
extensions: [
{
tiptapExtension: Extension.create({
addCommands() {
return {
toggleFoo: () => toggleFooSpy,
};
},
}),
},
],
renderMarkdown: () => true,
}).tiptapEditor;
jest.spyOn(tiptapEditor, 'isActive'); jest.spyOn(tiptapEditor, 'isActive');
}; };
@ -78,20 +62,28 @@ describe('content_editor/components/toolbar_button', () => {
describe('when button is clicked', () => { describe('when button is clicked', () => {
it('executes the content type command when executeCommand = true', async () => { it('executes the content type command when executeCommand = true', async () => {
buildWrapper({ editorCommand: 'toggleFoo' }); const editorCommand = 'toggleFoo';
const mockCommands = mockChainedCommands(tiptapEditor, [editorCommand, 'focus', 'run']);
buildWrapper({ editorCommand });
await findButton().trigger('click'); await findButton().trigger('click');
expect(toggleFooSpy).toHaveBeenCalled(); expect(mockCommands[editorCommand]).toHaveBeenCalled();
expect(mockCommands.focus).toHaveBeenCalled();
expect(mockCommands.run).toHaveBeenCalled();
expect(wrapper.emitted().execute).toHaveLength(1); expect(wrapper.emitted().execute).toHaveLength(1);
}); });
it('does not executes the content type command when executeCommand = false', async () => { it('does not executes the content type command when executeCommand = false', async () => {
const editorCommand = 'toggleFoo';
const mockCommands = mockChainedCommands(tiptapEditor, [editorCommand, 'run']);
buildWrapper(); buildWrapper();
await findButton().trigger('click'); await findButton().trigger('click');
expect(toggleFooSpy).not.toHaveBeenCalled(); expect(mockCommands[editorCommand]).not.toHaveBeenCalled();
expect(wrapper.emitted().execute).toHaveLength(1); expect(wrapper.emitted().execute).toHaveLength(1);
}); });
}); });

View File

@ -2,21 +2,16 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue'; import ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue';
import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants'; import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants';
import { createTestContentEditorExtension, createTestEditor } from '../test_utils'; import { tiptapExtension as Heading } from '~/content_editor/extensions/heading';
import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/toolbar_headings_dropdown', () => { describe('content_editor/components/toolbar_headings_dropdown', () => {
let wrapper; let wrapper;
let tiptapEditor; let tiptapEditor;
let commandMocks;
const buildEditor = () => { const buildEditor = () => {
const testExtension = createTestContentEditorExtension({
commands: TEXT_STYLE_DROPDOWN_ITEMS.map((item) => item.editorCommand),
});
commandMocks = testExtension.commandMocks;
tiptapEditor = createTestEditor({ tiptapEditor = createTestEditor({
extensions: [testExtension.tiptapExtension], extensions: [Heading],
}); });
jest.spyOn(tiptapEditor, 'isActive'); jest.spyOn(tiptapEditor, 'isActive');
@ -104,9 +99,12 @@ describe('content_editor/components/toolbar_headings_dropdown', () => {
TEXT_STYLE_DROPDOWN_ITEMS.forEach((textStyle, index) => { TEXT_STYLE_DROPDOWN_ITEMS.forEach((textStyle, index) => {
const { editorCommand, commandParams } = textStyle; const { editorCommand, commandParams } = textStyle;
const commands = mockChainedCommands(tiptapEditor, [editorCommand, 'focus', 'run']);
wrapper.findAllComponents(GlDropdownItem).at(index).vm.$emit('click'); wrapper.findAllComponents(GlDropdownItem).at(index).vm.$emit('click');
expect(commandMocks[editorCommand]).toHaveBeenCalledWith(commandParams || {}); expect(commands[editorCommand]).toHaveBeenCalledWith(commandParams || {});
expect(commands.focus).toHaveBeenCalled();
expect(commands.run).toHaveBeenCalled();
}); });
}); });

View File

@ -1,26 +1,23 @@
import { BulletList } from '@tiptap/extension-bullet-list';
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
import { Document } from '@tiptap/extension-document';
import { Heading } from '@tiptap/extension-heading';
import { ListItem } from '@tiptap/extension-list-item';
import { Paragraph } from '@tiptap/extension-paragraph';
import { Text } from '@tiptap/extension-text';
import { Editor } from '@tiptap/vue-2';
import { mockTracking } from 'helpers/tracking_helper'; import { mockTracking } from 'helpers/tracking_helper';
import { import {
KEYBOARD_SHORTCUT_TRACKING_ACTION, KEYBOARD_SHORTCUT_TRACKING_ACTION,
INPUT_RULE_TRACKING_ACTION, INPUT_RULE_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL, CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants'; } from '~/content_editor/constants';
import { tiptapExtension as BulletList } from '~/content_editor/extensions/bullet_list';
import { tiptapExtension as CodeBlockLowlight } from '~/content_editor/extensions/code_block_highlight';
import { tiptapExtension as Heading } from '~/content_editor/extensions/heading';
import { tiptapExtension as ListItem } from '~/content_editor/extensions/list_item';
import trackInputRulesAndShortcuts from '~/content_editor/services/track_input_rules_and_shortcuts'; import trackInputRulesAndShortcuts from '~/content_editor/services/track_input_rules_and_shortcuts';
import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys'; import { ENTER_KEY, BACKSPACE_KEY } from '~/lib/utils/keys';
import { createTestEditor } from '../test_utils';
describe('content_editor/services/track_input_rules_and_shortcuts', () => { describe('content_editor/services/track_input_rules_and_shortcuts', () => {
let trackingSpy; let trackingSpy;
let editor; let editor;
let trackedExtensions; let trackedExtensions;
const HEADING_TEXT = 'Heading text'; const HEADING_TEXT = 'Heading text';
const extensions = [Document, Paragraph, Text, Heading, CodeBlockLowlight, BulletList, ListItem]; const extensions = [Heading, CodeBlockLowlight, BulletList, ListItem];
beforeEach(() => { beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn); trackingSpy = mockTracking(undefined, null, jest.spyOn);
@ -29,7 +26,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => {
describe('given the heading extension is instrumented', () => { describe('given the heading extension is instrumented', () => {
beforeEach(() => { beforeEach(() => {
trackedExtensions = extensions.map(trackInputRulesAndShortcuts); trackedExtensions = extensions.map(trackInputRulesAndShortcuts);
editor = new Editor({ editor = createTestEditor({
extensions: extensions.map(trackInputRulesAndShortcuts), extensions: extensions.map(trackInputRulesAndShortcuts),
}); });
}); });

View File

@ -15,7 +15,7 @@ import { Editor } from '@tiptap/vue-2';
* include in the editor * include in the editor
* @returns An instance of a Tiptaps Editor class * @returns An instance of a Tiptaps Editor class
*/ */
export const createTestEditor = ({ extensions = [] }) => { export const createTestEditor = ({ extensions = [] } = {}) => {
return new Editor({ return new Editor({
extensions: [Document, Text, Paragraph, ...extensions], extensions: [Document, Text, Paragraph, ...extensions],
}); });

View File

@ -1,9 +1,9 @@
import '~/flash';
import { GlModal, GlLoadingIcon } from '@gitlab/ui'; import { GlModal, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue'; import Vue from 'vue';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import appComponent from '~/groups/components/app.vue'; import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue'; import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue'; import groupItemComponent from '~/groups/components/group_item.vue';
@ -27,6 +27,7 @@ import {
const $toast = { const $toast = {
show: jest.fn(), show: jest.fn(),
}; };
jest.mock('~/flash');
describe('AppComponent', () => { describe('AppComponent', () => {
let wrapper; let wrapper;
@ -123,12 +124,12 @@ describe('AppComponent', () => {
mock.onGet('/dashboard/groups.json').reply(400); mock.onGet('/dashboard/groups.json').reply(400);
jest.spyOn(window, 'scrollTo').mockImplementation(() => {}); jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
jest.spyOn(window, 'Flash').mockImplementation(() => {});
return vm.fetchGroups({}).then(() => { return vm.fetchGroups({}).then(() => {
expect(vm.isLoading).toBe(false); expect(vm.isLoading).toBe(false);
expect(window.scrollTo).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 }); expect(window.scrollTo).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 });
expect(window.Flash).toHaveBeenCalledWith('An error occurred. Please try again.'); expect(createFlash).toHaveBeenCalledWith({
message: 'An error occurred. Please try again.',
});
}); });
}); });
}); });
@ -324,15 +325,13 @@ describe('AppComponent', () => {
const message = 'An error occurred. Please try again.'; const message = 'An error occurred. Please try again.';
jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 500 }); jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 500 });
jest.spyOn(vm.store, 'removeGroup'); jest.spyOn(vm.store, 'removeGroup');
jest.spyOn(window, 'Flash').mockImplementation(() => {});
vm.leaveGroup(); vm.leaveGroup();
expect(vm.targetGroup.isBeingRemoved).toBe(true); expect(vm.targetGroup.isBeingRemoved).toBe(true);
expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
return waitForPromises().then(() => { return waitForPromises().then(() => {
expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(vm.store.removeGroup).not.toHaveBeenCalled();
expect(window.Flash).toHaveBeenCalledWith(message); expect(createFlash).toHaveBeenCalledWith({ message });
expect(vm.targetGroup.isBeingRemoved).toBe(false); expect(vm.targetGroup.isBeingRemoved).toBe(false);
}); });
}); });
@ -341,15 +340,13 @@ describe('AppComponent', () => {
const message = 'Failed to leave the group. Please make sure you are not the only owner.'; const message = 'Failed to leave the group. Please make sure you are not the only owner.';
jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 403 }); jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 403 });
jest.spyOn(vm.store, 'removeGroup'); jest.spyOn(vm.store, 'removeGroup');
jest.spyOn(window, 'Flash').mockImplementation(() => {});
vm.leaveGroup(childGroupItem, groupItem); vm.leaveGroup(childGroupItem, groupItem);
expect(vm.targetGroup.isBeingRemoved).toBe(true); expect(vm.targetGroup.isBeingRemoved).toBe(true);
expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
return waitForPromises().then(() => { return waitForPromises().then(() => {
expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(vm.store.removeGroup).not.toHaveBeenCalled();
expect(window.Flash).toHaveBeenCalledWith(message); expect(createFlash).toHaveBeenCalledWith({ message });
expect(vm.targetGroup.isBeingRemoved).toBe(false); expect(vm.targetGroup.isBeingRemoved).toBe(false);
}); });
}); });

View File

@ -190,7 +190,9 @@ describe('UploadBlobModal', () => {
}); });
it('creates a flash error', () => { it('creates a flash error', () => {
expect(createFlash).toHaveBeenCalledWith('Error uploading file. Please try again.'); expect(createFlash).toHaveBeenCalledWith({
message: 'Error uploading file. Please try again.',
});
}); });
afterEach(() => { afterEach(() => {

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!('cascade_delete_freeze_periods')
RSpec.describe CascadeDeleteFreezePeriods do
let(:namespace) { table(:namespaces).create!(name: 'deploy_freeze', path: 'deploy_freeze') }
let(:project) { table(:projects).create!(id: 1, namespace_id: namespace.id) }
let(:freeze_periods) { table(:ci_freeze_periods) }
describe "#up" do
it 'allows for a project to be deleted' do
freeze_periods.create!(id: 1, project_id: project.id, freeze_start: '5 * * * *', freeze_end: '6 * * * *', cron_timezone: 'UTC')
migrate!
project.delete
expect(freeze_periods.where(project_id: project.id).count).to be_zero
end
end
end

View File

@ -384,7 +384,7 @@ RSpec.describe Ci::Build do
context 'when there is a queuing entry already present' do context 'when there is a queuing entry already present' do
before do before do
::Ci::PendingBuild.create!(build: build, project: build.project) create(:ci_pending_build, build: build, project: build.project)
end end
it 'does not raise an error' do it 'does not raise an error' do
@ -396,7 +396,7 @@ RSpec.describe Ci::Build do
context 'when both failure scenario happen at the same time' do context 'when both failure scenario happen at the same time' do
before do before do
::Ci::Build.find(build.id).update_column(:lock_version, 100) ::Ci::Build.find(build.id).update_column(:lock_version, 100)
::Ci::PendingBuild.create!(build: build, project: build.project) create(:ci_pending_build, build: build, project: build.project)
end end
it 'raises stale object error exception' do it 'raises stale object error exception' do
@ -478,7 +478,7 @@ RSpec.describe Ci::Build do
let(:build) { create(:ci_build, :pending) } let(:build) { create(:ci_build, :pending) }
before do before do
::Ci::PendingBuild.create!(build: build, project: build.project) create(:ci_pending_build, build: build, project: build.project)
::Ci::Build.find(build.id).update_column(:lock_version, 100) ::Ci::Build.find(build.id).update_column(:lock_version, 100)
end end

View File

@ -20,7 +20,7 @@ RSpec.describe Ci::PendingBuild do
context 'when another queuing entry exists for given build' do context 'when another queuing entry exists for given build' do
before do before do
described_class.create!(build: build, project: project, protected: false) create(:ci_pending_build, build: build, project: project)
end end
it 'returns a build id as a result' do it 'returns a build id as a result' do

View File

@ -21,10 +21,7 @@ RSpec.describe Ci::RunningBuild do
context 'when another queuing entry exists for given build' do context 'when another queuing entry exists for given build' do
before do before do
described_class.create!(build: build, create(:ci_running_build, build: build, project: project, runner: runner)
project: project,
runner: runner,
runner_type: runner.runner_type)
end end
it 'returns a build id as a result' do it 'returns a build id as a result' do

View File

@ -45,7 +45,7 @@ RSpec.describe Ci::UpdateBuildQueueService do
context 'when duplicate entry exists' do context 'when duplicate entry exists' do
before do before do
::Ci::PendingBuild.create!(build: build, project: project) create(:ci_pending_build, build: build, project: build.project)
end end
it 'does nothing and returns build id' do it 'does nothing and returns build id' do
@ -66,7 +66,7 @@ RSpec.describe Ci::UpdateBuildQueueService do
context 'when pending build exists' do context 'when pending build exists' do
before do before do
Ci::PendingBuild.create!(build: build, project: project) create(:ci_pending_build, build: build, project: build.project)
end end
it 'removes pending build in a transaction' do it 'removes pending build in a transaction' do
@ -146,9 +146,7 @@ RSpec.describe Ci::UpdateBuildQueueService do
context 'when duplicate entry exists' do context 'when duplicate entry exists' do
before do before do
::Ci::RunningBuild.create!( create(:ci_running_build, build: build, project: project, runner: runner)
build: build, project: project, runner: runner, runner_type: runner.runner_type
)
end end
it 'does nothing and returns build id' do it 'does nothing and returns build id' do
@ -169,9 +167,7 @@ RSpec.describe Ci::UpdateBuildQueueService do
context 'when shared runner build tracking entry exists' do context 'when shared runner build tracking entry exists' do
before do before do
Ci::RunningBuild.create!( create(:ci_running_build, build: build, project: project, runner: runner)
build: build, project: project, runner: runner, runner_type: runner.runner_type
)
end end
it 'removes shared runner build' do it 'removes shared runner build' do