summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/issue_templates/Bug.md6
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/dispatcher.js2
-rw-r--r--app/assets/javascripts/dropzone_input.js1
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue189
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue7
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue79
-rw-r--r--app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue23
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue54
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description_template.vue111
-rw-r--r--app/assets/javascripts/issue_show/components/fields/project_move.vue83
-rw-r--r--app/assets/javascripts/issue_show/components/fields/title.vue31
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue104
-rw-r--r--app/assets/javascripts/issue_show/components/locked_warning.vue20
-rw-r--r--app/assets/javascripts/issue_show/event_hub.js3
-rw-r--r--app/assets/javascripts/issue_show/index.js81
-rw-r--r--app/assets/javascripts/issue_show/mixins/animate.js2
-rw-r--r--app/assets/javascripts/issue_show/mixins/update.js10
-rw-r--r--app/assets/javascripts/issue_show/services/index.js17
-rw-r--r--app/assets/javascripts/issue_show/stores/index.js22
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js2
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js4
-rw-r--r--app/assets/javascripts/task_list.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue107
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue113
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue33
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue58
-rw-r--r--app/assets/javascripts/vue_shared/mixins/tooltip.js4
-rw-r--r--app/assets/stylesheets/framework/filters.scss1
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss6
-rw-r--r--app/controllers/admin/jobs_controller.rb (renamed from app/controllers/admin/builds_controller.rb)4
-rw-r--r--app/controllers/concerns/issuable_actions.rb11
-rw-r--r--app/controllers/dashboard_controller.rb2
-rw-r--r--app/controllers/projects/artifacts_controller.rb4
-rw-r--r--app/controllers/projects/build_artifacts_controller.rb55
-rw-r--r--app/controllers/projects/builds_controller.rb122
-rw-r--r--app/controllers/projects/issues_controller.rb5
-rw-r--r--app/controllers/projects/jobs_controller.rb131
-rw-r--r--app/finders/projects_finder.rb33
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/helpers/builds_helper.rb8
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/helpers/issuables_helper.rb21
-rw-r--r--app/models/ci/build.rb6
-rw-r--r--app/models/ci/pipeline_schedule.rb4
-rw-r--r--app/models/project.rb5
-rw-r--r--app/models/user.rb6
-rw-r--r--app/policies/ci/build_policy.rb2
-rw-r--r--app/serializers/analytics_build_entity.rb2
-rw-r--r--app/serializers/build_action_entity.rb2
-rw-r--r--app/serializers/build_artifact_entity.rb2
-rw-r--r--app/serializers/build_entity.rb6
-rw-r--r--app/serializers/issue_entity.rb6
-rw-r--r--app/views/admin/dashboard/_head.html.haml2
-rw-r--r--app/views/admin/jobs/index.html.haml (renamed from app/views/admin/builds/index.html.haml)6
-rw-r--r--app/views/admin/runners/show.html.haml2
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/notify/links/ci/builds/_build.html.haml2
-rw-r--r--app/views/notify/links/ci/builds/_build.text.erb2
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml2
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml8
-rw-r--r--app/views/projects/artifacts/file.html.haml8
-rw-r--r--app/views/projects/ci/builds/_build.html.haml10
-rw-r--r--app/views/projects/deployments/_actions.haml1
-rw-r--r--app/views/projects/environments/show.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml8
-rw-r--r--app/views/projects/jobs/_header.html.haml (renamed from app/views/projects/builds/_header.html.haml)6
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml (renamed from app/views/projects/builds/_sidebar.html.haml)12
-rw-r--r--app/views/projects/jobs/_table.html.haml (renamed from app/views/projects/builds/_table.html.haml)0
-rw-r--r--app/views/projects/jobs/_user.html.haml (renamed from app/views/projects/builds/_user.html.haml)0
-rw-r--r--app/views/projects/jobs/index.html.haml (renamed from app/views/projects/builds/index.html.haml)4
-rw-r--r--app/views/projects/jobs/show.html.haml (renamed from app/views/projects/builds/show.html.haml)6
-rw-r--r--app/views/projects/pipelines/_head.html.haml2
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml2
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--changelogs/unreleased/30892-add-api-support-for-pipeline-schedule.yml4
-rw-r--r--changelogs/unreleased/33000-tag-list-in-project-create-api.yml4
-rw-r--r--changelogs/unreleased/UI-improvements-for-count-badges-and-permission-badges.yml5
-rw-r--r--changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml4
-rw-r--r--changelogs/unreleased/issue-edit-inline.yml4
-rw-r--r--changelogs/unreleased/issue-template-reproduce-in-example-project.yml4
-rw-r--r--changelogs/unreleased/rename-builds-controller.yml4
-rw-r--r--changelogs/unreleased/task-list-2.yml4
-rw-r--r--changelogs/unreleased/tc-improve-project-api-perf.yml4
-rw-r--r--config/initializers/relative_naming_ci_namespace.rb2
-rw-r--r--config/routes/admin.rb2
-rw-r--r--config/routes/project.rb55
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/pipeline_schedules.md273
-rw-r--r--doc/api/projects.md3
-rw-r--r--doc/ci/yaml/README.md32
-rw-r--r--features/steps/shared/builds.rb6
-rw-r--r--features/steps/shared/markdown.rb6
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities.rb11
-rw-r--r--lib/api/groups.rb4
-rw-r--r--lib/api/helpers.rb34
-rw-r--r--lib/api/pipeline_schedules.rb131
-rw-r--r--lib/api/projects.rb28
-rw-r--r--lib/api/v3/helpers.rb27
-rw-r--r--lib/api/v3/projects.rb2
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/build/common.rb2
-rw-r--r--lib/gitlab/ci/status/build/play.rb2
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb2
-rw-r--r--lib/gitlab/ci/status/build/stop.rb2
-rw-r--r--lib/gitlab/etag_caching/router.rb2
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/gitlab/routes/legacy_builds.rb36
-rw-r--r--lib/gitlab/workhorse.rb3
-rw-r--r--package.json1
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb48
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb4
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb2
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb (renamed from spec/controllers/projects/builds_controller_spec.rb)20
-rw-r--r--spec/features/admin/admin_builds_spec.rb16
-rw-r--r--spec/features/issues/move_spec.rb8
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb2
-rw-r--r--spec/features/projects/artifacts/browse_spec.rb25
-rw-r--r--spec/features/projects/artifacts/download_spec.rb61
-rw-r--r--spec/features/projects/artifacts/file_spec.rb20
-rw-r--r--spec/features/projects/artifacts/raw_spec.rb25
-rw-r--r--spec/features/projects/environments/environment_spec.rb57
-rw-r--r--spec/features/projects/jobs_spec.rb (renamed from spec/features/projects/builds_spec.rb)145
-rw-r--r--spec/features/security/project/internal_access_spec.rb6
-rw-r--r--spec/features/security/project/private_access_spec.rb6
-rw-r--r--spec/features/security/project/public_access_spec.rb6
-rw-r--r--spec/finders/projects_finder_spec.rb15
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedule.json41
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedules.json4
-rw-r--r--spec/javascripts/build_spec.js2
-rw-r--r--spec/javascripts/fixtures/jobs.rb (renamed from spec/javascripts/fixtures/builds.rb)2
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js306
-rw-r--r--spec/javascripts/issue_show/components/edit_actions_spec.js147
-rw-r--r--spec/javascripts/issue_show/components/fields/description_spec.js56
-rw-r--r--spec/javascripts/issue_show/components/fields/description_template_spec.js49
-rw-r--r--spec/javascripts/issue_show/components/fields/project_move_spec.js38
-rw-r--r--spec/javascripts/issue_show/components/fields/title_spec.js30
-rw-r--r--spec/javascripts/issue_show/components/form_spec.js68
-rw-r--r--spec/javascripts/issue_show/components/title_spec.js8
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js2
-rw-r--r--spec/javascripts/merge_request_spec.js4
-rw-r--r--spec/javascripts/notes_spec.js4
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js6
-rw-r--r--spec/javascripts/pipelines/graph/dropdown_action_component_spec.js4
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js24
-rw-r--r--spec/javascripts/vue_shared/components/markdown/field_spec.js121
-rw-r--r--spec/javascripts/vue_shared/components/markdown/header_spec.js67
-rw-r--r--spec/lib/gitlab/chat_commands/command_spec.rb7
-rw-r--r--spec/lib/gitlab/chat_commands/deploy_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/status/build/common_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb10
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb2
-rw-r--r--spec/models/environment_spec.rb5
-rw-r--r--spec/models/project_spec.rb14
-rw-r--r--spec/models/user_spec.rb19
-rw-r--r--spec/requests/api/pipeline_schedules_spec.rb297
-rw-r--r--spec/requests/api/projects_spec.rb8
-rw-r--r--spec/serializers/build_action_entity_spec.rb12
-rw-r--r--spec/serializers/build_artifact_entity_spec.rb10
-rw-r--r--spec/serializers/build_entity_spec.rb6
-rw-r--r--spec/services/ci/play_build_service_spec.rb17
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb7
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb7
-rw-r--r--spec/views/ci/status/_badge.html.haml_spec.rb2
-rw-r--r--spec/views/projects/jobs/_build.html.haml_spec.rb (renamed from spec/views/projects/builds/_build.html.haml_spec.rb)2
-rw-r--r--spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb (renamed from spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb)0
-rw-r--r--spec/views/projects/jobs/show.html.haml_spec.rb (renamed from spec/views/projects/builds/show.html.haml_spec.rb)4
-rw-r--r--vendor/assets/javascripts/task_list.js258
-rw-r--r--yarn.lock4
176 files changed, 3766 insertions, 831 deletions
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index 58af062e75e..9d53a48409a 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -20,6 +20,12 @@ Please remove this notice if you're confident your issue isn't a duplicate.
(How one can reproduce the issue - this is very important)
+### Example Project
+
+(If possible, please create an example project here on GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report)
+
+(If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version)
+
### What is the current *bug* behavior?
(What actually happens)
diff --git a/Gemfile b/Gemfile
index 9efb362e494..d5d7051ecc6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -109,7 +109,7 @@ gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
-gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
+gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.4'
gem 'RedCloth', '~> 4.3.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index 273a69792ef..b4252a2bdc9 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -141,10 +141,8 @@ GEM
database_cleaner (1.5.3)
debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
- deckar01-task_list (1.0.6)
- activesupport (~> 4.0)
+ deckar01-task_list (2.0.0)
html-pipeline
- rack (~> 1.0)
default_value_for (3.0.2)
activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
@@ -895,7 +893,7 @@ DEPENDENCIES
creole (~> 0.5.0)
d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
- deckar01-task_list (= 1.0.6)
+ deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0)
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 6e2f06112dd..53b25da18e5 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -118,7 +118,7 @@ import ShortcutsBlob from './shortcuts_blob';
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break;
- case 'projects:builds:show':
+ case 'projects:jobs:show':
new Build();
break;
case 'projects:merge_requests:index':
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 266cd3966c6..111449bb8f7 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -194,6 +194,7 @@ window.DropzoneInput = (function() {
$(child).val(beforeSelection + formattedText + afterSelection);
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`;
+ formTextarea.get(0).dispatchEvent(new Event('input'));
return formTextarea.trigger('input');
};
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index 770a0dcd27e..800bb9f1fe8 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -1,10 +1,14 @@
<script>
+/* global Flash */
import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll';
+import eventHub from '../event_hub';
import Service from '../services/index';
import Store from '../stores';
import titleComponent from './title.vue';
import descriptionComponent from './description.vue';
+import formComponent from './form.vue';
+import '../../lib/utils/url_utility';
export default {
props: {
@@ -12,15 +16,27 @@ export default {
required: true,
type: String,
},
+ canMove: {
+ required: true,
+ type: Boolean,
+ },
canUpdate: {
required: true,
type: Boolean,
},
+ canDestroy: {
+ required: true,
+ type: Boolean,
+ },
issuableRef: {
type: String,
required: true,
},
- initialTitle: {
+ initialTitleHtml: {
+ type: String,
+ required: true,
+ },
+ initialTitleText: {
type: String,
required: true,
},
@@ -34,10 +50,40 @@ export default {
required: false,
default: '',
},
+ issuableTemplates: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ isConfidential: {
+ type: Boolean,
+ required: true,
+ },
+ markdownPreviewUrl: {
+ type: String,
+ required: true,
+ },
+ markdownDocs: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ projectNamespace: {
+ type: String,
+ required: true,
+ },
+ projectsAutocompleteUrl: {
+ type: String,
+ required: true,
+ },
},
data() {
const store = new Store({
- titleHtml: this.initialTitle,
+ titleHtml: this.initialTitleHtml,
+ titleText: this.initialTitleText,
descriptionHtml: this.initialDescriptionHtml,
descriptionText: this.initialDescriptionText,
});
@@ -45,19 +91,97 @@ export default {
return {
store,
state: store.state,
+ showForm: false,
};
},
+ computed: {
+ formState() {
+ return this.store.formState;
+ },
+ },
components: {
descriptionComponent,
titleComponent,
+ formComponent,
+ },
+ methods: {
+ openForm() {
+ if (!this.showForm) {
+ this.showForm = true;
+ this.store.setFormState({
+ title: this.state.titleText,
+ confidential: this.isConfidential,
+ description: this.state.descriptionText,
+ lockedWarningVisible: false,
+ move_to_project_id: 0,
+ updateLoading: false,
+ });
+ }
+ },
+ closeForm() {
+ this.showForm = false;
+ },
+ updateIssuable() {
+ const canPostUpdate = this.store.formState.move_to_project_id !== 0 ?
+ confirm('Are you sure you want to move this issue to another project?') : true; // eslint-disable-line no-alert
+
+ if (!canPostUpdate) {
+ this.store.setFormState({
+ updateLoading: false,
+ });
+ return;
+ }
+
+ this.service.updateIssuable(this.store.formState)
+ .then(res => res.json())
+ .then((data) => {
+ if (location.pathname !== data.web_url) {
+ gl.utils.visitUrl(data.web_url);
+ } else if (data.confidential !== this.isConfidential) {
+ gl.utils.visitUrl(location.pathname);
+ }
+
+ return this.service.getData();
+ })
+ .then(res => res.json())
+ .then((data) => {
+ this.store.updateState(data);
+ eventHub.$emit('close.form');
+ })
+ .catch(() => {
+ eventHub.$emit('close.form');
+ return new Flash('Error updating issue');
+ });
+ },
+ deleteIssuable() {
+ this.service.deleteIssuable()
+ .then(res => res.json())
+ .then((data) => {
+ // Stop the poll so we don't get 404's with the issue not existing
+ this.poll.stop();
+
+ gl.utils.visitUrl(data.web_url);
+ })
+ .catch(() => {
+ eventHub.$emit('close.form');
+ return new Flash('Error deleting issue');
+ });
+ },
},
created() {
- const resource = new Service(this.endpoint);
- const poll = new Poll({
- resource,
+ this.service = new Service(this.endpoint);
+ this.poll = new Poll({
+ resource: this.service,
method: 'getData',
successCallback: (res) => {
- this.store.updateState(res.json());
+ const data = res.json();
+ const shouldUpdate = this.store.stateShouldUpdate(data);
+
+ this.store.updateState(data);
+
+ if (this.showForm && (shouldUpdate.title || shouldUpdate.description)) {
+ this.store.formState.lockedWarningVisible = true;
+ }
},
errorCallback(err) {
throw new Error(err);
@@ -65,32 +189,57 @@ export default {
});
if (!Visibility.hidden()) {
- poll.makeRequest();
+ this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
- poll.restart();
+ this.poll.restart();
} else {
- poll.stop();
+ this.poll.stop();
}
});
+
+ eventHub.$on('delete.issuable', this.deleteIssuable);
+ eventHub.$on('update.issuable', this.updateIssuable);
+ eventHub.$on('close.form', this.closeForm);
+ eventHub.$on('open.form', this.openForm);
+ },
+ beforeDestroy() {
+ eventHub.$off('delete.issuable', this.deleteIssuable);
+ eventHub.$off('update.issuable', this.updateIssuable);
+ eventHub.$off('close.form', this.closeForm);
+ eventHub.$off('open.form', this.openForm);
},
};
</script>
<template>
<div>
- <title-component
- :issuable-ref="issuableRef"
- :title-html="state.titleHtml"
- :title-text="state.titleText" />
- <description-component
- v-if="state.descriptionHtml"
- :can-update="canUpdate"
- :description-html="state.descriptionHtml"
- :description-text="state.descriptionText"
- :updated-at="state.updatedAt"
- :task-status="state.taskStatus" />
+ <form-component
+ v-if="canUpdate && showForm"
+ :form-state="formState"
+ :can-move="canMove"
+ :can-destroy="canDestroy"
+ :issuable-templates="issuableTemplates"
+ :markdown-docs="markdownDocs"
+ :markdown-preview-url="markdownPreviewUrl"
+ :project-path="projectPath"
+ :project-namespace="projectNamespace"
+ :projects-autocomplete-url="projectsAutocompleteUrl"
+ />
+ <div v-else>
+ <title-component
+ :issuable-ref="issuableRef"
+ :title-html="state.titleHtml"
+ :title-text="state.titleText" />
+ <description-component
+ v-if="state.descriptionHtml"
+ :can-update="canUpdate"
+ :description-html="state.descriptionHtml"
+ :description-text="state.descriptionText"
+ :updated-at="state.updatedAt"
+ :task-status="state.taskStatus" />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 4ad3eb7dfd7..3281ec6b172 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -18,11 +18,13 @@
},
updatedAt: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
taskStatus: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
},
data() {
@@ -83,6 +85,7 @@
<template>
<div
+ v-if="descriptionHtml"
class="description"
:class="{
'js-task-list-container': canUpdate
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
new file mode 100644
index 00000000000..8c81575fe6f
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -0,0 +1,79 @@
+<script>
+ import updateMixin from '../mixins/update';
+ import eventHub from '../event_hub';
+
+ export default {
+ mixins: [updateMixin],
+ props: {
+ canDestroy: {
+ type: Boolean,
+ required: true,
+ },
+ formState: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ deleteLoading: false,
+ };
+ },
+ computed: {
+ isSubmitEnabled() {
+ return this.formState.title.trim() !== '';
+ },
+ },
+ methods: {
+ closeForm() {
+ eventHub.$emit('close.form');
+ },
+ deleteIssuable() {
+ // eslint-disable-next-line no-alert
+ if (confirm('Issue will be removed! Are you sure?')) {
+ this.deleteLoading = true;
+
+ eventHub.$emit('delete.issuable');
+ }
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="prepend-top-default append-bottom-default clearfix">
+ <button
+ class="btn btn-save pull-left"
+ :class="{ disabled: formState.updateLoading || !isSubmitEnabled }"
+ type="submit"
+ :disabled="formState.updateLoading || !isSubmitEnabled"
+ @click.prevent="updateIssuable">
+ Save changes
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"
+ v-if="formState.updateLoading">
+ </i>
+ </button>
+ <button
+ class="btn btn-default pull-right"
+ type="button"
+ @click="closeForm">
+ Cancel
+ </button>
+ <button
+ v-if="canDestroy"
+ class="btn btn-danger pull-right append-right-default"
+ :class="{ disabled: deleteLoading }"
+ type="button"
+ :disabled="deleteLoading"
+ @click="deleteIssuable">
+ Delete
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"
+ v-if="deleteLoading">
+ </i>
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue b/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue
new file mode 100644
index 00000000000..a0ff08e9111
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue
@@ -0,0 +1,23 @@
+<script>
+ export default {
+ props: {
+ formState: {
+ type: Object,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <fieldset class="checkbox">
+ <label for="issue-confidential">
+ <input
+ type="checkbox"
+ value="1"
+ id="issue-confidential"
+ v-model="formState.confidential" />
+ This issue is confidential and should only be visible to team members with at least Reporter access.
+ </label>
+ </fieldset>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
new file mode 100644
index 00000000000..30a1be5cb50
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -0,0 +1,54 @@
+<script>
+ /* global Flash */
+ import updateMixin from '../../mixins/update';
+ import markdownField from '../../../vue_shared/components/markdown/field.vue';
+
+ export default {
+ mixins: [updateMixin],
+ props: {
+ formState: {
+ type: Object,
+ required: true,
+ },
+ markdownPreviewUrl: {
+ type: String,
+ required: true,
+ },
+ markdownDocs: {
+ type: String,
+ required: true,
+ },
+ },
+ components: {
+ markdownField,
+ },
+ mounted() {
+ this.$refs.textarea.focus();
+ },
+ };
+</script>
+
+<template>
+ <div class="common-note-form">
+ <label
+ class="sr-only"
+ for="issue-description">
+ Description
+ </label>
+ <markdown-field
+ :markdown-preview-url="markdownPreviewUrl"
+ :markdown-docs="markdownDocs">
+ <textarea
+ id="issue-description"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ data-supports-slash-commands="false"
+ aria-label="Description"
+ v-model="formState.description"
+ ref="textarea"
+ slot="textarea"
+ placeholder="Write a comment or drag your files here..."
+ @keydown.meta.enter="updateIssuable">
+ </textarea>
+ </markdown-field>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue
new file mode 100644
index 00000000000..1c40b286513
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue
@@ -0,0 +1,111 @@
+<script>
+ export default {
+ props: {
+ formState: {
+ type: Object,
+ required: true,
+ },
+ issuableTemplates: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ projectNamespace: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ issuableTemplatesJson() {
+ return JSON.stringify(this.issuableTemplates);
+ },
+ },
+ mounted() {
+ // Create the editor for the template
+ const editor = document.querySelector('.detail-page-description .note-textarea') || {};
+ editor.setValue = (val) => {
+ this.formState.description = val;
+ };
+ editor.getValue = () => this.formState.description;
+
+ this.issuableTemplate = new gl.IssuableTemplateSelectors({
+ $dropdowns: $(this.$refs.toggle),
+ editor,
+ });
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="dropdown js-issuable-selector-wrap"
+ data-issuable-type="issue">
+ <button
+ class="dropdown-menu-toggle js-issuable-selector"
+ type="button"
+ ref="toggle"
+ data-field-name="issuable_template"
+ data-selected="null"
+ data-toggle="dropdown"
+ :data-namespace-path="projectNamespace"
+ :data-project-path="projectPath"
+ :data-data="issuableTemplatesJson">
+ <span class="dropdown-toggle-text">
+ Choose a template
+ </span>
+ <i
+ aria-hidden="true"
+ class="fa fa-chevron-down">
+ </i>
+ </button>
+ <div class="dropdown-menu dropdown-select">
+ <div class="dropdown-title">
+ Choose a template
+ <button
+ class="dropdown-title-button dropdown-menu-close"
+ aria-label="Close"
+ type="button">
+ <i
+ aria-hidden="true"
+ class="fa fa-times dropdown-menu-close-icon">
+ </i>
+ </button>
+ </div>
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ placeholder="Filter"
+ autocomplete="off" />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search">
+ </i>
+ <i
+ role="button"
+ aria-label="Clear templates search input"
+ class="fa fa-times dropdown-input-clear js-dropdown-input-clear">
+ </i>
+ </div>
+ <div class="dropdown-content"></div>
+ <div class="dropdown-footer">
+ <ul class="dropdown-footer-list">
+ <li>
+ <a class="no-template">
+ No template
+ </a>
+ </li>
+ <li>
+ <a class="reset-template">
+ Reset template
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/fields/project_move.vue b/app/assets/javascripts/issue_show/components/fields/project_move.vue
new file mode 100644
index 00000000000..f811fb0de24
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/fields/project_move.vue
@@ -0,0 +1,83 @@
+<script>
+ import tooltipMixin from '../../../vue_shared/mixins/tooltip';
+
+ export default {
+ mixins: [
+ tooltipMixin,
+ ],
+ props: {
+ formState: {
+ type: Object,
+ required: true,
+ },
+ projectsAutocompleteUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ mounted() {
+ const $moveDropdown = $(this.$refs['move-dropdown']);
+
+ $moveDropdown.select2({
+ ajax: {
+ url: this.projectsAutocompleteUrl,
+ quietMillis: 125,
+ data(term, page, context) {
+ return {
+ search: term,
+ offset_id: context,
+ };
+ },
+ results(data) {
+ const more = data.length >= 50;
+ const context = data[data.length - 1] ? data[data.length - 1].id : null;
+
+ return {
+ results: data,
+ more,
+ context,
+ };
+ },
+ },
+ formatResult(project) {
+ return project.name_with_namespace;
+ },
+ formatSelection(project) {
+ return project.name_with_namespace;
+ },
+ })
+ .on('change', (e) => {
+ this.formState.move_to_project_id = parseInt(e.target.value, 10);
+ });
+ },
+ beforeDestroy() {
+ $(this.$refs['move-dropdown']).select2('destroy');
+ },
+ };
+</script>
+
+<template>
+ <fieldset>
+ <label
+ for="issuable-move"
+ class="sr-only">
+ Move
+ </label>
+ <div class="issuable-form-select-holder append-right-5">
+ <input
+ ref="move-dropdown"
+ type="hidden"
+ id="issuable-move"
+ data-placeholder="Move to a different project" />
+ </div>
+ <span
+ data-placement="auto top"
+ title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
+ ref="tooltip">
+ <i
+ class="fa fa-question-circle"
+ aria-hidden="true">
+ </i>
+ </span>
+ </fieldset>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue
new file mode 100644
index 00000000000..6556bf117e2
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/fields/title.vue
@@ -0,0 +1,31 @@
+<script>
+ import updateMixin from '../../mixins/update';
+
+ export default {
+ mixins: [updateMixin],
+ props: {
+ formState: {
+ type: Object,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <fieldset>
+ <label
+ class="sr-only"
+ for="issue-title">
+ Title
+ </label>
+ <input
+ id="issue-title"
+ class="form-control"
+ type="text"
+ placeholder="Issue title"
+ aria-label="Issue title"
+ v-model="formState.title"
+ @keydown.meta.enter="updateIssuable" />
+ </fieldset>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
new file mode 100644
index 00000000000..76ec3dc9a5d
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -0,0 +1,104 @@
+<script>
+ import lockedWarning from './locked_warning.vue';
+ import titleField from './fields/title.vue';
+ import descriptionField from './fields/description.vue';
+ import editActions from './edit_actions.vue';
+ import descriptionTemplate from './fields/description_template.vue';
+ import projectMove from './fields/project_move.vue';
+ import confidentialCheckbox from './fields/confidential_checkbox.vue';
+
+ export default {
+ props: {
+ canMove: {
+ type: Boolean,
+ required: true,
+ },
+ canDestroy: {
+ type: Boolean,
+ required: true,
+ },
+ formState: {
+ type: Object,
+ required: true,
+ },
+ issuableTemplates: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ markdownPreviewUrl: {
+ type: String,
+ required: true,
+ },
+ markdownDocs: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ projectNamespace: {
+ type: String,
+ required: true,
+ },
+ projectsAutocompleteUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ components: {
+ lockedWarning,
+ titleField,
+ descriptionField,
+ descriptionTemplate,
+ editActions,
+ projectMove,
+ confidentialCheckbox,
+ },
+ computed: {
+ hasIssuableTemplates() {
+ return this.issuableTemplates.length;
+ },
+ },
+ };
+</script>
+
+<template>
+ <form>
+ <locked-warning v-if="formState.lockedWarningVisible" />
+ <div class="row">
+ <div
+ class="col-sm-4 col-lg-3"
+ v-if="hasIssuableTemplates">
+ <description-template
+ :form-state="formState"
+ :issuable-templates="issuableTemplates"
+ :project-path="projectPath"
+ :project-namespace="projectNamespace" />
+ </div>
+ <div
+ :class="{
+ 'col-sm-8 col-lg-9': hasIssuableTemplates,
+ 'col-xs-12': !hasIssuableTemplates,
+ }">
+ <title-field
+ :form-state="formState"
+ :issuable-templates="issuableTemplates" />
+ </div>
+ </div>
+ <description-field
+ :form-state="formState"
+ :markdown-preview-url="markdownPreviewUrl"
+ :markdown-docs="markdownDocs" />
+ <confidential-checkbox
+ :form-state="formState" />
+ <project-move
+ v-if="canMove"
+ :form-state="formState"
+ :projects-autocomplete-url="projectsAutocompleteUrl" />
+ <edit-actions
+ :form-state="formState"
+ :can-destroy="canDestroy" />
+ </form>
+</template>
diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue
new file mode 100644
index 00000000000..1c2789f154a
--- /dev/null
+++ b/app/assets/javascripts/issue_show/components/locked_warning.vue
@@ -0,0 +1,20 @@
+<script>
+ export default {
+ computed: {
+ currentPath() {
+ return location.pathname;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="alert alert-danger">
+ Someone edited the issue at the same time you did. Please check out
+ <a
+ :href="currentPath"
+ target="_blank"
+ rel="nofollow">the issue</a>
+ and make sure your changes will not unintentionally remove theirs.
+ </div>
+</template>
diff --git a/app/assets/javascripts/issue_show/event_hub.js b/app/assets/javascripts/issue_show/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/issue_show/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index f06e33dee60..faf79471946 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,42 +1,49 @@
import Vue from 'vue';
+import eventHub from './event_hub';
import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor';
-document.addEventListener('DOMContentLoaded', () => new Vue({
- el: document.getElementById('js-issuable-app'),
- components: {
- issuableApp,
- },
- data() {
- const issuableElement = this.$options.el;
- const issuableTitleElement = issuableElement.querySelector('.title');
- const issuableDescriptionElement = issuableElement.querySelector('.wiki');
- const issuableDescriptionTextarea = issuableElement.querySelector('.js-task-list-field');
- const {
- canUpdate,
- endpoint,
- issuableRef,
- } = issuableElement.dataset;
+document.addEventListener('DOMContentLoaded', () => {
+ const initialDataEl = document.getElementById('js-issuable-app-initial-data');
+ const initialData = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
- return {
- canUpdate: gl.utils.convertPermissionToBoolean(canUpdate),
- endpoint,
- issuableRef,
- initialTitle: issuableTitleElement.innerHTML,
- initialDescriptionHtml: issuableDescriptionElement ? issuableDescriptionElement.innerHTML : '',
- initialDescriptionText: issuableDescriptionTextarea ? issuableDescriptionTextarea.textContent : '',
- };
- },
- render(createElement) {
- return createElement('issuable-app', {
- props: {
- canUpdate: this.canUpdate,
- endpoint: this.endpoint,
- issuableRef: this.issuableRef,
- initialTitle: this.initialTitle,
- initialDescriptionHtml: this.initialDescriptionHtml,
- initialDescriptionText: this.initialDescriptionText,
- },
- });
- },
-}));
+ $('.issuable-edit').on('click', (e) => {
+ e.preventDefault();
+
+ eventHub.$emit('open.form');
+ });
+
+ return new Vue({
+ el: document.getElementById('js-issuable-app'),
+ components: {
+ issuableApp,
+ },
+ data() {
+ return {
+ ...initialData,
+ };
+ },
+ render(createElement) {
+ return createElement('issuable-app', {
+ props: {
+ canUpdate: this.canUpdate,
+ canDestroy: this.canDestroy,
+ canMove: this.canMove,
+ endpoint: this.endpoint,
+ issuableRef: this.issuableRef,
+ initialTitleHtml: this.initialTitleHtml,
+ initialTitleText: this.initialTitleText,
+ initialDescriptionHtml: this.initialDescriptionHtml,
+ initialDescriptionText: this.initialDescriptionText,
+ issuableTemplates: this.issuableTemplates,
+ isConfidential: this.isConfidential,
+ markdownPreviewUrl: this.markdownPreviewUrl,
+ markdownDocs: this.markdownDocs,
+ projectPath: this.projectPath,
+ projectNamespace: this.projectNamespace,
+ projectsAutocompleteUrl: this.projectsAutocompleteUrl,
+ },
+ });
+ },
+ });
+});
diff --git a/app/assets/javascripts/issue_show/mixins/animate.js b/app/assets/javascripts/issue_show/mixins/animate.js
index eda6302aa8b..4816393da1f 100644
--- a/app/assets/javascripts/issue_show/mixins/animate.js
+++ b/app/assets/javascripts/issue_show/mixins/animate.js
@@ -4,7 +4,7 @@ export default {
this.preAnimation = true;
this.pulseAnimation = false;
- this.$nextTick(() => {
+ setTimeout(() => {
this.preAnimation = false;
this.pulseAnimation = true;
});
diff --git a/app/assets/javascripts/issue_show/mixins/update.js b/app/assets/javascripts/issue_show/mixins/update.js
new file mode 100644
index 00000000000..72be65b426f
--- /dev/null
+++ b/app/assets/javascripts/issue_show/mixins/update.js
@@ -0,0 +1,10 @@
+import eventHub from '../event_hub';
+
+export default {
+ methods: {
+ updateIssuable() {
+ this.formState.updateLoading = true;
+ eventHub.$emit('update.issuable');
+ },
+ },
+};
diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js
index 348ad8d6813..6f0fd0b1768 100644
--- a/app/assets/javascripts/issue_show/services/index.js
+++ b/app/assets/javascripts/issue_show/services/index.js
@@ -7,10 +7,23 @@ export default class Service {
constructor(endpoint) {
this.endpoint = endpoint;
- this.resource = Vue.resource(this.endpoint);
+ this.resource = Vue.resource(`${this.endpoint}.json`, {}, {
+ realtimeChanges: {
+ method: 'GET',
+ url: `${this.endpoint}/realtime_changes`,
+ },
+ });
}
getData() {
- return this.resource.get();
+ return this.resource.realtimeChanges();
+ }
+
+ deleteIssuable() {
+ return this.resource.delete();
+ }
+
+ updateIssuable(data) {
+ return this.resource.update(data);
}
}
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js
index 8e89a2b7730..4a16c3cb4dc 100644
--- a/app/assets/javascripts/issue_show/stores/index.js
+++ b/app/assets/javascripts/issue_show/stores/index.js
@@ -1,17 +1,26 @@
export default class Store {
constructor({
titleHtml,
+ titleText,
descriptionHtml,
descriptionText,
}) {
this.state = {
titleHtml,
- titleText: '',
+ titleText,
descriptionHtml,
descriptionText,
taskStatus: '',
updatedAt: '',
};
+ this.formState = {
+ title: '',
+ confidential: false,
+ description: '',
+ lockedWarningVisible: false,
+ move_to_project_id: 0,
+ updateLoading: false,
+ };
}
updateState(data) {
@@ -22,4 +31,15 @@ export default class Store {
this.state.taskStatus = data.task_status;
this.state.updatedAt = data.updated_at;
}
+
+ stateShouldUpdate(data) {
+ return {
+ title: this.state.titleText !== data.title_text,
+ description: this.state.descriptionText !== data.description_text,
+ };
+ }
+
+ setFormState(state) {
+ this.formState = Object.assign(this.formState, state);
+ }
}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index b43c1c3aac6..601d01e1be1 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -170,7 +170,7 @@ gl.text.init = function(form) {
});
};
gl.text.removeListeners = function(form) {
- return $('.js-md', form).off();
+ return $('.js-md', form).off('click');
};
gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index dace03554e8..51448252c0f 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -77,7 +77,9 @@ import './shortcuts_navigation';
ShortcutsIssuable.prototype.editIssue = function() {
var $editBtn;
$editBtn = $('.issuable-edit');
- return gl.utils.visitUrl($editBtn.attr('href'));
+ // Need to click the element as on issues, editing is inline
+ // on merge request, editing is on a different page
+ $editBtn.get(0).click();
};
ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index 3392cb9da29..419c458ff34 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,6 +1,6 @@
/* global Flash */
-import 'vendor/task_list';
+import 'deckar01-task_list';
class TaskList {
constructor(options = {}) {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
new file mode 100644
index 00000000000..e6977681e96
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -0,0 +1,107 @@
+<script>
+ /* global Flash */
+ import markdownHeader from './header.vue';
+ import markdownToolbar from './toolbar.vue';
+
+ export default {
+ props: {
+ markdownPreviewUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ markdownDocs: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ markdownPreview: '',
+ markdownPreviewLoading: false,
+ previewMarkdown: false,
+ };
+ },
+ components: {
+ markdownHeader,
+ markdownToolbar,
+ },
+ methods: {
+ toggleMarkdownPreview() {
+ this.previewMarkdown = !this.previewMarkdown;
+
+ if (!this.previewMarkdown) {
+ this.markdownPreview = '';
+ } else {
+ this.markdownPreviewLoading = true;
+ this.$http.post(
+ this.markdownPreviewUrl,
+ {
+ /*
+ Can't use `$refs` as the component is technically in the parent component
+ so we access the VNode & then get the element
+ */
+ text: this.$slots.textarea[0].elm.value,
+ },
+ )
+ .then((res) => {
+ const data = res.json();
+
+ this.markdownPreviewLoading = false;
+ this.markdownPreview = data.body;
+
+ this.$nextTick(() => {
+ $(this.$refs['markdown-preview']).renderGFM();
+ });
+ })
+ .catch(() => new Flash('Error loading markdown preview'));
+ }
+ },
+ },
+ mounted() {
+ /*
+ GLForm class handles all the toolbar buttons
+ */
+ return new gl.GLForm($(this.$refs['gl-form']), true);
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="md-area prepend-top-default append-bottom-default js-vue-markdown-field"
+ ref="gl-form">
+ <markdown-header
+ :preview-markdown="previewMarkdown"
+ @toggle-markdown="toggleMarkdownPreview" />
+ <div
+ class="md-write-holder"
+ v-show="!previewMarkdown">
+ <div class="zen-backdrop">
+ <slot name="textarea"></slot>
+ <a
+ class="zen-control zen-control-leave js-zen-leave"
+ href="#"
+ aria-label="Enter zen mode">
+ <i
+ class="fa fa-compress"
+ aria-hidden="true">
+ </i>
+ </a>
+ <markdown-toolbar
+ :markdown-docs="markdownDocs" />
+ </div>
+ </div>
+ <div
+ class="md md-preview-holder md-preview"
+ v-show="previewMarkdown">
+ <div
+ ref="markdown-preview"
+ v-html="markdownPreview">
+ </div>
+ <span v-if="markdownPreviewLoading">
+ Loading...
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
new file mode 100644
index 00000000000..1a11f493b7f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -0,0 +1,113 @@
+<script>
+ import tooltipMixin from '../../mixins/tooltip';
+ import toolbarButton from './toolbar_button.vue';
+
+ export default {
+ mixins: [
+ tooltipMixin,
+ ],
+ props: {
+ previewMarkdown: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ components: {
+ toolbarButton,
+ },
+ methods: {
+ toggleMarkdownPreview(e, form) {
+ if (form && !form.find('.js-vue-markdown-field').length) {
+ return;
+ } else if (e.target.blur) {
+ e.target.blur();
+ }
+
+ this.$emit('toggle-markdown');
+ },
+ },
+ mounted() {
+ $(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
+ $(document).on('markdown-preview:hide.vue', this.toggleMarkdownPreview);
+ },
+ beforeDestroy() {
+ $(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
+ $(document).off('markdown-preview:hide.vue', this.toggleMarkdownPreview);
+ },
+ };
+</script>
+
+<template>
+ <div class="md-header">
+ <ul class="nav-links clearfix">
+ <li :class="{ active: !previewMarkdown }">
+ <a
+ href="#md-write-holder"
+ tabindex="-1"
+ @click.prevent="toggleMarkdownPreview($event)">
+ Write
+ </a>
+ </li>
+ <li :class="{ active: previewMarkdown }">
+ <a
+ href="#md-preview-holder"
+ tabindex="-1"
+ @click.prevent="toggleMarkdownPreview($event)">
+ Preview
+ </a>
+ </li>
+ <li class="pull-right">
+ <div class="toolbar-group">
+ <toolbar-button
+ tag="**"
+ button-title="Add bold text"
+ icon="bold" />
+ <toolbar-button
+ tag="*"
+ button-title="Add italic text"
+ icon="italic" />
+ <toolbar-button
+ tag="> "
+ :prepend="true"
+ button-title="Insert a quote"
+ icon="quote-right" />
+ <toolbar-button
+ tag="`"
+ tag-block="```"
+ button-title="Insert code"
+ icon="code" />
+ <toolbar-button
+ tag="* "
+ :prepend="true"
+ button-title="Add a bullet list"
+ icon="list-ul" />
+ <toolbar-button
+ tag="1. "
+ :prepend="true"
+ button-title="Add a numbered list"
+ icon="list-ol" />
+ <toolbar-button
+ tag="* [ ] "
+ :prepend="true"
+ button-title="Add a task list"
+ icon="check-square-o" />
+ </div>
+ <div class="toolbar-group">
+ <button
+ aria-label="Go full screen"
+ class="toolbar-btn js-zen-enter"
+ data-container="body"
+ tabindex="-1"
+ title="Go full screen"
+ type="button"
+ ref="tooltip">
+ <i
+ aria-hidden="true"
+ class="fa fa-arrows-alt fa-fw">
+ </i>
+ </button>
+ </div>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
new file mode 100644
index 00000000000..93252293ba6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -0,0 +1,33 @@
+<script>
+ export default {
+ props: {
+ markdownDocs: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="comment-toolbar clearfix">
+ <div class="toolbar-text">
+ <a
+ :href="markdownDocs"
+ target="_blank"
+ tabindex="-1">
+ Markdown is supported
+ </a>
+ </div>
+ <button
+ class="toolbar-button markdown-selector"
+ type="button"
+ tabindex="-1">
+ <i
+ class="fa fa-file-image-o toolbar-button-icon"
+ aria-hidden="true">
+ </i>
+ Attach a file
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
new file mode 100644
index 00000000000..096be507625
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -0,0 +1,58 @@
+<script>
+ import tooltipMixin from '../../mixins/tooltip';
+
+ export default {
+ mixins: [
+ tooltipMixin,
+ ],
+ props: {
+ buttonTitle: {
+ type: String,
+ required: true,
+ },
+ icon: {
+ type: String,
+ required: true,
+ },
+ tag: {
+ type: String,
+ required: true,
+ },
+ tagBlock: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ prepend: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ iconClass() {
+ return `fa-${this.icon}`;
+ },
+ },
+ };
+</script>
+
+<template>
+ <button
+ type="button"
+ class="toolbar-btn js-md hidden-xs"
+ tabindex="-1"
+ ref="tooltip"
+ data-container="body"
+ :data-md-tag="tag"
+ :data-md-block="tagBlock"
+ :data-md-prepend="prepend"
+ :title="buttonTitle"
+ :aria-label="buttonTitle">
+ <i
+ aria-hidden="true"
+ class="fa fa-fw"
+ :class="iconClass">
+ </i>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/tooltip.js b/app/assets/javascripts/vue_shared/mixins/tooltip.js
index 9bb948bff66..995c0c98505 100644
--- a/app/assets/javascripts/vue_shared/mixins/tooltip.js
+++ b/app/assets/javascripts/vue_shared/mixins/tooltip.js
@@ -6,4 +6,8 @@ export default {
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
+
+ beforeDestroy() {
+ $(this.$refs.tooltip).tooltip('destroy');
+ },
};
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index d191bbb226c..90051ffe753 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -475,4 +475,5 @@
.filter-dropdown-loading {
padding: 8px 16px;
+ text-align: center;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 4db77752c0c..975a4b40383 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -293,7 +293,7 @@ $btn-white-active: #848484;
/*
* Badges
*/
-$badge-bg: #eee;
+$badge-bg: rgba(0, 0, 0, 0.07);
$badge-color: $gl-text-color-secondary;
/*
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index cffd3b6060d..f956e3757bf 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -550,13 +550,13 @@ ul.notes {
position: relative;
top: -2px;
display: inline-block;
- padding-left: 4px;
- padding-right: 4px;
+ padding-left: 7px;
+ padding-right: 7px;
color: $notes-role-color;
font-size: 12px;
line-height: 20px;
border: 1px solid $border-color;
- border-radius: $border-radius-base;
+ border-radius: $label-border-radius;
}
diff --git a/app/controllers/admin/builds_controller.rb b/app/controllers/admin/jobs_controller.rb
index 88f3c0e2fd4..5162273ef8a 100644
--- a/app/controllers/admin/builds_controller.rb
+++ b/app/controllers/admin/jobs_controller.rb
@@ -1,4 +1,4 @@
-class Admin::BuildsController < Admin::ApplicationController
+class Admin::JobsController < Admin::ApplicationController
def index
@scope = params[:scope]
@all_builds = Ci::Build
@@ -20,6 +20,6 @@ class Admin::BuildsController < Admin::ApplicationController
def cancel_all
Ci::Build.running_or_pending.each(&:cancel)
- redirect_to admin_builds_path
+ redirect_to admin_jobs_path
end
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 4cf645d6341..0c3b68a7ac3 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -14,7 +14,16 @@ module IssuableActions
name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted."
- redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
+ index_path = polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
+
+ respond_to do |format|
+ format.html { redirect_to index_path }
+ format.json do
+ render json: {
+ web_url: index_path
+ }
+ end
+ end
end
def bulk_update
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 6195121b931..f9c31920302 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -24,7 +24,7 @@ class DashboardController < Dashboard::ApplicationController
def load_events
projects =
if params[:filter] == "starred"
- current_user.viewable_starred_projects
+ ProjectsFinder.new(current_user: current_user, params: { starred: true }).execute
else
current_user.authorized_projects
end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 1224e9503c9..b46a33604ff 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -46,7 +46,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
def keep
build.keep_artifacts!
- redirect_to namespace_project_build_path(project.namespace, project, build)
+ redirect_to namespace_project_job_path(project.namespace, project, build)
end
def latest_succeeded
@@ -79,7 +79,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def build_from_id
- project.builds.find_by(id: params[:build_id]) if params[:build_id]
+ project.builds.find_by(id: params[:job_id]) if params[:job_id]
end
def build_from_ref
diff --git a/app/controllers/projects/build_artifacts_controller.rb b/app/controllers/projects/build_artifacts_controller.rb
new file mode 100644
index 00000000000..f34a198634e
--- /dev/null
+++ b/app/controllers/projects/build_artifacts_controller.rb
@@ -0,0 +1,55 @@
+class Projects::BuildArtifactsController < Projects::ApplicationController
+ include ExtractsPath
+ include RendersBlob
+
+ before_action :authorize_read_build!
+ before_action :extract_ref_name_and_path
+ before_action :validate_artifacts!
+
+ def download
+ redirect_to download_namespace_project_job_artifacts_path(project.namespace, project, job)
+ end
+
+ def browse
+ redirect_to browse_namespace_project_job_artifacts_path(project.namespace, project, job, path: params[:path])
+ end
+
+ def file
+ redirect_to file_namespace_project_job_artifacts_path(project.namespace, project, job, path: params[:path])
+ end
+
+ def raw
+ redirect_to raw_namespace_project_job_artifacts_path(project.namespace, project, job, path: params[:path])
+ end
+
+ def latest_succeeded
+ redirect_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, job, ref_name_and_path: params[:ref_name_and_path], job: params[:job])
+ end
+
+ private
+
+ def validate_artifacts!
+ render_404 unless job && job.artifacts?
+ end
+
+ def extract_ref_name_and_path
+ return unless params[:ref_name_and_path]
+
+ @ref_name, @path = extract_ref(params[:ref_name_and_path])
+ end
+
+ def job
+ @job ||= job_from_id || job_from_ref
+ end
+
+ def job_from_id
+ project.builds.find_by(id: params[:build_id]) if params[:build_id]
+ end
+
+ def job_from_ref
+ return unless @ref_name
+
+ jobs = project.latest_successful_builds_for(@ref_name)
+ jobs.find_by(name: params[:job])
+ end
+end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index dfaaea71b9c..1334a231788 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -1,131 +1,21 @@
class Projects::BuildsController < Projects::ApplicationController
- before_action :build, except: [:index, :cancel_all]
-
- before_action :authorize_read_build!,
- only: [:index, :show, :status, :raw, :trace]
- before_action :authorize_update_build!,
- except: [:index, :show, :status, :raw, :trace, :cancel_all]
-
- layout 'project'
+ before_action :authorize_read_build!
def index
- @scope = params[:scope]
- @all_builds = project.builds.relevant
- @builds = @all_builds.order('created_at DESC')
- @builds =
- case @scope
- when 'pending'
- @builds.pending.reverse_order
- when 'running'
- @builds.running.reverse_order
- when 'finished'
- @builds.finished
- else
- @builds
- end
- @builds = @builds.includes([
- { pipeline: :project },
- :project,
- :tags
- ])
- @builds = @builds.page(params[:page]).per(30)
- end
-
- def cancel_all
- return access_denied! unless can?(current_user, :update_build, project)
-
- @project.builds.running_or_pending.each do |build|
- build.cancel if can?(current_user, :update_build, build)
- end
-
- redirect_to namespace_project_builds_path(project.namespace, project)
+ redirect_to namespace_project_jobs_path(project.namespace, project)
end
def show
- @builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC')
- @builds = @builds.where("id not in (?)", @build.id)
- @pipeline = @build.pipeline
- end
-
- def trace
- build.trace.read do |stream|
- respond_to do |format|
- format.json do
- result = {
- id: @build.id, status: @build.status, complete: @build.complete?
- }
-
- if stream.valid?
- stream.limit
- state = params[:state].presence
- trace = stream.html_with_state(state)
- result.merge!(trace.to_h)
- end
-
- render json: result
- end
- end
- end
- end
-
- def retry
- return respond_422 unless @build.retryable?
-
- build = Ci::Build.retry(@build, current_user)
- redirect_to build_path(build)
- end
-
- def play
- return respond_422 unless @build.playable?
-
- build = @build.play(current_user)
- redirect_to build_path(build)
- end
-
- def cancel
- return respond_422 unless @build.cancelable?
-
- @build.cancel
- redirect_to build_path(@build)
- end
-
- def status
- render json: BuildSerializer
- .new(project: @project, current_user: @current_user)
- .represent_status(@build)
- end
-
- def erase
- if @build.erase(erased_by: current_user)
- redirect_to namespace_project_build_path(project.namespace, project, @build),
- notice: "Build has been successfully erased!"
- else
- respond_422
- end
+ redirect_to namespace_project_job_path(project.namespace, project, job)
end
def raw
- build.trace.read do |stream|
- if stream.file?
- send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
- else
- render_404
- end
- end
+ redirect_to raw_namespace_project_job_path(project.namespace, project, job)
end
private
- def authorize_update_build!
- return access_denied! unless can?(current_user, :update_build, build)
- end
-
- def build
- @build ||= project.builds.find(params[:id])
- .present(current_user: current_user)
- end
-
- def build_path(build)
- namespace_project_build_path(build.project.namespace, build.project, build)
+ def job
+ @job ||= project.builds.find(params[:id])
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index cbef8fa94d4..59df1e7b86a 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -148,10 +148,7 @@ class Projects::IssuesController < Projects::ApplicationController
format.json do
if @issue.valid?
- render json: @issue.to_json(methods: [:task_status, :task_status_short],
- include: { milestone: {},
- assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
- labels: { methods: :text_color } })
+ render json: IssueSerializer.new.represent(@issue)
else
render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
new file mode 100644
index 00000000000..d2cd1cfdab8
--- /dev/null
+++ b/app/controllers/projects/jobs_controller.rb
@@ -0,0 +1,131 @@
+class Projects::JobsController < Projects::ApplicationController
+ before_action :build, except: [:index, :cancel_all]
+
+ before_action :authorize_read_build!,
+ only: [:index, :show, :status, :raw, :trace]
+ before_action :authorize_update_build!,
+ except: [:index, :show, :status, :raw, :trace, :cancel_all]
+
+ layout 'project'
+
+ def index
+ @scope = params[:scope]
+ @all_builds = project.builds.relevant
+ @builds = @all_builds.order('created_at DESC')
+ @builds =
+ case @scope
+ when 'pending'
+ @builds.pending.reverse_order
+ when 'running'
+ @builds.running.reverse_order
+ when 'finished'
+ @builds.finished
+ else
+ @builds
+ end
+ @builds = @builds.includes([
+ { pipeline: :project },
+ :project,
+ :tags
+ ])
+ @builds = @builds.page(params[:page]).per(30)
+ end
+
+ def cancel_all
+ return access_denied! unless can?(current_user, :update_build, project)
+
+ @project.builds.running_or_pending.each do |build|
+ build.cancel if can?(current_user, :update_build, build)
+ end
+
+ redirect_to namespace_project_jobs_path(project.namespace, project)
+ end
+
+ def show
+ @builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC')
+ @builds = @builds.where("id not in (?)", @build.id)
+ @pipeline = @build.pipeline
+ end
+
+ def trace
+ build.trace.read do |stream|
+ respond_to do |format|
+ format.json do
+ result = {
+ id: @build.id, status: @build.status, complete: @build.complete?
+ }
+
+ if stream.valid?
+ stream.limit
+ state = params[:state].presence
+ trace = stream.html_with_state(state)
+ result.merge!(trace.to_h)
+ end
+
+ render json: result
+ end
+ end
+ end
+ end
+
+ def retry
+ return respond_422 unless @build.retryable?
+
+ build = Ci::Build.retry(@build, current_user)
+ redirect_to build_path(build)
+ end
+
+ def play
+ return respond_422 unless @build.playable?
+
+ build = @build.play(current_user)
+ redirect_to build_path(build)
+ end
+
+ def cancel
+ return respond_422 unless @build.cancelable?
+
+ @build.cancel
+ redirect_to build_path(@build)
+ end
+
+ def status
+ render json: BuildSerializer
+ .new(project: @project, current_user: @current_user)
+ .represent_status(@build)
+ end
+
+ def erase
+ if @build.erase(erased_by: current_user)
+ redirect_to namespace_project_job_path(project.namespace, project, @build),
+ notice: "Build has been successfully erased!"
+ else
+ respond_422
+ end
+ end
+
+ def raw
+ build.trace.read do |stream|
+ if stream.file?
+ send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
+ else
+ render_404
+ end
+ end
+ end
+
+ private
+
+ def authorize_update_build!
+ return access_denied! unless can?(current_user, :update_build, build)
+ end
+
+ def build
+ @build ||= project.builds.find(params[:id])
+ .present(current_user: current_user)
+ end
+
+ def build_path(build)
+ namespace_project_job_path(build.project.namespace, build.project, build)
+ end
+end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index f6d8226bf3f..5bf722d1ec6 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -7,6 +7,7 @@
# project_ids_relation: int[] - project ids to use
# params:
# trending: boolean
+# owned: boolean
# non_public: boolean
# starred: boolean
# sort: string
@@ -28,13 +29,17 @@ class ProjectsFinder < UnionFinder
def execute
items = init_collection
- items = by_ids(items)
+ items = items.map do |item|
+ item = by_ids(item)
+ item = by_personal(item)
+ item = by_starred(item)
+ item = by_trending(item)
+ item = by_visibilty_level(item)
+ item = by_tags(item)
+ item = by_search(item)
+ by_archived(item)
+ end
items = union(items)
- items = by_personal(items)
- items = by_visibilty_level(items)
- items = by_tags(items)
- items = by_search(items)
- items = by_archived(items)
sort(items)
end
@@ -43,10 +48,8 @@ class ProjectsFinder < UnionFinder
def init_collection
projects = []
- if params[:trending].present?
- projects << Project.trending
- elsif params[:starred].present? && current_user
- projects << current_user.viewable_starred_projects
+ if params[:owned].present?
+ projects << current_user.owned_projects if current_user
else
projects << current_user.authorized_projects if current_user
projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present?
@@ -56,7 +59,7 @@ class ProjectsFinder < UnionFinder
end
def by_ids(items)
- project_ids_relation ? items.map { |item| item.where(id: project_ids_relation) } : items
+ project_ids_relation ? items.where(id: project_ids_relation) : items
end
def union(items)
@@ -67,6 +70,14 @@ class ProjectsFinder < UnionFinder
(params[:personal].present? && current_user) ? items.personal(current_user) : items
end
+ def by_starred(items)
+ (params[:starred].present? && current_user) ? items.starred_by(current_user) : items
+ end
+
+ def by_trending(items)
+ params[:trending].present? ? items.trending : items
+ end
+
def by_visibilty_level(items)
params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index e5e64650708..36d9090b3ae 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -276,7 +276,7 @@ module ApplicationHelper
end
def show_user_callout?
- cookies[:user_callout_dismissed] == 'true'
+ cookies[:user_callout_dismissed].nil?
end
def linkedin_url(user)
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 622e14e21ff..11c972c6563 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -120,7 +120,7 @@ module BlobHelper
def blob_raw_url
if @build && @entry
- raw_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: @entry.path)
+ raw_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path: @entry.path)
elsif @snippet
if @snippet.project_id
raw_namespace_project_snippet_path(@project.namespace, @project, @snippet)
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index 2eb2c6c7389..f0a0d245dc0 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -2,7 +2,7 @@ module BuildsHelper
def build_summary(build, skip: false)
if build.has_trace?
if skip
- link_to "View job trace", pipeline_build_url(build.pipeline, build)
+ link_to "View job trace", pipeline_job_url(build.pipeline, build)
else
build.trace.html(last_lines: 10).html_safe
end
@@ -20,8 +20,8 @@ module BuildsHelper
def javascript_build_options
{
- page_url: namespace_project_build_url(@project.namespace, @project, @build),
- build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
+ page_url: namespace_project_job_url(@project.namespace, @project, @build),
+ build_url: namespace_project_job_url(@project.namespace, @project, @build, :json),
build_status: @build.status,
build_stage: @build.stage,
log_state: ''
@@ -31,7 +31,7 @@ module BuildsHelper
def build_failed_issue_options
{
title: "Build Failed ##{@build.id}",
- description: namespace_project_build_url(@project.namespace, @project, @build)
+ description: namespace_project_job_url(@project.namespace, @project, @build)
}
end
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index fc308b3960e..40864bed0ff 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -50,8 +50,8 @@ module GitlabRoutingHelper
namespace_project_cycle_analytics_path(project.namespace, project, *args)
end
- def project_builds_path(project, *args)
- namespace_project_builds_path(project.namespace, project, *args)
+ def project_jobs_path(project, *args)
+ namespace_project_jobs_path(project.namespace, project, *args)
end
def project_ref_path(project, ref_name, *args)
@@ -110,8 +110,8 @@ module GitlabRoutingHelper
namespace_project_pipeline_url(pipeline.project.namespace, pipeline.project, pipeline.id, *args)
end
- def pipeline_build_url(pipeline, build, *args)
- namespace_project_build_url(pipeline.project.namespace, pipeline.project, build.id, *args)
+ def pipeline_job_url(pipeline, build, *args)
+ namespace_project_job_url(pipeline.project.namespace, pipeline.project, build.id, *args)
end
def commits_url(entity, *args)
@@ -215,13 +215,13 @@ module GitlabRoutingHelper
case action
when 'download'
- download_namespace_project_build_artifacts_path(*args)
+ download_namespace_project_job_artifacts_path(*args)
when 'browse'
- browse_namespace_project_build_artifacts_path(*args)
+ browse_namespace_project_job_artifacts_path(*args)
when 'file'
- file_namespace_project_build_artifacts_path(*args)
+ file_namespace_project_job_artifacts_path(*args)
when 'raw'
- raw_namespace_project_build_artifacts_path(*args)
+ raw_namespace_project_job_artifacts_path(*args)
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 9290e4ec133..c380a10c82d 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -199,6 +199,27 @@ module IssuablesHelper
issuable_filter_params.any? { |k| params.key?(k) }
end
+ def issuable_initial_data(issuable)
+ {
+ endpoint: namespace_project_issue_path(@project.namespace, @project, issuable),
+ canUpdate: can?(current_user, :update_issue, issuable),
+ canDestroy: can?(current_user, :destroy_issue, issuable),
+ canMove: current_user ? issuable.can_move?(current_user) : false,
+ issuableRef: issuable.to_reference,
+ isConfidential: issuable.confidential,
+ markdownPreviewUrl: preview_markdown_path(@project),
+ markdownDocs: help_page_path('user/markdown'),
+ projectsAutocompleteUrl: autocomplete_projects_path(project_id: @project.id),
+ issuableTemplates: issuable_templates(issuable),
+ projectPath: ref_project.path,
+ projectNamespace: ref_project.namespace.full_path,
+ initialTitleHtml: markdown_field(issuable, :title),
+ initialTitleText: issuable.title,
+ initialDescriptionHtml: markdown_field(issuable, :description),
+ initialDescriptionText: issuable.description
+ }.to_json
+ end
+
private
def sidebar_gutter_collapsed?
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index b83068467ec..c7565c93811 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -51,6 +51,12 @@ module Ci
after_destroy :update_project_statistics
class << self
+ # This is needed for url_for to work,
+ # as the controller is JobsController
+ def model_name
+ ActiveModel::Name.new(self, nil, 'job')
+ end
+
def first_pending
pending.unstarted.order('created_at ASC').first
end
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 07213ca608a..45d8cd34359 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -24,6 +24,10 @@ module Ci
owner == current_user
end
+ def own!(user)
+ update(owner: user)
+ end
+
def inactive?
!active?
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 2922bebbaa7..d741af355fd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -242,6 +242,7 @@ class Project < ActiveRecord::Base
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
+ scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
@@ -350,10 +351,6 @@ class Project < ActiveRecord::Base
where("projects.id IN (#{union.to_sql})")
end
- def search_by_visibility(level)
- where(visibility_level: Gitlab::VisibilityLevel.string_options[level])
- end
-
def search_by_title(query)
pattern = "%#{query}%"
table = Project.arel_table
diff --git a/app/models/user.rb b/app/models/user.rb
index 3f816a250c2..9aad327b592 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -557,12 +557,6 @@ class User < ActiveRecord::Base
authorized_projects(Gitlab::Access::REPORTER).where(id: projects)
end
- def viewable_starred_projects
- starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (?)",
- [Project::PUBLIC, Project::INTERNAL],
- authorized_projects.select(:project_id))
- end
-
def owned_projects
@owned_projects ||=
Project.where('namespace_id IN (?) OR namespace_id = ?',
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index d4af4490608..2d7405dc240 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -23,7 +23,7 @@ module Ci
!::Gitlab::UserAccess
.new(user, project: build.project)
- .can_push_to_branch?(build.ref)
+ .can_merge_to_branch?(build.ref)
end
end
end
diff --git a/app/serializers/analytics_build_entity.rb b/app/serializers/analytics_build_entity.rb
index a0db5b8f0f4..ad7ad020b03 100644
--- a/app/serializers/analytics_build_entity.rb
+++ b/app/serializers/analytics_build_entity.rb
@@ -25,7 +25,7 @@ class AnalyticsBuildEntity < Grape::Entity
end
expose :url do |build|
- url_to(:namespace_project_build, build)
+ url_to(:namespace_project_job, build)
end
expose :commit_url do |build|
diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb
index 5e99204c658..301b718d060 100644
--- a/app/serializers/build_action_entity.rb
+++ b/app/serializers/build_action_entity.rb
@@ -6,7 +6,7 @@ class BuildActionEntity < Grape::Entity
end
expose :path do |build|
- play_namespace_project_build_path(
+ play_namespace_project_job_path(
build.project.namespace,
build.project,
build)
diff --git a/app/serializers/build_artifact_entity.rb b/app/serializers/build_artifact_entity.rb
index 8b643d8e783..dde17aa68b8 100644
--- a/app/serializers/build_artifact_entity.rb
+++ b/app/serializers/build_artifact_entity.rb
@@ -6,7 +6,7 @@ class BuildArtifactEntity < Grape::Entity
end
expose :path do |build|
- download_namespace_project_build_artifacts_path(
+ download_namespace_project_job_artifacts_path(
build.project.namespace,
build.project,
build)
diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb
index e2276808b90..05dd8270e92 100644
--- a/app/serializers/build_entity.rb
+++ b/app/serializers/build_entity.rb
@@ -5,15 +5,15 @@ class BuildEntity < Grape::Entity
expose :name
expose :build_path do |build|
- path_to(:namespace_project_build, build)
+ path_to(:namespace_project_job, build)
end
expose :retry_path do |build|
- path_to(:retry_namespace_project_build, build)
+ path_to(:retry_namespace_project_job, build)
end
expose :play_path, if: -> (*) { playable? } do |build|
- path_to(:play_namespace_project_build, build)
+ path_to(:play_namespace_project_job, build)
end
expose :playable?, as: :playable
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index bc4f68710b2..35df95549b7 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -1,4 +1,6 @@
class IssueEntity < IssuableEntity
+ include RequestAwareEntity
+
expose :branch_name
expose :confidential
expose :assignees, using: API::Entities::UserBasic
@@ -7,4 +9,8 @@ class IssueEntity < IssuableEntity
expose :project_id
expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity
+
+ expose :web_url do |issue|
+ namespace_project_issue_path(issue.project.namespace, issue.project, issue)
+ end
end
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index 163bd5662b0..dff549f502c 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -20,7 +20,7 @@
%span
Groups
= nav_link path: 'builds#index' do
- = link_to admin_builds_path, title: 'Jobs' do
+ = link_to admin_jobs_path, title: 'Jobs' do
%span
Jobs
= nav_link path: ['runners#index', 'runners#show'] do
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/jobs/index.html.haml
index 66d633119c2..09be17f07be 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/jobs/index.html.haml
@@ -4,15 +4,15 @@
%div{ class: container_class }
.top-area
- - build_path_proc = ->(scope) { admin_builds_path(scope: scope) }
+ - build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
.nav-controls
- if @all_builds.running_or_pending.any?
- = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
+ = link_to 'Cancel all', cancel_all_admin_jobs_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
.row-content-block.second-block
#{(@scope || 'all').capitalize} jobs
%ul.content-list.builds-content-list.admin-builds-table
- = render "projects/builds/table", builds: @builds, admin: true
+ = render "projects/jobs/table", builds: @builds, admin: true
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index dc4116e1ce0..801430e525e 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -85,7 +85,7 @@
%tr.build
%td.id
- if project
- = link_to namespace_project_build_path(project.namespace, project, build) do
+ = link_to namespace_project_job_path(project.namespace, project, build) do
%strong ##{build.id}
- else
%strong ##{build.id}
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 3b2a555a143..2890ae7173b 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -9,7 +9,7 @@
= render "projects/last_push"
%div{ class: container_class }
- - unless show_user_callout?
+ - if show_user_callout?
= render 'shared/user_callout'
- if @projects.any? || params[:name]
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index e4dfe0c8c08..29658da7792 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -92,7 +92,7 @@
-# Shortcut to Pipelines > Jobs
- if project_nav_tab? :builds
%li.hidden
- = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
+ = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
Jobs
-# Shortcut to commits page
diff --git a/app/views/notify/links/ci/builds/_build.html.haml b/app/views/notify/links/ci/builds/_build.html.haml
index d35b3839171..644cf506eff 100644
--- a/app/views/notify/links/ci/builds/_build.html.haml
+++ b/app/views/notify/links/ci/builds/_build.html.haml
@@ -1,2 +1,2 @@
-%a{ href: pipeline_build_url(pipeline, build), style: "color:#3777b0;text-decoration:none;" }
+%a{ href: pipeline_job_url(pipeline, build), style: "color:#3777b0;text-decoration:none;" }
= build.name
diff --git a/app/views/notify/links/ci/builds/_build.text.erb b/app/views/notify/links/ci/builds/_build.text.erb
index 741c7f344c8..773ae8174e9 100644
--- a/app/views/notify/links/ci/builds/_build.text.erb
+++ b/app/views/notify/links/ci/builds/_build.text.erb
@@ -1 +1 @@
-Job #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> )
+Job #<%= build.id %> ( <%= pipeline_job_url(pipeline, build) %> )
diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml
index 34d5c3b7285..e2966ec33c2 100644
--- a/app/views/projects/artifacts/_tree_directory.html.haml
+++ b/app/views/projects/artifacts/_tree_directory.html.haml
@@ -1,4 +1,4 @@
-- path_to_directory = browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path)
+- path_to_directory = browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path: directory.path)
%tr.tree-item{ 'data-link' => path_to_directory }
%td.tree-item-file-name
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
index ce7e25d774b..ea0b43b85cf 100644
--- a/app/views/projects/artifacts/_tree_file.html.haml
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -1,4 +1,4 @@
-- path_to_file = file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: file.path)
+- path_to_file = file_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path: file.path)
%tr.tree-item{ 'data-link' => path_to_file }
- blob = file.blob
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index 9fbb30f7c7c..961c805dc7c 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,22 +1,22 @@
- page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
= render "projects/pipelines/head"
-= render "projects/builds/header", show_controls: false
+= render "projects/jobs/header", show_controls: false
.tree-holder
.nav-block
.tree-controls
- = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
+ = link_to download_namespace_project_job_artifacts_path(@project.namespace, @project, @build),
rel: 'nofollow', download: '', class: 'btn btn-default download' do
= icon('download')
Download artifacts archive
%ul.breadcrumb.repo-breadcrumb
%li
- = link_to 'Artifacts', browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build)
+ = link_to 'Artifacts', browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build)
- path_breadcrumbs do |title, path|
%li
- = link_to truncate(title, length: 40), browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path)
+ = link_to truncate(title, length: 40), browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path)
.tree-content-holder
%table.table.tree-table
diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml
index d8da83b9a80..b25c7c95196 100644
--- a/app/views/projects/artifacts/file.html.haml
+++ b/app/views/projects/artifacts/file.html.haml
@@ -1,21 +1,21 @@
- page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
= render "projects/pipelines/head"
-= render "projects/builds/header", show_controls: false
+= render "projects/jobs/header", show_controls: false
#tree-holder.tree-holder
.nav-block
%ul.breadcrumb.repo-breadcrumb
%li
- = link_to 'Artifacts', browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build)
+ = link_to 'Artifacts', browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build)
- path_breadcrumbs do |title, path|
- title = truncate(title, length: 40)
%li
- if path == @path
- = link_to file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path) do
+ = link_to file_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path) do
%strong= title
- else
- = link_to title, browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path)
+ = link_to title, browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build, path)
%article.file-holder
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index a190a8760ef..d9f28d66b66 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -14,7 +14,7 @@
%td.branch-commit
- if can?(current_user, :read_build, job)
- = link_to namespace_project_build_url(job.project.namespace, job.project, job) do
+ = link_to namespace_project_job_url(job.project.namespace, job.project, job) do
%span.build-link ##{job.id}
- else
%span.build-link ##{job.id}
@@ -95,16 +95,16 @@
%td
.pull-right
- if can?(current_user, :read_build, job) && job.artifacts?
- = link_to download_namespace_project_build_artifacts_path(job.project.namespace, job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do
+ = link_to download_namespace_project_job_artifacts_path(job.project.namespace, job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do
= icon('download')
- if can?(current_user, :update_build, job)
- if job.active?
- = link_to cancel_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
+ = link_to cancel_namespace_project_job_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do
= icon('remove', class: 'cred')
- elsif allow_retry
- if job.playable? && !admin && can?(current_user, :update_build, job)
- = link_to play_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
+ = link_to play_namespace_project_job_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do
= custom_icon('icon_play')
- elsif job.retryable?
- = link_to retry_namespace_project_build_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
+ = link_to retry_namespace_project_job_path(job.project.namespace, job.project, job, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do
= icon('repeat')
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index 506246f2ee6..e2baaa625ae 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -8,6 +8,7 @@
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |action|
+ - next unless can?(current_user, :update_build, action)
%li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 7315e671056..9e221240cf2 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -13,7 +13,7 @@
= render 'projects/environments/metrics_button', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
- - if can?(current_user, :create_deployment, @environment) && @environment.can_stop?
+ - if can?(current_user, :stop_environment, @environment)
= link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
.environments-container
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 67403c36d7f..7bf271c2fc5 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -31,7 +31,7 @@
%ul
- if can_update_issue
%li
- = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
+ = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'issuable-edit'
%li
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li
@@ -55,10 +55,8 @@
.issue-details.issuable-details
.detail-page-description.content-block
- #js-issuable-app{ "data" => { "endpoint" => realtime_changes_namespace_project_issue_path(@project.namespace, @project, @issue),
- "can-update" => can?(current_user, :update_issue, @issue).to_s,
- "issuable-ref" => @issue.to_reference,
- } }
+ %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue)
+ #js-issuable-app
%h2.title= markdown_field(@issue, :title)
- if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/jobs/_header.html.haml
index d4cdb709b97..ad72ab5b199 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/jobs/_header.html.haml
@@ -6,7 +6,7 @@
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false, title: @build.status_title
%strong
Job
- = link_to "##{@build.id}", namespace_project_build_path(@project.namespace, @project, @build), class: 'js-build-id'
+ = link_to "##{@build.id}", namespace_project_job_path(@project.namespace, @project, @build), class: 'js-build-id'
in pipeline
%strong
= link_to "##{pipeline.id}", pipeline_path(pipeline)
@@ -17,7 +17,7 @@
%strong
= link_to @build.ref, project_ref_path(@project, @build.ref), class: 'ref-name'
- = render "projects/builds/user" if @build.user
+ = render "projects/jobs/user" if @build.user
= time_ago_with_tooltip(@build.created_at)
@@ -26,6 +26,6 @@
- if can?(current_user, :create_issue, @project) && @build.failed?
= link_to "New issue", new_namespace_project_issue_path(@project.namespace, @project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted'
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post
+ = link_to "Retry job", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index b3abc0e3da1..3e83142377b 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -30,21 +30,21 @@
- if @build.artifacts?
.btn-group.btn-group-justified{ role: :group }
- if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
- = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
+ = link_to keep_namespace_project_job_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep
- = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do
+ = link_to download_namespace_project_job_artifacts_path(@project.namespace, @project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do
Download
- if @build.artifacts_metadata?
- = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ = link_to browse_namespace_project_job_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
Browse
.block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
.title
Job details
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
+ = link_to "Retry job", retry_namespace_project_job_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- if @build.merge_request
%p.build-detail-row
%span.build-light-text Merge Request:
@@ -69,7 +69,7 @@
\##{@build.runner.id}
.btn-group.btn-group-justified{ role: :group }
- if @build.active?
- = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
+ = link_to "Cancel", cancel_namespace_project_job_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
- if @build.trigger_request
.build-widget
@@ -119,7 +119,7 @@
- HasStatus::ORDERED_STATUSES.each do |build_status|
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
- = link_to namespace_project_build_path(@project.namespace, @project, build) do
+ = link_to namespace_project_job_path(@project.namespace, @project, build) do
= icon('arrow-right')
%span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status)
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/jobs/_table.html.haml
index 82806f022ee..82806f022ee 100644
--- a/app/views/projects/builds/_table.html.haml
+++ b/app/views/projects/jobs/_table.html.haml
diff --git a/app/views/projects/builds/_user.html.haml b/app/views/projects/jobs/_user.html.haml
index 83f299da651..83f299da651 100644
--- a/app/views/projects/builds/_user.html.haml
+++ b/app/views/projects/jobs/_user.html.haml
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/jobs/index.html.haml
index a8c8afe2695..a33e3978ee1 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -4,13 +4,13 @@
%div{ class: container_class }
.top-area
- - build_path_proc = ->(scope) { project_builds_path(@project, scope: scope) }
+ - build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
.nav-controls
- if can?(current_user, :update_build, @project)
- if @all_builds.running_or_pending.any?
- = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project),
+ = link_to 'Cancel running', cancel_all_namespace_project_jobs_path(@project.namespace, @project),
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- unless @repository.gitlab_ci_yml
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/jobs/show.html.haml
index a5a9a6435e3..0d10dfcef70 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -62,17 +62,17 @@
Showing last
%span.js-truncated-info-size.truncated-info-size><
KiB of log -
- %a.js-raw-link.raw-link{ href: raw_namespace_project_build_path(@project.namespace, @project, @build) }>< Complete Raw
+ %a.js-raw-link.raw-link{ href: raw_namespace_project_job_path(@project.namespace, @project, @build) }>< Complete Raw
.controllers
- if @build.has_trace?
- = link_to raw_namespace_project_build_path(@project.namespace, @project, @build),
+ = link_to raw_namespace_project_job_path(@project.namespace, @project, @build),
title: 'Open raw trace',
data: { placement: 'top', container: 'body' },
class: 'js-raw-link-controller has-tooltip' do
= icon('download')
- if can?(current_user, :update_build, @project) && @build.erasable?
- = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
+ = link_to erase_namespace_project_job_path(@project.namespace, @project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
title: 'Erase Build',
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index db9d77dba16..a33da149c62 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -11,7 +11,7 @@
- if project_nav_tab? :builds
= nav_link(controller: [:builds, :artifacts]) do
- = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
+ = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
Jobs
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index aea8d13b7c5..01cf2cc80e5 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -51,5 +51,5 @@
%span.stage
= build.stage.titleize
%span.build-name
- = link_to build.name, pipeline_build_url(pipeline, build)
+ = link_to build.name, pipeline_job_url(pipeline, build)
%pre.build-log= build_summary(build, skip: index >= 10)
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index c587155bc4f..c239253c8d5 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -100,7 +100,7 @@
Snippets
%div{ class: container_class }
- - if @user == current_user && !show_user_callout?
+ - if @user == current_user && show_user_callout?
= render 'shared/user_callout'
.tab-content
#activity.tab-pane
diff --git a/changelogs/unreleased/30892-add-api-support-for-pipeline-schedule.yml b/changelogs/unreleased/30892-add-api-support-for-pipeline-schedule.yml
new file mode 100644
index 00000000000..26ce84697d0
--- /dev/null
+++ b/changelogs/unreleased/30892-add-api-support-for-pipeline-schedule.yml
@@ -0,0 +1,4 @@
+---
+title: Add API support for pipeline schedule
+merge_request: 11307
+author: dosuken123
diff --git a/changelogs/unreleased/33000-tag-list-in-project-create-api.yml b/changelogs/unreleased/33000-tag-list-in-project-create-api.yml
new file mode 100644
index 00000000000..b0d0d3cbeba
--- /dev/null
+++ b/changelogs/unreleased/33000-tag-list-in-project-create-api.yml
@@ -0,0 +1,4 @@
+---
+title: Add tag_list param to project api
+merge_request: 11799
+author: Ivan Chernov
diff --git a/changelogs/unreleased/UI-improvements-for-count-badges-and-permission-badges.yml b/changelogs/unreleased/UI-improvements-for-count-badges-and-permission-badges.yml
new file mode 100644
index 00000000000..374f643faa7
--- /dev/null
+++ b/changelogs/unreleased/UI-improvements-for-count-badges-and-permission-badges.yml
@@ -0,0 +1,5 @@
+---
+title: Count badges depend on translucent color to better adjust to different background
+ colors and permission badges now feature a pill shaped design similar to labels
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml b/changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml
new file mode 100644
index 00000000000..43c18502cd6
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-use-merge-ability-for-protected-manual-actions.yml
@@ -0,0 +1,4 @@
+---
+title: Respect merge, instead of push, permissions for protected actions
+merge_request: 11648
+author:
diff --git a/changelogs/unreleased/issue-edit-inline.yml b/changelogs/unreleased/issue-edit-inline.yml
new file mode 100644
index 00000000000..db03d1bdac4
--- /dev/null
+++ b/changelogs/unreleased/issue-edit-inline.yml
@@ -0,0 +1,4 @@
+---
+title: Enables inline editing for an issues title & description
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue-template-reproduce-in-example-project.yml b/changelogs/unreleased/issue-template-reproduce-in-example-project.yml
new file mode 100644
index 00000000000..8116007b459
--- /dev/null
+++ b/changelogs/unreleased/issue-template-reproduce-in-example-project.yml
@@ -0,0 +1,4 @@
+---
+title: Ask for an example project for bug reports
+merge_request:
+author:
diff --git a/changelogs/unreleased/rename-builds-controller.yml b/changelogs/unreleased/rename-builds-controller.yml
new file mode 100644
index 00000000000..7f6872ccf95
--- /dev/null
+++ b/changelogs/unreleased/rename-builds-controller.yml
@@ -0,0 +1,4 @@
+---
+title: Change /builds in the URL to /-/jobs. Backward URLs were also added
+merge_request: 11407
+author:
diff --git a/changelogs/unreleased/task-list-2.yml b/changelogs/unreleased/task-list-2.yml
new file mode 100644
index 00000000000..cbae8178081
--- /dev/null
+++ b/changelogs/unreleased/task-list-2.yml
@@ -0,0 +1,4 @@
+---
+title: Update task_list to version 2.0.0
+merge_request: 11525
+author: Jared Deckard <jared.deckard@gmail.com>
diff --git a/changelogs/unreleased/tc-improve-project-api-perf.yml b/changelogs/unreleased/tc-improve-project-api-perf.yml
new file mode 100644
index 00000000000..7e88466c058
--- /dev/null
+++ b/changelogs/unreleased/tc-improve-project-api-perf.yml
@@ -0,0 +1,4 @@
+---
+title: Improve performance of ProjectFinder used in /projects API endpoint
+merge_request: 11666
+author:
diff --git a/config/initializers/relative_naming_ci_namespace.rb b/config/initializers/relative_naming_ci_namespace.rb
index 59abe1b9b91..03ac55be0b6 100644
--- a/config/initializers/relative_naming_ci_namespace.rb
+++ b/config/initializers/relative_naming_ci_namespace.rb
@@ -4,7 +4,7 @@
# - [project.namespace, project, build]
#
# instead of:
-# - namespace_project_build_path(project.namespace, project, build)
+# - namespace_project_job_path(project.namespace, project, build)
#
# Without that, Ci:: namespace is used for resolving routes:
# - namespace_project_ci_build_path(project.namespace, project, build)
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index c20581b1333..ccfd85aed63 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -118,7 +118,7 @@ namespace :admin do
resources :cohorts, only: :index
- resources :builds, only: :index do
+ resources :jobs, only: :index do
collection do
post :cancel_all
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index bec1f04d1f9..5aac44fce10 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -1,4 +1,5 @@
require 'constraints/project_url_constrainer'
+require 'gitlab/routes/legacy_builds'
resources :projects, only: [:index, :new, :create]
@@ -180,38 +181,42 @@ constraints(ProjectUrlConstrainer.new) do
end
end
- resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
- collection do
- post :cancel_all
-
- resources :artifacts, only: [] do
- collection do
- get :latest_succeeded,
- path: '*ref_name_and_path',
- format: false
+ scope '-' do
+ resources :jobs, only: [:index, :show], constraints: { id: /\d+/ } do
+ collection do
+ post :cancel_all
+
+ resources :artifacts, only: [] do
+ collection do
+ get :latest_succeeded,
+ path: '*ref_name_and_path',
+ format: false
+ end
end
end
- end
- member do
- get :status
- post :cancel
- post :retry
- post :play
- post :erase
- get :trace, defaults: { format: 'json' }
- get :raw
- end
+ member do
+ get :status
+ post :cancel
+ post :retry
+ post :play
+ post :erase
+ get :trace, defaults: { format: 'json' }
+ get :raw
+ end
- resource :artifacts, only: [] do
- get :download
- get :browse, path: 'browse(/*path)', format: false
- get :file, path: 'file/*path', format: false
- get :raw, path: 'raw/*path', format: false
- post :keep
+ resource :artifacts, only: [] do
+ get :download
+ get :browse, path: 'browse(/*path)', format: false
+ get :file, path: 'file/*path', format: false
+ get :raw, path: 'raw/*path', format: false
+ post :keep
+ end
end
end
+ Gitlab::Routes::LegacyBuilds.new(self).draw
+
resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do
member do
get :test
diff --git a/doc/api/README.md b/doc/api/README.md
index 1b0f6470b13..45579ccac4e 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -33,6 +33,7 @@ following locations:
- [Notification settings](notification_settings.md)
- [Pipelines](pipelines.md)
- [Pipeline Triggers](pipeline_triggers.md)
+- [Pipeline Schedules](pipeline_schedules.md)
- [Projects](projects.md) including setting Webhooks
- [Project Access Requests](access_requests.md)
- [Project Members](members.md)
diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md
new file mode 100644
index 00000000000..433654c18cc
--- /dev/null
+++ b/doc/api/pipeline_schedules.md
@@ -0,0 +1,273 @@
+# Pipeline schedules
+
+You can read more about [pipeline schedules](../user/project/pipelines/schedules.md).
+
+## Get all pipeline schedules
+
+Get a list of the pipeline schedules of a project.
+
+```
+GET /projects/:id/pipeline_schedules
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `scope` | string | no | The scope of pipeline schedules, one of: `active`, `inactive` |
+
+```sh
+curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules"
+```
+
+```json
+[
+ {
+ "id": 13,
+ "description": "Test schedule pipeline",
+ "ref": "master",
+ "cron": "* * * * *",
+ "cron_timezone": "Asia/Tokyo",
+ "next_run_at": "2017-05-19T13:41:00.000Z",
+ "active": true,
+ "created_at": "2017-05-19T13:31:08.849Z",
+ "updated_at": "2017-05-19T13:40:17.727Z",
+ "owner": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/root"
+ }
+ }
+]
+```
+
+## Get a single pipeline schedule
+
+Get the pipeline schedule of a project.
+
+```
+GET /projects/:id/pipeline_schedules/:pipeline_schedule_id
+```
+
+| Attribute | Type | required | Description |
+|--------------|---------|----------|--------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+
+```sh
+curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13"
+```
+
+```json
+{
+ "id": 13,
+ "description": "Test schedule pipeline",
+ "ref": "master",
+ "cron": "* * * * *",
+ "cron_timezone": "Asia/Tokyo",
+ "next_run_at": "2017-05-19T13:41:00.000Z",
+ "active": true,
+ "created_at": "2017-05-19T13:31:08.849Z",
+ "updated_at": "2017-05-19T13:40:17.727Z",
+ "last_pipeline": {
+ "id": 332,
+ "sha": "0e788619d0b5ec17388dffb973ecd505946156db",
+ "ref": "master",
+ "status": "pending"
+ },
+ "owner": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/root"
+ }
+}
+```
+
+## Create a new pipeline schedule
+
+Create a new pipeline schedule of a project.
+
+```
+POST /projects/:id/pipeline_schedules
+```
+
+| Attribute | Type | required | Description |
+|---------------|---------|----------|--------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `description` | string | yes | The description of pipeline schedule |
+| `ref` | string | yes | The branch/tag name will be triggered |
+| `cron ` | string | yes | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
+| `cron_timezone ` | string | no | The timezone supproted by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) (default: `'UTC'`) |
+| `active ` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially (default: `true`) |
+
+```sh
+curl --request POST --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form description="Build packages" --form ref="master" --form cron="0 1 * * 5" --form cron_timezone="UTC" --form active="true" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules"
+```
+
+```json
+{
+ "id": 14,
+ "description": "Build packages",
+ "ref": "master",
+ "cron": "0 1 * * 5",
+ "cron_timezone": "UTC",
+ "next_run_at": "2017-05-26T01:00:00.000Z",
+ "active": true,
+ "created_at": "2017-05-19T13:43:08.169Z",
+ "updated_at": "2017-05-19T13:43:08.169Z",
+ "last_pipeline": null,
+ "owner": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/root"
+ }
+}
+```
+
+## Edit a pipeline schedule
+
+Updates the pipeline schedule of a project. Once the update is done, it will be rescheduled automatically.
+
+```
+PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id
+```
+
+| Attribute | Type | required | Description |
+|---------------|---------|----------|--------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+| `description` | string | no | The description of pipeline schedule |
+| `ref` | string | no | The branch/tag name will be triggered |
+| `cron ` | string | no | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
+| `cron_timezone ` | string | no | The timezone supproted by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) or `TZInfo::Timezone` (e.g. `America/Los_Angeles`) |
+| `active ` | boolean | no | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially. |
+
+```sh
+curl --request PUT --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form cron="0 2 * * *" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13"
+```
+
+```json
+{
+ "id": 13,
+ "description": "Test schedule pipeline",
+ "ref": "master",
+ "cron": "0 2 * * *",
+ "cron_timezone": "Asia/Tokyo",
+ "next_run_at": "2017-05-19T17:00:00.000Z",
+ "active": true,
+ "created_at": "2017-05-19T13:31:08.849Z",
+ "updated_at": "2017-05-19T13:44:16.135Z",
+ "last_pipeline": {
+ "id": 332,
+ "sha": "0e788619d0b5ec17388dffb973ecd505946156db",
+ "ref": "master",
+ "status": "pending"
+ },
+ "owner": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/root"
+ }
+}
+```
+
+## Take ownership of a pipeline schedule
+
+Update the owner of the pipeline schedule of a project.
+
+```
+POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership
+```
+
+| Attribute | Type | required | Description |
+|---------------|---------|----------|--------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+
+```sh
+curl --request POST --header "PRIVATE-TOKEN: hf2CvZXB9w8Uc5pZKpSB" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/take_ownership"
+```
+
+```json
+{
+ "id": 13,
+ "description": "Test schedule pipeline",
+ "ref": "master",
+ "cron": "0 2 * * *",
+ "cron_timezone": "Asia/Tokyo",
+ "next_run_at": "2017-05-19T17:00:00.000Z",
+ "active": true,
+ "created_at": "2017-05-19T13:31:08.849Z",
+ "updated_at": "2017-05-19T13:46:37.468Z",
+ "last_pipeline": {
+ "id": 332,
+ "sha": "0e788619d0b5ec17388dffb973ecd505946156db",
+ "ref": "master",
+ "status": "pending"
+ },
+ "owner": {
+ "name": "shinya",
+ "username": "maeda",
+ "id": 50,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/8ca0a796a679c292e3a11da50f99e801?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/maeda"
+ }
+}
+```
+
+## Delete a pipeline schedule
+
+Delete the pipeline schedule of a project.
+
+```
+DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id
+```
+
+| Attribute | Type | required | Description |
+|----------------|---------|----------|--------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+
+```sh
+curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13"
+```
+
+```json
+{
+ "id": 13,
+ "description": "Test schedule pipeline",
+ "ref": "master",
+ "cron": "0 2 * * *",
+ "cron_timezone": "Asia/Tokyo",
+ "next_run_at": "2017-05-19T17:00:00.000Z",
+ "active": true,
+ "created_at": "2017-05-19T13:31:08.849Z",
+ "updated_at": "2017-05-19T13:46:37.468Z",
+ "last_pipeline": {
+ "id": 332,
+ "sha": "0e788619d0b5ec17388dffb973ecd505946156db",
+ "ref": "master",
+ "status": "pending"
+ },
+ "owner": {
+ "name": "shinya",
+ "username": "maeda",
+ "id": 50,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/8ca0a796a679c292e3a11da50f99e801?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/maeda"
+ }
+}
+```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 345f93a6017..62c78ddc32e 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -473,6 +473,7 @@ Parameters:
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
+| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
### Create project for user
@@ -506,6 +507,7 @@ Parameters:
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
+| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
### Edit project
@@ -538,6 +540,7 @@ Parameters:
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
+| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
### Fork project
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index da20076da52..fab5d14ac54 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -591,7 +591,7 @@ Optional manual actions have `allow_failure: true` set by default.
**Manual actions are considered to be write actions, so permissions for
protected branches are used when user wants to trigger an action. In other
words, in order to trigger a manual action assigned to a branch that the
-pipeline is running for, user needs to have ability to push to this branch.**
+pipeline is running for, user needs to have ability to merge to this branch.**
### environment
@@ -1105,6 +1105,36 @@ variables:
GIT_STRATEGY: none
```
+## Git Checkout
+
+> Introduced in GitLab Runner 9.3
+
+The `GIT_CHECKOUT` variable can be used when the `GIT_STRATEGY` is set to either
+`clone` or `fetch` to specify whether a `git checkout` should be run. If not
+specified, it defaults to true. Like `GIT_STRATEGY`, it can be set in either the
+global [`variables`](#variables) section or the [`variables`](#job-variables)
+section for individual jobs.
+
+If set to `false`, the Runner will:
+
+- when doing `fetch` - update the repository and leave working copy on
+ the current revision,
+- when doing `clone` - clone the repository and leave working copy on the
+ default branch.
+
+Having this setting set to `true` will mean that for both `clone` and `fetch`
+strategies the Runner will checkout the working copy to a revision related
+to the CI pipeline:
+
+```yaml
+variables:
+ GIT_STRATEGY: clone
+ GIT_CHECKOUT: false
+script:
+ - git checkout master
+ - git merge $CI_BUILD_REF_NAME
+```
+
## Git Submodule Strategy
> Requires GitLab Runner v1.10+.
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index 5549fc25525..624f1a7858b 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -27,11 +27,11 @@ module SharedBuilds
end
step 'I visit recent build details page' do
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ visit namespace_project_job_path(@project.namespace, @project, @build)
end
step 'I visit project builds page' do
- visit namespace_project_builds_path(@project.namespace, @project)
+ visit namespace_project_jobs_path(@project.namespace, @project)
end
step 'recent build has artifacts available' do
@@ -56,7 +56,7 @@ module SharedBuilds
end
step 'I access artifacts download page' do
- visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build)
+ visit download_namespace_project_job_artifacts_path(@project.namespace, @project, @build)
end
step 'I see details of a build' do
diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb
index 6610b97ecb2..c2bec2a6320 100644
--- a/features/steps/shared/markdown.rb
+++ b/features/steps/shared/markdown.rb
@@ -30,7 +30,7 @@ module SharedMarkdown
end
step 'I should see the Markdown write tab' do
- expect(find('.gfm-form')).to have_css('.js-md-write-button', visible: true)
+ expect(first('.gfm-form')).to have_link('Write', visible: true)
end
step 'I should see the Markdown preview' do
@@ -49,9 +49,9 @@ module SharedMarkdown
end
step 'I preview a description text like "Bug fixed :smile:"' do
- page.within('.gfm-form') do
+ page.within(first('.gfm-form')) do
fill_in 'Description', with: 'Bug fixed :smile:'
- find('.js-md-preview-button').click
+ click_link 'Preview'
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index ac113c5200d..bbdd2039f43 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -110,6 +110,7 @@ module API
mount ::API::Notes
mount ::API::NotificationSettings
mount ::API::Pipelines
+ mount ::API::PipelineSchedules
mount ::API::ProjectHooks
mount ::API::Projects
mount ::API::ProjectSnippets
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 2e2b95b7994..620e3409d6f 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -687,6 +687,17 @@ module API
expose :coverage
end
+ class PipelineSchedule < Grape::Entity
+ expose :id
+ expose :description, :ref, :cron, :cron_timezone, :next_run_at, :active
+ expose :created_at, :updated_at
+ expose :owner, using: Entities::UserBasic
+ end
+
+ class PipelineScheduleDetails < PipelineSchedule
+ expose :last_pipeline, using: Entities::PipelineBasic
+ end
+
class EnvironmentBasic < Grape::Entity
expose :id, :name, :slug, :external_url
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index ee85b777aff..e14a988a153 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -151,8 +151,8 @@ module API
end
get ":id/projects" do
group = find_group!(params[:id])
- projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute
- projects = filter_projects(projects)
+ projects = GroupProjectsFinder.new(group: group, current_user: current_user, params: project_finder_params).execute
+ projects = reorder_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, current_user: current_user
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 226a7ddd50e..d61450f8258 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -256,31 +256,21 @@ module API
# project helpers
- def filter_projects(projects)
- if params[:membership]
- projects = projects.merge(current_user.authorized_projects)
- end
-
- if params[:owned]
- projects = projects.merge(current_user.owned_projects)
- end
-
- if params[:starred]
- projects = projects.merge(current_user.starred_projects)
- end
-
- if params[:search].present?
- projects = projects.search(params[:search])
- end
-
- if params[:visibility].present?
- projects = projects.search_by_visibility(params[:visibility])
- end
-
- projects = projects.where(archived: params[:archived])
+ def reorder_projects(projects)
projects.reorder(params[:order_by] => params[:sort])
end
+ def project_finder_params
+ finder_params = {}
+ finder_params[:owned] = true if params[:owned].present?
+ finder_params[:non_public] = true if params[:membership].present?
+ finder_params[:starred] = true if params[:starred].present?
+ finder_params[:visibility_level] = Gitlab::VisibilityLevel.level_value(params[:visibility]) if params[:visibility]
+ finder_params[:archived] = params[:archived]
+ finder_params[:search] = params[:search] if params[:search]
+ finder_params
+ end
+
# file helpers
def uploaded_file(field, uploads_path)
diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb
new file mode 100644
index 00000000000..93d89209934
--- /dev/null
+++ b/lib/api/pipeline_schedules.rb
@@ -0,0 +1,131 @@
+module API
+ class PipelineSchedules < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get all pipeline schedules' do
+ success Entities::PipelineSchedule
+ end
+ params do
+ use :pagination
+ optional :scope, type: String, values: %w[active inactive],
+ desc: 'The scope of pipeline schedules'
+ end
+ get ':id/pipeline_schedules' do
+ authorize! :read_pipeline_schedule, user_project
+
+ schedules = PipelineSchedulesFinder.new(user_project).execute(scope: params[:scope])
+ .preload([:owner, :last_pipeline])
+ present paginate(schedules), with: Entities::PipelineSchedule
+ end
+
+ desc 'Get a single pipeline schedule' do
+ success Entities::PipelineScheduleDetails
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ end
+ get ':id/pipeline_schedules/:pipeline_schedule_id' do
+ authorize! :read_pipeline_schedule, user_project
+
+ not_found!('PipelineSchedule') unless pipeline_schedule
+
+ present pipeline_schedule, with: Entities::PipelineScheduleDetails
+ end
+
+ desc 'Create a new pipeline schedule' do
+ success Entities::PipelineScheduleDetails
+ end
+ params do
+ requires :description, type: String, desc: 'The description of pipeline schedule'
+ requires :ref, type: String, desc: 'The branch/tag name will be triggered'
+ requires :cron, type: String, desc: 'The cron'
+ optional :cron_timezone, type: String, default: 'UTC', desc: 'The timezone'
+ optional :active, type: Boolean, default: true, desc: 'The activation of pipeline schedule'
+ end
+ post ':id/pipeline_schedules' do
+ authorize! :create_pipeline_schedule, user_project
+
+ pipeline_schedule = Ci::CreatePipelineScheduleService
+ .new(user_project, current_user, declared_params(include_missing: false))
+ .execute
+
+ if pipeline_schedule.persisted?
+ present pipeline_schedule, with: Entities::PipelineScheduleDetails
+ else
+ render_validation_error!(pipeline_schedule)
+ end
+ end
+
+ desc 'Edit a pipeline schedule' do
+ success Entities::PipelineScheduleDetails
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ optional :description, type: String, desc: 'The description of pipeline schedule'
+ optional :ref, type: String, desc: 'The branch/tag name will be triggered'
+ optional :cron, type: String, desc: 'The cron'
+ optional :cron_timezone, type: String, desc: 'The timezone'
+ optional :active, type: Boolean, desc: 'The activation of pipeline schedule'
+ end
+ put ':id/pipeline_schedules/:pipeline_schedule_id' do
+ authorize! :update_pipeline_schedule, user_project
+
+ not_found!('PipelineSchedule') unless pipeline_schedule
+
+ if pipeline_schedule.update(declared_params(include_missing: false))
+ present pipeline_schedule, with: Entities::PipelineScheduleDetails
+ else
+ render_validation_error!(pipeline_schedule)
+ end
+ end
+
+ desc 'Take ownership of a pipeline schedule' do
+ success Entities::PipelineScheduleDetails
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ end
+ post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do
+ authorize! :update_pipeline_schedule, user_project
+
+ not_found!('PipelineSchedule') unless pipeline_schedule
+
+ if pipeline_schedule.own!(current_user)
+ present pipeline_schedule, with: Entities::PipelineScheduleDetails
+ else
+ render_validation_error!(pipeline_schedule)
+ end
+ end
+
+ desc 'Delete a pipeline schedule' do
+ success Entities::PipelineScheduleDetails
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ end
+ delete ':id/pipeline_schedules/:pipeline_schedule_id' do
+ authorize! :admin_pipeline_schedule, user_project
+
+ not_found!('PipelineSchedule') unless pipeline_schedule
+
+ status :accepted
+ present pipeline_schedule.destroy, with: Entities::PipelineScheduleDetails
+ end
+ end
+
+ helpers do
+ def pipeline_schedule
+ @pipeline_schedule ||=
+ user_project.pipeline_schedules
+ .preload(:owner, :last_pipeline)
+ .find_by(id: params.delete(:pipeline_schedule_id))
+ end
+ end
+ end
+end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index d4fe5c023bf..d00d4fe1737 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -21,6 +21,7 @@ module API
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
+ optional :tag_list, type: Array[String], desc: 'The list of tags for a project'
end
params :optional_params do
@@ -67,20 +68,19 @@ module API
optional :import_url, type: String, desc: 'URL from which the project is imported'
end
- def present_projects(projects, options = {})
+ def present_projects(options = {})
+ projects = ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute
+ projects = reorder_projects(projects)
+ projects = projects.with_statistics if params[:statistics]
+ projects = projects.with_issues_enabled if params[:with_issues_enabled]
+ projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
+
options = options.reverse_merge(
- with: Entities::Project,
- current_user: current_user,
- simple: params[:simple],
- with_issues_enabled: params[:with_issues_enabled],
- with_merge_requests_enabled: params[:with_merge_requests_enabled]
+ with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
+ statistics: params[:statistics],
+ current_user: current_user
)
-
- projects = filter_projects(projects)
- projects = projects.with_statistics if options[:statistics]
- projects = projects.with_issues_enabled if options[:with_issues_enabled]
- projects = projects.with_merge_requests_enabled if options[:with_merge_requests_enabled]
- options[:with] = Entities::BasicProjectDetails if options[:simple]
+ options[:with] = Entities::BasicProjectDetails if params[:simple]
present paginate(projects), options
end
@@ -94,8 +94,7 @@ module API
use :statistics_params
end
get do
- entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
- present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity, statistics: params[:statistics]
+ present_projects
end
desc 'Create new project' do
@@ -231,6 +230,7 @@ module API
:request_access_enabled,
:shared_runners_enabled,
:snippets_enabled,
+ :tag_list,
:visibility,
:wiki_enabled
]
diff --git a/lib/api/v3/helpers.rb b/lib/api/v3/helpers.rb
index 0f234d4cdad..d9e76560d03 100644
--- a/lib/api/v3/helpers.rb
+++ b/lib/api/v3/helpers.rb
@@ -14,6 +14,33 @@ module API
authorize! access_level, merge_request
merge_request
end
+
+ # project helpers
+
+ def filter_projects(projects)
+ if params[:membership]
+ projects = projects.merge(current_user.authorized_projects)
+ end
+
+ if params[:owned]
+ projects = projects.merge(current_user.owned_projects)
+ end
+
+ if params[:starred]
+ projects = projects.merge(current_user.starred_projects)
+ end
+
+ if params[:search].present?
+ projects = projects.search(params[:search])
+ end
+
+ if params[:visibility].present?
+ projects = projects.where(visibility_level: Gitlab::VisibilityLevel.level_value(params[:visibility]))
+ end
+
+ projects = projects.where(archived: params[:archived])
+ projects.reorder(params[:order_by] => params[:sort])
+ end
end
end
end
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index 164612cb8dd..896c00b88e7 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -147,7 +147,7 @@ module API
get '/starred' do
authenticate!
- present_projects current_user.viewable_starred_projects
+ present_projects ProjectsFinder.new(current_user: current_user, params: { starred: true }).execute
end
desc 'Get all projects for admin user' do
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index 57b533bad99..439ef0ce015 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -12,7 +12,7 @@ module Gitlab
end
def action_path
- cancel_namespace_project_build_path(subject.project.namespace,
+ cancel_namespace_project_job_path(subject.project.namespace,
subject.project,
subject)
end
diff --git a/lib/gitlab/ci/status/build/common.rb b/lib/gitlab/ci/status/build/common.rb
index 3fec2c5d4db..b173c23fba4 100644
--- a/lib/gitlab/ci/status/build/common.rb
+++ b/lib/gitlab/ci/status/build/common.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def details_path
- namespace_project_build_path(subject.project.namespace,
+ namespace_project_job_path(subject.project.namespace,
subject.project,
subject)
end
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index c6139f1b716..e80f3263794 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -20,7 +20,7 @@ module Gitlab
end
def action_path
- play_namespace_project_build_path(subject.project.namespace,
+ play_namespace_project_job_path(subject.project.namespace,
subject.project,
subject)
end
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
index 505f80848b2..56303e4cb17 100644
--- a/lib/gitlab/ci/status/build/retryable.rb
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -16,7 +16,7 @@ module Gitlab
end
def action_path
- retry_namespace_project_build_path(subject.project.namespace,
+ retry_namespace_project_job_path(subject.project.namespace,
subject.project,
subject)
end
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index 0b5199e5483..2778d6f3b52 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -20,7 +20,7 @@ module Gitlab
end
def action_path
- play_namespace_project_build_path(subject.project.namespace,
+ play_namespace_project_job_path(subject.project.namespace,
subject.project,
subject)
end
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index cc285162b44..d137cc1bae6 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -11,7 +11,7 @@ module Gitlab
USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
commit pipelines merge_requests new].freeze
RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES
- RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS)
+ RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS.map(&Regexp.method(:escape)))
ROUTES = [
Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/noteable/issue/\d+/notes\z),
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 1c0abc9f7cf..9ff6829cd49 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -80,6 +80,7 @@ module Gitlab
# By rejecting `badges` the router can _count_ on the fact that `badges` will
# be preceded by the `namespace/project`.
PROJECT_WILDCARD_ROUTES = %w[
+ -
badges
blame
blob
diff --git a/lib/gitlab/routes/legacy_builds.rb b/lib/gitlab/routes/legacy_builds.rb
new file mode 100644
index 00000000000..36d1a8a6f64
--- /dev/null
+++ b/lib/gitlab/routes/legacy_builds.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Routes
+ class LegacyBuilds
+ def initialize(map)
+ @map = map
+ end
+
+ def draw
+ @map.instance_eval do
+ resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
+ collection do
+ resources :artifacts, only: [], controller: 'build_artifacts' do
+ collection do
+ get :latest_succeeded,
+ path: '*ref_name_and_path',
+ format: false
+ end
+ end
+ end
+
+ member do
+ get :raw
+ end
+
+ resource :artifacts, only: [], controller: 'build_artifacts' do
+ get :download
+ get :browse, path: 'browse(/*path)', format: false
+ get :file, path: 'file/*path', format: false
+ get :raw, path: 'raw/*path', format: false
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index fe37e4da94f..18d8b4f4744 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -31,8 +31,7 @@ module Gitlab
feature_enabled = case action.to_s
when 'git_receive_pack'
- # Disabled for now, see https://gitlab.com/gitlab-org/gitaly/issues/172
- false
+ Gitlab::GitalyClient.feature_enabled?(:post_receive_pack)
when 'git_upload_pack'
Gitlab::GitalyClient.feature_enabled?(:post_upload_pack)
when 'info_refs'
diff --git a/package.json b/package.json
index 8e280e77d67..29165fd4182 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"core-js": "^2.4.1",
"css-loader": "^0.28.0",
"d3": "^3.5.11",
+ "deckar01-task_list": "^2.0.0",
"document-register-element": "^1.3.0",
"dropzone": "^4.2.0",
"emoji-unicode-version": "^0.2.1",
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index eff9fab8da2..428bc45b842 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -12,7 +12,7 @@ describe Projects::ArtifactsController do
status: 'success')
end
- let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
before do
project.team << [user, :developer]
@@ -22,16 +22,16 @@ describe Projects::ArtifactsController do
describe 'GET download' do
it 'sends the artifacts file' do
- expect(controller).to receive(:send_file).with(build.artifacts_file.path, disposition: 'attachment').and_call_original
+ expect(controller).to receive(:send_file).with(job.artifacts_file.path, disposition: 'attachment').and_call_original
- get :download, namespace_id: project.namespace, project_id: project, build_id: build
+ get :download, namespace_id: project.namespace, project_id: project, job_id: job
end
end
describe 'GET browse' do
context 'when the directory exists' do
it 'renders the browse view' do
- get :browse, namespace_id: project.namespace, project_id: project, build_id: build, path: 'other_artifacts_0.1.2'
+ get :browse, namespace_id: project.namespace, project_id: project, job_id: job, path: 'other_artifacts_0.1.2'
expect(response).to render_template('projects/artifacts/browse')
end
@@ -39,7 +39,7 @@ describe Projects::ArtifactsController do
context 'when the directory does not exist' do
it 'responds Not Found' do
- get :browse, namespace_id: project.namespace, project_id: project, build_id: build, path: 'unknown'
+ get :browse, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
expect(response).to be_not_found
end
@@ -49,7 +49,7 @@ describe Projects::ArtifactsController do
describe 'GET file' do
context 'when the file exists' do
it 'renders the file view' do
- get :file, namespace_id: project.namespace, project_id: project, build_id: build, path: 'ci_artifacts.txt'
+ get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
expect(response).to render_template('projects/artifacts/file')
end
@@ -57,7 +57,7 @@ describe Projects::ArtifactsController do
context 'when the file does not exist' do
it 'responds Not Found' do
- get :file, namespace_id: project.namespace, project_id: project, build_id: build, path: 'unknown'
+ get :file, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
expect(response).to be_not_found
end
@@ -67,7 +67,7 @@ describe Projects::ArtifactsController do
describe 'GET raw' do
context 'when the file exists' do
it 'serves the file using workhorse' do
- get :raw, namespace_id: project.namespace, project_id: project, build_id: build, path: 'ci_artifacts.txt'
+ get :raw, namespace_id: project.namespace, project_id: project, job_id: job, path: 'ci_artifacts.txt'
send_data = response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]
@@ -84,7 +84,7 @@ describe Projects::ArtifactsController do
context 'when the file does not exist' do
it 'responds Not Found' do
- get :raw, namespace_id: project.namespace, project_id: project, build_id: build, path: 'unknown'
+ get :raw, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
expect(response).to be_not_found
end
@@ -92,29 +92,29 @@ describe Projects::ArtifactsController do
end
describe 'GET latest_succeeded' do
- def params_from_ref(ref = pipeline.ref, job = build.name, path = 'browse')
+ def params_from_ref(ref = pipeline.ref, job_name = job.name, path = 'browse')
{
namespace_id: project.namespace,
project_id: project,
ref_name_and_path: File.join(ref, path),
- job: job
+ job: job_name
}
end
- context 'cannot find the build' do
+ context 'cannot find the job' do
shared_examples 'not found' do
it { expect(response).to have_http_status(:not_found) }
end
context 'has no such ref' do
before do
- get :latest_succeeded, params_from_ref('TAIL', build.name)
+ get :latest_succeeded, params_from_ref('TAIL', job.name)
end
it_behaves_like 'not found'
end
- context 'has no such build' do
+ context 'has no such job' do
before do
get :latest_succeeded, params_from_ref(pipeline.ref, 'NOBUILD')
end
@@ -124,20 +124,20 @@ describe Projects::ArtifactsController do
context 'has no path' do
before do
- get :latest_succeeded, params_from_ref(pipeline.sha, build.name, '')
+ get :latest_succeeded, params_from_ref(pipeline.sha, job.name, '')
end
it_behaves_like 'not found'
end
end
- context 'found the build and redirect' do
- shared_examples 'redirect to the build' do
+ context 'found the job and redirect' do
+ shared_examples 'redirect to the job' do
it 'redirects' do
- path = browse_namespace_project_build_artifacts_path(
+ path = browse_namespace_project_job_artifacts_path(
project.namespace,
project,
- build)
+ job)
expect(response).to redirect_to(path)
end
@@ -151,7 +151,7 @@ describe Projects::ArtifactsController do
get :latest_succeeded, params_from_ref('master')
end
- it_behaves_like 'redirect to the build'
+ it_behaves_like 'redirect to the job'
end
context 'with branch name containing slash' do
@@ -162,7 +162,7 @@ describe Projects::ArtifactsController do
get :latest_succeeded, params_from_ref('improve/awesome')
end
- it_behaves_like 'redirect to the build'
+ it_behaves_like 'redirect to the job'
end
context 'with branch name and path containing slashes' do
@@ -170,14 +170,14 @@ describe Projects::ArtifactsController do
pipeline.update(ref: 'improve/awesome',
sha: project.commit('improve/awesome').sha)
- get :latest_succeeded, params_from_ref('improve/awesome', build.name, 'file/README.md')
+ get :latest_succeeded, params_from_ref('improve/awesome', job.name, 'file/README.md')
end
it 'redirects' do
- path = file_namespace_project_build_artifacts_path(
+ path = file_namespace_project_job_artifacts_path(
project.namespace,
project,
- build,
+ job,
'README.md')
expect(response).to redirect_to(path)
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 20f99b209eb..de13f17012b 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -177,7 +177,7 @@ describe Projects::EnvironmentsController do
expect(response).to have_http_status(200)
expect(json_response).to eq(
{ 'redirect_url' =>
- "http://test.host/#{project.path_with_namespace}/builds/#{action.id}" })
+ namespace_project_job_url(project.namespace, project, action) })
end
end
@@ -191,7 +191,7 @@ describe Projects::EnvironmentsController do
expect(response).to have_http_status(200)
expect(json_response).to eq(
{ 'redirect_url' =>
- "http://test.host/#{project.path_with_namespace}/environments/#{environment.id}" })
+ namespace_project_environment_url(project.namespace, project, environment) })
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 04afd07c59e..a38ae2eb990 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -204,7 +204,7 @@ describe Projects::IssuesController do
body = JSON.parse(response.body)
expect(body['assignees'].first.keys)
- .to match_array(%w(id name username avatar_url))
+ .to match_array(%w(id name username avatar_url state web_url))
end
end
diff --git a/spec/controllers/projects/builds_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index f41503fd34e..838bdae1445 100644
--- a/spec/controllers/projects/builds_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::BuildsController do
+describe Projects::JobsController do
include ApiHelpers
let(:project) { create(:empty_project, :public) }
@@ -213,7 +213,7 @@ describe Projects::BuildsController do
it 'redirects to the retried build page' do
expect(response).to have_http_status(:found)
- expect(response).to redirect_to(namespace_project_build_path(id: Ci::Build.last.id))
+ expect(response).to redirect_to(namespace_project_job_path(id: Ci::Build.last.id))
end
end
@@ -234,7 +234,11 @@ describe Projects::BuildsController do
describe 'POST play' do
before do
- project.add_master(user)
+ project.add_developer(user)
+
+ create(:protected_branch, :developers_can_merge,
+ name: 'master', project: project)
+
sign_in(user)
post_play
@@ -245,7 +249,7 @@ describe Projects::BuildsController do
it 'redirects to the played build page' do
expect(response).to have_http_status(:found)
- expect(response).to redirect_to(namespace_project_build_path(id: build.id))
+ expect(response).to redirect_to(namespace_project_job_path(id: build.id))
end
it 'transits to pending' do
@@ -281,7 +285,7 @@ describe Projects::BuildsController do
it 'redirects to the canceled build page' do
expect(response).to have_http_status(:found)
- expect(response).to redirect_to(namespace_project_build_path(id: build.id))
+ expect(response).to redirect_to(namespace_project_job_path(id: build.id))
end
it 'transits to canceled' do
@@ -319,7 +323,7 @@ describe Projects::BuildsController do
it 'redirects to a index page' do
expect(response).to have_http_status(:found)
- expect(response).to redirect_to(namespace_project_builds_path)
+ expect(response).to redirect_to(namespace_project_jobs_path)
end
it 'transits to canceled' do
@@ -336,7 +340,7 @@ describe Projects::BuildsController do
it 'redirects to a index page' do
expect(response).to have_http_status(:found)
- expect(response).to redirect_to(namespace_project_builds_path)
+ expect(response).to redirect_to(namespace_project_jobs_path)
end
end
@@ -359,7 +363,7 @@ describe Projects::BuildsController do
it 'redirects to the erased build page' do
expect(response).to have_http_status(:found)
- expect(response).to redirect_to(namespace_project_build_path(id: build.id))
+ expect(response).to redirect_to(namespace_project_job_path(id: build.id))
end
it 'erases artifacts' do
diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb
index 9d5ce876c29..999ce3611b5 100644
--- a/spec/features/admin/admin_builds_spec.rb
+++ b/spec/features/admin/admin_builds_spec.rb
@@ -16,7 +16,7 @@ describe 'Admin Builds' do
create(:ci_build, pipeline: pipeline, status: :success)
create(:ci_build, pipeline: pipeline, status: :failed)
- visit admin_builds_path
+ visit admin_jobs_path
expect(page).to have_selector('.nav-links li.active', text: 'All')
expect(page).to have_selector('.row-content-block', text: 'All jobs')
@@ -27,7 +27,7 @@ describe 'Admin Builds' do
context 'when have no jobs' do
it 'shows a message' do
- visit admin_builds_path
+ visit admin_jobs_path
expect(page).to have_selector('.nav-links li.active', text: 'All')
expect(page).to have_content 'No jobs to show'
@@ -44,7 +44,7 @@ describe 'Admin Builds' do
build3 = create(:ci_build, pipeline: pipeline, status: :success)
build4 = create(:ci_build, pipeline: pipeline, status: :failed)
- visit admin_builds_path(scope: :pending)
+ visit admin_jobs_path(scope: :pending)
expect(page).to have_selector('.nav-links li.active', text: 'Pending')
expect(page.find('.build-link')).to have_content(build1.id)
@@ -59,7 +59,7 @@ describe 'Admin Builds' do
it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :success)
- visit admin_builds_path(scope: :pending)
+ visit admin_jobs_path(scope: :pending)
expect(page).to have_selector('.nav-links li.active', text: 'Pending')
expect(page).to have_content 'No jobs to show'
@@ -76,7 +76,7 @@ describe 'Admin Builds' do
build3 = create(:ci_build, pipeline: pipeline, status: :failed)
build4 = create(:ci_build, pipeline: pipeline, status: :pending)
- visit admin_builds_path(scope: :running)
+ visit admin_jobs_path(scope: :running)
expect(page).to have_selector('.nav-links li.active', text: 'Running')
expect(page.find('.build-link')).to have_content(build1.id)
@@ -91,7 +91,7 @@ describe 'Admin Builds' do
it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :success)
- visit admin_builds_path(scope: :running)
+ visit admin_jobs_path(scope: :running)
expect(page).to have_selector('.nav-links li.active', text: 'Running')
expect(page).to have_content 'No jobs to show'
@@ -107,7 +107,7 @@ describe 'Admin Builds' do
build2 = create(:ci_build, pipeline: pipeline, status: :running)
build3 = create(:ci_build, pipeline: pipeline, status: :success)
- visit admin_builds_path(scope: :finished)
+ visit admin_jobs_path(scope: :finished)
expect(page).to have_selector('.nav-links li.active', text: 'Finished')
expect(page.find('.build-link')).not_to have_content(build1.id)
@@ -121,7 +121,7 @@ describe 'Admin Builds' do
it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :running)
- visit admin_builds_path(scope: :finished)
+ visit admin_jobs_path(scope: :finished)
expect(page).to have_selector('.nav-links li.active', text: 'Finished')
expect(page).to have_content 'No jobs to show'
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 6c09903a2f6..e75bf059218 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -38,9 +38,11 @@ feature 'issue move to another project' do
end
scenario 'moving issue to another project', js: true do
- find('#move_to_project_id', visible: false).set(new_project.id)
+ find('#issuable-move', visible: false).set(new_project.id)
click_button('Save changes')
+ wait_for_requests
+
expect(current_url).to include project_path(new_project)
expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}")
@@ -51,7 +53,7 @@ feature 'issue move to another project' do
scenario 'searching project dropdown', js: true do
new_project_search.team << [user, :reporter]
- page.within '.js-move-dropdown' do
+ page.within '.detail-page-description' do
first('.select2-choice').click
end
@@ -69,7 +71,7 @@ feature 'issue move to another project' do
background { another_project.team << [user, :guest] }
scenario 'browsing projects in projects select' do
- click_link 'Select project'
+ click_link 'Move to a different project'
page.within '.select2-results' do
expect(page).to have_content 'No project'
diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
index 51e7467c14c..3ceb91d951d 100644
--- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -85,7 +85,7 @@ feature 'Mini Pipeline Graph', :js, :feature do
build_item.click
find('.build-page')
- expect(current_path).to eql(namespace_project_build_path(project.namespace, project, build))
+ expect(current_path).to eql(namespace_project_job_path(project.namespace, project, build))
end
it 'should show tooltip when hovered' do
diff --git a/spec/features/projects/artifacts/browse_spec.rb b/spec/features/projects/artifacts/browse_spec.rb
new file mode 100644
index 00000000000..68375956273
--- /dev/null
+++ b/spec/features/projects/artifacts/browse_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+feature 'Browse artifact', :js, feature: true do
+ let(:project) { create(:project, :public) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ def browse_path(path)
+ browse_namespace_project_job_artifacts_path(project.namespace, project, job, path)
+ end
+
+ context 'when visiting old URL' do
+ let(:browse_url) do
+ browse_path('other_artifacts_0.1.2')
+ end
+
+ before do
+ visit browse_url.sub('/-/jobs', '/builds')
+ end
+
+ it "redirects to new URL" do
+ expect(page.current_path).to eq(browse_url)
+ end
+ end
+end
diff --git a/spec/features/projects/artifacts/download_spec.rb b/spec/features/projects/artifacts/download_spec.rb
new file mode 100644
index 00000000000..dd9454840ee
--- /dev/null
+++ b/spec/features/projects/artifacts/download_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+feature 'Download artifact', :js, feature: true do
+ let(:project) { create(:project, :public) }
+ let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project, sha: project.commit.sha, ref: 'master') }
+ let(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) }
+
+ shared_examples 'downloading' do
+ it 'downloads the zip' do
+ expect(page.response_headers['Content-Disposition'])
+ .to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"})
+
+ # Check the content does match, but don't print this as error message
+ expect(page.source.b == job.artifacts_file.file.read.b)
+ end
+ end
+
+ context 'when downloading' do
+ before do
+ visit download_url
+ end
+
+ context 'via job id' do
+ let(:download_url) do
+ download_namespace_project_job_artifacts_path(project.namespace, project, job)
+ end
+
+ it_behaves_like 'downloading'
+ end
+
+ context 'via branch name and job name' do
+ let(:download_url) do
+ latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{pipeline.ref}/download", job: job.name)
+ end
+
+ it_behaves_like 'downloading'
+ end
+ end
+
+ context 'when visiting old URL' do
+ before do
+ visit download_url.sub('/-/jobs', '/builds')
+ end
+
+ context 'via job id' do
+ let(:download_url) do
+ download_namespace_project_job_artifacts_path(project.namespace, project, job)
+ end
+
+ it_behaves_like 'downloading'
+ end
+
+ context 'via branch name and job name' do
+ let(:download_url) do
+ latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{pipeline.ref}/download", job: job.name)
+ end
+
+ it_behaves_like 'downloading'
+ end
+ end
+end
diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb
index 25db908d917..25c4f3c87a2 100644
--- a/spec/features/projects/artifacts/file_spec.rb
+++ b/spec/features/projects/artifacts/file_spec.rb
@@ -6,7 +6,11 @@ feature 'Artifact file', :js, feature: true do
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
def visit_file(path)
- visit file_namespace_project_build_artifacts_path(project.namespace, project, build, path)
+ visit file_path(path)
+ end
+
+ def file_path(path)
+ file_namespace_project_job_artifacts_path(project.namespace, project, build, path)
end
context 'Text file' do
@@ -56,4 +60,18 @@ feature 'Artifact file', :js, feature: true do
end
end
end
+
+ context 'when visiting old URL' do
+ let(:file_url) do
+ file_path('other_artifacts_0.1.2/doc_sample.txt')
+ end
+
+ before do
+ visit file_url.sub('/-/jobs', '/builds')
+ end
+
+ it "redirects to new URL" do
+ expect(page.current_path).to eq(file_url)
+ end
+ end
end
diff --git a/spec/features/projects/artifacts/raw_spec.rb b/spec/features/projects/artifacts/raw_spec.rb
new file mode 100644
index 00000000000..b589701729d
--- /dev/null
+++ b/spec/features/projects/artifacts/raw_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+feature 'Raw artifact', :js, feature: true do
+ let(:project) { create(:project, :public) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') }
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ def raw_path(path)
+ raw_namespace_project_job_artifacts_path(project.namespace, project, job, path)
+ end
+
+ context 'when visiting old URL' do
+ let(:raw_url) do
+ raw_path('other_artifacts_0.1.2/doc_sample.txt')
+ end
+
+ before do
+ visit raw_url.sub('/-/jobs', '/builds')
+ end
+
+ it "redirects to new URL" do
+ expect(page.current_path).to eq(raw_url)
+ end
+ end
+end
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 86ce50c976f..18b608c863e 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -12,6 +12,7 @@ feature 'Environment', :feature do
feature 'environment details page' do
given!(:environment) { create(:environment, project: project) }
+ given!(:permissions) { }
given!(:deployment) { }
given!(:action) { }
@@ -62,20 +63,31 @@ feature 'Environment', :feature do
name: 'deploy to production')
end
- given(:role) { :master }
+ context 'when user has ability to trigger deployment' do
+ given(:permissions) do
+ create(:protected_branch, :developers_can_merge,
+ name: action.ref, project: project)
+ end
- scenario 'does show a play button' do
- expect(page).to have_link(action.name.humanize)
- end
+ it 'does show a play button' do
+ expect(page).to have_link(action.name.humanize)
+ end
+
+ it 'does allow to play manual action' do
+ expect(action).to be_manual
- scenario 'does allow to play manual action' do
- expect(action).to be_manual
+ expect { click_link(action.name.humanize) }
+ .not_to change { Ci::Pipeline.count }
- expect { click_link(action.name.humanize) }
- .not_to change { Ci::Pipeline.count }
+ expect(page).to have_content(action.name)
+ expect(action.reload).to be_pending
+ end
+ end
- expect(page).to have_content(action.name)
- expect(action.reload).to be_pending
+ context 'when user has no ability to trigger a deployment' do
+ it 'does not show a play button' do
+ expect(page).not_to have_link(action.name.humanize)
+ end
end
context 'with external_url' do
@@ -134,12 +146,23 @@ feature 'Environment', :feature do
on_stop: 'close_app')
end
- given(:role) { :master }
+ context 'when user has ability to stop environment' do
+ given(:permissions) do
+ create(:protected_branch, :developers_can_merge,
+ name: action.ref, project: project)
+ end
- scenario 'does allow to stop environment' do
- click_link('Stop')
+ it 'allows to stop environment' do
+ click_link('Stop')
- expect(page).to have_content('close_app')
+ expect(page).to have_content('close_app')
+ end
+ end
+
+ context 'when user has no ability to stop environment' do
+ it 'does not allow to stop environment' do
+ expect(page).to have_no_link('Stop')
+ end
end
context 'for reporter' do
@@ -150,12 +173,6 @@ feature 'Environment', :feature do
end
end
end
-
- context 'without stop action' do
- scenario 'does allow to stop environment' do
- click_link('Stop')
- end
- end
end
context 'when environment is stopped' do
diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/jobs_spec.rb
index 8f4dfa7c48b..0eda46649db 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require 'tempfile'
-feature 'Builds', :feature do
+feature 'Jobs', :feature do
let(:user) { create(:user) }
let(:user_access_level) { :developer }
let(:project) { create(:project) }
@@ -19,12 +19,12 @@ feature 'Builds', :feature do
login_as(user)
end
- describe "GET /:project/builds" do
+ describe "GET /:project/jobs" do
let!(:build) { create(:ci_build, pipeline: pipeline) }
context "Pending scope" do
before do
- visit namespace_project_builds_path(project.namespace, project, scope: :pending)
+ visit namespace_project_jobs_path(project.namespace, project, scope: :pending)
end
it "shows Pending tab jobs" do
@@ -39,7 +39,7 @@ feature 'Builds', :feature do
context "Running scope" do
before do
build.run!
- visit namespace_project_builds_path(project.namespace, project, scope: :running)
+ visit namespace_project_jobs_path(project.namespace, project, scope: :running)
end
it "shows Running tab jobs" do
@@ -54,7 +54,7 @@ feature 'Builds', :feature do
context "Finished scope" do
before do
build.run!
- visit namespace_project_builds_path(project.namespace, project, scope: :finished)
+ visit namespace_project_jobs_path(project.namespace, project, scope: :finished)
end
it "shows Finished tab jobs" do
@@ -67,7 +67,7 @@ feature 'Builds', :feature do
context "All jobs" do
before do
project.builds.running_or_pending.each(&:success)
- visit namespace_project_builds_path(project.namespace, project)
+ visit namespace_project_jobs_path(project.namespace, project)
end
it "shows All tab jobs" do
@@ -78,12 +78,26 @@ feature 'Builds', :feature do
expect(page).not_to have_link 'Cancel running'
end
end
+
+ context "when visiting old URL" do
+ let(:jobs_url) do
+ namespace_project_jobs_path(project.namespace, project)
+ end
+
+ before do
+ visit jobs_url.sub('/-/jobs', '/builds')
+ end
+
+ it "redirects to new URL" do
+ expect(page.current_path).to eq(jobs_url)
+ end
+ end
end
- describe "POST /:project/builds/:id/cancel_all" do
+ describe "POST /:project/jobs/:id/cancel_all" do
before do
build.run!
- visit namespace_project_builds_path(project.namespace, project)
+ visit namespace_project_jobs_path(project.namespace, project)
click_link "Cancel running"
end
@@ -97,10 +111,10 @@ feature 'Builds', :feature do
end
end
- describe "GET /:project/builds/:id" do
+ describe "GET /:project/jobs/:id" do
context "Job from project" do
before do
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
end
it 'shows commit`s data' do
@@ -117,7 +131,7 @@ feature 'Builds', :feature do
context "Job from other project" do
before do
- visit namespace_project_build_path(project.namespace, project, build2)
+ visit namespace_project_job_path(project.namespace, project, build2)
end
it { expect(page.status_code).to eq(404) }
@@ -126,7 +140,7 @@ feature 'Builds', :feature do
context "Download artifacts" do
before do
build.update_attributes(artifacts_file: artifacts_file)
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
end
it 'has button to download artifacts' do
@@ -139,7 +153,7 @@ feature 'Builds', :feature do
build.update_attributes(artifacts_file: artifacts_file,
artifacts_expire_at: expire_at)
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
end
context 'no expire date defined' do
@@ -183,10 +197,25 @@ feature 'Builds', :feature do
end
end
+ context "when visiting old URL" do
+ let(:job_url) do
+ namespace_project_job_path(project.namespace, project, build)
+ end
+
+ before do
+ visit job_url.sub('/-/jobs', '/builds')
+ end
+
+ it "redirects to new URL" do
+ expect(page.current_path).to eq(job_url)
+ end
+ end
+
feature 'Raw trace' do
before do
build.run!
- visit namespace_project_build_path(project.namespace, project, build)
+
+ visit namespace_project_job_path(project.namespace, project, build)
end
it do
@@ -198,7 +227,7 @@ feature 'Builds', :feature do
before do
build.run!
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
end
context 'when job has an initial trace' do
@@ -222,7 +251,7 @@ feature 'Builds', :feature do
end
before do
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
end
it 'shows variable key and value after click', js: true do
@@ -247,17 +276,17 @@ feature 'Builds', :feature do
let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) }
it 'shows a link for the job' do
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
expect(page).to have_link environment.name
end
end
- context 'job is complete and not successfull' do
+ context 'job is complete and not successful' do
let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) }
it 'shows a link for the job' do
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
expect(page).to have_link environment.name
end
@@ -268,7 +297,7 @@ feature 'Builds', :feature do
let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) }
it 'shows a link to latest deployment' do
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
expect(page).to have_link('latest deployment')
end
@@ -276,11 +305,11 @@ feature 'Builds', :feature do
end
end
- describe "POST /:project/builds/:id/cancel" do
+ describe "POST /:project/jobs/:id/cancel" do
context "Job from project" do
before do
build.run!
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
click_link "Cancel"
end
@@ -294,19 +323,19 @@ feature 'Builds', :feature do
context "Job from other project" do
before do
build.run!
- visit namespace_project_build_path(project.namespace, project, build)
- page.driver.post(cancel_namespace_project_build_path(project.namespace, project, build2))
+ visit namespace_project_job_path(project.namespace, project, build)
+ page.driver.post(cancel_namespace_project_job_path(project.namespace, project, build2))
end
it { expect(page.status_code).to eq(404) }
end
end
- describe "POST /:project/builds/:id/retry" do
+ describe "POST /:project/jobs/:id/retry" do
context "Job from project" do
before do
build.run!
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
click_link 'Cancel'
page.within('.build-header') do
click_link 'Retry job'
@@ -322,18 +351,18 @@ feature 'Builds', :feature do
end
end
- context "Build from other project" do
+ context "Job from other project" do
before do
build.run!
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
click_link 'Cancel'
- page.driver.post(retry_namespace_project_build_path(project.namespace, project, build2))
+ page.driver.post(retry_namespace_project_job_path(project.namespace, project, build2))
end
it { expect(page).to have_http_status(404) }
end
- context "Build that current user is not allowed to retry" do
+ context "Job that current user is not allowed to retry" do
before do
build.run!
build.cancel!
@@ -341,7 +370,7 @@ feature 'Builds', :feature do
logout_direct
login_with(create(:user))
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
end
it 'does not show the Retry button' do
@@ -352,30 +381,30 @@ feature 'Builds', :feature do
end
end
- describe "GET /:project/builds/:id/download" do
+ describe "GET /:project/jobs/:id/download" do
before do
build.update_attributes(artifacts_file: artifacts_file)
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
click_link 'Download'
end
context "Build from other project" do
before do
build2.update_attributes(artifacts_file: artifacts_file)
- visit download_namespace_project_build_artifacts_path(project.namespace, project, build2)
+ visit download_namespace_project_job_artifacts_path(project.namespace, project, build2)
end
it { expect(page.status_code).to eq(404) }
end
end
- describe 'GET /:project/builds/:id/raw', :js do
+ describe 'GET /:project/jobs/:id/raw', :js do
context 'access source' do
- context 'build from project' do
+ context 'job from project' do
before do
Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
build.run!
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
find('.js-raw-link-controller').click()
end
@@ -386,11 +415,11 @@ feature 'Builds', :feature do
end
end
- context 'build from other project' do
+ context 'job from other project' do
before do
Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' }
build2.run!
- visit raw_namespace_project_build_path(project.namespace, project, build2)
+ visit raw_namespace_project_job_path(project.namespace, project, build2)
end
it 'sends the right headers' do
@@ -410,7 +439,7 @@ feature 'Builds', :feature do
allow_any_instance_of(Gitlab::Ci::Trace).to receive(:paths)
.and_return(paths)
- visit namespace_project_build_path(project.namespace, project, build)
+ visit namespace_project_job_path(project.namespace, project, build)
end
context 'when build has trace in file', :js do
@@ -429,7 +458,7 @@ feature 'Builds', :feature do
end
end
- context 'when build has trace in DB' do
+ context 'when job has trace in DB' do
let(:paths) { [] }
it 'sends the right headers' do
@@ -437,38 +466,52 @@ feature 'Builds', :feature do
end
end
end
+
+ context "when visiting old URL" do
+ let(:raw_job_url) do
+ raw_namespace_project_job_path(project.namespace, project, build)
+ end
+
+ before do
+ visit raw_job_url.sub('/-/jobs', '/builds')
+ end
+
+ it "redirects to new URL" do
+ expect(page.current_path).to eq(raw_job_url)
+ end
+ end
end
- describe "GET /:project/builds/:id/trace.json" do
- context "Build from project" do
+ describe "GET /:project/jobs/:id/trace.json" do
+ context "Job from project" do
before do
- visit trace_namespace_project_build_path(project.namespace, project, build, format: :json)
+ visit trace_namespace_project_job_path(project.namespace, project, build, format: :json)
end
it { expect(page.status_code).to eq(200) }
end
- context "Build from other project" do
+ context "Job from other project" do
before do
- visit trace_namespace_project_build_path(project.namespace, project, build2, format: :json)
+ visit trace_namespace_project_job_path(project.namespace, project, build2, format: :json)
end
it { expect(page.status_code).to eq(404) }
end
end
- describe "GET /:project/builds/:id/status" do
- context "Build from project" do
+ describe "GET /:project/jobs/:id/status" do
+ context "Job from project" do
before do
- visit status_namespace_project_build_path(project.namespace, project, build)
+ visit status_namespace_project_job_path(project.namespace, project, build)
end
it { expect(page.status_code).to eq(200) }
end
- context "Build from other project" do
+ context "Job from other project" do
before do
- visit status_namespace_project_build_path(project.namespace, project, build2)
+ visit status_namespace_project_job_path(project.namespace, project, build2)
end
it { expect(page.status_code).to eq(404) }
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 78a76d9c112..2a2655bbdb5 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -334,7 +334,7 @@ describe "Internal Project Access", feature: true do
end
describe "GET /:project_path/builds" do
- subject { namespace_project_builds_path(project.namespace, project) }
+ subject { namespace_project_jobs_path(project.namespace, project) }
context "when allowed for public and internal" do
before { project.update(public_builds: true) }
@@ -368,7 +368,7 @@ describe "Internal Project Access", feature: true do
describe "GET /:project_path/builds/:id" do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- subject { namespace_project_build_path(project.namespace, project, build.id) }
+ subject { namespace_project_job_path(project.namespace, project, build.id) }
context "when allowed for public and internal" do
before { project.update(public_builds: true) }
@@ -402,7 +402,7 @@ describe "Internal Project Access", feature: true do
describe 'GET /:project_path/builds/:id/trace' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- subject { trace_namespace_project_build_path(project.namespace, project, build.id) }
+ subject { trace_namespace_project_job_path(project.namespace, project, build.id) }
context 'when allowed for public and internal' do
before do
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index a66f6e09055..b676c236758 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -330,7 +330,7 @@ describe "Private Project Access", feature: true do
end
describe "GET /:project_path/builds" do
- subject { namespace_project_builds_path(project.namespace, project) }
+ subject { namespace_project_jobs_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
@@ -358,7 +358,7 @@ describe "Private Project Access", feature: true do
describe "GET /:project_path/builds/:id" do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- subject { namespace_project_build_path(project.namespace, project, build.id) }
+ subject { namespace_project_job_path(project.namespace, project, build.id) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
@@ -391,7 +391,7 @@ describe "Private Project Access", feature: true do
describe 'GET /:project_path/builds/:id/trace' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- subject { trace_namespace_project_build_path(project.namespace, project, build.id) }
+ subject { trace_namespace_project_job_path(project.namespace, project, build.id) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 5cd575500c3..35d5163941e 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -154,7 +154,7 @@ describe "Public Project Access", feature: true do
end
describe "GET /:project_path/builds" do
- subject { namespace_project_builds_path(project.namespace, project) }
+ subject { namespace_project_jobs_path(project.namespace, project) }
context "when allowed for public" do
before { project.update(public_builds: true) }
@@ -188,7 +188,7 @@ describe "Public Project Access", feature: true do
describe "GET /:project_path/builds/:id" do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- subject { namespace_project_build_path(project.namespace, project, build.id) }
+ subject { namespace_project_job_path(project.namespace, project, build.id) }
context "when allowed for public" do
before { project.update(public_builds: true) }
@@ -222,7 +222,7 @@ describe "Public Project Access", feature: true do
describe 'GET /:project_path/builds/:id/trace' do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- subject { trace_namespace_project_build_path(project.namespace, project, build.id) }
+ subject { trace_namespace_project_job_path(project.namespace, project, build.id) }
context 'when allowed for public' do
before do
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 148adcffe3b..03d98459e8c 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -137,6 +137,13 @@ describe ProjectsFinder do
it { is_expected.to eq([public_project]) }
end
+ describe 'filter by owned' do
+ let(:params) { { owned: true } }
+ let!(:owned_project) { create(:empty_project, :private, namespace: current_user.namespace) }
+
+ it { is_expected.to eq([owned_project]) }
+ end
+
describe 'filter by non_public' do
let(:params) { { non_public: true } }
before do
@@ -146,13 +153,19 @@ describe ProjectsFinder do
it { is_expected.to eq([private_project]) }
end
- describe 'filter by viewable_starred_projects' do
+ describe 'filter by starred' do
let(:params) { { starred: true } }
before do
current_user.toggle_star(public_project)
end
it { is_expected.to eq([public_project]) }
+
+ it 'returns only projects the user has access to' do
+ current_user.toggle_star(private_project)
+
+ is_expected.to eq([public_project])
+ end
end
describe 'sorting' do
diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json
new file mode 100644
index 00000000000..f6346bd0fb6
--- /dev/null
+++ b/spec/fixtures/api/schemas/pipeline_schedule.json
@@ -0,0 +1,41 @@
+{
+ "type": "object",
+ "properties" : {
+ "id": { "type": "integer" },
+ "description": { "type": "string" },
+ "ref": { "type": "string" },
+ "cron": { "type": "string" },
+ "cron_timezone": { "type": "string" },
+ "next_run_at": { "type": "date" },
+ "active": { "type": "boolean" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "last_pipeline": {
+ "type": ["object", "null"],
+ "properties": {
+ "id": { "type": "integer" },
+ "sha": { "type": "string" },
+ "ref": { "type": "string" },
+ "status": { "type": "string" }
+ },
+ "additionalProperties": false
+ },
+ "owner": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "id", "description", "ref", "cron", "cron_timezone", "next_run_at",
+ "active", "created_at", "updated_at", "owner"
+ ],
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/pipeline_schedules.json b/spec/fixtures/api/schemas/pipeline_schedules.json
new file mode 100644
index 00000000000..173a28d2505
--- /dev/null
+++ b/spec/fixtures/api/schemas/pipeline_schedules.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "pipeline_schedule.json" }
+}
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
index 278bd1f9179..461908f3fde 100644
--- a/spec/javascripts/build_spec.js
+++ b/spec/javascripts/build_spec.js
@@ -8,7 +8,7 @@ import '~/breakpoints';
import 'vendor/jquery.nicescroll';
describe('Build', () => {
- const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
+ const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
preloadFixtures('builds/build-with-artifacts.html.raw');
diff --git a/spec/javascripts/fixtures/builds.rb b/spec/javascripts/fixtures/jobs.rb
index 320de791b08..dc7dde1138c 100644
--- a/spec/javascripts/fixtures/builds.rb
+++ b/spec/javascripts/fixtures/jobs.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::BuildsController, '(JavaScript fixtures)', type: :controller do
+describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin) }
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index ee456869c53..0030a953119 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import '~/render_math';
import '~/render_gfm';
import issuableApp from '~/issue_show/components/app.vue';
+import eventHub from '~/issue_show/event_hub';
import issueShowData from '../mock_data';
const issueShowInterceptor = data => (request, next) => {
@@ -22,14 +23,25 @@ describe('Issuable output', () => {
const IssuableDescriptionComponent = Vue.extend(issuableApp);
Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
+ spyOn(eventHub, '$emit');
+
vm = new IssuableDescriptionComponent({
propsData: {
canUpdate: true,
+ canDestroy: true,
+ canMove: true,
endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
issuableRef: '#1',
- initialTitle: '',
+ initialTitleHtml: '',
+ initialTitleText: '',
initialDescriptionHtml: '',
initialDescriptionText: '',
+ markdownPreviewUrl: '/',
+ markdownDocs: '/',
+ projectsAutocompleteUrl: '/',
+ isConfidential: false,
+ projectNamespace: '/',
+ projectPath: '/',
},
}).$mount();
});
@@ -57,4 +69,296 @@ describe('Issuable output', () => {
});
});
});
+
+ it('shows actions if permissions are correct', (done) => {
+ vm.showForm = true;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('does not show actions if permissions are incorrect', (done) => {
+ vm.showForm = true;
+ vm.canUpdate = false;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn'),
+ ).toBeNull();
+
+ done();
+ });
+ });
+
+ it('does not update formState if form is already open', (done) => {
+ vm.openForm();
+
+ vm.state.titleText = 'testing 123';
+
+ vm.openForm();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.store.formState.title,
+ ).not.toBe('testing 123');
+
+ done();
+ });
+ });
+
+ describe('updateIssuable', () => {
+ it('fetches new data after update', (done) => {
+ spyOn(vm.service, 'getData');
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ confidential: false,
+ web_url: location.pathname,
+ };
+ },
+ });
+ }));
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ vm.service.getData,
+ ).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('reloads the page if the confidential status has changed', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ confidential: true,
+ web_url: location.pathname,
+ };
+ },
+ });
+ }));
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ gl.utils.visitUrl,
+ ).toHaveBeenCalledWith(location.pathname);
+
+ done();
+ });
+ });
+
+ it('correctly updates issuable data', (done) => {
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve();
+ }));
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ vm.service.updateIssuable,
+ ).toHaveBeenCalledWith(vm.formState);
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('close.form');
+
+ done();
+ });
+ });
+
+ it('does not redirect if issue has not moved', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ web_url: location.pathname,
+ confidential: vm.isConfidential,
+ };
+ },
+ });
+ }));
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ gl.utils.visitUrl,
+ ).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('redirects if issue is moved', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ web_url: '/testing-issue-move',
+ confidential: vm.isConfidential,
+ };
+ },
+ });
+ }));
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ gl.utils.visitUrl,
+ ).toHaveBeenCalledWith('/testing-issue-move');
+
+ done();
+ });
+ });
+
+ it('does not update issuable if project move confirm is false', (done) => {
+ spyOn(window, 'confirm').and.returnValue(false);
+ spyOn(vm.service, 'updateIssuable');
+
+ vm.store.formState.move_to_project_id = 1;
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ vm.service.updateIssuable,
+ ).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('closes form on error', (done) => {
+ spyOn(window, 'Flash').and.callThrough();
+ spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
+ reject();
+ }));
+
+ vm.updateIssuable();
+
+ setTimeout(() => {
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('close.form');
+ expect(
+ window.Flash,
+ ).toHaveBeenCalledWith('Error updating issue');
+
+ done();
+ });
+ });
+ });
+
+ describe('deleteIssuable', () => {
+ it('changes URL when deleted', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return { web_url: '/test' };
+ },
+ });
+ }));
+
+ vm.deleteIssuable();
+
+ setTimeout(() => {
+ expect(
+ gl.utils.visitUrl,
+ ).toHaveBeenCalledWith('/test');
+
+ done();
+ });
+ });
+
+ it('stops polling when deleting', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ spyOn(vm.poll, 'stop');
+ spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return { web_url: '/test' };
+ },
+ });
+ }));
+
+ vm.deleteIssuable();
+
+ setTimeout(() => {
+ expect(
+ vm.poll.stop,
+ ).toHaveBeenCalledWith();
+
+ done();
+ });
+ });
+
+ it('closes form on error', (done) => {
+ spyOn(window, 'Flash').and.callThrough();
+ spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve, reject) => {
+ reject();
+ }));
+
+ vm.deleteIssuable();
+
+ setTimeout(() => {
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('close.form');
+ expect(
+ window.Flash,
+ ).toHaveBeenCalledWith('Error deleting issue');
+
+ done();
+ });
+ });
+ });
+
+ describe('open form', () => {
+ it('shows locked warning if form is open & data is different', (done) => {
+ Vue.http.interceptors.push(issueShowInterceptor(issueShowData.initialRequest));
+
+ Vue.nextTick()
+ .then(() => new Promise((resolve) => {
+ setTimeout(resolve);
+ }))
+ .then(() => {
+ vm.openForm();
+
+ Vue.http.interceptors.push(issueShowInterceptor(issueShowData.secondRequest));
+
+ return new Promise((resolve) => {
+ setTimeout(resolve);
+ });
+ })
+ .then(() => {
+ expect(
+ vm.formState.lockedWarningVisible,
+ ).toBeTruthy();
+
+ expect(
+ vm.$el.querySelector('.alert'),
+ ).not.toBeNull();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/issue_show/components/edit_actions_spec.js b/spec/javascripts/issue_show/components/edit_actions_spec.js
new file mode 100644
index 00000000000..f6625b748b6
--- /dev/null
+++ b/spec/javascripts/issue_show/components/edit_actions_spec.js
@@ -0,0 +1,147 @@
+import Vue from 'vue';
+import editActions from '~/issue_show/components/edit_actions.vue';
+import eventHub from '~/issue_show/event_hub';
+import Store from '~/issue_show/stores';
+
+describe('Edit Actions components', () => {
+ let vm;
+
+ beforeEach((done) => {
+ const Component = Vue.extend(editActions);
+ const store = new Store({
+ titleHtml: '',
+ descriptionHtml: '',
+ issuableRef: '',
+ });
+ store.formState.title = 'test';
+
+ spyOn(eventHub, '$emit');
+
+ vm = new Component({
+ propsData: {
+ canDestroy: true,
+ formState: store.formState,
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders all buttons as enabled', () => {
+ expect(
+ vm.$el.querySelectorAll('.disabled').length,
+ ).toBe(0);
+
+ expect(
+ vm.$el.querySelectorAll('[disabled]').length,
+ ).toBe(0);
+ });
+
+ it('does not render delete button if canUpdate is false', (done) => {
+ vm.canDestroy = false;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn-danger'),
+ ).toBeNull();
+
+ done();
+ });
+ });
+
+ it('disables submit button when title is blank', (done) => {
+ vm.formState.title = '';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn-save').getAttribute('disabled'),
+ ).toBe('disabled');
+
+ done();
+ });
+ });
+
+ describe('updateIssuable', () => {
+ it('sends update.issauble event when clicking save button', () => {
+ vm.$el.querySelector('.btn-save').click();
+
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('update.issuable');
+ });
+
+ it('shows loading icon after clicking save button', (done) => {
+ vm.$el.querySelector('.btn-save').click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn-save .fa'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('disabled button after clicking save button', (done) => {
+ vm.$el.querySelector('.btn-save').click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn-save').getAttribute('disabled'),
+ ).toBe('disabled');
+
+ done();
+ });
+ });
+ });
+
+ describe('closeForm', () => {
+ it('emits close.form when clicking cancel', () => {
+ vm.$el.querySelector('.btn-default').click();
+
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('close.form');
+ });
+ });
+
+ describe('deleteIssuable', () => {
+ it('sends delete.issuable event when clicking save button', () => {
+ spyOn(window, 'confirm').and.returnValue(true);
+ vm.$el.querySelector('.btn-danger').click();
+
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('delete.issuable');
+ });
+
+ it('shows loading icon after clicking delete button', (done) => {
+ spyOn(window, 'confirm').and.returnValue(true);
+ vm.$el.querySelector('.btn-danger').click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn-danger .fa'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('does no actions when confirm is false', (done) => {
+ spyOn(window, 'confirm').and.returnValue(false);
+ vm.$el.querySelector('.btn-danger').click();
+
+ Vue.nextTick(() => {
+ expect(
+ eventHub.$emit,
+ ).not.toHaveBeenCalledWith('delete.issuable');
+ expect(
+ vm.$el.querySelector('.btn-danger .fa'),
+ ).toBeNull();
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/fields/description_spec.js b/spec/javascripts/issue_show/components/fields/description_spec.js
new file mode 100644
index 00000000000..f5b35b1e8b0
--- /dev/null
+++ b/spec/javascripts/issue_show/components/fields/description_spec.js
@@ -0,0 +1,56 @@
+import Vue from 'vue';
+import Store from '~/issue_show/stores';
+import descriptionField from '~/issue_show/components/fields/description.vue';
+
+describe('Description field component', () => {
+ let vm;
+ let store;
+
+ beforeEach((done) => {
+ const Component = Vue.extend(descriptionField);
+ const el = document.createElement('div');
+ store = new Store({
+ titleHtml: '',
+ descriptionHtml: '',
+ issuableRef: '',
+ });
+ store.formState.description = 'test';
+
+ document.body.appendChild(el);
+
+ vm = new Component({
+ el,
+ propsData: {
+ markdownPreviewUrl: '/',
+ markdownDocs: '/',
+ formState: store.formState,
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders markdown field with description', () => {
+ expect(
+ vm.$el.querySelector('.md-area textarea').value,
+ ).toBe('test');
+ });
+
+ it('renders markdown field with a markdown description', (done) => {
+ store.formState.description = '**test**';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.md-area textarea').value,
+ ).toBe('**test**');
+
+ done();
+ });
+ });
+
+ it('focuses field when mounted', () => {
+ expect(
+ document.activeElement,
+ ).toBe(vm.$refs.textarea);
+ });
+});
diff --git a/spec/javascripts/issue_show/components/fields/description_template_spec.js b/spec/javascripts/issue_show/components/fields/description_template_spec.js
new file mode 100644
index 00000000000..2b7ee65094b
--- /dev/null
+++ b/spec/javascripts/issue_show/components/fields/description_template_spec.js
@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import descriptionTemplate from '~/issue_show/components/fields/description_template.vue';
+import '~/templates/issuable_template_selector';
+import '~/templates/issuable_template_selectors';
+
+describe('Issue description template component', () => {
+ let vm;
+ let formState;
+
+ beforeEach((done) => {
+ const Component = Vue.extend(descriptionTemplate);
+ formState = {
+ description: 'test',
+ };
+
+ vm = new Component({
+ propsData: {
+ formState,
+ issuableTemplates: [{ name: 'test' }],
+ projectPath: '/',
+ projectNamespace: '/',
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders templates as JSON array in data attribute', () => {
+ expect(
+ vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data'),
+ ).toBe('[{"name":"test"}]');
+ });
+
+ it('updates formState when changing template', () => {
+ vm.issuableTemplate.editor.setValue('test new template');
+
+ expect(
+ formState.description,
+ ).toBe('test new template');
+ });
+
+ it('returns formState description with editor getValue', () => {
+ formState.description = 'testing new template';
+
+ expect(
+ vm.issuableTemplate.editor.getValue(),
+ ).toBe('testing new template');
+ });
+});
diff --git a/spec/javascripts/issue_show/components/fields/project_move_spec.js b/spec/javascripts/issue_show/components/fields/project_move_spec.js
new file mode 100644
index 00000000000..86d35c33ff4
--- /dev/null
+++ b/spec/javascripts/issue_show/components/fields/project_move_spec.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+import projectMove from '~/issue_show/components/fields/project_move.vue';
+
+describe('Project move field component', () => {
+ let vm;
+ let formState;
+
+ beforeEach((done) => {
+ const Component = Vue.extend(projectMove);
+
+ formState = {
+ move_to_project_id: 0,
+ };
+
+ vm = new Component({
+ propsData: {
+ formState,
+ projectsAutocompleteUrl: '/autocomplete',
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('mounts select2 element', () => {
+ expect(
+ vm.$el.querySelector('.select2-container'),
+ ).not.toBeNull();
+ });
+
+ it('updates formState on change', () => {
+ $(vm.$refs['move-dropdown']).val(2).trigger('change');
+
+ expect(
+ formState.move_to_project_id,
+ ).toBe(2);
+ });
+});
diff --git a/spec/javascripts/issue_show/components/fields/title_spec.js b/spec/javascripts/issue_show/components/fields/title_spec.js
new file mode 100644
index 00000000000..53ae038a6a2
--- /dev/null
+++ b/spec/javascripts/issue_show/components/fields/title_spec.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+import Store from '~/issue_show/stores';
+import titleField from '~/issue_show/components/fields/title.vue';
+
+describe('Title field component', () => {
+ let vm;
+ let store;
+
+ beforeEach(() => {
+ const Component = Vue.extend(titleField);
+ store = new Store({
+ titleHtml: '',
+ descriptionHtml: '',
+ issuableRef: '',
+ });
+ store.formState.title = 'test';
+
+ vm = new Component({
+ propsData: {
+ formState: store.formState,
+ },
+ }).$mount();
+ });
+
+ it('renders form control with formState title', () => {
+ expect(
+ vm.$el.querySelector('.form-control').value,
+ ).toBe('test');
+ });
+});
diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js
new file mode 100644
index 00000000000..9a85223208c
--- /dev/null
+++ b/spec/javascripts/issue_show/components/form_spec.js
@@ -0,0 +1,68 @@
+import Vue from 'vue';
+import formComponent from '~/issue_show/components/form.vue';
+import '~/templates/issuable_template_selector';
+import '~/templates/issuable_template_selectors';
+
+describe('Inline edit form component', () => {
+ let vm;
+
+ beforeEach((done) => {
+ const Component = Vue.extend(formComponent);
+
+ vm = new Component({
+ propsData: {
+ canDestroy: true,
+ canMove: true,
+ formState: {
+ title: 'b',
+ description: 'a',
+ lockedWarningVisible: false,
+ },
+ markdownPreviewUrl: '/',
+ markdownDocs: '/',
+ projectsAutocompleteUrl: '/',
+ projectPath: '/',
+ projectNamespace: '/',
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('does not render template selector if no templates exist', () => {
+ expect(
+ vm.$el.querySelector('.js-issuable-selector-wrap'),
+ ).toBeNull();
+ });
+
+ it('renders template selector when templates exists', (done) => {
+ spyOn(gl, 'IssuableTemplateSelectors');
+ vm.issuableTemplates = ['test'];
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.js-issuable-selector-wrap'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('hides locked warning by default', () => {
+ expect(
+ vm.$el.querySelector('.alert'),
+ ).toBeNull();
+ });
+
+ it('shows locked warning if formState is different', (done) => {
+ vm.formState.lockedWarningVisible = true;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.alert'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js
index 2f953e7e92e..a2d90a9b9f5 100644
--- a/spec/javascripts/issue_show/components/title_spec.js
+++ b/spec/javascripts/issue_show/components/title_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import Store from '~/issue_show/stores';
import titleComponent from '~/issue_show/components/title.vue';
describe('Title component', () => {
@@ -6,11 +7,18 @@ describe('Title component', () => {
beforeEach(() => {
const Component = Vue.extend(titleComponent);
+ const store = new Store({
+ titleHtml: '',
+ descriptionHtml: '',
+ issuableRef: '',
+ });
vm = new Component({
propsData: {
issuableRef: '#1',
titleHtml: 'Testing <img />',
titleText: 'Testing',
+ showForm: false,
+ formState: store.formState,
},
}).$mount();
});
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index e9bffd74d90..e3938a77680 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -356,7 +356,7 @@ import '~/lib/utils/common_utils';
describe('gl.utils.setCiStatusFavicon', () => {
it('should set page favicon to CI status favicon based on provided status', () => {
- const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1/status.json`;
+ const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`;
const FAVICON_PATH = '//icon_status_success';
const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub();
const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub();
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 1173fa40947..f444bcaf847 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -13,7 +13,9 @@ import '~/merge_request';
});
it('modifies the Markdown field', function() {
spyOn(jQuery, 'ajax').and.stub();
- $('input[type=checkbox]').attr('checked', true).trigger('change');
+ const changeEvent = document.createEvent('HTMLEvents');
+ changeEvent.initEvent('change', true, true);
+ $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent);
return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
return it('submits an ajax request on tasklist:changed', function() {
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 04cf0fe2bf8..17aa70ff3f1 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -34,7 +34,9 @@ import '~/notes';
});
it('modifies the Markdown field', function() {
- $('input[type=checkbox]').attr('checked', true).trigger('change');
+ const changeEvent = document.createEvent('HTMLEvents');
+ changeEvent.initEvent('change', true, true);
+ $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent);
expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index f033956c071..85bd87318db 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -4,7 +4,7 @@ import actionComponent from '~/pipelines/components/graph/action_component.vue';
describe('pipeline graph action component', () => {
let component;
- beforeEach(() => {
+ beforeEach((done) => {
const ActionComponent = Vue.extend(actionComponent);
component = new ActionComponent({
propsData: {
@@ -14,6 +14,8 @@ describe('pipeline graph action component', () => {
actionIcon: 'icon_action_cancel',
},
}).$mount();
+
+ Vue.nextTick(done);
});
it('should render a link', () => {
@@ -27,7 +29,7 @@ describe('pipeline graph action component', () => {
it('should update bootstrap tooltip when title changes', (done) => {
component.tooltipText = 'changed';
- Vue.nextTick(() => {
+ setTimeout(() => {
expect(component.$el.getAttribute('data-original-title')).toBe('changed');
done();
});
diff --git a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
index 14ff1b0d25c..25fd18b197e 100644
--- a/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/dropdown_action_component_spec.js
@@ -4,7 +4,7 @@ import dropdownActionComponent from '~/pipelines/components/graph/dropdown_actio
describe('action component', () => {
let component;
- beforeEach(() => {
+ beforeEach((done) => {
const DropdownActionComponent = Vue.extend(dropdownActionComponent);
component = new DropdownActionComponent({
propsData: {
@@ -14,6 +14,8 @@ describe('action component', () => {
actionIcon: 'icon_action_cancel',
},
}).$mount();
+
+ Vue.nextTick(done);
});
it('should render a link', () => {
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index 63986b6c0db..e90593e0f40 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -27,26 +27,30 @@ describe('pipeline graph job component', () => {
});
describe('name with link', () => {
- it('should render the job name and status with a link', () => {
+ it('should render the job name and status with a link', (done) => {
const component = new JobComponent({
propsData: {
job: mockJob,
},
}).$mount();
- const link = component.$el.querySelector('a');
+ Vue.nextTick(() => {
+ const link = component.$el.querySelector('a');
- expect(link.getAttribute('href')).toEqual(mockJob.status.details_path);
+ expect(link.getAttribute('href')).toEqual(mockJob.status.details_path);
- expect(
- link.getAttribute('data-original-title'),
- ).toEqual(`${mockJob.name} - ${mockJob.status.label}`);
+ expect(
+ link.getAttribute('data-original-title'),
+ ).toEqual(`${mockJob.name} - ${mockJob.status.label}`);
- expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined();
+ expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined();
- expect(
- component.$el.querySelector('.ci-status-text').textContent.trim(),
- ).toEqual(mockJob.name);
+ expect(
+ component.$el.querySelector('.ci-status-text').textContent.trim(),
+ ).toEqual(mockJob.name);
+
+ done();
+ });
});
});
diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js
new file mode 100644
index 00000000000..4bbaff561fc
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js
@@ -0,0 +1,121 @@
+import Vue from 'vue';
+import fieldComponent from '~/vue_shared/components/markdown/field.vue';
+
+describe('Markdown field component', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = new Vue({
+ render(createElement) {
+ return createElement(
+ fieldComponent,
+ {
+ props: {
+ markdownPreviewUrl: '/preview',
+ markdownDocs: '/docs',
+ },
+ },
+ [
+ createElement('textarea', {
+ slot: 'textarea',
+ }),
+ ],
+ );
+ },
+ });
+ });
+
+ it('creates a new instance of GL form', (done) => {
+ spyOn(gl, 'GLForm');
+ vm.$mount();
+
+ Vue.nextTick(() => {
+ expect(
+ gl.GLForm,
+ ).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ describe('mounted', () => {
+ beforeEach((done) => {
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders textarea inside backdrop', () => {
+ expect(
+ vm.$el.querySelector('.zen-backdrop textarea'),
+ ).not.toBeNull();
+ });
+
+ describe('markdown preview', () => {
+ let previewLink;
+
+ beforeEach(() => {
+ spyOn(Vue.http, 'post').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ body: '<p>markdown preview</p>',
+ };
+ },
+ });
+ }));
+
+ previewLink = vm.$el.querySelector('.nav-links li:nth-child(2) a');
+ });
+
+ it('sets preview link as active', (done) => {
+ previewLink.click();
+
+ Vue.nextTick(() => {
+ expect(
+ previewLink.parentNode.classList.contains('active'),
+ ).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('shows preview loading text', (done) => {
+ previewLink.click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.md-preview').textContent.trim(),
+ ).toContain('Loading...');
+
+ done();
+ });
+ });
+
+ it('renders markdown preview', (done) => {
+ previewLink.click();
+
+ setTimeout(() => {
+ expect(
+ vm.$el.querySelector('.md-preview').innerHTML,
+ ).toContain('<p>markdown preview</p>');
+
+ done();
+ });
+ });
+
+ it('renders GFM with jQuery', (done) => {
+ spyOn($.fn, 'renderGFM');
+ previewLink.click();
+
+ setTimeout(() => {
+ expect(
+ $.fn.renderGFM,
+ ).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js
new file mode 100644
index 00000000000..7110ff36937
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js
@@ -0,0 +1,67 @@
+import Vue from 'vue';
+import headerComponent from '~/vue_shared/components/markdown/header.vue';
+
+describe('Markdown field header component', () => {
+ let vm;
+
+ beforeEach((done) => {
+ const Component = Vue.extend(headerComponent);
+
+ vm = new Component({
+ propsData: {
+ previewMarkdown: false,
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders markdown buttons', () => {
+ expect(
+ vm.$el.querySelectorAll('.js-md').length,
+ ).toBe(7);
+ });
+
+ it('renders `write` link as active when previewMarkdown is false', () => {
+ expect(
+ vm.$el.querySelector('li:nth-child(1)').classList.contains('active'),
+ ).toBeTruthy();
+ });
+
+ it('renders `preview` link as active when previewMarkdown is true', (done) => {
+ vm.previewMarkdown = true;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('li:nth-child(2)').classList.contains('active'),
+ ).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('emits toggle markdown event when clicking preview', () => {
+ spyOn(vm, '$emit');
+
+ vm.$el.querySelector('li:nth-child(2) a').click();
+
+ expect(
+ vm.$emit,
+ ).toHaveBeenCalledWith('toggle-markdown');
+ });
+
+ it('blurs preview link after click', (done) => {
+ const link = vm.$el.querySelector('li:nth-child(2) a');
+ spyOn(HTMLElement.prototype, 'blur');
+
+ link.click();
+
+ setTimeout(() => {
+ expect(
+ link.blur,
+ ).toHaveBeenCalled();
+
+ done();
+ });
+ });
+});
diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb
index eb4f06b371c..13e6953147b 100644
--- a/spec/lib/gitlab/chat_commands/command_spec.rb
+++ b/spec/lib/gitlab/chat_commands/command_spec.rb
@@ -58,9 +58,12 @@ describe Gitlab::ChatCommands::Command, service: true do
end
end
- context 'and user does have deployment permission' do
+ context 'and user has deployment permission' do
before do
- build.project.add_master(user)
+ build.project.add_developer(user)
+
+ create(:protected_branch, :developers_can_merge,
+ name: build.ref, project: project)
end
it 'returns action' do
diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb
index b33389d959e..46dbdeae37c 100644
--- a/spec/lib/gitlab/chat_commands/deploy_spec.rb
+++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb
@@ -7,7 +7,12 @@ describe Gitlab::ChatCommands::Deploy, service: true do
let(:regex_match) { described_class.match('deploy staging to production') }
before do
- project.add_master(user)
+ # Make it possible to trigger protected manual actions for developers.
+ #
+ project.add_developer(user)
+
+ create(:protected_branch, :developers_can_merge,
+ name: 'master', project: project)
end
subject do
diff --git a/spec/lib/gitlab/ci/status/build/common_spec.rb b/spec/lib/gitlab/ci/status/build/common_spec.rb
index 40b96b1807b..72bd7c4eb93 100644
--- a/spec/lib/gitlab/ci/status/build/common_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/common_spec.rb
@@ -31,7 +31,7 @@ describe Gitlab::Ci::Status::Build::Common do
describe '#details_path' do
it 'links to the build details page' do
- expect(subject.details_path).to include "builds/#{build.id}"
+ expect(subject.details_path).to include "jobs/#{build.id}"
end
end
end
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 185bb9098da..3f30b2c38f2 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -224,7 +224,10 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when user has ability to play action' do
before do
- build.project.add_master(user)
+ project.add_developer(user)
+
+ create(:protected_branch, :developers_can_merge,
+ name: build.ref, project: project)
end
it 'fabricates status that has action' do
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index f5d0f977768..0e15a5f3c6b 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Build::Play do
let(:user) { create(:user) }
+ let(:project) { build.project }
let(:build) { create(:ci_build, :manual) }
let(:status) { Gitlab::Ci::Status::Core.new(build, user) }
@@ -15,8 +16,13 @@ describe Gitlab::Ci::Status::Build::Play do
describe '#has_action?' do
context 'when user is allowed to update build' do
- context 'when user can push to branch' do
- before { build.project.add_master(user) }
+ context 'when user is allowed to trigger protected action' do
+ before do
+ project.add_developer(user)
+
+ create(:protected_branch, :developers_can_merge,
+ name: build.ref, project: project)
+ end
it { is_expected.to have_action }
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index fdbb55fc874..b1999409170 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -244,7 +244,7 @@ describe Gitlab::Workhorse, lib: true do
context "when git_receive_pack action is passed" do
let(:action) { 'git_receive_pack' }
- it { expect(subject).not_to include(gitaly_params) }
+ it { expect(subject).to include(gitaly_params) }
end
context "when info_refs action is passed" do
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 12519de8636..9fbe19b04d5 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -227,7 +227,10 @@ describe Environment, models: true do
context 'when user is allowed to stop environment' do
before do
- project.add_master(user)
+ project.add_developer(user)
+
+ create(:protected_branch, :developers_can_merge,
+ name: 'master', project: project)
end
context 'when action did not yet finish' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index a953faaaedf..360fcae29a5 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -948,6 +948,20 @@ describe Project, models: true do
end
end
+ describe '.starred_by' do
+ it 'returns only projects starred by the given user' do
+ user1 = create(:user)
+ user2 = create(:user)
+ project1 = create(:empty_project)
+ project2 = create(:empty_project)
+ create(:empty_project)
+ user1.toggle_star(project1)
+ user2.toggle_star(project2)
+
+ expect(Project.starred_by(user1)).to contain_exactly(project1)
+ end
+ end
+
describe '.visible_to_user' do
let!(:project) { create(:empty_project, :private) }
let!(:user) { create(:user) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 9edf34b05ad..fe9df3360ff 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1496,25 +1496,6 @@ describe User, models: true do
end
end
- describe '#viewable_starred_projects' do
- let(:user) { create(:user) }
- let(:public_project) { create(:empty_project, :public) }
- let(:private_project) { create(:empty_project, :private) }
- let(:private_viewable_project) { create(:empty_project, :private) }
-
- before do
- private_viewable_project.team << [user, Gitlab::Access::MASTER]
-
- [public_project, private_project, private_viewable_project].each do |project|
- user.toggle_star(project)
- end
- end
-
- it 'returns only starred projects the user can view' do
- expect(user.viewable_starred_projects).not_to include(private_project)
- end
- end
-
describe '#projects_with_reporter_access_limited_to' do
let(:project1) { create(:empty_project) }
let(:project2) { create(:empty_project) }
diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb
new file mode 100644
index 00000000000..85d11deb26f
--- /dev/null
+++ b/spec/requests/api/pipeline_schedules_spec.rb
@@ -0,0 +1,297 @@
+require 'spec_helper'
+
+describe API::PipelineSchedules do
+ set(:developer) { create(:user) }
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+
+ before do
+ project.add_developer(developer)
+ end
+
+ describe 'GET /projects/:id/pipeline_schedules' do
+ context 'authenticated user with valid permissions' do
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) }
+
+ before do
+ pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
+ end
+
+ it 'returns list of pipeline_schedules' do
+ get api("/projects/#{project.id}/pipeline_schedules", developer)
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(response).to match_response_schema('pipeline_schedules')
+ end
+
+ it 'avoids N + 1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ get api("/projects/#{project.id}/pipeline_schedules", developer)
+ end.count
+
+ create_list(:ci_pipeline_schedule, 10, project: project)
+ .each do |pipeline_schedule|
+ create(:user).tap do |user|
+ project.add_developer(user)
+ pipeline_schedule.update_attributes(owner: user)
+ end
+ pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
+ end
+
+ expect do
+ get api("/projects/#{project.id}/pipeline_schedules", developer)
+ end.not_to exceed_query_limit(control_count)
+ end
+
+ %w[active inactive].each do |target|
+ context "when scope is #{target}" do
+ before do
+ create(:ci_pipeline_schedule, project: project, active: active?(target))
+ end
+
+ it 'returns matched pipeline schedules' do
+ get api("/projects/#{project.id}/pipeline_schedules", developer), scope: target
+
+ expect(json_response.map{ |r| r['active'] }).to all(eq(active?(target)))
+ end
+ end
+
+ def active?(str)
+ (str == 'active') ? true : false
+ end
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not return pipeline_schedules list' do
+ get api("/projects/#{project.id}/pipeline_schedules", user)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not return pipeline_schedules list' do
+ get api("/projects/#{project.id}/pipeline_schedules")
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/pipeline_schedules/:pipeline_schedule_id' do
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) }
+
+ before do
+ pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
+ end
+
+ context 'authenticated user with valid permissions' do
+ it 'returns pipeline_schedule details' do
+ get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('pipeline_schedule')
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do
+ get api("/projects/#{project.id}/pipeline_schedules/-5", developer)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not return pipeline_schedules list' do
+ get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not return pipeline_schedules list' do
+ get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipeline_schedules' do
+ let(:params) { attributes_for(:ci_pipeline_schedule) }
+
+ context 'authenticated user with valid permissions' do
+ context 'with required parameters' do
+ it 'creates pipeline_schedule' do
+ expect do
+ post api("/projects/#{project.id}/pipeline_schedules", developer),
+ params
+ end.to change { project.pipeline_schedules.count }.by(1)
+
+ expect(response).to have_http_status(:created)
+ expect(response).to match_response_schema('pipeline_schedule')
+ expect(json_response['description']).to eq(params[:description])
+ expect(json_response['ref']).to eq(params[:ref])
+ expect(json_response['cron']).to eq(params[:cron])
+ expect(json_response['cron_timezone']).to eq(params[:cron_timezone])
+ expect(json_response['owner']['id']).to eq(developer.id)
+ end
+ end
+
+ context 'without required parameters' do
+ it 'does not create pipeline_schedule' do
+ post api("/projects/#{project.id}/pipeline_schedules", developer)
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+
+ context 'when cron has validation error' do
+ it 'does not create pipeline_schedule' do
+ post api("/projects/#{project.id}/pipeline_schedules", developer),
+ params.merge('cron' => 'invalid-cron')
+
+ expect(response).to have_http_status(:bad_request)
+ expect(json_response['message']).to have_key('cron')
+ end
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not create pipeline_schedule' do
+ post api("/projects/#{project.id}/pipeline_schedules", user), params
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not create pipeline_schedule' do
+ post api("/projects/#{project.id}/pipeline_schedules"), params
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id' do
+ let(:pipeline_schedule) do
+ create(:ci_pipeline_schedule, project: project, owner: developer)
+ end
+
+ context 'authenticated user with valid permissions' do
+ it 'updates cron' do
+ put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer),
+ cron: '1 2 3 4 *'
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('pipeline_schedule')
+ expect(json_response['cron']).to eq('1 2 3 4 *')
+ end
+
+ context 'when cron has validation error' do
+ it 'does not update pipeline_schedule' do
+ put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer),
+ cron: 'invalid-cron'
+
+ expect(response).to have_http_status(:bad_request)
+ expect(json_response['message']).to have_key('cron')
+ end
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not update pipeline_schedule' do
+ put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not update pipeline_schedule' do
+ put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do
+ let(:pipeline_schedule) do
+ create(:ci_pipeline_schedule, project: project, owner: developer)
+ end
+
+ context 'authenticated user with valid permissions' do
+ it 'updates owner' do
+ post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", developer)
+
+ expect(response).to have_http_status(:created)
+ expect(response).to match_response_schema('pipeline_schedule')
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not update owner' do
+ post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", user)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not update owner' do
+ post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership")
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id' do
+ let(:master) { create(:user) }
+
+ let!(:pipeline_schedule) do
+ create(:ci_pipeline_schedule, project: project, owner: developer)
+ end
+
+ before do
+ project.add_master(master)
+ end
+
+ context 'authenticated user with valid permissions' do
+ it 'deletes pipeline_schedule' do
+ expect do
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", master)
+ end.to change { project.pipeline_schedules.count }.by(-1)
+
+ expect(response).to have_http_status(:accepted)
+ expect(response).to match_response_schema('pipeline_schedule')
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do
+ delete api("/projects/#{project.id}/pipeline_schedules/-5", master)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not delete pipeline_schedule' do
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
+
+ expect(response).to have_http_status(:forbidden)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not delete pipeline_schedule' do
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index f95a287a184..3d98551628b 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -390,6 +390,14 @@ describe API::Projects do
expect(json_response['visibility']).to eq('private')
end
+ it 'sets tag list to a project' do
+ project = attributes_for(:project, tag_list: %w[tagFirst tagSecond])
+
+ post api('/projects', user), project
+
+ expect(json_response['tag_list']).to eq(%w[tagFirst tagSecond])
+ end
+
it 'sets a project as allowing merge even if build fails' do
project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false })
post api('/projects', user), project
diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb
index 059deba5416..15720d86583 100644
--- a/spec/serializers/build_action_entity_spec.rb
+++ b/spec/serializers/build_action_entity_spec.rb
@@ -1,26 +1,26 @@
require 'spec_helper'
describe BuildActionEntity do
- let(:build) { create(:ci_build, name: 'test_build') }
+ let(:job) { create(:ci_build, name: 'test_job') }
let(:request) { double('request') }
let(:entity) do
- described_class.new(build, request: spy('request'))
+ described_class.new(job, request: spy('request'))
end
describe '#as_json' do
subject { entity.as_json }
- it 'contains original build name' do
- expect(subject[:name]).to eq 'test_build'
+ it 'contains original job name' do
+ expect(subject[:name]).to eq 'test_job'
end
it 'contains path to the action play' do
- expect(subject[:path]).to include "builds/#{build.id}/play"
+ expect(subject[:path]).to include "jobs/#{job.id}/play"
end
it 'contains whether it is playable' do
- expect(subject[:playable]).to eq build.playable?
+ expect(subject[:playable]).to eq job.playable?
end
end
end
diff --git a/spec/serializers/build_artifact_entity_spec.rb b/spec/serializers/build_artifact_entity_spec.rb
index 2fc60aa9de6..b4eef20d6a6 100644
--- a/spec/serializers/build_artifact_entity_spec.rb
+++ b/spec/serializers/build_artifact_entity_spec.rb
@@ -1,22 +1,22 @@
require 'spec_helper'
describe BuildArtifactEntity do
- let(:build) { create(:ci_build, name: 'test:build') }
+ let(:job) { create(:ci_build, name: 'test:job') }
let(:entity) do
- described_class.new(build, request: double)
+ described_class.new(job, request: double)
end
describe '#as_json' do
subject { entity.as_json }
- it 'contains build name' do
- expect(subject[:name]).to eq 'test:build'
+ it 'contains job name' do
+ expect(subject[:name]).to eq 'test:job'
end
it 'contains path to the artifacts' do
expect(subject[:path])
- .to include "builds/#{build.id}/artifacts/download"
+ .to include "jobs/#{job.id}/artifacts/download"
end
end
end
diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb
index b5eb84ae43b..6d5e1046e86 100644
--- a/spec/serializers/build_entity_spec.rb
+++ b/spec/serializers/build_entity_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe BuildEntity do
let(:user) { create(:user) }
let(:build) { create(:ci_build) }
+ let(:project) { build.project }
let(:request) { double('request') }
before do
@@ -52,7 +53,10 @@ describe BuildEntity do
context 'when user is allowed to trigger action' do
before do
- build.project.add_master(user)
+ project.add_developer(user)
+
+ create(:protected_branch, :developers_can_merge,
+ name: 'master', project: project)
end
it 'contains path to play action' do
diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb
index d6f9fa42045..ea211de1f82 100644
--- a/spec/services/ci/play_build_service_spec.rb
+++ b/spec/services/ci/play_build_service_spec.rb
@@ -13,8 +13,11 @@ describe Ci::PlayBuildService, '#execute', :services do
context 'when project does not have repository yet' do
let(:project) { create(:empty_project) }
- it 'allows user with master role to play build' do
- project.add_master(user)
+ it 'allows user to play build if protected branch rules are met' do
+ project.add_developer(user)
+
+ create(:protected_branch, :developers_can_merge,
+ name: build.ref, project: project)
service.execute(build)
@@ -45,7 +48,10 @@ describe Ci::PlayBuildService, '#execute', :services do
let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
before do
- project.add_master(user)
+ project.add_developer(user)
+
+ create(:protected_branch, :developers_can_merge,
+ name: build.ref, project: project)
end
it 'enqueues the build' do
@@ -64,7 +70,10 @@ describe Ci::PlayBuildService, '#execute', :services do
let(:build) { create(:ci_build, when: :manual, pipeline: pipeline) }
before do
- project.add_master(user)
+ project.add_developer(user)
+
+ create(:protected_branch, :developers_can_merge,
+ name: build.ref, project: project)
end
it 'duplicates the build' do
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index fc5de5d069a..1557cb3c938 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -333,10 +333,11 @@ describe Ci::ProcessPipelineService, '#execute', :services do
context 'when pipeline is promoted sequentially up to the end' do
before do
- # We are using create(:empty_project), and users has to be master in
- # order to execute manual action when repository does not exist.
+ # Users need ability to merge into a branch in order to trigger
+ # protected manual actions.
#
- project.add_master(user)
+ create(:protected_branch, :developers_can_merge,
+ name: 'master', project: project)
end
it 'properly processes entire pipeline' do
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index d941d56c0d8..3e860203063 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -6,9 +6,12 @@ describe Ci::RetryPipelineService, '#execute', :services do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:service) { described_class.new(project, user) }
- context 'when user has ability to modify pipeline' do
+ context 'when user has full ability to modify pipeline' do
before do
- project.add_master(user)
+ project.add_developer(user)
+
+ create(:protected_branch, :developers_can_merge,
+ name: pipeline.ref, project: project)
end
context 'when there are already retried jobs present' do
diff --git a/spec/views/ci/status/_badge.html.haml_spec.rb b/spec/views/ci/status/_badge.html.haml_spec.rb
index c62450fb8e2..72323da2838 100644
--- a/spec/views/ci/status/_badge.html.haml_spec.rb
+++ b/spec/views/ci/status/_badge.html.haml_spec.rb
@@ -16,7 +16,7 @@ describe 'ci/status/_badge', :view do
end
it 'has link to build details page' do
- details_path = namespace_project_build_path(
+ details_path = namespace_project_job_path(
project.namespace, project, build)
render_status(build)
diff --git a/spec/views/projects/builds/_build.html.haml_spec.rb b/spec/views/projects/jobs/_build.html.haml_spec.rb
index 751482cac42..1d58891036e 100644
--- a/spec/views/projects/builds/_build.html.haml_spec.rb
+++ b/spec/views/projects/jobs/_build.html.haml_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'projects/ci/builds/_build' do
+describe 'projects/ci/jobs/_build' do
include Devise::Test::ControllerHelpers
let(:project) { create(:project, :repository) }
diff --git a/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb b/spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb
index dc2ffc9dc47..dc2ffc9dc47 100644
--- a/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb
+++ b/spec/views/projects/jobs/_generic_commit_status.html.haml_spec.rb
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb
index 0f39df0f250..8f2822f5dc5 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/jobs/show.html.haml_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'projects/builds/show', :view do
+describe 'projects/jobs/show', :view do
let(:project) { create(:project, :repository) }
let(:build) { create(:ci_build, pipeline: pipeline) }
@@ -278,7 +278,7 @@ describe 'projects/builds/show', :view do
it 'links to issues/new with the title and description filled in' do
title = "Build Failed ##{build.id}"
- build_url = namespace_project_build_url(project.namespace, project, build)
+ build_url = namespace_project_job_url(project.namespace, project, build)
href = new_namespace_project_issue_path(
project.namespace,
project,
diff --git a/vendor/assets/javascripts/task_list.js b/vendor/assets/javascripts/task_list.js
deleted file mode 100644
index 9fbfef03f6d..00000000000
--- a/vendor/assets/javascripts/task_list.js
+++ /dev/null
@@ -1,258 +0,0 @@
-// The MIT License (MIT)
-//
-// Copyright (c) 2014 GitHub, Inc.
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-// TaskList Behavior
-//
-/*= provides tasklist:enabled */
-/*= provides tasklist:disabled */
-/*= provides tasklist:change */
-/*= provides tasklist:changed */
-//
-//
-// Enables Task List update behavior.
-//
-// ### Example Markup
-//
-// <div class="js-task-list-container">
-// <ul class="task-list">
-// <li class="task-list-item">
-// <input type="checkbox" class="js-task-list-item-checkbox" disabled />
-// text
-// </li>
-// </ul>
-// <form>
-// <textarea class="js-task-list-field">- [ ] text</textarea>
-// </form>
-// </div>
-//
-// ### Specification
-//
-// TaskLists MUST be contained in a `(div).js-task-list-container`.
-//
-// TaskList Items SHOULD be an a list (`UL`/`OL`) element.
-//
-// Task list items MUST match `(input).task-list-item-checkbox` and MUST be
-// `disabled` by default.
-//
-// TaskLists MUST have a `(textarea).js-task-list-field` form element whose
-// `value` attribute is the source (Markdown) to be udpated. The source MUST
-// follow the syntax guidelines.
-//
-// TaskList updates trigger `tasklist:change` events. If the change is
-// successful, `tasklist:changed` is fired. The change can be canceled.
-//
-// jQuery is required.
-//
-// ### Methods
-//
-// `.taskList('enable')` or `.taskList()`
-//
-// Enables TaskList updates for the container.
-//
-// `.taskList('disable')`
-//
-// Disables TaskList updates for the container.
-//
-//# ### Events
-//
-// `tasklist:enabled`
-//
-// Fired when the TaskList is enabled.
-//
-// * **Synchronicity** Sync
-// * **Bubbles** Yes
-// * **Cancelable** No
-// * **Target** `.js-task-list-container`
-//
-// `tasklist:disabled`
-//
-// Fired when the TaskList is disabled.
-//
-// * **Synchronicity** Sync
-// * **Bubbles** Yes
-// * **Cancelable** No
-// * **Target** `.js-task-list-container`
-//
-// `tasklist:change`
-//
-// Fired before the TaskList item change takes affect.
-//
-// * **Synchronicity** Sync
-// * **Bubbles** Yes
-// * **Cancelable** Yes
-// * **Target** `.js-task-list-field`
-//
-// `tasklist:changed`
-//
-// Fired once the TaskList item change has taken affect.
-//
-// * **Synchronicity** Sync
-// * **Bubbles** Yes
-// * **Cancelable** No
-// * **Target** `.js-task-list-field`
-//
-// ### NOTE
-//
-// Task list checkboxes are rendered as disabled by default because rendered
-// user content is cached without regard for the viewer.
-(function() {
- var codeFencesPattern, complete, completePattern, disableTaskList, disableTaskLists, enableTaskList, enableTaskLists, escapePattern, incomplete, incompletePattern, itemPattern, itemsInParasPattern, updateTaskList, updateTaskListItem,
- indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
-
- incomplete = "[ ]";
-
- complete = "[x]";
-
- // Escapes the String for regular expression matching.
- escapePattern = function(str) {
- return str.replace(/([\[\]])/g, "\\$1").replace(/\s/, "\\s").replace("x", "[xX]");
- };
-
- incompletePattern = RegExp("" + (escapePattern(incomplete))); // escape square brackets
- // match all white space
- completePattern = RegExp("" + (escapePattern(complete))); // match all cases
-
- // Pattern used to identify all task list items.
- // Useful when you need iterate over all items.
- itemPattern = RegExp("^(?:\\s*(?:>\\s*)*(?:[-+*]|(?:\\d+\\.)))\\s*(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ")\\s+(?!\\(.*?\\))(?=(?:\\[.*?\\]\\s*(?:\\[.*?\\]|\\(.*?\\))\\s*)*(?:[^\\[]|$))");
-
- // prefix, consisting of
- // optional leading whitespace
- // zero or more blockquotes
- // list item indicator
- // optional whitespace prefix
- // checkbox
- // is followed by whitespace
- // is not part of a [foo](url) link
- // and is followed by zero or more links
- // and either a non-link or the end of the string
- // Used to filter out code fences from the source for comparison only.
- // http://rubular.com/r/x5EwZVrloI
- // Modified slightly due to issues with JS
- codeFencesPattern = /^`{3}(?:\s*\w+)?[\S\s].*[\S\s]^`{3}$/mg;
-
- // ```
- // followed by optional language
- // whitespace
- // code
- // whitespace
- // ```
- // Used to filter out potential mismatches (items not in lists).
- // http://rubular.com/r/OInl6CiePy
- itemsInParasPattern = RegExp("^(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ").+$", "g");
-
- // Given the source text, updates the appropriate task list item to match the
- // given checked value.
- //
- // Returns the updated String text.
- updateTaskListItem = function(source, itemIndex, checked) {
- var clean, index, line, result;
- clean = source.replace(/\r/g, '').replace(codeFencesPattern, '').replace(itemsInParasPattern, '').split("\n");
- index = 0;
- result = (function() {
- var i, len, ref, results;
- ref = source.split("\n");
- results = [];
- for (i = 0, len = ref.length; i < len; i++) {
- line = ref[i];
- if (indexOf.call(clean, line) >= 0 && line.match(itemPattern)) {
- index += 1;
- if (index === itemIndex) {
- line = checked ? line.replace(incompletePattern, complete) : line.replace(completePattern, incomplete);
- }
- }
- results.push(line);
- }
- return results;
- })();
- return result.join("\n");
- };
-
- // Updates the $field value to reflect the state of $item.
- // Triggers the `tasklist:change` event before the value has changed, and fires
- // a `tasklist:changed` event once the value has changed.
- updateTaskList = function($item) {
- var $container, $field, checked, event, index;
- $container = $item.closest('.js-task-list-container');
- $field = $container.find('.js-task-list-field');
- index = 1 + $container.find('.task-list-item-checkbox').index($item);
- checked = $item.prop('checked');
- event = $.Event('tasklist:change');
- $field.trigger(event, [index, checked]);
- if (!event.isDefaultPrevented()) {
- $field.val(updateTaskListItem($field.val(), index, checked));
- $field.trigger('change');
- return $field.trigger('tasklist:changed', [index, checked]);
- }
- };
-
- // When the task list item checkbox is updated, submit the change
- $(document).on('change', '.task-list-item-checkbox', function() {
- return updateTaskList($(this));
- });
-
- // Enables TaskList item changes.
- enableTaskList = function($container) {
- if ($container.find('.js-task-list-field').length > 0) {
- $container.find('.task-list-item').addClass('enabled').find('.task-list-item-checkbox').attr('disabled', null);
- return $container.addClass('is-task-list-enabled').trigger('tasklist:enabled');
- }
- };
-
- // Enables a collection of TaskList containers.
- enableTaskLists = function($containers) {
- var container, i, len, results;
- results = [];
- for (i = 0, len = $containers.length; i < len; i++) {
- container = $containers[i];
- results.push(enableTaskList($(container)));
- }
- return results;
- };
-
- // Disable TaskList item changes.
- disableTaskList = function($container) {
- $container.find('.task-list-item').removeClass('enabled').find('.task-list-item-checkbox').attr('disabled', 'disabled');
- return $container.removeClass('is-task-list-enabled').trigger('tasklist:disabled');
- };
-
- // Disables a collection of TaskList containers.
- disableTaskLists = function($containers) {
- var container, i, len, results;
- results = [];
- for (i = 0, len = $containers.length; i < len; i++) {
- container = $containers[i];
- results.push(disableTaskList($(container)));
- }
- return results;
- };
-
- $.fn.taskList = function(method) {
- var $container, methods;
- $container = $(this).closest('.js-task-list-container');
- methods = {
- enable: enableTaskLists,
- disable: disableTaskLists
- };
- return methods[method || 'enable']($container);
- };
-
-}).call(this);
diff --git a/yarn.lock b/yarn.lock
index 8221711960d..1db64aead8d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1570,6 +1570,10 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+deckar01-task_list@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/deckar01-task_list/-/deckar01-task_list-2.0.0.tgz#7f7a595430d21b3036ed5dfbf97d6b65de18e2c9"
+
deep-extend@~0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253"