Commit c55bbe06 authored by Stan Hu's avatar Stan Hu

Merge branch 'master' into ce-to-ee-2018-02-26

parents ae1b6ea6 90488d20
<script>
import Sortable from 'vendor/Sortable';
import boardNewIssue from './board_new_issue';
import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
......
/* global ListIssue */
<script>
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import ProjectSelect from 'ee/boards/components/project_select.vue'; // eslint-disable-line import/first
import ListIssue from '../models/issue';
const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardNewIssue',
components: {
ProjectSelect,
},
props: {
groupId: {
type: Number,
......@@ -24,9 +28,6 @@ export default {
selectedProject: {},
};
},
components: {
'project-select': ProjectSelect,
},
computed: {
disabled() {
if (this.groupId) {
......@@ -35,6 +36,10 @@ export default {
return this.title === '';
},
},
mounted() {
this.$refs.input.focus();
eventHub.$on('setSelectedProject', this.setSelectedProject);
},
methods: {
submit(e) {
e.preventDefault();
......@@ -81,49 +86,57 @@ export default {
this.selectedProject = selectedProject;
},
},
mounted() {
this.$refs.input.focus();
eventHub.$on('setSelectedProject', this.setSelectedProject);
},
template: `
};
</script>
<template>
<div class="board-new-issue-form">
<div class="card">
<form @submit="submit($event)">
<div class="flash-container"
v-if="error">
<div
class="flash-container"
v-if="error"
>
<div class="flash-alert">
An error occurred. Please try again.
</div>
</div>
<label class="label-light"
:for="list.id + '-title'">
<label
class="label-light"
:for="list.id + '-title'"
>
Title
</label>
<input class="form-control"
<input
class="form-control"
type="text"
v-model="title"
ref="input"
autocomplete="off"
:id="list.id + '-title'" />
:id="list.id + '-title'"
/>
<project-select
v-if="groupId"
:groupId="groupId"
:group-id="groupId"
/>
<div class="clearfix prepend-top-10">
<button class="btn btn-success pull-left"
<button
class="btn btn-success pull-left"
type="submit"
:disabled="disabled"
ref="submit-button">
ref="submit-button"
>
Submit issue
</button>
<button class="btn btn-default pull-right"
<button
class="btn btn-default pull-right"
type="button"
@click="cancel">
@click="cancel"
>
Cancel
</button>
</div>
</form>
</div>
</div>
`,
};
</template>
......@@ -2,16 +2,17 @@
import _ from 'underscore';
import Vue from 'vue';
import Flash from '../flash';
import { __ } from '../locale';
import Flash from '~/flash';
import { __ } from '~/locale';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
import sidebarEventHub from '../sidebar/event_hub';
import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first
import './models/issue';
import './models/label';
import './models/list';
import './models/milestone';
import './models/project';
import './models/assignee';
import './stores/boards_store';
import './stores/modal_store';
......@@ -23,16 +24,15 @@ import './components/board';
import './components/board_sidebar';
import './components/new_list_dropdown';
import './components/modal/index';
import '../vue_shared/vue_resource_interceptor';
import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first
import 'ee/boards/models/project'; // eslint-disable-line import/first
import 'ee/boards/components/boards_selector'; // eslint-disable-line import/first
import collapseIcon from 'ee/boards/icons/fullscreen_collapse.svg'; // eslint-disable-line import/first
import expandIcon from 'ee/boards/icons/fullscreen_expand.svg'; // eslint-disable-line import/first
import tooltip from '~/vue_shared/directives/tooltip'; // eslint-disable-line import/first
<<<<<<< HEAD:app/assets/javascripts/boards/index.js
import './components/boards_selector';
import collapseIcon from './icons/fullscreen_collapse.svg';
import expandIcon from './icons/fullscreen_expand.svg';
import tooltip from '../vue_shared/directives/tooltip';
=======
>>>>>>> upstream/master:app/assets/javascripts/boards/index.js
export default () => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
......
/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
/* global DocumentTouch */
import sortableConfig from '../../sortable/sortable_config';
import sortableConfig from 'ee/sortable/sortable_config';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
......
......@@ -4,7 +4,7 @@
/* global ListAssignee */
import Vue from 'vue';
import IssueProject from './project';
import IssueProject from 'ee/boards/models/project';
class ListIssue {
constructor (obj, defaultAvatar) {
......@@ -122,3 +122,5 @@ class ListIssue {
}
window.ListIssue = ListIssue;
export default ListIssue;
......@@ -3,7 +3,7 @@
import _ from 'underscore';
import Cookies from 'js-cookie';
import boardsStoreEE from 'ee/boards/stores/boards_store_ee';
import { getUrlParamsArray } from '../../lib/utils/common_utils';
import { getUrlParamsArray } from '~/lib/utils/common_utils';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
......
......@@ -6,43 +6,21 @@ import GlFieldErrors from './gl_field_errors';
import Shortcuts from './shortcuts';
import SearchAutocomplete from './search_autocomplete';
var Dispatcher;
(function() {
Dispatcher = (function() {
function Dispatcher() {
this.initSearch();
this.initFieldErrors();
this.initPageScripts();
}
Dispatcher.prototype.initPageScripts = function() {
var path, shortcut_handler;
const page = $('body').attr('data-page');
if (!page) {
return false;
function initSearch() {
// Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
}
}
const fail = () => Flash('Error loading dynamic module');
const callDefault = m => m.default();
path = page.split(':');
shortcut_handler = null;
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
issues: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
});
function initFieldErrors() {
$('.gl-show-field-errors').each((i, form) => {
new GlFieldErrors(form);
});
}
const shortcutHandlerPages = [
function initPageShortcuts(page) {
const pagesWithCustomShortcuts = [
'projects:activity',
'projects:artifacts:browse',
'projects:artifacts:file',
......@@ -66,117 +44,42 @@ var Dispatcher;
'groups:show',
];
if (shortcutHandlerPages.indexOf(page) !== -1) {
shortcut_handler = true;
}
switch (path[0]) {
case 'admin':
switch (path[1]) {
case 'broadcast_messages':
import('./pages/admin/broadcast_messages')
.then(callDefault)
.catch(fail);
break;
case 'cohorts':
import('./pages/admin/cohorts')
.then(callDefault)
.catch(fail);
break;
case 'groups':
switch (path[2]) {
case 'show':
import('./pages/admin/groups/show')
.then(callDefault)
.catch(fail);
break;
}
break;
case 'projects':
import('./pages/admin/projects')
.then(callDefault)
.catch(fail);
break;
case 'labels':
switch (path[2]) {
case 'new':
import('./pages/admin/labels/new')
.then(callDefault)
.catch(fail);
break;
case 'edit':
import('./pages/admin/labels/edit')
.then(callDefault)
.catch(fail);
break;
}
case 'abuse_reports':
import('./pages/admin/abuse_reports')
.then(callDefault)
.catch(fail);
break;
}
break;
case 'profiles':
import('./pages/profiles/index')
.then(callDefault)
.catch(fail);
break;
case 'projects':
import('./pages/projects')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
switch (path[1]) {
case 'compare':
import('./pages/projects/compare')
.then(callDefault)
.catch(fail);
break;
case 'create':
case 'new':
import('./pages/projects/new')
.then(callDefault)
.catch(fail);
break;
case 'wikis':
import('./pages/projects/wikis')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
}
break;
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
if (pagesWithCustomShortcuts.indexOf(page) === -1) {
new Shortcuts();
}
}
function initGFMInput() {
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
emojis: true,
members: enableGFM,
issues: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
});
});
}
function initPerformanceBar() {
if (document.querySelector('#peek')) {
import('./performance_bar')
.then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
.catch(fail);
.catch(() => Flash('Error loading performance bar module'));
}
};
Dispatcher.prototype.initSearch = function() {
// Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
}
};
Dispatcher.prototype.initFieldErrors = function() {
$('.gl-show-field-errors').each((i, form) => {
new GlFieldErrors(form);
});
};
}
return Dispatcher;
})();
})();
export default () => {
initSearch();
initFieldErrors();
export default function initDispatcher() {
return new Dispatcher();
}
const page = $('body').attr('data-page');
if (page) {
initPageShortcuts(page);
initGFMInput();
initPerformanceBar();
}
};
......@@ -2,9 +2,10 @@
/**
* Render environments table.
*/
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import environmentItem from './environment_item.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import deployBoard from './deploy_board_component.vue';
import deployBoard from 'ee/environments/components/deploy_board_component.vue'; // eslint-disable-line import/first
export default {
components: {
......
<script>
import { mapState } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
import newDropdown from './new_dropdown/index.vue';
import fileIcon from '../../vue_shared/components/file_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
import fileStatusIcon from 'ee/ide/components/repo_file_status_icon.vue'; // eslint-disable-line import/first
import changedFileIcon from 'ee/ide/components/changed_file_icon.vue'; // eslint-disable-line import/first
export default {
components: {
......
<script>
import { mapActions } from 'vuex';
import fileStatusIcon from './repo_file_status_icon.vue';
import fileIcon from '../../vue_shared/components/file_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
import changedFileIcon from './changed_file_icon.vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
import icon from '~/vue_shared/components/icon.vue';
import fileStatusIcon from 'ee/ide/components/repo_file_status_icon.vue';
import changedFileIcon from 'ee/ide/components/changed_file_icon.vue';
export default {
components: {
......
......@@ -5,7 +5,7 @@ import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
import editorOptions from './editor_options';
import gitlabTheme from './themes/gl_theme';
import gitlabTheme from 'ee/ide/lib/themes/gl_theme'; // eslint-disable-line import/first
export default class Editor {
static create(monaco) {
......
import AbuseReports from './abuse_reports';
export default () => new AbuseReports();
document.addEventListener('DOMContentLoaded', () => new AbuseReports());
......@@ -3,7 +3,7 @@ import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
export default function initBroadcastMessagesForm() {
export default () => {
$('input#broadcast_message_color').on('input', function onMessageColorInput() {
const previewColor = $(this).val();
$('div.broadcast-message-preview').css('background-color', previewColor);
......@@ -32,4 +32,4 @@ export default function initBroadcastMessagesForm() {
.catch(() => flash(__('An error occurred while rendering preview broadcast message')));
}
}, 250));
}
};
import initBroadcastMessagesForm from './broadcast_message';
export default () => initBroadcastMessagesForm();
document.addEventListener('DOMContentLoaded', initBroadcastMessagesForm);
import initUsagePing from './usage_ping';
export default () => initUsagePing();
document.addEventListener('DOMContentLoaded', initUsagePing);
import UsersSelect from '../../../../users_select';
export default () => new UsersSelect();
document.addEventListener('DOMContentLoaded', () => new UsersSelect());
import Labels from '../../../../labels';
export default () => new Labels();
document.addEventListener('DOMContentLoaded', () => new Labels());
import Labels from '../../../../labels';
export default () => new Labels();
document.addEventListener('DOMContentLoaded', () => new Labels());
import ProjectsList from '../../../projects_list';
import NamespaceSelect from '../../../namespace_select';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
document.querySelectorAll('.js-namespace-select')
.forEach(dropdown => new NamespaceSelect({ dropdown }));
};
});
import NotificationsForm from '../../../notifications_form';
import notificationsDropdown from '../../../notifications_dropdown';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new
notificationsDropdown();
};
});
import initCompareAutocomplete from '~/compare_autocomplete';
export default () => {
initCompareAutocomplete();
};
document.addEventListener('DOMContentLoaded', initCompareAutocomplete);
import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
};
});
......@@ -2,8 +2,8 @@ import ProjectNew from '../shared/project_new';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new ProjectNew(); // eslint-disable-line no-new
initProjectVisibilitySelector();
initProjectNew.bindEvents();
};
});
......@@ -3,9 +3,9 @@ import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form';
export default () => {
document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
};
});
<script>
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import linkedPipelinesColumn from './linked_pipelines_column.vue';
import stageColumnComponent from './stage_column_component.vue';
import linkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue'; // eslint-disable-line import/first
export default {
components: {
linkedPipelinesColumn,
......
<script>
/* eslint-disable vue/require-default-prop */
import pipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
import linkedPipelinesMiniList from '../../vue_shared/components/linked_pipelines_mini_list.vue';
import pipelineStage from '~/pipelines/components/stage.vue';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import icon from '~/vue_shared/components/icon.vue';
import linkedPipelinesMiniList from 'ee/vue_shared/components/linked_pipelines_mini_list.vue';
export default {
name: 'MRWidgetPipeline',
......
......@@ -39,7 +39,7 @@ class LabelsFinder < UnionFinder
end
end
elsif only_group_labels?
label_ids << Label.where(group_id: group.id)
label_ids << Label.where(group_id: group_ids)
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
......@@ -59,10 +59,11 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
def group
strong_memoize(:group) do
def group_ids
strong_memoize(:group_ids) do
group = Group.find(params[:group_id])
authorized_to_read_labels?(group) && group
groups = params[:include_ancestor_groups].present? ? group.self_and_ancestors : [group]
groups_user_can_read_labels(groups).map(&:id)
end
end
......@@ -120,4 +121,10 @@ class LabelsFinder < UnionFinder
Ability.allowed?(current_user, :read_label, label_parent)
end
def groups_user_can_read_labels(groups)
DeclarativePolicy.user_scope do
groups.select { |group| authorized_to_read_labels?(group) }
end
end
end
......@@ -79,8 +79,12 @@ class IssuableBaseService < BaseService
return unless labels
params[:label_ids] = labels.split(",").map do |label_name|
service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
label = service.execute
label = Labels::FindOrCreateService.new(
current_user,
parent,
title: label_name.strip,
available_labels: available_labels
).execute
label.try(:id)
end.compact
......@@ -104,7 +108,7 @@ class IssuableBaseService < BaseService
end
def available_labels
LabelsFinder.new(current_user, project_id: @project.id).execute
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end
def merge_quick_actions_into_params!(issuable)
......@@ -305,4 +309,8 @@ class IssuableBaseService < BaseService
def update_project_counter_caches?(issuable)
issuable.state_changed?
end
def parent
project
end
end
module Labels
class FindOrCreateService
def initialize(current_user, project, params = {})
def initialize(current_user, parent, params = {})
@current_user = current_user
@project = project
@parent = parent
@available_labels = params.delete(:available_labels)
@params = params.dup.with_indifferent_access
end
......@@ -13,12 +14,13 @@ module Labels
private
attr_reader :current_user, :project, :params, :skip_authorization
attr_reader :current_user, :parent, :params, :skip_authorization
def available_labels
@available_labels ||= LabelsFinder.new(
current_user,
project_id: project.id
"#{parent_type}_id".to_sym => parent.id,
only_group_labels: parent_is_group?
).execute(skip_authorization: skip_authorization)
end
......@@ -27,8 +29,8 @@ module Labels
def find_or_create_label
new_label = available_labels.find_by(title: title)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project))
new_label = Labels::CreateService.new(params).execute(project: project)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent)
end
new_label
......@@ -37,5 +39,13 @@ module Labels
def title
params[:title] || params[:name]
end
def parent_type
parent.model_name.param_key
end
def parent_is_group?
parent_type == "group"
end
end
end
......@@ -27,6 +27,3 @@
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
= commit_in_fork_help
- content_for :page_specific_javascripts do
= webpack_bundle_tag('blob')
......@@ -25,16 +25,10 @@ var NO_COMPRESSION = process.env.NO_COMPRESSION;
var autoEntries = {};
var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
// filter out entries currently imported dynamically in dispatcher.js
var dispatcher = fs.readFileSync(path.join(ROOT_PATH, 'app/assets/javascripts/dispatcher.js')).toString();
var dispatcherChunks = dispatcher.match(/(?!import\(')\.\/pages\/[^']+/g);
function generateAutoEntries(path, prefix = '.') {
const chunkPath = path.replace(/\/index\.js$/, '');
if (!dispatcherChunks.includes(`${prefix}/${chunkPath}`)) {
const chunkName = chunkPath.replace(/\//g, '.');
autoEntries[chunkName] = `${prefix}/${path}`;
}
}
pageEntries.forEach(( path ) => generateAutoEntries(path));
......@@ -98,19 +92,19 @@ var config = {
webpack_runtime: './webpack.js',
// EE-only
add_gitlab_slack_application: './add_gitlab_slack_application/index.js',
burndown_chart: './burndown_chart/index.js',
add_gitlab_slack_application: 'ee/add_gitlab_slack_application/index.js',
burndown_chart: 'ee/burndown_chart/index.js',
epic_show: 'ee/epics/epic_show/epic_show_bundle.js',
new_epic: 'ee/epics/new_epic/new_epic_bundle.js',
geo_nodes: 'ee/geo_nodes',
issuable: './issuable/issuable_bundle.js',
issues: './issues/issues_bundle.js',
ldap_group_links: './groups/ldap_group_links.js',
mirrors: './mirrors',
issuable: 'ee/issuable/issuable_bundle.js',
issues: 'ee/issues/issues_bundle.js',
ldap_group_links: 'ee/groups/ldap_group_links.js',
mirrors: 'ee/mirrors',
ee_protected_branches: 'ee/protected_branches',
ee_protected_tags: 'ee/protected_tags',
service_desk: './projects/settings_service_desk/service_desk_bundle.js',
service_desk_issues: './service_desk_issues/index.js',
service_desk: 'ee/projects/settings_service_desk/service_desk_bundle.js',
service_desk_issues: 'ee/service_desk_issues/index.js',
roadmap: 'ee/roadmap',
ee_sidebar: 'ee/sidebar/sidebar_bundle.js',
},
......@@ -331,6 +325,7 @@ var config = {
'images': path.join(ROOT_PATH, 'app/assets/images'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
'vue$': 'vue/dist/vue.esm.js',
'spec': path.join(ROOT_PATH, 'spec/javascripts'),
// EE-only
'ee': path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
......
......@@ -17,12 +17,14 @@ Gets all epics of the requested group and its subgroups.
```
GET /groups/:id/-/epics
GET /groups/:id/-/epics?author_id=5
GET /groups/:id/-/epics?labels=bug,reproduced
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `author_id` | integer | no | Return epics created by the given user `id` |
| `labels` | string | no | Return epics matching a comma separated list of labels names. Label names from the epic group or a parent group can be used |
| `order_by` | string | no | Return epics ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return epics sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search epics against their `title` and `description` |
......@@ -49,6 +51,7 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon",
"web_url": "http://localhost:3001/kam"
},
"labels": [],
"start_date": null,
"end_date": null,
"created_at": "2018-01-21T06:21:13.165Z",
......@@ -110,6 +113,7 @@ POST /groups/:id/-/epics
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `title` | string | yes | The title of the epic |
| `labels` | string | no | The comma separated list of labels |
| `description` | string | no | The description of the epic |
| `start_date` | string | no | The start date of the epic |
| `end_date` | string. | no | The end date of the epic |
......@@ -135,6 +139,7 @@ Example response:
"id" : 18,
"username" : "eileen.lowe"
},
"labels": [],
"start_date": null,
"end_date": null,
"created_at": "2018-01-21T06:21:13.165Z",
......@@ -156,6 +161,7 @@ PUT /groups/:id/-/epics/:epic_iid
| `epic_iid` | integer/string | yes | The internal ID of the epic |
| `title` | string | no | The title of an epic |
| `description` | string | no | The description of an epic |
| `labels` | string | no | The comma separated list of labels |
| `start_date` | string | no | The start date of an epic |
| `end_date` | string. | no | The end date of an epic |
......@@ -180,6 +186,7 @@ Example response:
"id" : 18,
"username" : "eileen.lowe"
},
"labels": [],
"start_date": null,
"end_date": null,
"created_at": "2018-01-21T06:21:13.165Z",
......
<script>
import Flash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import GitlabSlackService from '../services/gitlab_slack_service';
import { redirectTo } from '../../lib/utils/url_utility';
export default {
props: {
......
import axios from '../../lib/utils/axios_utils';
import axios from '~/lib/utils/axios_utils';
export default {
addToSlack(url, projectId) {
......
......@@ -2,8 +2,8 @@
/* global BoardService */
import Flash from '~/flash';
import modal from '../../vue_shared/components/modal.vue';
import { visitUrl } from '../../lib/utils/url_utility';
import modal from '~/vue_shared/components/modal.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import BoardMilestoneSelect from './milestone_select.vue';
import BoardWeightSelect from './weight_select.vue';
import BoardLabelsSelect from './labels_select.vue';
......
<script>
/* global ListIssue */
import _ from 'underscore';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Api from '../../api';
import eventHub from '~/boards/eventhub';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import Api from '~/api';
export default {
name: 'BoardProjectSelect',
......
......@@ -9,10 +9,10 @@
* [Mockup](https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png)
*/
import _ from 'underscore';
import { n__ } from '~/locale';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import deployBoardSvg from 'ee_empty_states/icons/_deploy_board.svg';
import { n__ } from '../../locale';
import instanceComponent from './deploy_board_instance_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
......
......@@ -12,7 +12,7 @@
* this information in the tooltip and the colors.
* Mockup is https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1551#note_26595150
*/
import tooltip from '../../vue_shared/directives/tooltip';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
......
<script>
/* eslint-disable vue/require-default-prop */
import issuableApp from '~/issue_show/components/app.vue';
import relatedIssuesRoot from '~/issuable/related_issues/components/related_issues_root.vue';
import relatedIssuesRoot from 'ee/issuable/related_issues/components/related_issues_root.vue';
import issuableAppEventHub from '~/issue_show/event_hub';
import epicHeader from './epic_header.vue';
import epicSidebar from '../../sidebar/components/sidebar_app.vue';
......
<script>
import icon from '../../vue_shared/components/icon.vue';
import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
......
<script>
import icon from '../../vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import '../../lib/utils/datetime_utility';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility';
export default {
components: {
......
import Vue from 'vue';
import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
import RelatedIssuesRoot from './related_issues/components/related_issues_root.vue';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
document.addEventListener('DOMContentLoaded', () => {
const relatedIssuesRootElement = document.querySelector('.js-related-issues-root');
......
......@@ -2,7 +2,7 @@
import Sortable from 'vendor/Sortable';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import sortableConfig from '~/sortable/sortable_config';
import sortableConfig from 'ee/sortable/sortable_config';
import eventHub from '../event_hub';
import issueItem from './issue_item.vue';
import addIssuableForm from './add_issuable_form.vue';
......
......@@ -24,7 +24,7 @@ Your caret can stop touching a `rawReference` can happen in a variety of ways:
*/
import _ from 'underscore';
import Flash from '../../../flash';
import Flash from '~/flash';
import eventHub from '../event_hub';
import RelatedIssuesBlock from './related_issues_block.vue';
import RelatedIssuesStore from '../stores/related_issues_store';
......
import tooltip from '../../../vue_shared/directives/tooltip';
import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
const mixins = {
......
import _ from 'underscore';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import Flash from '../flash';
import Flash from '~/flash';
import { backOff } from '~/lib/utils/common_utils';
import AUTH_METHOD from './constants';
import { backOff } from '../lib/utils/common_utils';
export default class MirrorPull {
constructor(formSelector) {
......
<script>
import ciStatus from '../../../vue_shared/components/ci_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import ciStatus from '~/vue_shared/components/ci_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
......
<script>
import Flash from '../../../flash';
import Flash from '~/flash';
import serviceDeskSetting from './service_desk_setting.vue';
import ServiceDeskStore from '../stores/service_desk_store';
import ServiceDeskService from '../services/service_desk_service';
......
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../event_hub';
import tooltip from '../../../vue_shared/directives/tooltip';
export default {
name: 'ServiceDeskSetting',
......
import Vue from 'vue';
import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
import serviceDeskRoot from './components/service_desk_root.vue';
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
document.addEventListener('DOMContentLoaded', () => {
const serviceDeskRootElement = document.querySelector('.js-service-desk-setting-root');
......
import Flash from '~/flash';
import LinkToMemberAvatar from '~/vue_shared/components/link_to_member_avatar';
import LinkToMemberAvatar from 'ee/vue_shared/components/link_to_member_avatar';
import { s__ } from '~/locale';
import eventHub from '~/vue_merge_request_widget/event_hub';
......
<script>
import arrowSvg from 'ee_icons/_arrow_mini_pipeline_graph.svg';
import icon from './icon.vue';
import ciStatus from './ci_icon.vue';
import tooltip from '../directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import ciStatus from '~/vue_shared/components/ci_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
......
......@@ -54,11 +54,12 @@ class Groups::EpicsController < Groups::ApplicationController
end
def epic_params_attributes
%i[
title
description
start_date
end_date
[
:title,
:description,
:start_date,
:end_date,
label_ids: []
]
end
......@@ -67,7 +68,7 @@ class Groups::EpicsController < Groups::ApplicationController
end
def update_service
Epics::UpdateService.new(nil, current_user, epic_params)
Epics::UpdateService.new(@group, current_user, epic_params)
end
def finder_type
......
......@@ -11,6 +11,7 @@ class EpicsFinder < IssuableFinder
items = by_search(items)
items = by_author(items)
items = by_timeframe(items)
items = by_label(items)
sort(items)
end
......
......@@ -141,11 +141,25 @@ module Geo
# @return [ActiveRecord::Relation<Project>] list of projects updated recently
def legacy_find_projects_updated_recently
legacy_inner_join_registry_ids(
current_node.projects,
Geo::ProjectRegistry.dirty.retry_due.pluck(:project_id),
Project
)
registries = Geo::ProjectRegistry.dirty.retry_due.pluck(:project_id, :last_repository_successful_sync_at)
return Project.none if registries.empty?
id_and_last_sync_values = registries.map do |id, last_repository_successful_sync_at|
"(#{id}, #{quote_value(last_repository_successful_sync_at)})"
end
joined_relation = current_node.projects.joins(<<~SQL)
INNER JOIN
(VALUES #{id_and_last_sync_values.join(',')})
project_registry(id, last_repository_successful_sync_at)
ON #{Project.table_name}.id = project_registry.id
SQL
joined_relation
end
def quote_value(value)
::Gitlab::SQL::Glob.q(value)
end
# @return [ActiveRecord::Relation<Geo::ProjectRegistry>] list of synced projects
......
......@@ -11,4 +11,5 @@ class EpicEntity < IssuableEntity
expose :web_url do |epic|
group_epic_path(epic.group, epic)
end
expose :labels, using: LabelEntity
end
module Epics
class BaseService < IssuableBaseService
attr_reader :group
def initialize(group, current_user, params)
@group, @current_user, @params = group, current_user, params
end
private
def available_labels
@available_labels ||= LabelsFinder.new(
current_user,
group_id: group.id,
only_group_labels: true,
include_ancestor_groups: true
).execute
end
def parent
group
end
end
end
module Epics
class CreateService < IssuableBaseService
attr_reader :group
def initialize(group, current_user, params)
@group, @current_user, @params = group, current_user, params
end
class CreateService < Epics::BaseService
def execute
@epic = group.epics.new(whitelisted_epic_params)
create(@epic)
......
module Epics
class UpdateService < ::IssuableBaseService
class UpdateService < Epics::BaseService
def execute(epic)
update(epic)
end
......
......@@ -65,7 +65,7 @@ module Geo
def find_project_ids_updated_recently(batch_size:)
shard_restriction(finder.find_projects_updated_recently(batch_size: batch_size))
.order(Gitlab::Database.nulls_first_order(:last_repository_updated_at, :desc))
.order('project_registry.last_repository_successful_sync_at ASC NULLS FIRST, projects.last_repository_updated_at ASC')
.pluck(:id)
end
......
---
title: Geo - Fix repository synchronization order for projects updated recently
merge_request:
author:
type: fixed
---
title: Allow adding or removing labels from epics and filter epics by labels
merge_request:
author:
type: added
---
title: Move BoardNewIssue vue component
merge_request: 16947
author: George Tsiolis
type: performance
......@@ -32,8 +32,9 @@ module API
def find_epics(args = {})
args = declared_params.merge(args)
args[:label_name] = args.delete(:labels)
epics = EpicsFinder.new(current_user, args).execute
epics = EpicsFinder.new(current_user, args).execute.preload(:labels)
epics.reorder(args[:order_by] => args[:sort])
end
......@@ -54,6 +55,7 @@ module API
desc: 'Return epics sorted in `asc` or `desc` order.'
optional :search, type: String, desc: 'Search epics for text present in the title or description'
optional :author_id, type: Integer, desc: 'Return epics which are authored by the user with the given ID'
optional :labels, type: String, desc: 'Comma-separated list of label names'
end
get ':id/-/epics' do
present find_epics(group_id: user_group.id), with: Entities::Epic
......@@ -79,6 +81,7 @@ module API
optional :description, type: String, desc: 'The description of an epic'
optional :start_date, type: String, desc: 'The start date of an epic'
optional :end_date, type: String, desc: 'The end date of an epic'
optional :labels, type: String, desc: 'Comma-separated list of label names'
end
post ':id/-/epics' do
authorize_can_create!
......@@ -100,14 +103,15 @@ module API
optional :description, type: String, desc: 'The description of an epic'
optional :start_date, type: String, desc: 'The start date of an epic'
optional :end_date, type: String, desc: 'The end date of an epic'
at_least_one_of :title, :description, :start_date, :end_date
optional :labels, type: String, desc: 'Comma-separated list of label names'
at_least_one_of :title, :description, :start_date, :end_date, :labels
end
put ':id/-/epics/:epic_iid' do
authorize_can_admin!
update_params = declared_params(include_missing: false)
update_params.delete(:epic_iid)
result = ::Epics::UpdateService.new(nil, current_user, update_params).execute(epic)
result = ::Epics::UpdateService.new(user_group, current_user, update_params).execute(epic)
if result.valid?
present result, with: Entities::Epic
......
......@@ -4,6 +4,7 @@ describe Groups::EpicsController do
let(:group) { create(:group, :private) }
let(:epic) { create(:epic, group: group) }
let(:user) { create(:user) }
let(:label) { create(:group_label, group: group, title: 'Bug') }
before do
sign_in(user)
......@@ -170,7 +171,7 @@ describe Groups::EpicsController do
describe 'PUT #update' do
before do
group.add_developer(user)
put :update, group_id: group, id: epic.to_param, epic: { title: 'New title' }, format: :json
put :update, group_id: group, id: epic.to_param, epic: { title: 'New title', label_ids: [label.id] }, format: :json
end
it 'returns status 200' do
......@@ -178,7 +179,10 @@ describe Groups::EpicsController do
end
it 'updates the epic correctly' do
expect(epic.reload.title).to eq('New title')
epic.reload
expect(epic.title).to eq('New title')
expect(epic.labels).to eq([label])
end
end
......@@ -210,7 +214,7 @@ describe Groups::EpicsController do
describe '#create' do
subject do
post :create, group_id: group, epic: { title: 'new epic', description: 'some descripition' }
post :create, group_id: group, epic: { title: 'new epic', description: 'some descripition', label_ids: [label.id] }
end
context 'when user has permissions to create an epic' do
......@@ -229,6 +233,10 @@ describe Groups::EpicsController do
expect { subject }.to change { Epic.count }.from(0).to(1)
end
it 'assigns labels to the new epic' do
expect { subject }.to change { LabelLink.count }.from(0).to(1)
end
it 'returns the correct json' do
subject
......
......@@ -3,5 +3,15 @@ FactoryBot.define do
title { generate(:title) }
group
author
factory :labeled_epic do
transient do
labels []
end
after(:create) do |epic, evaluator|
epic.update_attributes(labels: evaluator.labels)
end
end
end
end
......@@ -79,6 +79,15 @@ describe EpicsFinder do
end
end
context 'by label' do
let(:label) { create(:label) }
let!(:labeled_epic) { create(:labeled_epic, group: group, labels: [label]) }
it 'returns all epics with given label' do
expect(epics(label_name: label.title)).to contain_exactly(labeled_epic)
end
end
context 'when subgroups are supported', :nested_groups do
let(:subgroup) { create(:group, :private, parent: group) }
let(:subgroup2) { create(:group, :private, parent: subgroup) }
......
......@@ -18,6 +18,12 @@
},
"additionalProperties": false
},
"labels": {
"type": "array",
"items": {
"type": "string"
}
},
"start_date": { "type": ["string", "null"] },
"end_date": { "type": ["string", "null"] },
"created_at": { "type": ["string", "null"] },
......
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