Commit 12f4a202 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents f15e5d07 67d0cc39
...@@ -165,7 +165,7 @@ export default { ...@@ -165,7 +165,7 @@ export default {
</p> </p>
<a <a
:href="links.webIDEHelpPagePath" :href="links.webIDEHelpPagePath"
class="btn btn-primary" class="btn gl-button btn-confirm"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
......
<script> <script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
export default { export default {
name: 'PipelineFailed', name: 'PipelineFailed',
components: { components: {
GlLink,
GlSprintf,
statusIcon, statusIcon,
}, },
computed: {
troubleshootingDocsPath() {
return helpPagePath('ci/troubleshooting', { anchor: 'merge-request-status-messages' });
},
},
i18n: {
failedMessage: s__(
`mrWidget|The pipeline for this merge request did not complete. Push a new commit to fix the failure or check the %{linkStart}troubleshooting documentation%{linkEnd} to see other possible actions.`,
),
},
}; };
</script> </script>
...@@ -14,10 +29,13 @@ export default { ...@@ -14,10 +29,13 @@ export default {
<status-icon :show-disabled-button="true" status="warning" /> <status-icon :show-disabled-button="true" status="warning" />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
{{ <gl-sprintf :message="$options.i18n.failedMessage">
s__(`mrWidget|The pipeline for this merge request failed. <template #link="{ content }">
Please retry the job or push a new commit to fix the failure`) <gl-link :href="troubleshootingDocsPath" target="_blank">
}} {{ content }}
</gl-link>
</template>
</gl-sprintf>
</span> </span>
</div> </div>
</div> </div>
......
...@@ -2,19 +2,18 @@ ...@@ -2,19 +2,18 @@
%h4 %h4
= _('Variables') = _('Variables')
= link_to sprite_icon('question-o', css_class: 'gl-vertical-align-baseline!'), help_page_path('ci/variables/README', anchor: 'custom-environment-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' } %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p %p
= html_escape(_('Environment variables are applied to environments via the Runner. You can use environment variables for passwords, secret keys, etc. Make variables available to the running application by prepending the variable key with %{code_open}K8S_SECRET_%{code_close}. You can set variables to be:')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } = _('Variables store information, like passwords and secret keys, that you can use in job scripts. All projects on the instance can use these variables.')
= link_to s_('Learn more.'), help_page_path('ci/variables/README', anchor: 'instance-level-cicd-environment-variables'), target: '_blank', rel: 'noopener noreferrer'
%p
= _('Variables can be:')
%ul %ul
%li %li
= html_escape(_('%{code_open}Protected%{code_close} variables are only exposed to protected branches or tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } = html_escape(_('%{code_open}Protected:%{code_close} Only exposed to protected branches or tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li %li
= html_escape(_('%{code_open}Masked%{code_close} variables are hidden in job logs (though they must match certain regexp requirements to do so).')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } = html_escape(_('%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to _('Learn more.'), help_page_path('ci/variables/README', anchor: 'masked-variable-requirements'), target: '_blank', rel: 'noopener noreferrer'
%p
= link_to _('More information'), help_page_path('ci/variables/README', anchor: 'instance-level-cicd-environment-variables')
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
- if ci_variable_protected_by_default? - if ci_variable_protected_by_default?
%p.settings-message.text-center %p.settings-message.text-center
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protect-a-custom-variable') } - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protect-a-custom-variable') }
= s_('Environment variables on this GitLab instance are configured to be %{link_start}protected%{link_end} by default').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } = s_('Environment variables on this GitLab instance are configured to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
#js-instance-variables{ data: { endpoint: admin_ci_variables_path, group: 'true', maskable_regex: ci_variable_maskable_regex, protected_by_default: ci_variable_protected_by_default?.to_s} } #js-instance-variables{ data: { endpoint: admin_ci_variables_path, group: 'true', maskable_regex: ci_variable_maskable_regex, protected_by_default: ci_variable_protected_by_default?.to_s} }
%section.settings.as-ci-cd.no-animate#js-ci-cd-settings{ class: ('expanded' if expanded_by_default?) } %section.settings.as-ci-cd.no-animate#js-ci-cd-settings{ class: ('expanded' if expanded_by_default?) }
......
...@@ -45,5 +45,5 @@ ...@@ -45,5 +45,5 @@
= render "shared/tokens/scopes_list", token: @application = render "shared/tokens/scopes_list", token: @application
.form-actions .form-actions
= link_to 'Edit', edit_admin_application_path(@application), class: 'gl-button btn btn-primary wide float-left' = link_to 'Edit', edit_admin_application_path(@application), class: 'gl-button btn btn-confirm wide float-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger gl-ml-3' = render 'delete_form', application: @application, submit_btn_css: 'gl-button btn btn-danger gl-ml-3'
...@@ -32,6 +32,6 @@ ...@@ -32,6 +32,6 @@
= render 'shared/projects/dropdown' = render 'shared/projects/dropdown'
= link_to new_project_path, class: 'gl-button btn btn-success' do = link_to new_project_path, class: 'gl-button btn btn-success' do
New Project New Project
= button_tag "Search", class: "gl-button btn btn-primary btn-search hide" = button_tag "Search", class: "gl-button btn btn-confirm btn-search hide"
= render 'projects' = render 'projects'
...@@ -150,7 +150,7 @@ ...@@ -150,7 +150,7 @@
.form-group.row .form-group.row
.offset-sm-3.col-sm-9 .offset-sm-3.col-sm-9
= f.submit _('Transfer'), class: 'gl-button btn btn-primary' = f.submit _('Transfer'), class: 'gl-button btn btn-confirm'
.card.repository-check .card.repository-check
.card-header .card-header
...@@ -170,7 +170,7 @@ ...@@ -170,7 +170,7 @@
= link_to sprite_icon('question-o'), help_page_path('administration/repository_checks') = link_to sprite_icon('question-o'), help_page_path('administration/repository_checks')
.form-group .form-group
= f.submit _('Trigger repository check'), class: 'gl-button btn btn-primary' = f.submit _('Trigger repository check'), class: 'gl-button btn btn-confirm'
.col-md-6 .col-md-6
- if @group - if @group
......
= _("These variables are configured in the parent group settings, and will be active in the current project in addition to the project variables.") = _("These variables are inherited from the parent group.")
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
.bold.table-section.section-40.gl-mr-3 .bold.table-section.section-40.gl-mr-3
= s_('Key') = s_('Key')
.bold.table-section.section-40.gl-mr-3 .bold.table-section.section-40.gl-mr-3
= s_('Origin') = s_('Group')
= html_escape(_('Environment variables are applied to environments via the Runner. You can use environment variables for passwords, secret keys, etc. Make variables available to the running application by prepending the variable key with %{code_open}K8S_SECRET_%{code_close}. You can set variables to be:')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } = _('Variables store information, like passwords and secret keys, that you can use in job scripts.')
= link_to s_('Learn more.'), help_page_path('ci/variables/README'), target: '_blank', rel: 'noopener noreferrer'
%p
= _('Variables can be:')
%ul %ul
%li %li
= html_escape(_('%{code_open}Protected%{code_close} variables are only exposed to protected branches or tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } = html_escape(_('%{code_open}Protected:%{code_close} Only exposed to protected branches or tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li %li
= html_escape(_('%{code_open}Masked%{code_close} variables are hidden in job logs (though they must match certain regexp requirements to do so).')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } = html_escape(_('%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to _('Learn more.'), help_page_path('ci/variables/README', anchor: 'masked-variable-requirements'), target: '_blank', rel: 'noopener noreferrer'
= link_to _('More information'), help_page_path('ci/variables/README', anchor: 'custom-environment-variables')
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
%h4 %h4
= _('Variables') = _('Variables')
= link_to sprite_icon('question-o', css_class: 'gl-vertical-align-baseline!'), help_page_path('ci/variables/README', anchor: 'custom-environment-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: 'button' } %button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- if ci_variable_protected_by_default? - if ci_variable_protected_by_default?
%p.settings-message.text-center %p.settings-message.text-center
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protect-a-custom-variable') } - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protect-a-custom-variable') }
= s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } = s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- is_group = !@group.nil? - is_group = !@group.nil?
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
= f.label 'Confirm new password', for: "user_password_confirmation" = f.label 'Confirm new password', for: "user_password_confirmation"
= f.password_field :password_confirmation, class: "form-control gl-form-input bottom", title: 'This field is required', data: { qa_selector: 'password_confirmation_field' }, required: true = f.password_field :password_confirmation, class: "form-control gl-form-input bottom", title: 'This field is required', data: { qa_selector: 'password_confirmation_field' }, required: true
.clearfix .clearfix
= f.submit "Change your password", class: "gl-button btn btn-info", data: { qa_selector: 'change_password_button' } = f.submit "Change your password", class: "gl-button btn btn-confirm", data: { qa_selector: 'change_password_button' }
.clearfix.prepend-top-20 .clearfix.prepend-top-20
%p %p
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= f.label :email = f.label :email
= f.email_field :email, class: "form-control gl-form-input", required: true, value: params[:user_email], autofocus: true, title: 'Please provide a valid email address.' = f.email_field :email, class: "form-control gl-form-input", required: true, value: params[:user_email], autofocus: true, title: 'Please provide a valid email address.'
.clearfix .clearfix
= f.submit "Reset password", class: "gl-button btn-info btn" = f.submit "Reset password", class: "gl-button btn-confirm btn"
.clearfix.prepend-top-20 .clearfix.prepend-top-20
= render 'devise/shared/sign_in_link' = render 'devise/shared/sign_in_link'
...@@ -43,5 +43,5 @@ ...@@ -43,5 +43,5 @@
= render "shared/tokens/scopes_list", token: @application = render "shared/tokens/scopes_list", token: @application
.form-actions .form-actions
= link_to _('Edit'), edit_oauth_application_path(@application), class: 'gl-button btn btn-primary wide float-left' = link_to _('Edit'), edit_oauth_application_path(@application), class: 'gl-button btn btn-confirm wide float-left'
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger gl-ml-3' = render 'delete_form', application: @application, submit_btn_css: 'gl-button btn btn-danger gl-ml-3'
...@@ -141,12 +141,12 @@ ...@@ -141,12 +141,12 @@
%img.modal-profile-crop-image{ alt: s_("Profiles|Avatar cropper") } %img.modal-profile-crop-image{ alt: s_("Profiles|Avatar cropper") }
.crop-controls .crop-controls
.btn-group .btn-group
%button.btn.btn-primary{ data: { method: 'zoom', option: '-0.1' } } %button.btn.gl-button.btn-confirm{ data: { method: 'zoom', option: '-0.1' } }
%span %span
= sprite_icon('search-minus') = sprite_icon('search-minus')
%button.btn.btn-primary{ data: { method: 'zoom', option: '0.1' } } %button.btn.gl-button.btn-confirm{ data: { method: 'zoom', option: '0.1' } }
%span %span
= sprite_icon('search-plus') = sprite_icon('search-plus')
.modal-footer .modal-footer
%button.btn.btn-primary.js-upload-user-avatar{ type: 'button' } %button.btn.gl-button.btn-confirm.js-upload-user-avatar{ type: 'button' }
= s_("Profiles|Set new profile picture") = s_("Profiles|Set new profile picture")
.btn-group.ml-0.w-100 .btn-group.ml-0.w-100
- Gitlab::Workhorse::ARCHIVE_FORMATS.each_with_index do |fmt, index| - Gitlab::Workhorse::ARCHIVE_FORMATS.each_with_index do |fmt, index|
- archive_path = project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt) - archive_path = project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt)
= link_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', class: "gl-button btn btn-xs #{"btn-primary" if index == 0}" = link_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', class: "gl-button btn btn-xs #{"btn-confirm" if index == 0}"
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- if @project&.context_commits_enabled? && can_update_merge_request - if @project&.context_commits_enabled? && can_update_merge_request
%p %p
= _('Push commits to the source branch or add previously merged commits to review them.') = _('Push commits to the source branch or add previously merged commits to review them.')
%button.btn.btn-primary.add-review-item-modal-trigger{ type: "button", data: { commits_empty: 'true', context_commits_empty: 'true' } } %button.btn.gl-button.btn-confirm.add-review-item-modal-trigger{ type: "button", data: { commits_empty: 'true', context_commits_empty: 'true' } }
= _('Add previously merged commits') = _('Add previously merged commits')
- else - else
%ol#commits-list.list-unstyled %ol#commits-list.list-unstyled
......
...@@ -15,5 +15,5 @@ ...@@ -15,5 +15,5 @@
%p %p
= link_to _('Unsubscribe'), unsubscribe_sent_notification_path(@sent_notification, force: true), = link_to _('Unsubscribe'), unsubscribe_sent_notification_path(@sent_notification, force: true),
class: 'gl-button btn btn-primary gl-mr-3' class: 'gl-button btn btn-confirm gl-mr-3'
= link_to _('Cancel'), new_user_session_path, class: 'gl-button btn gl-mr-3' = link_to _('Cancel'), new_user_session_path, class: 'gl-button btn gl-mr-3'
---
title: Improve variable settings ui text
merge_request: 52462
author:
type: other
---
title: Change UI text for failed pipeline on an MR
merge_request: 52023
author:
type: changed
---
title: Move btn-primary to btn-confirm class as a part of Pajamas migration
merge_request: 52090
author: Yogi (@yo)
type: changed
...@@ -222,6 +222,29 @@ This also applies if the pipeline has not been created yet, or if you are waitin ...@@ -222,6 +222,29 @@ This also applies if the pipeline has not been created yet, or if you are waitin
for an external CI service. If you don't use pipelines for your project, then you for an external CI service. If you don't use pipelines for your project, then you
should disable **Pipelines must succeed** so you can accept merge requests. should disable **Pipelines must succeed** so you can accept merge requests.
### "The pipeline for this merge request did not complete. Push a new commit to fix the failure or check the troubleshooting documentation to see other possible actions." message
This message is shown if the [merge request pipeline](merge_request_pipelines/index.md),
[merged results pipeline](merge_request_pipelines/pipelines_for_merged_results/index.md),
or [merge train pipeline](merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md)
has failed or been canceled.
If a merge request pipeline or merged result pipeline was canceled or failed, you can:
- Re-run the entire pipeline by clicking **Run pipeline** in the pipeline tab in the merge request.
- [Retry only the jobs that failed](pipelines/index.md#view-pipelines). If you re-run the entire pipeline, this is not necessary.
- Push a new commit to fix the failure.
If the merge train pipeline has failed, you can:
- Check the failure and determine if you can use the [`/merge` quick action](../user/project/quick_actions.md) to immediately add the merge request to the train again.
- Re-run the entire pipeline by clicking **Run pipeline** in the pipeline tab in the merge request, then add the merge request to the train again.
- Push a commit to fix the failure, then add the merge request to the train again.
If the merge train pipeline was canceled before the merge request was merged, without a failure, you can:
- Add it to the train again.
## Pipeline warnings ## Pipeline warnings
Pipeline configuration warnings are shown when you: Pipeline configuration warnings are shown when you:
......
...@@ -16,13 +16,15 @@ to them. ...@@ -16,13 +16,15 @@ to them.
> - The New Epic form [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211533) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2. > - The New Epic form [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211533) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
> - In [GitLab 13.7](https://gitlab.com/gitlab-org/gitlab/-/issues/229621) and later, the New Epic button on the Epics list opens the New Epic form. > - In [GitLab 13.7](https://gitlab.com/gitlab-org/gitlab/-/issues/229621) and later, the New Epic button on the Epics list opens the New Epic form.
> - In [GitLab 13.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45948) and later, you can create a new epic from an empty Roadmap.
To create an epic in the group you're in: To create an epic in the group you're in:
1. Get to the New Epic form: 1. Get to the New Epic form:
- From the **Epics** list in your group, select the **New Epic** button. - From the **Epics** list in your group, select **New epic**.
- From an epic in your group, select the **New Epic** button. - From an epic in your group, select **New epic**.
- From anywhere, in the top menu, select **New...** (**{plus-square}**) **> New epic**. - From anywhere, in the top menu, select **New...** (**{plus-square}**) **> New epic**.
- In an empty [roadmap](../roadmap/index.md), select **New epic**.
![New epic from an open epic](img/new_epic_from_groups_v13.7.png) ![New epic from an open epic](img/new_epic_from_groups_v13.7.png)
...@@ -39,7 +41,7 @@ To create an epic in the group you're in: ...@@ -39,7 +41,7 @@ To create an epic in the group you're in:
## Edit an epic ## Edit an epic
After you create an epic, you can edit change the following details: After you create an epic, you can edit the following details:
- Title - Title
- Description - Description
...@@ -152,6 +154,9 @@ To make an epic confidential: ...@@ -152,6 +154,9 @@ To make an epic confidential:
## Manage issues assigned to an epic ## Manage issues assigned to an epic
This section collects instructions for all the things you can do with [issues](../../project/issues/index.md)
in relation to epics.
### Add a new issue to an epic ### Add a new issue to an epic
You can add an existing issue to an epic, or create a new issue that's You can add an existing issue to an epic, or create a new issue that's
......
<script>
import { mapState, mapActions } from 'vuex';
import {
GlForm,
GlFormInput,
GlFormCheckbox,
GlIcon,
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
import { __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
GlFormCheckbox,
GlIcon,
GlButton,
GlForm,
GlFormInput,
},
directives: {
autofocusonshow,
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
alignRight: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState(['newEpicTitle', 'newEpicConfidential', 'epicCreateInProgress']),
buttonLabel() {
return this.epicCreateInProgress ? __('Creating epic') : __('Create epic');
},
isEpicCreateDisabled() {
return !this.newEpicTitle.length;
},
epicTitle: {
set(value) {
this.setEpicCreateTitle({
newEpicTitle: value,
});
},
get() {
return this.newEpicTitle;
},
},
epicConfidential: {
set(value) {
this.setEpicCreateConfidential({
newEpicConfidential: value,
});
},
get() {
return this.newEpicConfidential;
},
},
},
methods: {
...mapActions(['setEpicCreateTitle', 'createEpic', 'setEpicCreateConfidential']),
},
};
</script>
<template>
<div class="dropdown epic-create-dropdown">
<gl-button
category="primary"
variant="success"
data-qa-selector="new_epic_button"
data-toggle="dropdown"
>
{{ __('New epic') }}
</gl-button>
<div :class="{ 'dropdown-menu-right': alignRight }" class="dropdown-menu">
<gl-form>
<gl-form-input
ref="epicTitleInput"
v-model="epicTitle"
v-autofocusonshow
:disabled="epicCreateInProgress"
:placeholder="__('Title')"
type="text"
class="form-control"
data-qa-selector="epic_title_field"
@keyup.enter.exact="createEpic"
/>
<gl-form-checkbox
v-model="epicConfidential"
class="mt-3 mb-3 mr-0"
data-qa-selector="confidential_epic_checkbox"
><span> {{ __('Make this epic confidential') }} </span>
<span
v-gl-tooltip.viewport.top.hover
:title="
__(
'This epic and its child elements will only be visible to team members with at minimum Reporter access.',
)
"
:aria-label="
__(
'This epic and its child elements will only be visible to team members with at minimum Reporter access.',
)
"
>
<gl-icon name="question" :size="12"
/></span>
</gl-form-checkbox>
<gl-button
:disabled="isEpicCreateDisabled"
:loading="epicCreateInProgress"
category="primary"
variant="success"
class="gl-mt-3"
data-qa-selector="create_epic_button"
@click.stop="createEpic"
>{{ buttonLabel }}</gl-button
>
</gl-form>
</div>
</div>
</template>
...@@ -9,10 +9,9 @@ import { parseIssuableData } from '~/issue_show/utils/parse_data'; ...@@ -9,10 +9,9 @@ import { parseIssuableData } from '~/issue_show/utils/parse_data';
import createStore from './store'; import createStore from './store';
import EpicApp from './components/epic_app.vue'; import EpicApp from './components/epic_app.vue';
import EpicCreateApp from './components/epic_create.vue';
export default (epicCreate = false) => { export default () => {
const el = document.getElementById(epicCreate ? 'epic-create-root' : 'epic-app-root'); const el = document.getElementById('epic-app-root');
if (!el) { if (!el) {
return false; return false;
...@@ -21,28 +20,6 @@ export default (epicCreate = false) => { ...@@ -21,28 +20,6 @@ export default (epicCreate = false) => {
const store = createStore(); const store = createStore();
store.registerModule('labelsSelect', labelsSelectModule()); store.registerModule('labelsSelect', labelsSelectModule());
if (epicCreate) {
return new Vue({
el,
store,
components: { EpicCreateApp },
created() {
this.setEpicMeta({
endpoint: el.dataset.endpoint,
});
},
methods: {
...mapActions(['setEpicMeta']),
},
render: (createElement) =>
createElement('epic-create-app', {
props: {
alignRight: el.dataset.alignRight,
},
}),
});
}
const epicMeta = convertObjectPropsToCamelCase(JSON.parse(el.dataset.meta), { deep: true }); const epicMeta = convertObjectPropsToCamelCase(JSON.parse(el.dataset.meta), { deep: true });
const epicData = parseIssuableData(el); const epicData = parseIssuableData(el);
......
<script> <script>
/* eslint-disable vue/no-v-html */ import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { dateInWords } from '~/lib/utils/datetime_utility'; import { dateInWords } from '~/lib/utils/datetime_utility';
import CommonMixin from '../mixins/common_mixin'; import CommonMixin from '../mixins/common_mixin';
import { emptyStateDefault, emptyStateWithFilters } from '../constants'; import { emptyStateDefault, emptyStateWithFilters } from '../constants';
import initEpicCreate from '../../epic/epic_bundle';
export default { export default {
components: { components: {
GlButton, GlButton,
}, },
directives: {
SafeHtml: GlSafeHtmlDirective,
},
mixins: [CommonMixin], mixins: [CommonMixin],
inject: ['newEpicPath', 'listEpicsPath', 'epicsDocsPath'],
props: { props: {
presetType: { presetType: {
type: String, type: String,
...@@ -31,10 +32,6 @@ export default { ...@@ -31,10 +32,6 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
newEpicEndpoint: {
type: String,
required: true,
},
emptyStateIllustrationPath: { emptyStateIllustrationPath: {
type: String, type: String,
required: true, required: true,
...@@ -96,8 +93,7 @@ export default { ...@@ -96,8 +93,7 @@ export default {
'GroupRoadmap|To view the roadmap, add a start or due date to one of the %{linkStart}child epics%{linkEnd}.', 'GroupRoadmap|To view the roadmap, add a start or due date to one of the %{linkStart}child epics%{linkEnd}.',
), ),
{ {
linkStart: linkStart: `<a href="${this.epicsDocsPath}#multi-level-child-epics" target="_blank" rel="noopener noreferrer nofollow">`,
'<a href="https://docs.gitlab.com/ee/user/group/epics/#multi-level-child-epics" target="_blank" rel="noopener noreferrer nofollow">',
linkEnd: '</a>', linkEnd: '</a>',
}, },
false, false,
...@@ -116,36 +112,38 @@ export default { ...@@ -116,36 +112,38 @@ export default {
}); });
}, },
}, },
mounted() {
// If filters are not applied and yet user
// is seeing empty state, we need to show
// `New epic` button, so boot-up Epic app
// in create mode.
if (!this.hasFiltersApplied) {
initEpicCreate(true);
}
},
}; };
</script> </script>
<template> <template>
<div class="row empty-state"> <div class="row empty-state">
<div class="col-12"> <div class="col-12">
<div class="svg-content"><img :src="emptyStateIllustrationPath" /></div> <div class="svg-content">
<img :src="emptyStateIllustrationPath" data-testid="illustration" />
</div>
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="text-content"> <div class="text-content">
<h4>{{ message }}</h4> <h4 data-testid="title">{{ message }}</h4>
<p v-html="subMessage"></p> <p v-safe-html="subMessage" data-testid="sub-title"></p>
<div class="text-center">
<div <div class="gl-text-center">
<gl-button
v-if="!hasFiltersApplied" v-if="!hasFiltersApplied"
id="epic-create-root" :href="newEpicPath"
:data-endpoint="newEpicEndpoint" variant="success"
></div> class="gl-mt-3 gl-sm-mt-0! gl-w-full gl-sm-w-auto!"
<gl-button :title="__('List')" :href="newEpicEndpoint">{{ data-testid="new-epic-button"
s__('View epics list') >
}}</gl-button> {{ __('New epic') }}
</gl-button>
<gl-button
:href="listEpicsPath"
class="gl-mt-3 gl-sm-mt-0! gl-sm-ml-3 gl-w-full gl-sm-w-auto!"
data-testid="list-epics-button"
>
{{ __('View epics list') }}
</gl-button>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -37,10 +37,6 @@ export default { ...@@ -37,10 +37,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
newEpicEndpoint: {
type: String,
required: true,
},
emptyStateIllustrationPath: { emptyStateIllustrationPath: {
type: String, type: String,
required: true, required: true,
...@@ -179,7 +175,6 @@ export default { ...@@ -179,7 +175,6 @@ export default {
:timeframe-start="timeframeStart" :timeframe-start="timeframeStart"
:timeframe-end="timeframeEnd" :timeframe-end="timeframeEnd"
:has-filters-applied="hasFiltersApplied" :has-filters-applied="hasFiltersApplied"
:new-epic-endpoint="newEpicEndpoint"
:empty-state-illustration-path="emptyStateIllustrationPath" :empty-state-illustration-path="emptyStateIllustrationPath"
:is-child-epics="isChildEpics" :is-child-epics="isChildEpics"
/> />
......
...@@ -50,6 +50,15 @@ export default () => { ...@@ -50,6 +50,15 @@ export default () => {
components: { components: {
roadmapApp, roadmapApp,
}, },
provide() {
const { dataset } = this.$options.el;
return {
newEpicPath: dataset.newEpicPath,
listEpicsPath: dataset.listEpicsPath,
epicsDocsPath: dataset.epicsDocsPath,
};
},
data() { data() {
const supportedPresetTypes = Object.keys(PRESET_TYPES); const supportedPresetTypes = Object.keys(PRESET_TYPES);
const { dataset } = this.$options.el; const { dataset } = this.$options.el;
...@@ -83,7 +92,6 @@ export default () => { ...@@ -83,7 +92,6 @@ export default () => {
basePath: dataset.epicsPath, basePath: dataset.epicsPath,
fullPath: dataset.fullPath, fullPath: dataset.fullPath,
epicIid: dataset.iid, epicIid: dataset.iid,
newEpicEndpoint: dataset.newEpicEndpoint,
groupLabelsEndpoint: dataset.groupLabelsEndpoint, groupLabelsEndpoint: dataset.groupLabelsEndpoint,
groupMilestonesEndpoint: dataset.groupMilestonesEndpoint, groupMilestonesEndpoint: dataset.groupMilestonesEndpoint,
epicsState: dataset.epicsState, epicsState: dataset.epicsState,
...@@ -119,7 +127,6 @@ export default () => { ...@@ -119,7 +127,6 @@ export default () => {
return createElement('roadmap-app', { return createElement('roadmap-app', {
props: { props: {
presetType: this.presetType, presetType: this.presetType,
newEpicEndpoint: this.newEpicEndpoint,
emptyStateIllustrationPath: this.emptyStateIllustrationPath, emptyStateIllustrationPath: this.emptyStateIllustrationPath,
}, },
}); });
......
...@@ -25,4 +25,4 @@ ...@@ -25,4 +25,4 @@
= license_key = license_key
.modal-footer.form-actions .modal-footer.form-actions
%button.btn.gl-button.btn-default{ type: 'button', data: { dismiss: 'modal' } } Cancel %button.btn.gl-button.btn-default{ type: 'button', data: { dismiss: 'modal' } } Cancel
= f.submit 'Install license', class: 'gl-button btn btn-primary' = f.submit 'Install license', class: 'gl-button btn btn-confirm'
...@@ -56,4 +56,4 @@ ...@@ -56,4 +56,4 @@
= _('Unless otherwise agreed to in writing with GitLab, by clicking "Upload License" you agree that your use of GitLab Software is subject to the %{eula_link_start}Terms of Service%{eula_link_end}.').html_safe % { eula_link_start: eula_link_start, eula_url: eula_url, eula_link_end: '</a>'.html_safe } = _('Unless otherwise agreed to in writing with GitLab, by clicking "Upload License" you agree that your use of GitLab Software is subject to the %{eula_link_start}Terms of Service%{eula_link_end}.').html_safe % { eula_link_start: eula_link_start, eula_url: eula_url, eula_link_end: '</a>'.html_safe }
.form-actions .form-actions
= f.submit 'Upload License', class: 'gl-button btn btn-primary', disabled: true, id: 'js-upload-license' = f.submit 'Upload License', class: 'gl-button btn btn-confirm', disabled: true, id: 'js-upload-license'
...@@ -66,7 +66,9 @@ ...@@ -66,7 +66,9 @@
full_path: @group.full_path, full_path: @group.full_path,
empty_state_illustration: image_path('illustrations/epics/roadmap.svg'), empty_state_illustration: image_path('illustrations/epics/roadmap.svg'),
has_filters_applied: 'false', has_filters_applied: 'false',
new_epic_endpoint: group_epics_path(@group), new_epic_path: new_group_epic_path(@group),
list_epics_path: group_epics_path(@group),
epics_docs_path: help_page_path('user/group/epics/index'),
preset_type: roadmap_layout, preset_type: roadmap_layout,
epics_state: 'all', epics_state: 'all',
sorted_by: roadmap_sort_order, sorted_by: roadmap_sort_order,
......
...@@ -20,7 +20,9 @@ ...@@ -20,7 +20,9 @@
full_path: @group.full_path, full_path: @group.full_path,
empty_state_illustration: image_path('illustrations/epics/roadmap.svg'), empty_state_illustration: image_path('illustrations/epics/roadmap.svg'),
has_filters_applied: "#{has_filters_applied}", has_filters_applied: "#{has_filters_applied}",
new_epic_endpoint: group_epics_path(@group), new_epic_path: new_group_epic_path(@group),
list_epics_path: group_epics_path(@group),
epics_docs_path: help_page_path('user/group/epics/index'),
group_labels_endpoint: group_labels_path(@group, format: :json), group_labels_endpoint: group_labels_path(@group, format: :json),
group_milestones_endpoint: group_milestones_path(@group, format: :json), group_milestones_endpoint: group_milestones_path(@group, format: :json),
preset_type: roadmap_layout, preset_type: roadmap_layout,
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
.form-text.text-muted.js-scim-token-helper-text .form-text.text-muted.js-scim-token-helper-text
%span %span
= s_('GroupSAML|The SCIM token is now hidden. To see the value of the token again, you need to ') = s_('GroupSAML|The SCIM token is now hidden. To see the value of the token again, you need to ')
%button.btn.gl-button.btn-primary.btn-link.d-inline.align-baseline.js-reset-scim-token{ type: 'button' } %button.btn.gl-button.btn-confirm.btn-link.d-inline.align-baseline.js-reset-scim-token{ type: 'button' }
= _('reset it.') = _('reset it.')
%span.d-none %span.d-none
= s_("GroupSAML|Make sure you save this token — you won't be able to access it again.") = s_("GroupSAML|Make sure you save this token — you won't be able to access it again.")
......
---
title: New epic button in Epic Roadmap empty state should direct the user to a full
epic creation page
merge_request: 45948
author:
type: changed
import Vue from 'vue';
import EpicCreate from 'ee/epic/components/epic_create.vue';
import createStore from 'ee/epic/store';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { mockEpicMeta } from '../mock_data';
describe('EpicCreateComponent', () => {
let vm;
let store;
beforeEach(() => {
const Component = Vue.extend(EpicCreate);
store = createStore();
store.dispatch('setEpicMeta', mockEpicMeta);
vm = mountComponentWithStore(Component, {
store,
});
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('buttonLabel', () => {
it('returns string `Create epic` when `epicCreateInProgress` is false', () => {
vm.$store.state.epicCreateInProgress = false;
expect(vm.buttonLabel).toBe('Create epic');
});
it('returns string `Creating epic` when `epicCreateInProgress` is true', () => {
vm.$store.state.epicCreateInProgress = true;
expect(vm.buttonLabel).toBe('Creating epic');
});
});
describe('isEpicCreateDisabled', () => {
it('returns `true` when `newEpicTitle` is an empty string', () => {
vm.$store.state.newEpicTitle = '';
expect(vm.isEpicCreateDisabled).toBe(true);
});
it('returns `false` when `newEpicTitle` is not empty', () => {
vm.$store.state.newEpicTitle = 'foobar';
expect(vm.isEpicCreateDisabled).toBe(false);
});
});
describe('epicTitle', () => {
describe('set', () => {
it('calls `setEpicCreateTitle` with param `value`', () => {
jest.spyOn(vm, 'setEpicCreateTitle');
const newEpicTitle = 'foobar';
vm.epicTitle = newEpicTitle;
expect(vm.setEpicCreateTitle).toHaveBeenCalledWith(
expect.objectContaining({
newEpicTitle,
}),
);
});
});
describe('get', () => {
it('returns value of `newEpicTitle` from state', () => {
const newEpicTitle = 'foobar';
vm.$store.state.newEpicTitle = newEpicTitle;
expect(vm.epicTitle).toBe(newEpicTitle);
});
});
});
describe('epicConfidential', () => {
describe('set', () => {
it('calls `setEpicCreateConfidential` with param `value`', () => {
jest.spyOn(vm, 'setEpicCreateConfidential');
const newEpicConfidential = true;
vm.epicConfidential = newEpicConfidential;
expect(vm.setEpicCreateConfidential).toHaveBeenCalledWith(
expect.objectContaining({
newEpicConfidential,
}),
);
});
});
describe('get', () => {
it('returns value of `newEpicConfidential` from state', () => {
const newEpicConfidential = true;
vm.$store.state.newEpicConfidential = newEpicConfidential;
expect(vm.epicConfidential).toBe(newEpicConfidential);
});
});
});
});
describe('template', () => {
it('renders component container element with classes `dropdown` & `epic-create-dropdown`', () => {
expect(vm.$el.classList.contains('dropdown')).toBe(true);
expect(vm.$el.classList.contains('epic-create-dropdown')).toBe(true);
});
it('renders new epic button element', () => {
const newEpicButtonEl = vm.$el.querySelector('button.btn-success');
expect(newEpicButtonEl).not.toBeNull();
expect(newEpicButtonEl.innerText.trim()).toBe('New epic');
});
it('renders new epic dropdown menu element', () => {
const dropdownMenuEl = vm.$el.querySelector('.dropdown-menu');
expect(dropdownMenuEl).not.toBeNull();
});
it('renders epic input textbox element', () => {
const inputEl = vm.$el.querySelector('.dropdown-menu input.form-control');
expect(inputEl).not.toBeNull();
expect(inputEl.placeholder).toBe('Title');
});
it('renders create epic button element', () => {
const createEpicButtonEl = vm.$el.querySelector('.dropdown-menu button.btn-success');
expect(createEpicButtonEl).not.toBeNull();
expect(createEpicButtonEl.innerText.trim()).toBe('Create epic');
});
});
});
import Vue from 'vue'; import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import epicsListEmptyComponent from 'ee/roadmap/components/epics_list_empty.vue'; import EpicsListEmpty from 'ee/roadmap/components/epics_list_empty.vue';
import { mockTimeframeInitialDate, mockSvgPath } from 'ee_jest/roadmap/mock_data';
import { PRESET_TYPES } from 'ee/roadmap/constants'; import { PRESET_TYPES } from 'ee/roadmap/constants';
import { TEST_HOST } from 'helpers/test_constants';
import { import {
getTimeframeForQuartersView, getTimeframeForQuartersView,
getTimeframeForWeeksView, getTimeframeForWeeksView,
getTimeframeForMonthsView, getTimeframeForMonthsView,
} from 'ee/roadmap/utils/roadmap_utils'; } from 'ee/roadmap/utils/roadmap_utils';
import { const TEST_EPICS_PATH = '/epics';
mockTimeframeInitialDate, const TEST_NEW_EPIC_PATH = '/epics/new';
mockSvgPath,
mockNewEpicEndpoint,
} from 'ee_jest/roadmap/mock_data';
import mountComponent from 'helpers/vue_mount_component_helper';
const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate); const mockTimeframeQuarters = getTimeframeForQuartersView(mockTimeframeInitialDate);
const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate); const mockTimeframeMonths = getTimeframeForMonthsView(mockTimeframeInitialDate);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate); const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
const createComponent = ({ describe('ee/roadmap/components/epics_list_empty.vue', () => {
hasFiltersApplied = false, let wrapper;
presetType = PRESET_TYPES.MONTHS,
timeframeStart = mockTimeframeMonths[0], const createWrapper = ({
timeframeEnd = mockTimeframeMonths[mockTimeframeMonths.length - 1], isChildEpics = false,
}) => { hasFiltersApplied = false,
const Component = Vue.extend(epicsListEmptyComponent); presetType = PRESET_TYPES.MONTHS,
timeframeStart = mockTimeframeMonths[0],
return mountComponent(Component, { timeframeEnd = mockTimeframeMonths[mockTimeframeMonths.length - 1],
presetType, } = {}) => {
timeframeStart, wrapper = extendedWrapper(
timeframeEnd, shallowMount(EpicsListEmpty, {
emptyStateIllustrationPath: mockSvgPath, propsData: {
newEpicEndpoint: mockNewEpicEndpoint, presetType,
hasFiltersApplied, timeframeStart,
timeframeEnd,
emptyStateIllustrationPath: mockSvgPath,
hasFiltersApplied,
isChildEpics,
},
provide: {
newEpicPath: TEST_NEW_EPIC_PATH,
listEpicsPath: TEST_EPICS_PATH,
epicsDocsPath: TEST_HOST,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
}); });
};
describe('EpicsListEmptyComponent', () => { const findTitle = () => wrapper.findByTestId('title');
let vm; const findSubTitle = () => wrapper.findByTestId('sub-title');
const findNewEpicButton = () => wrapper.findByTestId('new-epic-button');
const findListEpicsButton = () => wrapper.findByTestId('list-epics-button');
const findIllustration = () => wrapper.findByTestId('illustration');
beforeEach(() => { it('renders default message', () => {
vm = createComponent({}); createWrapper({});
expect(findTitle().text()).toBe(wrapper.vm.message);
}); });
afterEach(() => { it('renders empty state message when `hasFiltersApplied` prop is true', () => {
vm.$destroy(); createWrapper({ hasFiltersApplied: true });
expect(findTitle().text()).toBe('Sorry, no epics matched your search');
}); });
describe('computed', () => { describe('with presetType `QUARTERS`', () => {
describe('message', () => { it('renders default empty state sub-title when `hasFiltersApplied` props is false', () => {
it('returns default empty state message', () => { createWrapper({
expect(vm.message).toBe('The roadmap shows the progress of your epics along a timeline'); presetType: PRESET_TYPES.QUARTERS,
timeframeStart: mockTimeframeQuarters[0],
timeframeEnd: mockTimeframeQuarters[mockTimeframeQuarters.length - 1],
}); });
it('returns empty state message when `hasFiltersApplied` prop is true', (done) => { expect(findSubTitle().text()).toBe(
vm.hasFiltersApplied = true; 'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Jul 1, 2017 to Mar 31, 2019.',
Vue.nextTick() );
.then(() => {
expect(vm.message).toBe('Sorry, no epics matched your search');
})
.then(done)
.catch(done.fail);
});
}); });
describe('subMessage', () => { it('renders empty state sub-title when `hasFiltersApplied` prop is true', () => {
describe('with presetType `QUARTERS`', () => { createWrapper({
beforeEach(() => { presetType: PRESET_TYPES.QUARTERS,
vm.presetType = PRESET_TYPES.QUARTERS; timeframeStart: mockTimeframeQuarters[0],
[vm.timeframeStart] = mockTimeframeQuarters; timeframeEnd: mockTimeframeQuarters[mockTimeframeQuarters.length - 1],
vm.timeframeEnd = mockTimeframeQuarters[mockTimeframeQuarters.length - 1]; hasFiltersApplied: true,
});
it('returns default empty state sub-message when `hasFiltersApplied` props is false', (done) => {
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Jul 1, 2017 to Mar 31, 2019.',
);
})
.then(done)
.catch(done.fail);
});
it('returns empty state sub-message when `hasFiltersApplied` prop is true', (done) => {
vm.hasFiltersApplied = true;
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To widen your search, change or remove filters; from Jul 1, 2017 to Mar 31, 2019.',
);
})
.then(done)
.catch(done.fail);
});
}); });
describe('with presetType `MONTHS`', () => { expect(findSubTitle().text()).toBe(
beforeEach(() => { 'To widen your search, change or remove filters; from Jul 1, 2017 to Mar 31, 2019.',
vm.presetType = PRESET_TYPES.MONTHS; );
}); });
});
it('returns default empty state sub-message when `hasFiltersApplied` props is false', (done) => {
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Nov 1, 2017 to Jun 30, 2018.',
);
})
.then(done)
.catch(done.fail);
});
it('returns empty state sub-message when `hasFiltersApplied` prop is true', (done) => {
vm.hasFiltersApplied = true;
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To widen your search, change or remove filters; from Nov 1, 2017 to Jun 30, 2018.',
);
})
.then(done)
.catch(done.fail);
});
});
describe('with presetType `WEEKS`', () => { describe('with presetType `MONTHS`', () => {
beforeEach(() => { it('renders default empty state sub-title when `hasFiltersApplied` props is false', () => {
const timeframeEnd = mockTimeframeWeeks[mockTimeframeWeeks.length - 1]; createWrapper({
timeframeEnd.setDate(timeframeEnd.getDate() + 6); presetType: PRESET_TYPES.MONTHS,
vm.presetType = PRESET_TYPES.WEEKS;
[vm.timeframeStart] = mockTimeframeWeeks;
vm.timeframeEnd = timeframeEnd;
});
it('returns default empty state sub-message when `hasFiltersApplied` props is false', (done) => {
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Dec 17, 2017 to Feb 9, 2018.',
);
})
.then(done)
.catch(done.fail);
});
it('returns empty state sub-message when `hasFiltersApplied` prop is true', (done) => {
vm.hasFiltersApplied = true;
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To widen your search, change or remove filters; from Dec 17, 2017 to Feb 15, 2018.',
);
})
.then(done)
.catch(done.fail);
});
}); });
describe('with child epics context', () => { expect(findSubTitle().text()).toBe(
it('returns empty state sub-message when `isChildEpics` is set to `true`', (done) => { 'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Nov 1, 2017 to Jun 30, 2018.',
vm.isChildEpics = true; );
Vue.nextTick()
.then(() => {
expect(vm.subMessage).toBe(
'To view the roadmap, add a start or due date to one of the <a href="https://docs.gitlab.com/ee/user/group/epics/#multi-level-child-epics" target="_blank" rel="noopener noreferrer nofollow">child epics</a>.',
);
})
.then(done)
.catch(done.fail);
});
});
}); });
describe('timeframeRange', () => { it('renders empty state sub-title when `hasFiltersApplied` prop is true', () => {
it('returns correct timeframe startDate and endDate in words', () => { createWrapper({
expect(vm.timeframeRange.startDate).toBe('Nov 1, 2017'); presetType: PRESET_TYPES.MONTHS,
expect(vm.timeframeRange.endDate).toBe('Jun 30, 2018'); hasFiltersApplied: true,
}); });
});
});
describe('template', () => { expect(findSubTitle().text()).toBe(
it('renders empty state illustration in image element with provided `emptyStateIllustrationPath`', () => { 'To widen your search, change or remove filters; from Nov 1, 2017 to Jun 30, 2018.',
expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toBe(
vm.emptyStateIllustrationPath,
); );
}); });
});
it('renders mount point for new epic button to boot via Epic app', () => { describe('with presetType `WEEKS`', () => {
expect(vm.$el.querySelector('#epic-create-root')).not.toBeNull(); let timeframeEnd;
beforeEach(() => {
timeframeEnd = mockTimeframeWeeks[mockTimeframeWeeks.length - 1];
timeframeEnd.setDate(timeframeEnd.getDate() + 6);
}); });
it('does not render new epic button element when `hasFiltersApplied` prop is true', (done) => { it('renders default empty state sub-title when `hasFiltersApplied` props is false', () => {
vm.hasFiltersApplied = true; createWrapper({
Vue.nextTick() presetType: PRESET_TYPES.WEEKS,
.then(() => { timeframeStart: mockTimeframeWeeks[0],
expect(vm.$el.querySelector('.epic-create-dropdown')).toBeNull(); timeframeEnd,
}) });
.then(done)
.catch(done.fail); expect(findSubTitle().text()).toBe(
'To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from Dec 17, 2017 to Feb 9, 2018.',
);
}); });
it('renders view epics list link element', () => { it('renders empty state sub-title when `hasFiltersApplied` prop is true', () => {
const viewEpicsListEl = vm.$el.querySelector('a.btn'); createWrapper({
presetType: PRESET_TYPES.WEEKS,
timeframeStart: mockTimeframeWeeks[0],
timeframeEnd,
hasFiltersApplied: true,
});
expect(viewEpicsListEl).not.toBeNull(); expect(findSubTitle().text()).toBe(
expect(viewEpicsListEl.getAttribute('href')).toBe(mockNewEpicEndpoint); 'To widen your search, change or remove filters; from Dec 17, 2017 to Feb 15, 2018.',
expect(viewEpicsListEl.querySelector('span').innerText.trim()).toBe('View epics list'); );
}); });
}); });
it('renders empty state sub-title when `isChildEpics` is set to `true`', () => {
createWrapper({ isChildEpics: true });
expect(findSubTitle().text()).toBe(
'To view the roadmap, add a start or due date to one of the child epics.',
);
});
it('renders empty state illustration in image element with provided `emptyStateIllustrationPath`', () => {
createWrapper({});
expect(findIllustration().attributes('src')).toBe(mockSvgPath);
});
it('renders buttons for create and list epics', () => {
createWrapper({});
expect(findNewEpicButton().attributes('href')).toBe(TEST_NEW_EPIC_PATH);
expect(findListEpicsButton().attributes('href')).toBe(TEST_EPICS_PATH);
});
it('does not render new epic button element when `hasFiltersApplied` prop is true', () => {
createWrapper({ hasFiltersApplied: true });
expect(findNewEpicButton().exists()).toBe(false);
});
}); });
...@@ -17,7 +17,6 @@ import { ...@@ -17,7 +17,6 @@ import {
mockFormattedEpic, mockFormattedEpic,
mockFormattedChildEpic2, mockFormattedChildEpic2,
mockGroupId, mockGroupId,
mockNewEpicEndpoint,
mockSortedBy, mockSortedBy,
mockSvgPath, mockSvgPath,
mockTimeframeInitialDate, mockTimeframeInitialDate,
...@@ -35,7 +34,6 @@ describe('RoadmapApp', () => { ...@@ -35,7 +34,6 @@ describe('RoadmapApp', () => {
const emptyStateIllustrationPath = mockSvgPath; const emptyStateIllustrationPath = mockSvgPath;
const epics = [mockFormattedEpic]; const epics = [mockFormattedEpic];
const hasFiltersApplied = true; const hasFiltersApplied = true;
const newEpicEndpoint = mockNewEpicEndpoint;
const presetType = PRESET_TYPES.MONTHS; const presetType = PRESET_TYPES.MONTHS;
const timeframe = getTimeframeForMonthsView(mockTimeframeInitialDate); const timeframe = getTimeframeForMonthsView(mockTimeframeInitialDate);
...@@ -44,7 +42,6 @@ describe('RoadmapApp', () => { ...@@ -44,7 +42,6 @@ describe('RoadmapApp', () => {
localVue, localVue,
propsData: { propsData: {
emptyStateIllustrationPath, emptyStateIllustrationPath,
newEpicEndpoint,
presetType, presetType,
}, },
provide: { provide: {
...@@ -122,10 +119,6 @@ describe('RoadmapApp', () => { ...@@ -122,10 +119,6 @@ describe('RoadmapApp', () => {
expect(wrapper.find(EpicsListEmpty).props('isChildEpics')).toBe(false); expect(wrapper.find(EpicsListEmpty).props('isChildEpics')).toBe(false);
}); });
it('contains endpoint to create a new epic', () => {
expect(wrapper.find(EpicsListEmpty).props('newEpicEndpoint')).toBe(mockNewEpicEndpoint);
});
it('contains the preset type', () => { it('contains the preset type', () => {
expect(wrapper.find(EpicsListEmpty).props('presetType')).toBe(presetType); expect(wrapper.find(EpicsListEmpty).props('presetType')).toBe(presetType);
}); });
......
...@@ -374,10 +374,10 @@ msgstr "" ...@@ -374,10 +374,10 @@ msgstr ""
msgid "%{board_target} not found" msgid "%{board_target} not found"
msgstr "" msgstr ""
msgid "%{code_open}Masked%{code_close} variables are hidden in job logs (though they must match certain regexp requirements to do so)." msgid "%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements."
msgstr "" msgstr ""
msgid "%{code_open}Protected%{code_close} variables are only exposed to protected branches or tags." msgid "%{code_open}Protected:%{code_close} Only exposed to protected branches or tags."
msgstr "" msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}" msgid "%{commit_author_link} authored %{commit_timeago}"
...@@ -11025,13 +11025,10 @@ msgstr "" ...@@ -11025,13 +11025,10 @@ msgstr ""
msgid "Environment scope" msgid "Environment scope"
msgstr "" msgstr ""
msgid "Environment variables are applied to environments via the Runner. You can use environment variables for passwords, secret keys, etc. Make variables available to the running application by prepending the variable key with %{code_open}K8S_SECRET_%{code_close}. You can set variables to be:" msgid "Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default."
msgstr "" msgstr ""
msgid "Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default" msgid "Environment variables on this GitLab instance are configured to be %{link_start}protected%{link_end} by default."
msgstr ""
msgid "Environment variables on this GitLab instance are configured to be %{link_start}protected%{link_end} by default"
msgstr "" msgstr ""
msgid "Environment:" msgid "Environment:"
...@@ -17223,9 +17220,6 @@ msgstr "" ...@@ -17223,9 +17220,6 @@ msgstr ""
msgid "Make sure you save it - you won't be able to access it again." msgid "Make sure you save it - you won't be able to access it again."
msgstr "" msgstr ""
msgid "Make this epic confidential"
msgstr ""
msgid "Makes this issue confidential." msgid "Makes this issue confidential."
msgstr "" msgstr ""
...@@ -20205,9 +20199,6 @@ msgstr "" ...@@ -20205,9 +20199,6 @@ msgstr ""
msgid "Or you can choose one of the suggested colors below" msgid "Or you can choose one of the suggested colors below"
msgstr "" msgstr ""
msgid "Origin"
msgstr ""
msgid "Orphaned member" msgid "Orphaned member"
msgstr "" msgstr ""
...@@ -28865,7 +28856,7 @@ msgstr "" ...@@ -28865,7 +28856,7 @@ msgstr ""
msgid "These runners are specific to this project." msgid "These runners are specific to this project."
msgstr "" msgstr ""
msgid "These variables are configured in the parent group settings, and will be active in the current project in addition to the project variables." msgid "These variables are inherited from the parent group."
msgstr "" msgstr ""
msgid "Third Party Advisory Link" msgid "Third Party Advisory Link"
...@@ -29039,9 +29030,6 @@ msgstr "" ...@@ -29039,9 +29030,6 @@ msgstr ""
msgid "This epic already has the maximum number of child epics." msgid "This epic already has the maximum number of child epics."
msgstr "" msgstr ""
msgid "This epic and its child elements will only be visible to team members with at minimum Reporter access."
msgstr ""
msgid "This epic does not exist or you don't have sufficient permission." msgid "This epic does not exist or you don't have sufficient permission."
msgstr "" msgstr ""
...@@ -31310,6 +31298,15 @@ msgstr "" ...@@ -31310,6 +31298,15 @@ msgstr ""
msgid "Variables" msgid "Variables"
msgstr "" msgstr ""
msgid "Variables can be:"
msgstr ""
msgid "Variables store information, like passwords and secret keys, that you can use in job scripts."
msgstr ""
msgid "Variables store information, like passwords and secret keys, that you can use in job scripts. All projects on the instance can use these variables."
msgstr ""
msgid "Various container registry settings." msgid "Various container registry settings."
msgstr "" msgstr ""
...@@ -34179,7 +34176,7 @@ msgstr "" ...@@ -34179,7 +34176,7 @@ msgstr ""
msgid "mrWidget|The changes will be merged into" msgid "mrWidget|The changes will be merged into"
msgstr "" msgstr ""
msgid "mrWidget|The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure" msgid "mrWidget|The pipeline for this merge request did not complete. Push a new commit to fix the failure or check the %{linkStart}troubleshooting documentation%{linkEnd} to see other possible actions."
msgstr "" msgstr ""
msgid "mrWidget|The source branch HEAD has recently changed. Please reload the page and review the changes before merging" msgid "mrWidget|The source branch HEAD has recently changed. Please reload the page and review the changes before merging"
......
...@@ -57,7 +57,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do ...@@ -57,7 +57,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do
wait_for_requests wait_for_requests
expect(page).to have_css('button[disabled="disabled"]', text: 'Merge') expect(page).to have_css('button[disabled="disabled"]', text: 'Merge')
expect(page).to have_content('Please retry the job or push a new commit to fix the failure') expect(page).to have_content('The pipeline for this merge request did not complete. Push a new commit to fix the failure or check the troubleshooting documentation to see other possible actions.')
end end
end end
...@@ -70,7 +70,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do ...@@ -70,7 +70,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do
wait_for_requests wait_for_requests
expect(page).not_to have_button 'Merge' expect(page).not_to have_button 'Merge'
expect(page).to have_content('Please retry the job or push a new commit to fix the failure') expect(page).to have_content('The pipeline for this merge request did not complete. Push a new commit to fix the failure or check the troubleshooting documentation to see other possible actions.')
end end
end end
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelineFailed should render error message with a disabled merge button 1`] = `
<div
class="mr-widget-body media"
>
<status-icon-stub
showdisabledbutton="true"
status="warning"
/>
<div
class="media-body space-children"
>
<span
class="bold"
>
<gl-sprintf-stub
message="The pipeline for this merge request did not complete. Push a new commit to fix the failure or check the %{linkStart}troubleshooting documentation%{linkEnd} to see other possible actions."
/>
</span>
</div>
</div>
`;
import Vue from 'vue'; import { shallowMount } from '@vue/test-utils';
import { removeBreakLine } from 'helpers/text_helper';
import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue'; import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
describe('PipelineFailed', () => { describe('PipelineFailed', () => {
describe('template', () => { let wrapper;
const Component = Vue.extend(PipelineFailed);
const vm = new Component({ const createComponent = () => {
el: document.createElement('div'), wrapper = shallowMount(PipelineFailed);
}); };
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); const findStatusIcon = () => wrapper.find(statusIcon);
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(removeBreakLine(vm.$el.innerText).trim()).toContain( beforeEach(() => {
'The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure', createComponent();
); });
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should render error message with a disabled merge button', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('merge button should be disabled', () => {
expect(findStatusIcon().props('showDisabledButton')).toBe(true);
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment