Commit cd753a8c authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee

parents bf079a07 db38f9ae
...@@ -35,4 +35,10 @@ if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') { ...@@ -35,4 +35,10 @@ if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') {
plugins.push('babel-plugin-rewire'); plugins.push('babel-plugin-rewire');
} }
// Jest is running in node environment
if (BABEL_ENV === 'jest') {
plugins.push('transform-es2015-modules-commonjs');
plugins.push('dynamic-import-node');
}
module.exports = { presets, plugins }; module.exports = { presets, plugins };
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/config/ /config/
/builds/ /builds/
/coverage/ /coverage/
/coverage-frontend/
/coverage-javascript/ /coverage-javascript/
/node_modules/ /node_modules/
/public/ /public/
......
...@@ -78,5 +78,5 @@ eslint-report.html ...@@ -78,5 +78,5 @@ eslint-report.html
/plugins/* /plugins/*
/.gitlab_pages_secret /.gitlab_pages_secret
package-lock.json package-lock.json
/junit_rspec.xml /junit_*.xml
/junit_karma.xml /coverage-frontend/
...@@ -877,6 +877,32 @@ karma: ...@@ -877,6 +877,32 @@ karma:
reports: reports:
junit: junit_karma.xml junit: junit_karma.xml
jest:
<<: *dedicated-no-docs-and-no-qa-pull-cache-job
<<: *use-pg
dependencies:
- compile-assets
- setup-test-env
script:
- scripts/gitaly-test-spawn
- date
- bundle exec rake karma:fixtures
- date
- yarn jest --ci --coverage
artifacts:
name: coverage-frontend
expire_in: 31d
when: always
paths:
- coverage-frontend/
- junit_jest.xml
reports:
junit: junit_jest.xml
cache:
key: jest
paths:
- tmp/jest/jest/
code_quality: code_quality:
<<: *dedicated-no-docs-no-db-pull-cache-job <<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:stable image: docker:stable
......
...@@ -14,6 +14,7 @@ const Api = { ...@@ -14,6 +14,7 @@ const Api = {
projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
projectRunnersPath: '/api/:version/projects/:id/runners',
mergeRequestsPath: '/api/:version/merge_requests', mergeRequestsPath: '/api/:version/merge_requests',
groupLabelsPath: '/groups/:namespace_path/-/labels', groupLabelsPath: '/groups/:namespace_path/-/labels',
ldapGroupsPath: '/api/:version/ldap/:provider/groups.json', ldapGroupsPath: '/api/:version/ldap/:provider/groups.json',
...@@ -126,6 +127,15 @@ const Api = { ...@@ -126,6 +127,15 @@ const Api = {
return axios.get(url); return axios.get(url);
}, },
projectRunners(projectPath, config = {}) {
const url = Api.buildUrl(Api.projectRunnersPath).replace(
':id',
encodeURIComponent(projectPath),
);
return axios.get(url, config);
},
mergeRequests(params = {}) { mergeRequests(params = {}) {
const url = Api.buildUrl(Api.mergeRequestsPath); const url = Api.buildUrl(Api.mergeRequestsPath);
......
...@@ -147,13 +147,19 @@ export const scrollToLineIfNeededParallel = (_, line) => { ...@@ -147,13 +147,19 @@ export const scrollToLineIfNeededParallel = (_, line) => {
} }
}; };
export const loadCollapsedDiff = ({ commit }, file) => export const loadCollapsedDiff = ({ commit, getters }, file) =>
axios.get(file.load_collapsed_diff_url).then(res => { axios
commit(types.ADD_COLLAPSED_DIFFS, { .get(file.load_collapsed_diff_url, {
file, params: {
data: res.data, commit_id: getters.commitId,
},
})
.then(res => {
commit(types.ADD_COLLAPSED_DIFFS, {
file,
data: res.data,
});
}); });
});
export const expandAllFiles = ({ commit }) => { export const expandAllFiles = ({ commit }) => {
commit(types.EXPAND_ALL_FILES); commit(types.EXPAND_ALL_FILES);
......
...@@ -130,7 +130,7 @@ export default { ...@@ -130,7 +130,7 @@ export default {
if (file.highlighted_diff_lines) { if (file.highlighted_diff_lines) {
file.highlighted_diff_lines = file.highlighted_diff_lines.map(line => { file.highlighted_diff_lines = file.highlighted_diff_lines.map(line => {
if (lineCheck(line)) { if (!line.discussions.some(({ id }) => discussion.id === id) && lineCheck(line)) {
return { return {
...line, ...line,
discussions: line.discussions.concat(discussion), discussions: line.discussions.concat(discussion),
...@@ -150,11 +150,17 @@ export default { ...@@ -150,11 +150,17 @@ export default {
return { return {
left: { left: {
...line.left, ...line.left,
discussions: left ? line.left.discussions.concat(discussion) : [], discussions:
left && !line.left.discussions.some(({ id }) => id === discussion.id)
? line.left.discussions.concat(discussion)
: (line.left && line.left.discussions) || [],
}, },
right: { right: {
...line.right, ...line.right,
discussions: right && !left ? line.right.discussions.concat(discussion) : [], discussions:
right && !left && !line.right.discussions.some(({ id }) => id === discussion.id)
? line.right.discussions.concat(discussion)
: (line.right && line.right.discussions) || [],
}, },
}; };
} }
......
import Vue from 'vue'; import Vue from 'vue';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import _ from 'underscore';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import ide from './components/ide.vue'; import ide from './components/ide.vue';
import store from './stores'; import store from './stores';
...@@ -13,19 +14,19 @@ Vue.use(Translate); ...@@ -13,19 +14,19 @@ Vue.use(Translate);
* *
* @param {Element} el - The element that will contain the IDE. * @param {Element} el - The element that will contain the IDE.
* @param {Object} options - Extra options for the IDE (Used by EE). * @param {Object} options - Extra options for the IDE (Used by EE).
* @param {(e:Element) => Object} options.extraInitialData -
* Function that returns extra properties to seed initial data.
* @param {Component} options.rootComponent - * @param {Component} options.rootComponent -
* Component that overrides the root component. * Component that overrides the root component.
* @param {(store:Vuex.Store, el:Element) => Vuex.Store} options.extendStore -
* Function that receives the default store and returns an extended one.
*/ */
export function initIde(el, options = {}) { export function initIde(el, options = {}) {
if (!el) return null; if (!el) return null;
const { extraInitialData = () => ({}), rootComponent = ide } = options; const { rootComponent = ide, extendStore = _.identity } = options;
return new Vue({ return new Vue({
el, el,
store, store: extendStore(store, el),
router, router,
created() { created() {
this.setEmptyStateSvgs({ this.setEmptyStateSvgs({
...@@ -41,7 +42,6 @@ export function initIde(el, options = {}) { ...@@ -41,7 +42,6 @@ export function initIde(el, options = {}) {
}); });
this.setInitialData({ this.setInitialData({
clientsidePreviewEnabled: parseBoolean(el.dataset.clientsidePreviewEnabled), clientsidePreviewEnabled: parseBoolean(el.dataset.clientsidePreviewEnabled),
...extraInitialData(el),
}); });
}, },
methods: { methods: {
......
...@@ -16,7 +16,9 @@ const httpStatusCodes = { ...@@ -16,7 +16,9 @@ const httpStatusCodes = {
IM_USED: 226, IM_USED: 226,
MULTIPLE_CHOICES: 300, MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400, BAD_REQUEST: 400,
FORBIDDEN: 403,
NOT_FOUND: 404, NOT_FOUND: 404,
UNPROCESSABLE_ENTITY: 422,
}; };
export const successCodes = [ export const successCodes = [
......
...@@ -19,3 +19,4 @@ $info: $blue-500; ...@@ -19,3 +19,4 @@ $info: $blue-500;
$warning: $orange-500; $warning: $orange-500;
$danger: $red-500; $danger: $red-500;
$zindex-modal-backdrop: 1040; $zindex-modal-backdrop: 1040;
$nav-divider-margin-y: ($grid-size / 2);
...@@ -9,7 +9,7 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -9,7 +9,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_update_build!, before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase] except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
before_action :authorize_erase_build!, only: [:erase] before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_workhorse_authorize] before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :verify_api_request!, only: :terminal_websocket_authorize
layout 'project' layout 'project'
......
...@@ -24,12 +24,9 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -24,12 +24,9 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def render_diffs def render_diffs
@environment = @merge_request.environments_for(current_user).last @environment = @merge_request.environments_for(current_user).last
notes_grouped_by_path = renderable_notes.group_by { |note| note.position.file_path }
@diffs.diff_files.each do |diff_file| note_positions = renderable_notes.map(&:position).compact
notes = notes_grouped_by_path.fetch(diff_file.file_path, []) @diffs.unfold_diff_files(note_positions)
notes.each { |note| diff_file.unfold_diff_lines(note.position) }
end
@diffs.write_cache @diffs.write_cache
......
# frozen_string_literal: true
module IdeHelper
def ide_data
{
"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
"pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'),
"promotion-svg-path": image_path('illustrations/web-ide_promotion.svg'),
"ci-help-page-path" => help_page_path('ci/quick_start/README'),
"web-ide-help-page-path" => help_page_path('user/project/web_ide/index.html'),
"clientside-preview-enabled": Gitlab::CurrentSettings.current_application_settings.web_ide_clientside_preview_enabled.to_s
}
end
end
...@@ -114,7 +114,8 @@ module Ci ...@@ -114,7 +114,8 @@ module Ci
cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at
chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout,
error_message: 'Maximum job timeout has a value which could not be accepted'
validates :maximum_timeout, allow_nil: true, validates :maximum_timeout, allow_nil: true,
numericality: { greater_than_or_equal_to: 600, numericality: { greater_than_or_equal_to: 600,
......
...@@ -24,7 +24,7 @@ module ChronicDurationAttribute ...@@ -24,7 +24,7 @@ module ChronicDurationAttribute
end end
end end
validates virtual_attribute, allow_nil: true, duration: true validates virtual_attribute, allow_nil: true, duration: { message: parameters[:error_message] }
end end
alias_method :chronic_duration_attr, :chronic_duration_attr_writer alias_method :chronic_duration_attr, :chronic_duration_attr_writer
......
...@@ -404,7 +404,8 @@ class Project < ActiveRecord::Base ...@@ -404,7 +404,8 @@ class Project < ActiveRecord::Base
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600 chronic_duration_attr :build_timeout_human_readable, :build_timeout,
default: 3600, error_message: 'Maximum job timeout has a value which could not be accepted'
validates :build_timeout, allow_nil: true, validates :build_timeout, allow_nil: true,
numericality: { greater_than_or_equal_to: 10.minutes, numericality: { greater_than_or_equal_to: 10.minutes,
......
...@@ -14,6 +14,10 @@ class DurationValidator < ActiveModel::EachValidator ...@@ -14,6 +14,10 @@ class DurationValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
ChronicDuration.parse(value) ChronicDuration.parse(value)
rescue ChronicDuration::DurationParseError rescue ChronicDuration::DurationParseError
record.errors.add(attribute, "is not a correct duration") if options[:message]
record.errors.add(:base, options[:message])
else
record.errors.add(attribute, "is not a correct duration")
end
end end
end end
- @body_class = 'ide-layout'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/ide'
#ide.ide-loading{ data: ide_data() }
.text-center
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
- @body_class = 'ide-layout' = render 'ide/show'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/ide'
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
"pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'),
"promotion-svg-path": image_path('illustrations/web-ide_promotion.svg'),
"ci-help-page-path" => help_page_path('ci/quick_start/README'),
"web-ide-help-page-path" => help_page_path('user/project/web_ide/index.html'),
"clientside-preview-enabled": Gitlab::CurrentSettings.current_application_settings.web_ide_clientside_preview_enabled.to_s } }
.text-center
= icon('spinner spin 2x')
%h2.clgray= _('Loading the GitLab IDE...')
- page_title import_in_progress_title - page_title import_in_progress_title
- @no_container = true
- @content_class = "limit-container-width" unless fluid_layout
.save-project-loader .save-project-loader
.center .center
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
= f.label :build_timeout_human_readable, _('Timeout'), class: 'label-bold' = f.label :build_timeout_human_readable, _('Timeout'), class: 'label-bold'
= f.text_field :build_timeout_human_readable, class: 'form-control' = f.text_field :build_timeout_human_readable, class: 'form-control'
%p.form-text.text-muted %p.form-text.text-muted
= _("Per job. If a job passes this threshold, it will be marked as failed") = _('If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like "1 hour". Values without specification represent seconds.')
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank' = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr %hr
......
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
= _('Maximum job timeout') = _('Maximum job timeout')
.col-sm-10 .col-sm-10
= f.text_field :maximum_timeout_human_readable, class: 'form-control' = f.text_field :maximum_timeout_human_readable, class: 'form-control'
.form-text.text-muted= _('This timeout will take precedence when lower than Project-defined timeout') .form-text.text-muted= _('This timeout will take precedence when lower than project-defined timeout and accepts a human readable time input language like "1 hour". Values without specification represent seconds.')
.form-group.row .form-group.row
= label_tag :tag_list, class: 'col-form-label col-sm-2' do = label_tag :tag_list, class: 'col-form-label col-sm-2' do
= _('Tags') = _('Tags')
......
---
title: Use read_repository scope on read-only files API
merge_request: 23534
author:
type: fixed
---
title: Fixed diff files expanding not loading commit content
merge_request:
author:
type: fixed
---
title: Improve help and validation sections of maximum build timeout inputs
merge_request: 23586
author:
type: fixed
---
title: Change container width for project import
merge_request: 23318
author: George Tsiolis
type: fixed
---
title: Fixed duplicate discussions getting added to diff lines
merge_request:
author:
type: fixed
---
title: Avoid 500's when serializing legacy diff notes
merge_request: 23544
author:
type: fixed
---
title: Adjust divider margin to comply with design specs
merge_request: 23548
author:
type: changed
/* eslint-disable filenames/match-regex */
const reporters = ['default'];
if (process.env.CI) {
reporters.push([
'jest-junit',
{
output: './junit_jest.xml',
},
]);
}
// eslint-disable-next-line import/no-commonjs
module.exports = {
testMatch: ['<rootDir>/spec/frontend/**/*_spec.js'],
moduleNameMapper: {
'^~(.*)$': '<rootDir>/app/assets/javascripts$1',
},
collectCoverageFrom: ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'],
coverageDirectory: '<rootDir>/coverage-frontend/',
coverageReporters: ['json', 'lcov', 'text-summary', 'clover'],
cacheDirectory: '<rootDir>/tmp/cache/jest',
modulePathIgnorePatterns: ['<rootDir>/.yarn-cache/'],
reporters,
rootDir: '..', // necessary because this file is in the config/ subdirectory
};
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'yaml' require 'yaml'
NO_CHANGELOG_LABELS = %w[backstage Documentation QA test].freeze NO_CHANGELOG_LABELS = %w[backstage ci-build Documentation meta QA test].freeze
SEE_DOC = "See [the documentation](https://docs.gitlab.com/ce/development/changelog.html).".freeze SEE_DOC = "See [the documentation](https://docs.gitlab.com/ce/development/changelog.html).".freeze
CREATE_CHANGELOG_MESSAGE = <<~MSG.freeze CREATE_CHANGELOG_MESSAGE = <<~MSG.freeze
You can create one with: You can create one with:
......
...@@ -2834,6 +2834,7 @@ ActiveRecord::Schema.define(version: 20181204135932) do ...@@ -2834,6 +2834,7 @@ ActiveRecord::Schema.define(version: 20181204135932) do
t.integer "merge_request_notes_filter", limit: 2, default: 0, null: false t.integer "merge_request_notes_filter", limit: 2, default: 0, null: false
t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime_with_timezone "updated_at", null: false
t.string "epics_sort"
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree
end end
......
...@@ -4,6 +4,16 @@ ...@@ -4,6 +4,16 @@
**Create, read, update and delete repository files using this API** **Create, read, update and delete repository files using this API**
The different scopes available using [personal access tokens](../user/profile/personal_access_tokens.md) are depicted
in the following table.
| Scope | Description |
| ----- | ----------- |
| `read_repository` | Allows read-access to the repository files. |
| `api` | Allows read-write access to the repository files. |
> `read_repository` scope was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23534) in GitLab 11.6.
## Get file from repository ## Get file from repository
Allows you to receive information about file in repository like name, size, Allows you to receive information about file in repository like name, size,
......
...@@ -57,12 +57,14 @@ are very appreciative of the work done by translators and proofreaders! ...@@ -57,12 +57,14 @@ are very appreciative of the work done by translators and proofreaders!
- Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [Crowdin](https://crowdin.com/profile/zzazang) - Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [Crowdin](https://crowdin.com/profile/zzazang)
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve) - Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Ji Hun Oh - [GitLab](https://gitlab.com/Baw-Appie), [Crowdin](https://crowdin.com/profile/BawAppie) - Ji Hun Oh - [GitLab](https://gitlab.com/Baw-Appie), [Crowdin](https://crowdin.com/profile/BawAppie)
- Jeongwhan Choi - [GitLab](https://gitlab.com/jeongwhanchoi), [Crowdin](https://crowdin.com/profile/jeongwhanchoi)
- Mongolian - Mongolian
- Proofreaders needed. - Proofreaders needed.
- Norwegian Bokmal - Norwegian Bokmal
- Proofreaders needed. - Proofreaders needed.
- Polish - Polish
- Filip Mech - [GitLab](https://gitlab.com/mehenz), [Crowdin](https://crowdin.com/profile/mehenz) - Filip Mech - [GitLab](https://gitlab.com/mehenz), [Crowdin](https://crowdin.com/profile/mehenz)
- Maksymilian Roman - [GitLab](https://gitlab.com/villaincandle), [Crowdin](https://crowdin.com/profile/villaincandle)
- Portuguese - Portuguese
- Proofreaders needed. - Proofreaders needed.
- Portuguese, Brazilian - Portuguese, Brazilian
......
...@@ -6,9 +6,15 @@ Tests relevant for frontend development can be found at two places: ...@@ -6,9 +6,15 @@ Tests relevant for frontend development can be found at two places:
- [frontend unit tests](#frontend-unit-tests) - [frontend unit tests](#frontend-unit-tests)
- [frontend component tests](#frontend-component-tests) - [frontend component tests](#frontend-component-tests)
- [frontend integration tests](#frontend-integration-tests) - [frontend integration tests](#frontend-integration-tests)
- `spec/frontend/` which are run by Jest and contain
- [frontend unit tests](#frontend-unit-tests)
- [frontend component tests](#frontend-component-tests)
- [frontend integration tests](#frontend-integration-tests)
- `spec/features/` which are run by RSpec and contain - `spec/features/` which are run by RSpec and contain
- [feature tests](#feature-tests) - [feature tests](#feature-tests)
All tests in `spec/javascripts/` will eventually be migrated to `spec/frontend/` (see also [#53757]).
In addition there were feature tests in `features/` run by Spinach in the past. In addition there were feature tests in `features/` run by Spinach in the past.
These have been removed from our codebase in May 2018 ([#23036](https://gitlab.com/gitlab-org/gitlab-ce/issues/23036)). These have been removed from our codebase in May 2018 ([#23036](https://gitlab.com/gitlab-org/gitlab-ce/issues/23036)).
...@@ -17,6 +23,8 @@ See also: ...@@ -17,6 +23,8 @@ See also:
- [old testing guide](../../testing_guide/frontend_testing.html) - [old testing guide](../../testing_guide/frontend_testing.html)
- [notes on testing Vue components](../../fe_guide/vue.html#testing-vue-components) - [notes on testing Vue components](../../fe_guide/vue.html#testing-vue-components)
[#53757]: https://gitlab.com/gitlab-org/gitlab-ce/issues/53757
## Frontend unit tests ## Frontend unit tests
Unit tests are on the lowest abstraction level and typically test functionality that is not directly perceivable by a user. Unit tests are on the lowest abstraction level and typically test functionality that is not directly perceivable by a user.
......
...@@ -7,9 +7,10 @@ GitLab provides official Docker images to allowing you to easily take advantage ...@@ -7,9 +7,10 @@ GitLab provides official Docker images to allowing you to easily take advantage
## Omnibus GitLab based images ## Omnibus GitLab based images
GitLab maintains a set of [official Docker images](https://hub.docker.com/r/gitlab) based on our [Omnibus GitLab package](https://docs.gitlab.com/omnibus/README.html). These images include: GitLab maintains a set of [official Docker images](https://hub.docker.com/r/gitlab) based on our [Omnibus GitLab package](https://docs.gitlab.com/omnibus/README.html). These images include:
* [GitLab Community Edition](https://hub.docker.com/r/gitlab/gitlab-ce/)
* [GitLab Enterprise Edition](https://hub.docker.com/r/gitlab/gitlab-ee/) - [GitLab Community Edition](https://hub.docker.com/r/gitlab/gitlab-ce/).
* [GitLab Runner](https://hub.docker.com/r/gitlab/gitlab-runner/) - [GitLab Enterprise Edition](https://hub.docker.com/r/gitlab/gitlab-ee/).
- [GitLab Runner](https://hub.docker.com/r/gitlab/gitlab-runner/).
A [complete usage guide](https://docs.gitlab.com/omnibus/docker/) to these images is available, as well as the [Dockerfile used for building the images](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker). A [complete usage guide](https://docs.gitlab.com/omnibus/docker/) to these images is available, as well as the [Dockerfile used for building the images](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker).
......
...@@ -6,7 +6,7 @@ password to login, they'll be prompted for a code generated by an application on ...@@ -6,7 +6,7 @@ password to login, they'll be prompted for a code generated by an application on
their phone. their phone.
You can read more about it here: You can read more about it here:
[Two-factor Authentication (2FA)](../profile/two_factor_authentication.md) [Two-factor Authentication (2FA)](../user/profile/account/two_factor_authentication.md)
## Enforcing 2FA for all users ## Enforcing 2FA for all users
......
...@@ -121,7 +121,9 @@ You can also sort epics list by: ...@@ -121,7 +121,9 @@ You can also sort epics list by:
- **Created date** - **Created date**
- **Last updated** - **Last updated**
- **Start date** - **Start date**
- **Due date** - **Due date**
Each option contains a button that can toggle the order between **ascending** and **descending**. The sort option and order will be persisted to be used wherever epics are browsed including the [roadmap](../roadmap/index.md).
![epics sort](img/epics_sort.png) ![epics sort](img/epics_sort.png)
......
...@@ -13,6 +13,8 @@ Epics in the view can be sorted by: ...@@ -13,6 +13,8 @@ Epics in the view can be sorted by:
- **Start date** - **Start date**
- **Due date** - **Due date**
Each option contains a button that can toggle the order between **ascending** and **descending**. The sort option and order will be persisted to be used wherever epics are browsed including the [epics list view](../epics/index.md).
![roadmap view](img/roadmap_view.png) ![roadmap view](img/roadmap_view.png)
## Timeline duration ## Timeline duration
......
<script> <script>
import { mapState } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
import RightPane from '~/ide/components/panes/right.vue'; import RightPane from '~/ide/components/panes/right.vue';
import TerminalView from '../terminal/view.vue'; import TerminalView from '../terminal/view.vue';
...@@ -8,17 +9,14 @@ export default { ...@@ -8,17 +9,14 @@ export default {
components: { components: {
RightPane, RightPane,
}, },
data() {
return {
// this will come from Vuex store in https://gitlab.com/gitlab-org/gitlab-ee/issues/5426
isTerminalEnabled: false,
};
},
computed: { computed: {
...mapState('terminal', {
isTerminalVisible: 'isVisible',
}),
extensionTabs() { extensionTabs() {
return [ return [
{ {
show: this.isTerminalEnabled, show: this.isTerminalVisible,
title: __('Terminal'), title: __('Terminal'),
views: [{ name: 'terminal', keepAlive: true, component: TerminalView }], views: [{ name: 'terminal', keepAlive: true, component: TerminalView }],
icon: 'terminal', icon: 'terminal',
......
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui';
export default { export default {
components: {
GlLoadingIcon,
},
props: { props: {
isLoading: {
type: Boolean,
required: false,
default: true,
},
isValid: {
type: Boolean,
required: false,
default: false,
},
message: {
type: String,
required: false,
default: '',
},
helpPath: {
type: String,
required: false,
default: '',
},
illustrationPath: { illustrationPath: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
}, },
methods: {
onStart() {
this.$emit('start');
},
},
}; };
</script> </script>
<template> <template>
<div class="text-center"> <div class="text-center">
<div v-if="illustrationPath" class="svg-content svg-130"><img :src="illustrationPath" /></div> <div v-if="illustrationPath" class="svg-content svg-130"><img :src="illustrationPath" /></div>
<h4>{{ __('Web Terminal') }}</h4> <h4>{{ __('Web Terminal') }}</h4>
<p>{{ __('Run tests against your code live using the Web Terminal') }}</p> <gl-loading-icon v-if="isLoading" :size="2" class="prepend-top-default" />
<div class="bs-callout text-left">The Web Terminal is coming soon. Stay tuned!</div> <template v-else>
<p>{{ __('Run tests against your code live using the Web Terminal') }}</p>
<p>
<button :disabled="!isValid" class="btn btn-info" type="button" @click="onStart">
{{ __('Start Web Terminal') }}
</button>
</p>
<div v-if="!isValid && message" class="bs-callout text-left" v-html="message"></div>
<p v-else>
<a
v-if="helpPath"
:href="helpPath"
target="_blank"
v-text="__('Learn more about Web Terminal')"
></a>
</p>
</template>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
export default { export default {
...@@ -7,15 +7,32 @@ export default { ...@@ -7,15 +7,32 @@ export default {
EmptyState, EmptyState,
}, },
computed: { computed: {
...mapState(['emptyStateSvgPath']), ...mapState('terminal', ['isShowSplash', 'paths']),
...mapGetters('terminal', ['allCheck']),
},
methods: {
...mapActions('terminal', ['hideSplash']),
start() {
this.hideSplash();
},
}, },
}; };
</script> </script>
<template> <template>
<div class="h-100"> <div class="h-100">
<div class="h-100 d-flex flex-column justify-content-center"> <div v-if="isShowSplash" class="h-100 d-flex flex-column justify-content-center">
<empty-state :illustration-path="emptyStateSvgPath" /> <empty-state
:is-loading="allCheck.isLoading"
:is-valid="allCheck.isValid"
:message="allCheck.message"
:help-path="paths.webTerminalHelpPath"
:illustration-path="paths.webTerminalSvgPath"
@start="start();"
/>
</div> </div>
<template v-else>
<h5>{{ __('Web Terminal') }}</h5>
</template>
</div> </div>
</template> </template>
export const CHECK_CONFIG = 'config';
export const CHECK_RUNNERS = 'runners';
export const RETRY_RUNNERS_INTERVAL = 10000;
import * as mutationTypes from '~/ide/stores/mutation_types';
import terminalModule from './modules/terminal';
function getPathsFromData(el) {
return {
ciYamlHelpPath: el.dataset.eeCiYamlHelpPath,
ciRunnersHelpPath: el.dataset.eeCiRunnersHelpPath,
webTerminalHelpPath: el.dataset.eeWebTerminalHelpPath,
webTerminalSvgPath: el.dataset.eeWebTerminalSvgPath,
};
}
export default (store, el) => {
store.registerModule('terminal', terminalModule());
store.dispatch('terminal/setPaths', getPathsFromData(el));
store.subscribe(({ type }) => {
if (type === mutationTypes.SET_BRANCH_WORKING_REFERENCE) {
store.dispatch('terminal/init');
}
});
return store;
};
import Api from '~/api';
import httpStatus from '~/lib/utils/http_status';
import * as types from '../mutation_types';
import * as messages from '../messages';
import { CHECK_CONFIG, CHECK_RUNNERS, RETRY_RUNNERS_INTERVAL } from '../../../../constants';
export const requestConfigCheck = ({ commit }) => {
commit(types.REQUEST_CHECK, CHECK_CONFIG);
};
export const receiveConfigCheckSuccess = ({ commit }) => {
commit(types.SET_VISIBLE, true);
commit(types.RECEIVE_CHECK_SUCCESS, CHECK_CONFIG);
};
export const receiveConfigCheckError = ({ commit, state }, e) => {
const { status } = e.response;
const { paths } = state;
const isVisible = status !== httpStatus.FORBIDDEN && status !== httpStatus.NOT_FOUND;
commit(types.SET_VISIBLE, isVisible);
const message = messages.configCheckError(status, paths.ciYamlHelpPath);
commit(types.RECEIVE_CHECK_ERROR, { type: CHECK_CONFIG, message });
};
export const fetchConfigCheck = ({ dispatch }) => {
dispatch('requestConfigCheck');
// This will use a real endpoint in https://gitlab.com/gitlab-org/gitlab-ee/issues/5426
Promise.resolve({})
.then(() => {
dispatch('receiveConfigCheckSuccess');
})
.catch(e => {
dispatch('receiveConfigCheckError', e);
});
};
export const requestRunnersCheck = ({ commit }) => {
commit(types.REQUEST_CHECK, CHECK_RUNNERS);
};
export const receiveRunnersCheckSuccess = ({ commit, dispatch, state }, data) => {
if (data.length) {
commit(types.RECEIVE_CHECK_SUCCESS, CHECK_RUNNERS);
} else {
const { paths } = state;
commit(types.RECEIVE_CHECK_ERROR, {
type: CHECK_RUNNERS,
message: messages.runnersCheckEmpty(paths.ciRunnersHelpPath),
});
dispatch('retryRunnersCheck');
}
};
export const receiveRunnersCheckError = ({ commit }) => {
commit(types.RECEIVE_CHECK_ERROR, {
type: CHECK_RUNNERS,
message: messages.UNEXPECTED_ERROR_RUNNERS,
});
};
export const retryRunnersCheck = ({ dispatch, state }) => {
// if the overall check has failed, don't worry about retrying
const check = state.checks[CHECK_CONFIG];
if (!check.isLoading && !check.isValid) {
return;
}
setTimeout(() => {
dispatch('fetchRunnersCheck', { background: true });
}, RETRY_RUNNERS_INTERVAL);
};
export const fetchRunnersCheck = ({ dispatch, rootGetters }, options = {}) => {
const { background = false } = options;
if (!background) {
dispatch('requestRunnersCheck');
}
const { currentProject } = rootGetters;
Api.projectRunners(currentProject.id, { params: { scope: 'active' } })
.then(({ data }) => {
dispatch('receiveRunnersCheckSuccess', data);
})
.catch(e => {
dispatch('receiveRunnersCheckError', e);
});
};
export * from './setup';
export * from './checks';
export default () => {};
import * as types from '../mutation_types';
// This will be used in https://gitlab.com/gitlab-org/gitlab-ee/issues/5426
// export const init = ({ dispatch }) => {
// dispatch('fetchConfigCheck');
// dispatch('fetchRunnersCheck');
// };
export const init = () => {};
export const hideSplash = ({ commit }) => {
commit(types.HIDE_SPLASH);
};
export const setPaths = ({ commit }, paths) => {
commit(types.SET_PATHS, paths);
};
export const allCheck = state => {
const checks = Object.values(state.checks);
if (checks.some(check => check.isLoading)) {
return { isLoading: true };
}
const invalidCheck = checks.find(check => !check.isValid);
const isValid = !invalidCheck;
const message = !invalidCheck ? '' : invalidCheck.message;
return {
isLoading: false,
isValid,
message,
};
};
export default () => {};
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
export default () => ({
namespaced: true,
actions,
getters,
mutations,
state: state(),
});
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status';
export const UNEXPECTED_ERROR_CONFIG = __(
'An unexpected error occurred while checking the project environment.',
);
export const UNEXPECTED_ERROR_RUNNERS = __(
'An unexpected error occurred while checking the project runners.',
);
export const EMPTY_RUNNERS = __(
'Configure GitLab runners to start using Web Terminal. %{helpStart}Learn more.%{helpEnd}',
);
export const ERROR_CONFIG = __(
'Configure a <code>.gitlab-webide.yml</code> file in the <code>.gitlab</code> directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}',
);
export const ERROR_PERMISSION = __(
'You do not have permission to run the Web Terminal. Please contact a project administrator.',
);
export const configCheckError = (status, helpUrl) => {
if (status === httpStatus.UNPROCESSABLE_ENTITY) {
return sprintf(
ERROR_CONFIG,
{
helpStart: `<a href="${_.escape(helpUrl)}" target="_blank">`,
helpEnd: '</a>',
},
false,
);
} else if (status === httpStatus.FORBIDDEN) {
return ERROR_PERMISSION;
}
return UNEXPECTED_ERROR_CONFIG;
};
export const runnersCheckEmpty = helpUrl =>
sprintf(
EMPTY_RUNNERS,
{
helpStart: `<a href="${_.escape(helpUrl)}" target="_blank">`,
helpEnd: '</a>',
},
false,
);
export const SET_VISIBLE = 'SET_VISIBLE';
export const HIDE_SPLASH = 'HIDE_SPLASH';
export const SET_PATHS = 'SET_PATHS';
export const REQUEST_CHECK = 'REQUEST_CHECK';
export const RECEIVE_CHECK_SUCCESS = 'RECEIVE_CHECK_SUCCESS';
export const RECEIVE_CHECK_ERROR = 'RECEIVE_CHECK_ERROR';
import * as types from './mutation_types';
export default {
[types.SET_VISIBLE](state, isVisible) {
Object.assign(state, {
isVisible,
});
},
[types.HIDE_SPLASH](state) {
Object.assign(state, {
isShowSplash: false,
});
},
[types.SET_PATHS](state, paths) {
Object.assign(state, {
paths,
});
},
[types.REQUEST_CHECK](state, type) {
Object.assign(state.checks, {
[type]: {
isLoading: true,
},
});
},
[types.RECEIVE_CHECK_ERROR](state, { type, message }) {
Object.assign(state.checks, {
[type]: {
isLoading: false,
isValid: false,
message,
},
});
},
[types.RECEIVE_CHECK_SUCCESS](state, type) {
Object.assign(state.checks, {
[type]: {
isLoading: false,
isValid: true,
message: null,
},
});
},
};
import { CHECK_CONFIG, CHECK_RUNNERS } from '../../../constants';
export default () => ({
checks: {
[CHECK_CONFIG]: { isLoading: true },
[CHECK_RUNNERS]: { isLoading: true },
},
isVisible: false,
isShowSplash: true,
paths: {},
});
import { startIde } from '~/ide/index'; import { startIde } from '~/ide/index';
import EEIde from 'ee/ide/components/ide.vue'; import EEIde from 'ee/ide/components/ide.vue';
import extendStore from 'ee/ide/stores/extend';
function extraInitialData() {
// This is empty now, but it will be used in: https://gitlab.com/gitlab-org/gitlab-ee/issues/5426
return {};
}
startIde({ startIde({
extraInitialData, extendStore,
rootComponent: EEIde, rootComponent: EEIde,
}); });
...@@ -114,7 +114,7 @@ export default { ...@@ -114,7 +114,7 @@ export default {
RelatedIssuesService.remove(issueToRemove.relation_path) RelatedIssuesService.remove(issueToRemove.relation_path)
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
this.store.setRelatedIssues(data.issues); this.store.setRelatedIssues(data.issuables);
}) })
.catch(res => { .catch(res => {
if (res && res.status !== 404) { if (res && res.status !== 404) {
...@@ -142,7 +142,7 @@ export default { ...@@ -142,7 +142,7 @@ export default {
.then(data => { .then(data => {
// We could potentially lose some pending issues in the interim here // We could potentially lose some pending issues in the interim here
this.store.setPendingReferences([]); this.store.setPendingReferences([]);
this.store.setRelatedIssues(data.issues); this.store.setRelatedIssues(data.issuables);
this.isSubmitting = false; this.isSubmitting = false;
// Close the form on submission // Close the form on submission
......
...@@ -16,7 +16,7 @@ class RelatedIssuesService { ...@@ -16,7 +16,7 @@ class RelatedIssuesService {
return this.relatedIssuesResource.save( return this.relatedIssuesResource.save(
{}, {},
{ {
issue_references: newIssueReferences, issuable_references: newIssueReferences,
}, },
); );
} }
......
...@@ -4,9 +4,21 @@ $details-cell-width: 320px; ...@@ -4,9 +4,21 @@ $details-cell-width: 320px;
$border-style: 1px solid $border-gray-normal; $border-style: 1px solid $border-gray-normal;
$roadmap-gradient-dark-gray: rgba(0, 0, 0, 0.15); $roadmap-gradient-dark-gray: rgba(0, 0, 0, 0.15);
$roadmap-gradient-gray: rgba(255, 255, 255, 0.001); $roadmap-gradient-gray: rgba(255, 255, 255, 0.001);
$scroll-top-gradient: linear-gradient(to bottom, $roadmap-gradient-dark-gray 0%, $roadmap-gradient-gray 100%); $scroll-top-gradient: linear-gradient(
$scroll-bottom-gradient: linear-gradient(to bottom, $roadmap-gradient-gray 0%, $roadmap-gradient-dark-gray 100%); to bottom,
$column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%, $roadmap-gradient-gray 100%); $roadmap-gradient-dark-gray 0%,
$roadmap-gradient-gray 100%
);
$scroll-bottom-gradient: linear-gradient(
to bottom,
$roadmap-gradient-gray 0%,
$roadmap-gradient-dark-gray 100%
);
$column-right-gradient: linear-gradient(
to right,
$roadmap-gradient-dark-gray 0%,
$roadmap-gradient-gray 100%
);
@mixin roadmap-scroll-mixin { @mixin roadmap-scroll-mixin {
height: $grid-size; height: $grid-size;
...@@ -14,6 +26,39 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -14,6 +26,39 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
pointer-events: none; pointer-events: none;
} }
.epics-details-filters {
.btn-group {
.dropdown-toggle {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.btn-sort-direction {
border-left: 0;
&:hover {
border-color: $gray-darkest;
}
}
@include media-breakpoint-down(xs) {
display: flex;
.dropdown-menu-sort {
// This is a hack to fix dropdown alignment in small screens
// where Bootstrap applies inline `transform: translate3d(...)`
// and since our dropdown button has sort direction button
// present, alignment needs to compensate for that space
// without which it appears shifted towards left.
//
// One more approach is to override `transform` using `!important`
// but that too involves using magic number
margin-left: 27px;
}
}
}
}
.epics-roadmap-filters { .epics-roadmap-filters {
.epics-details-filters { .epics-details-filters {
.btn-roadmap-preset { .btn-roadmap-preset {
...@@ -52,7 +97,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -52,7 +97,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
.roadmap-timeline-section .timeline-header-blank::after, .roadmap-timeline-section .timeline-header-blank::after,
.epics-list-section .epic-details-cell::after { .epics-list-section .epic-details-cell::after {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
right: -$grid-size; right: -$grid-size;
...@@ -136,7 +181,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -136,7 +181,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
} }
.today-bar::before { .today-bar::before {
content: ''; content: "";
position: absolute; position: absolute;
top: -2px; top: -2px;
left: -3px; left: -3px;
...@@ -150,7 +195,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -150,7 +195,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
&.scroll-top-shadow .timeline-header-blank::before { &.scroll-top-shadow .timeline-header-blank::before {
@include roadmap-scroll-mixin; @include roadmap-scroll-mixin;
content: ''; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
bottom: -$grid-size; bottom: -$grid-size;
...@@ -256,7 +301,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -256,7 +301,7 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
&.start-date-outside::before, &.start-date-outside::before,
&.end-date-outside::after { &.end-date-outside::after {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
height: 100%; height: 100%;
...@@ -269,11 +314,21 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0% ...@@ -269,11 +314,21 @@ $column-right-gradient: linear-gradient(to right, $roadmap-gradient-dark-gray 0%
} }
&.start-date-undefined { &.start-date-undefined {
background: linear-gradient(to right, $roadmap-gradient-gray 0%, $blue-200 50%, $blue-500 100%); background: linear-gradient(
to right,
$roadmap-gradient-gray 0%,
$blue-200 50%,
$blue-500 100%
);
} }
&.end-date-undefined { &.end-date-undefined {
background: linear-gradient(to right, $blue-500 0%, $blue-200 50%, $roadmap-gradient-gray 100%); background: linear-gradient(
to right,
$blue-500 0%,
$blue-200 50%,
$roadmap-gradient-gray 100%
);
} }
&.start-date-outside { &.start-date-outside {
......
# frozen_string_literal: true
module EpicRelations
extend ActiveSupport::Concern
include IssuableLinks
included do
skip_before_action :authorize_destroy_issuable!
skip_before_action :authorize_create_epic!
skip_before_action :authorize_update_issuable!
before_action :authorize_admin_epic!, only: [:create, :destroy, :update]
end
def authorize_admin_epic!
render_403 unless can?(current_user, :admin_epic, epic)
end
end
...@@ -11,11 +11,13 @@ module EpicsActions ...@@ -11,11 +11,13 @@ module EpicsActions
end end
def default_sort_order def default_sort_order
sort_value_end_date sort_value_recently_created
end end
def update_cookie_value(value) def update_cookie_value(value)
case value case value
when 'created_asc' then sort_value_oldest_created
when 'created_desc' then sort_value_recently_created
when 'start_date_asc' then sort_value_start_date when 'start_date_asc' then sort_value_start_date
when 'end_date_asc' then sort_value_end_date when 'end_date_asc' then sort_value_end_date
else else
......
...@@ -2,25 +2,33 @@ ...@@ -2,25 +2,33 @@
module IssuableLinks module IssuableLinks
def index def index
render json: issues render json: issuables
end end
def create def create
result = create_service.execute result = create_service.execute
render json: { message: result[:message], issues: issues }, status: result[:http_status] render json: { message: result[:message], issuables: issuables }, status: result[:http_status]
end end
def destroy def destroy
result = destroy_service.execute result = destroy_service.execute
render json: { issues: issues }, status: result[:http_status] render json: { issuables: issuables }, status: result[:http_status]
end end
private private
def issuables
list_service.execute
end
def list_service
raise NotImplementedError
end
def create_params def create_params
params.slice(:issue_references) params.slice(:issuable_references)
end end
def create_service def create_service
......
# frozen_string_literal: true # frozen_string_literal: true
class Groups::EpicIssuesController < Groups::EpicsController class Groups::EpicIssuesController < Groups::EpicsController
include IssuableLinks include EpicRelations
skip_before_action :authorize_destroy_issuable!
skip_before_action :authorize_create_epic!
skip_before_action :authorize_update_issuable!
before_action :authorize_admin_epic!, only: [:create, :destroy, :update]
before_action :authorize_issue_link_association!, only: [:destroy, :update] before_action :authorize_issue_link_association!, only: [:destroy, :update]
def update def update
...@@ -26,12 +21,8 @@ class Groups::EpicIssuesController < Groups::EpicsController ...@@ -26,12 +21,8 @@ class Groups::EpicIssuesController < Groups::EpicsController
EpicIssues::DestroyService.new(link, current_user) EpicIssues::DestroyService.new(link, current_user)
end end
def issues def list_service
EpicIssues::ListService.new(epic, current_user).execute EpicIssues::ListService.new(epic, current_user)
end
def authorize_admin_epic!
render_403 unless can?(current_user, :admin_epic, epic)
end end
def authorize_issue_link_association! def authorize_issue_link_association!
......
...@@ -95,6 +95,10 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -95,6 +95,10 @@ class Groups::EpicsController < Groups::ApplicationController
EpicsFinder EpicsFinder
end end
def issuable_sorting_field
:epics_sort
end
def preload_for_collection def preload_for_collection
@preload_for_collection ||= [:group, :author] @preload_for_collection ||= [:group, :author]
end end
......
...@@ -9,14 +9,20 @@ module Groups ...@@ -9,14 +9,20 @@ module Groups
before_action :group before_action :group
before_action :persist_roadmap_layout, only: [:show] before_action :persist_roadmap_layout, only: [:show]
# show roadmap for a group
def show def show
# show roadmap for a group # Used to persist the order and show the correct sorting dropdown on UI.
@sort = set_sort_order_from_cookie || default_sort_order @sort = set_sort_order
@epics_count = EpicsFinder.new(current_user, group_id: @group.id).execute.count @epics_count = EpicsFinder.new(current_user, group_id: @group.id).execute.count
end end
private private
def issuable_sorting_field
:epics_sort
end
def persist_roadmap_layout def persist_roadmap_layout
return unless current_user return unless current_user
......
...@@ -9,10 +9,6 @@ module Projects ...@@ -9,10 +9,6 @@ module Projects
private private
def issues
IssueLinks::ListService.new(issue, current_user).execute
end
def authorize_admin_issue_link! def authorize_admin_issue_link!
render_403 unless can?(current_user, :admin_issue_link, @project) render_403 unless can?(current_user, :admin_issue_link, @project)
end end
...@@ -29,6 +25,10 @@ module Projects ...@@ -29,6 +25,10 @@ module Projects
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def list_service
IssueLinks::ListService.new(issue, current_user)
end
def create_service def create_service
IssueLinks::CreateService.new(issue, current_user, create_params) IssueLinks::CreateService.new(issue, current_user, create_params)
end end
......
# frozen_string_literal: true
class Projects::WebIdeTerminalsController < Projects::ApplicationController
before_action :authenticate_user!
before_action :build, except: [:check_config, :create]
before_action :authorize_create_web_ide_terminal!
before_action :authorize_read_web_ide_terminal!, except: [:check_config, :create]
before_action :authorize_update_web_ide_terminal!, only: [:cancel, :retry]
def check_config
return respond_422 unless branch_sha
result = ::Ci::WebIdeConfigService.new(project, current_user, sha: branch_sha).execute
if result[:status] == :success
head :ok
else
respond_422
end
end
def show
render_terminal(build)
end
def create
result = ::Ci::CreateWebIdeTerminalService.new(project,
current_user,
ref: params[:branch])
.execute
if result[:status] == :error
render status: :bad_request, json: result[:message]
else
pipeline = result[:pipeline]
current_build = pipeline.builds.last
if current_build
render_terminal(current_build)
else
render status: :bad_request, json: pipeline.errors.full_messages
end
end
end
def cancel
return respond_422 unless build.cancelable?
build.cancel
head :ok
end
def retry
return respond_422 unless build.retryable?
new_build = Ci::Build.retry(build, current_user)
render_terminal(new_build)
end
private
def authorize_create_web_ide_terminal!
return access_denied! unless can?(current_user, :create_web_ide_terminal, project)
end
def authorize_read_web_ide_terminal!
authorize_build_ability!(:read_web_ide_terminal)
end
def authorize_update_web_ide_terminal!
authorize_build_ability!(:update_web_ide_terminal)
end
def authorize_build_ability!(ability)
return access_denied! unless can?(current_user, ability, build)
end
def build
@build ||= project.builds.find(params[:id])
end
def branch_sha
return unless params[:branch].present?
project.commit(params[:branch])&.id
end
def render_terminal(current_build)
render json: WebIdeTerminalSerializer
.new(project: project, current_user: current_user)
.represent(current_build)
end
end
# frozen_string_literal: true
module EE
module IdeHelper
extend ::Gitlab::Utils::Override
override :ide_data
def ide_data
super.merge({
"ee-web-terminal-svg-path" => image_path('illustrations/web-ide_promotion.svg'),
"ee-ci-yaml-help-path" => help_page_path('ci/yaml/README.md'),
"ee-ci-runners-help-path" => help_page_path('ci/runners/README.md'),
"ee-web-terminal-help-path" => help_page_path('user/project/web_ide/index.md', anchor: 'client-side-evaluation')
})
end
end
end
::IdeHelper.prepend(::EE::IdeHelper)
...@@ -14,6 +14,44 @@ module EE ...@@ -14,6 +14,44 @@ module EE
}.merge(super) }.merge(super)
end end
def epics_sort_options_hash
{
sort_value_created_date => sort_title_created_date,
sort_value_oldest_created => sort_title_created_date,
sort_value_recently_created => sort_title_created_date,
sort_value_oldest_updated => sort_title_recently_updated,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_start_date_later => sort_title_start_date,
sort_value_start_date_soon => sort_title_start_date,
sort_value_end_date_later => sort_title_end_date,
sort_value_end_date => sort_title_end_date
}
end
# This method is used to find the opposite ordering parameter for the sort button in the UI.
# Hash key is the descending sorting order and the sort value is the opposite of it for the same field.
# For example: created_at_asc => created_at_desc
def epics_ordering_options_hash
{
sort_value_oldest_created => sort_value_recently_created,
sort_value_oldest_updated => sort_value_recently_updated,
sort_value_start_date_soon => sort_value_start_date_later,
sort_value_end_date => sort_value_end_date_later
}
end
# Creates a button with the opposite ordering for the current field in UI.
def sort_order_button(sort)
opposite_sorting_param = epics_ordering_options_hash[sort] || epics_ordering_options_hash.key(sort)
sort_icon = sort.end_with?('desc') ? 'sort-highest' : 'sort-lowest'
link_to sprite_icon(sort_icon, size: 16),
page_filter_path(sort: opposite_sorting_param, label: true),
class: "btn btn-default has-tooltip qa-reverse-sort btn-sort-direction",
title: _("Sort direction")
end
def sort_title_start_date def sort_title_start_date
s_('SortOptions|Start date') s_('SortOptions|Start date')
end end
...@@ -42,6 +80,10 @@ module EE ...@@ -42,6 +80,10 @@ module EE
'end_date_asc' 'end_date_asc'
end end
def sort_value_end_date_later
'end_date_desc'
end
def sort_value_less_weight def sort_value_less_weight
'weight_asc' 'weight_asc'
end end
......
...@@ -56,6 +56,14 @@ module EE ...@@ -56,6 +56,14 @@ module EE
reorder(::Gitlab::Database.nulls_last_order('end_date'), 'id DESC') reorder(::Gitlab::Database.nulls_last_order('end_date'), 'id DESC')
end end
scope :order_end_date_desc, -> do
reorder(::Gitlab::Database.nulls_last_order('end_date', 'DESC'), 'id DESC')
end
scope :order_start_date_desc, -> do
reorder(::Gitlab::Database.nulls_last_order('start_date', 'DESC'), 'id DESC')
end
def etag_caching_enabled? def etag_caching_enabled?
true true
end end
...@@ -104,7 +112,9 @@ module EE ...@@ -104,7 +112,9 @@ module EE
case method.to_s case method.to_s
when 'start_or_end_date' then order_start_or_end_date_asc when 'start_or_end_date' then order_start_or_end_date_asc
when 'start_date_asc' then order_start_date_asc when 'start_date_asc' then order_start_date_asc
when 'start_date_desc' then order_start_date_desc
when 'end_date_asc' then order_end_date_asc when 'end_date_asc' then order_end_date_asc
when 'end_date_desc' then order_end_date_desc
else else
super super
end end
......
...@@ -92,7 +92,7 @@ class License < ActiveRecord::Base ...@@ -92,7 +92,7 @@ class License < ActiveRecord::Base
prometheus_alerts prometheus_alerts
operations_dashboard operations_dashboard
tracing tracing
webide_terminal web_ide_terminal
].freeze ].freeze
# List all features available for early adopters, # List all features available for early adopters,
......
# frozen_string_literal: true
class WebIdeTerminal
include ::Gitlab::Routing
attr_reader :build, :project
delegate :id, :status, to: :build
def initialize(build)
@build = build
@project = build.project
end
def show_path
web_ide_terminal_route_generator(:show)
end
def retry_path
web_ide_terminal_route_generator(:retry)
end
def cancel_path
web_ide_terminal_route_generator(:cancel)
end
def terminal_path
terminal_project_job_path(project, build, format: :ws)
end
private
def web_ide_terminal_route_generator(action)
url_for(action: action,
controller: 'projects/web_ide_terminals',
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: build.id,
only_path: true)
end
end
...@@ -11,6 +11,23 @@ module EE ...@@ -11,6 +11,23 @@ module EE
prevent :update_build prevent :update_build
end end
condition(:is_web_ide_terminal, scope: :subject) do
@subject.pipeline.webide?
end
rule { is_web_ide_terminal & can?(:create_web_ide_terminal) & (admin | owner_of_job) }.policy do
enable :read_web_ide_terminal
enable :update_web_ide_terminal
end
rule { is_web_ide_terminal & ~can?(:update_web_ide_terminal) }.policy do
prevent :create_build_terminal
end
rule { can?(:update_web_ide_terminal) & terminal }.policy do
enable :create_build_terminal
end
private private
alias_method :current_user, :user alias_method :current_user, :user
......
...@@ -203,7 +203,7 @@ module EE ...@@ -203,7 +203,7 @@ module EE
end end
condition(:web_ide_terminal_available) do condition(:web_ide_terminal_available) do
@subject.feature_available?(:webide_terminal) @subject.feature_available?(:web_ide_terminal)
end end
rule { web_ide_terminal_available & can?(:create_pipeline) & can?(:maintainer_access) }.enable :create_web_ide_terminal rule { web_ide_terminal_available & can?(:create_pipeline) & can?(:maintainer_access) }.enable :create_web_ide_terminal
......
...@@ -66,7 +66,7 @@ module EE ...@@ -66,7 +66,7 @@ module EE
end end
expose :managed_licenses_path do |merge_request| expose :managed_licenses_path do |merge_request|
api_v4_projects_managed_licenses_path(id: merge_request.source_project.id) api_v4_projects_managed_licenses_path(id: merge_request.target_project.id)
end end
expose :can_manage_licenses do |merge_request| expose :can_manage_licenses do |merge_request|
......
# frozen_string_literal: true
class WebIdeTerminalEntity < Grape::Entity
expose :id
expose :status
expose :show_path
expose :cancel_path
expose :retry_path
expose :terminal_path
end
# frozen_string_literal: true
class WebIdeTerminalSerializer < BaseSerializer
entity WebIdeTerminalEntity
def represent(resource, opts = {})
resource = WebIdeTerminal.new(resource) if resource.is_a?(Ci::Build)
super
end
end
# frozen_string_literal: true # frozen_string_literal: true
module Ci module Ci
class CreateWebideTerminalService < ::BaseService class CreateWebIdeTerminalService < ::BaseService
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
TerminalCreationError = Class.new(StandardError) TerminalCreationError = Class.new(StandardError)
...@@ -62,7 +62,7 @@ module Ci ...@@ -62,7 +62,7 @@ module Ci
end end
def load_terminal_config! def load_terminal_config!
result = ::Ci::WebideConfigService.new(project, current_user, sha: sha).execute result = ::Ci::WebIdeConfigService.new(project, current_user, sha: sha).execute
raise TerminalCreationError, result[:message] if result[:status] != :success raise TerminalCreationError, result[:message] if result[:status] != :success
@terminal = result[:terminal] @terminal = result[:terminal]
......
# frozen_string_literal: true # frozen_string_literal: true
module Ci module Ci
class WebideConfigService < ::BaseService class WebIdeConfigService < ::BaseService
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
ValidationError = Class.new(StandardError) ValidationError = Class.new(StandardError)
...@@ -37,12 +37,12 @@ module Ci ...@@ -37,12 +37,12 @@ module Ci
end end
def load_config! def load_config!
@config = Gitlab::Webide::Config.new(config_content) @config = Gitlab::WebIde::Config.new(config_content)
unless @config.valid? unless @config.valid?
raise ValidationError, @config.errors.first raise ValidationError, @config.errors.first
end end
rescue Gitlab::Webide::Config::ConfigError => e rescue Gitlab::WebIde::Config::ConfigError => e
raise ValidationError, e.message raise ValidationError, e.message
end end
......
...@@ -25,7 +25,7 @@ module EE ...@@ -25,7 +25,7 @@ module EE
epic_param = params.delete(:epic) epic_param = params.delete(:epic)
if epic_param if epic_param
EpicIssues::CreateService.new(epic_param, current_user, { target_issue: issue }).execute EpicIssues::CreateService.new(epic_param, current_user, { target_issuable: issue }).execute
else else
link = EpicIssue.find_by(issue_id: issue.id) # rubocop: disable CodeReuse/ActiveRecord link = EpicIssue.find_by(issue_id: issue.id) # rubocop: disable CodeReuse/ActiveRecord
......
...@@ -3,7 +3,7 @@ module EpicIssues ...@@ -3,7 +3,7 @@ module EpicIssues
private private
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def relate_issues(referenced_issue) def relate_issuables(referenced_issue)
link = EpicIssue.find_or_initialize_by(issue: referenced_issue) link = EpicIssue.find_or_initialize_by(issue: referenced_issue)
affected_epics = [issuable] affected_epics = [issuable]
...@@ -41,17 +41,17 @@ module EpicIssues ...@@ -41,17 +41,17 @@ module EpicIssues
{ group: issuable.group } { group: issuable.group }
end end
def linkable_issues(issues) def linkable_issuables(issues)
@linkable_issues ||= begin @linkable_issues ||= begin
return [] unless can?(current_user, :admin_epic, issuable.group) return [] unless can?(current_user, :admin_epic, issuable.group)
issues.select do |issue| issues.select do |issue|
issuable_group_descendants.include?(issue.project.group) && !previous_related_issues.include?(issue) issuable_group_descendants.include?(issue.project.group) && !previous_related_issuables.include?(issue)
end end
end end
end end
def previous_related_issues def previous_related_issuables
@related_issues ||= issuable.issues.to_a @related_issues ||= issuable.issues.to_a
end end
......
...@@ -2,7 +2,7 @@ module EpicIssues ...@@ -2,7 +2,7 @@ module EpicIssues
class ListService < IssuableLinks::ListService class ListService < IssuableLinks::ListService
private private
def issues def child_issuables
return [] unless issuable&.group&.feature_available?(:epics) return [] unless issuable&.group&.feature_available?(:epics)
issuable.issues_readable_by(current_user) issuable.issues_readable_by(current_user)
......
...@@ -11,58 +11,62 @@ module IssuableLinks ...@@ -11,58 +11,62 @@ module IssuableLinks
# otherwise create issue links for the issues which # otherwise create issue links for the issues which
# are still not assigned and return success message. # are still not assigned and return success message.
if render_conflict_error? if render_conflict_error?
return error('Issue(s) already assigned', 409) return error(issuables_assigned_message, 409)
end end
if render_not_found_error? if render_not_found_error?
return error('No Issue found for given params', 404) return error(issuables_not_found_message, 404)
end end
create_issue_links create_links
success success
end end
private private
def render_conflict_error? def render_conflict_error?
referenced_issues.present? && (referenced_issues - previous_related_issues).empty? referenced_issuables.present? && (referenced_issuables - previous_related_issuables).empty?
end end
def render_not_found_error? def render_not_found_error?
linkable_issues(referenced_issues).empty? linkable_issuables(referenced_issuables).empty?
end end
def create_issue_links def create_links
issues = linkable_issues(referenced_issues) objects = linkable_issuables(referenced_issuables)
issues.each do |referenced_issue| objects.each do |referenced_object|
relate_issues(referenced_issue) do |params| relate_issuables(referenced_object) do |params|
create_notes(referenced_issue, params) create_notes(referenced_object, params)
end end
end end
end end
def referenced_issues def referenced_issuables
@referenced_issues ||= begin @referenced_issuables ||= begin
target_issue = params[:target_issue] target_issuable = params[:target_issuable]
if params[:issue_references].present? if params[:issuable_references].present?
extract_issues_from_references extract_references
elsif target_issue elsif target_issuable
[target_issue] [target_issuable]
else else
[] []
end end
end end
end end
def extract_issues_from_references def extract_references
issue_references = params[:issue_references] issuable_references = params[:issuable_references]
text = issue_references.join(' ') text = issuable_references.join(' ')
extractor = Gitlab::ReferenceExtractor.new(issuable.project, @current_user) extractor = Gitlab::ReferenceExtractor.new(issuable.project, current_user)
extractor.analyze(text, extractor_context) extractor.analyze(text, extractor_context)
references(extractor)
end
def references(extractor)
extractor.issues extractor.issues
end end
...@@ -70,16 +74,24 @@ module IssuableLinks ...@@ -70,16 +74,24 @@ module IssuableLinks
{} {}
end end
def linkable_issues(issues) def linkable_issuables(objects)
raise NotImplementedError raise NotImplementedError
end end
def previous_related_issues def previous_related_issuables
raise NotImplementedError raise NotImplementedError
end end
def relate_issues(referenced_issue) def relate_issuables(referenced_object)
raise NotImplementedError raise NotImplementedError
end end
def issuables_assigned_message
'Issue(s) already assigned'
end
def issuables_not_found_message
'No Issue found for given params'
end
end end
end end
...@@ -9,29 +9,33 @@ module IssuableLinks ...@@ -9,29 +9,33 @@ module IssuableLinks
end end
def execute def execute
issues.map do |referenced_issue| child_issuables.map do |referenced_object|
to_hash(referenced_issue) to_hash(referenced_object)
end end
end end
private private
def relation_path(issue) def relation_path(object)
raise NotImplementedError raise NotImplementedError
end end
def reference(issue) def reference(object)
issue.to_reference(issuable.project) object.to_reference(issuable.project)
end end
def to_hash(issue) def issuable_path(object)
project_issue_path(object.project, object.iid)
end
def to_hash(object)
{ {
id: issue.id, id: object.id,
title: issue.title, title: object.title,
state: issue.state, state: object.state,
reference: reference(issue), reference: reference(object),
path: project_issue_path(issue.project, issue.iid), path: issuable_path(object),
relation_path: relation_path(issue) relation_path: relation_path(object)
} }
end end
end end
......
module IssueLinks module IssueLinks
class CreateService < IssuableLinks::CreateService class CreateService < IssuableLinks::CreateService
def relate_issues(referenced_issue) def relate_issuables(referenced_issue)
link = IssueLink.create(source: issuable, target: referenced_issue) link = IssueLink.create(source: issuable, target: referenced_issue)
yield if link.persisted? yield if link.persisted?
end end
def linkable_issues(issues) def linkable_issuables(issues)
@linkable_issues ||= begin @linkable_issuables ||= begin
issues.select { |issue| can?(current_user, :admin_issue_link, issue) } issues.select { |issue| can?(current_user, :admin_issue_link, issue) }
end end
end end
...@@ -17,7 +17,7 @@ module IssueLinks ...@@ -17,7 +17,7 @@ module IssueLinks
SystemNoteService.relate_issue(referenced_issue, issuable, current_user) SystemNoteService.relate_issue(referenced_issue, issuable, current_user)
end end
def previous_related_issues def previous_related_issuables
@related_issues ||= issuable.related_issues(current_user).to_a @related_issues ||= issuable.related_issues(current_user).to_a
end end
end end
......
...@@ -4,7 +4,7 @@ module IssueLinks ...@@ -4,7 +4,7 @@ module IssueLinks
private private
def issues def child_issuables
issuable.related_issues(current_user, preload: { project: :namespace }) issuable.related_issues(current_user, preload: { project: :namespace })
end end
......
= render partial: "ide/show"
- sorted_by = sort_options_hash[@sort] - sorted_by = epics_sort_options_hash[@sort]
.dropdown.inline.prepend-left-10 .dropdown.inline.prepend-left-10
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } } .btn-group
= sorted_by %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
= icon('chevron-down') = sorted_by
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort = icon('chevron-down')
%li %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
= sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by) %li
= sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sorted_by) = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_recently_created, label: true), sorted_by)
= sortable_item(sort_title_start_date, page_filter_path(sort: sort_value_start_date, label: true), sorted_by) = sortable_item(sort_title_recently_updated, page_filter_path(sort: sort_value_recently_updated, label: true), sorted_by)
= sortable_item(sort_title_end_date, page_filter_path(sort: sort_value_end_date, label: true), sorted_by) = sortable_item(sort_title_start_date, page_filter_path(sort: sort_value_start_date_soon, label: true), sorted_by)
= sortable_item(sort_title_end_date, page_filter_path(sort: sort_value_end_date, label: true), sorted_by)
= sort_order_button(@sort)
---
title: Fix issue board api with special milestones
merge_request: 8653
author:
type: fixed
---
title: Add sort direction button with sort dropdown for Epics and Roadmap
merge_request:
author:
type: changed
---
title: Fixed license managment path in MR widget for fork cases
merge_request: 8700
author:
type: fixed
---
title: Added web terminals to Web IDE
merge_request: 7386
author:
type: added
...@@ -20,6 +20,17 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -20,6 +20,17 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'epics' get 'epics'
end end
end end
resources :web_ide_terminals, path: :ide_terminals, only: [:create, :show], constraints: { id: /\d+/, format: :json } do
member do
post :cancel
post :retry
end
collection do
post :check_config
end
end
end end
end end
end end
class AddEpicsSortToUserPreference < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :user_preferences, :epics_sort, :string
end
end
...@@ -89,7 +89,7 @@ module API ...@@ -89,7 +89,7 @@ module API
issue = Issue.find(params[:issue_id]) issue = Issue.find(params[:issue_id])
create_params = { target_issue: issue } create_params = { target_issuable: issue }
result = ::EpicIssues::CreateService.new(epic, current_user, create_params).execute result = ::EpicIssues::CreateService.new(epic, current_user, create_params).execute
......
...@@ -35,7 +35,7 @@ module API ...@@ -35,7 +35,7 @@ module API
target_issue = find_project_issue(declared_params[:target_issue_iid], target_issue = find_project_issue(declared_params[:target_issue_iid],
declared_params[:target_project_id]) declared_params[:target_project_id])
create_params = { target_issue: target_issue } create_params = { target_issuable: target_issue }
result = ::IssueLinks::CreateService result = ::IssueLinks::CreateService
.new(source_issue, current_user, create_params) .new(source_issue, current_user, create_params)
......
...@@ -93,17 +93,22 @@ module EE ...@@ -93,17 +93,22 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
def scoped_issue_available?(board)
board.parent.feature_available?(:scoped_issue_board)
end
# Default filtering configuration # Default filtering configuration
expose :name expose :name
expose :group expose :group
expose :milestone, using: ::API::Entities::Milestone, if: ->(board, _) { scoped_issue_available?(board) }
expose :assignee, using: ::API::Entities::UserBasic, if: ->(board, _) { scoped_issue_available?(board) } with_options if: ->(board, _) { board.parent.feature_available?(:scoped_issue_board) } do
expose :labels, using: ::API::Entities::LabelBasic, if: ->(board, _) { scoped_issue_available?(board) } expose :milestone do |board|
expose :weight, if: ->(board, _) { scoped_issue_available?(board) } if board.milestone.is_a?(Milestone)
::API::Entities::Milestone.represent(board.milestone)
else
SpecialBoardFilter.represent(board.milestone)
end
end
expose :assignee, using: ::API::Entities::UserBasic
expose :labels, using: ::API::Entities::LabelBasic
expose :weight
end
end end
end end
...@@ -213,6 +218,10 @@ module EE ...@@ -213,6 +218,10 @@ module EE
expose :target, as: :target_issue, using: ::API::Entities::IssueBasic expose :target, as: :target_issue, using: ::API::Entities::IssueBasic
end end
class SpecialBoardFilter < Grape::Entity
expose :title
end
class Approvals < Grape::Entity class Approvals < Grape::Entity
expose :user, using: ::API::Entities::UserBasic expose :user, using: ::API::Entities::UserBasic
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment