Skip to content
Snippets Groups Projects
Commit dd071c4b authored by Felipe Artur's avatar Felipe Artur
Browse files

Bring one group board to CE

parent 98fecb5f
No related branches found
No related tags found
No related merge requests found
Showing
with 389 additions and 72 deletions
<script>
/* eslint-disable vue/require-default-prop */
import './issue_card_inner';
import eventHub from '../eventhub';
 
Loading
Loading
@@ -34,6 +35,9 @@ export default {
type: String,
default: '',
},
groupId: {
type: Number,
},
},
data() {
return {
Loading
Loading
@@ -88,6 +92,7 @@ export default {
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:group-id="groupId"
:root-path="rootPath"
:update-filters="true"
/>
Loading
Loading
Loading
Loading
@@ -15,6 +15,11 @@ export default {
loadingIcon,
},
props: {
groupId: {
type: Number,
required: false,
default: 0,
},
disabled: {
type: Boolean,
required: true,
Loading
Loading
@@ -170,6 +175,7 @@ export default {
<loading-icon />
</div>
<board-new-issue
:group-id="groupId"
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
Loading
Loading
@@ -185,6 +191,7 @@ export default {
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:group-id="groupId"
:root-path="rootPath"
:disabled="disabled"
:key="issue.id" />
Loading
Loading
<script>
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue';
 
const Store = gl.issueBoards.BoardsStore;
 
export default {
name: 'BoardNewIssue',
components: {
ProjectSelect,
},
props: {
groupId: {
type: Number,
required: false,
default: 0,
},
list: {
type: Object,
required: true,
Loading
Loading
@@ -16,10 +25,20 @@ export default {
return {
title: '',
error: false,
selectedProject: {},
};
},
computed: {
disabled() {
if (this.groupId) {
return this.title === '' || !this.selectedProject.name;
}
return this.title === '';
},
},
mounted() {
this.$refs.input.focus();
eventHub.$on('setSelectedProject', this.setSelectedProject);
},
methods: {
submit(e) {
Loading
Loading
@@ -34,6 +53,7 @@ export default {
labels,
subscribed: true,
assignees: [],
project_id: this.selectedProject.id,
});
 
eventHub.$emit(`scroll-board-list-${this.list.id}`);
Loading
Loading
@@ -62,52 +82,62 @@ export default {
this.title = '';
eventHub.$emit(`hide-issue-form-${this.list.id}`);
},
setSelectedProject(selectedProject) {
this.selectedProject = selectedProject;
},
},
};
</script>
 
<template>
<div class="card board-new-issue-form">
<form @submit="submit($event)">
<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'"
>
Title
</label>
<input
class="form-control"
type="text"
v-model="title"
ref="input"
autocomplete="off"
:id="list.id + '-title'"
/>
<div class="clearfix prepend-top-10">
<button
class="btn btn-success pull-left"
type="submit"
:disabled="title === ''"
ref="submit-button"
<div class="board-new-issue-form">
<div class="card">
<form @submit="submit($event)">
<div
class="flash-container"
v-if="error"
>
Submit issue
</button>
<button
class="btn btn-default pull-right"
type="button"
@click="cancel"
<div class="flash-alert">
An error occurred. Please try again.
</div>
</div>
<label
class="label-light"
:for="list.id + '-title'"
>
Cancel
</button>
</div>
</form>
Title
</label>
<input
class="form-control"
type="text"
v-model="title"
ref="input"
autocomplete="off"
:id="list.id + '-title'"
/>
<project-select
v-if="groupId"
:group-id="groupId"
/>
<div class="clearfix prepend-top-10">
<button
class="btn btn-success pull-left"
type="submit"
:disabled="disabled"
ref="submit-button"
>
Submit issue
</button>
<button
class="btn btn-default pull-right"
type="button"
@click="cancel"
>
Cancel
</button>
</div>
</form>
</div>
</div>
</template>
Loading
Loading
@@ -31,6 +31,10 @@ gl.issueBoards.IssueCardInner = Vue.extend({
required: false,
default: false,
},
groupId: {
type: Number,
required: false,
},
},
data() {
return {
Loading
Loading
@@ -64,7 +68,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return this.issue.assignees.length > this.numberOverLimit;
},
cardUrl() {
return `${this.issueLinkBase}/${this.issue.iid}`;
let baseUrl = this.issueLinkBase;
if (this.groupId && this.issue.project) {
baseUrl = this.issueLinkBase.replace(':project_path', this.issue.project.path);
}
return `${baseUrl}/${this.issue.iid}`;
},
issueId() {
if (this.issue.iid) {
Loading
Loading
@@ -148,7 +158,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
class="card-number"
v-if="issueId"
>
{{ issueId }}
<template v-if="groupId && issue.project">{{issue.project.path}}</template>{{ issueId }}
</span>
</h4>
<div class="card-assignee">
Loading
Loading
<script>
/* global ListIssue */
import _ from 'underscore';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Api from '../../api';
export default {
name: 'BoardProjectSelect',
components: {
loadingIcon,
},
props: {
groupId: {
type: Number,
required: true,
default: 0,
},
},
data() {
return {
loading: true,
selectedProject: {},
};
},
computed: {
selectedProjectName() {
return this.selectedProject.name || 'Select a project';
},
},
mounted() {
$(this.$refs.projectsDropdown).glDropdown({
filterable: true,
filterRemote: true,
search: {
fields: ['name_with_namespace'],
},
clicked: ({ $el, e }) => {
e.preventDefault();
this.selectedProject = {
id: $el.data('project-id'),
name: $el.data('project-name'),
};
eventHub.$emit('setSelectedProject', this.selectedProject);
},
selectable: true,
data: (term, callback) => {
this.loading = true;
return Api.groupProjects(this.groupId, term, (projects) => {
this.loading = false;
callback(projects);
});
},
renderRow(project) {
return `
<li>
<a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
${_.escape(project.name)}
</a>
</li>
`;
},
text: project => project.name,
});
},
};
</script>
<template>
<div>
<label class="label-light prepend-top-10">
Project
</label>
<div
ref="projectsDropdown"
class="dropdown"
>
<button
class="dropdown-menu-toggle wide"
type="button"
data-toggle="dropdown"
aria-expanded="false"
>
{{ selectedProjectName }}
<i
class="fa fa-chevron-down"
aria-hidden="true"
>
</i>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">
<span>Projects</span>
<button
aria-label="Close"
type="button"
class="dropdown-title-button dropdown-menu-close"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-times dropdown-menu-close-icon"
>
</i>
</button>
</div>
<div class="dropdown-input">
<input
class="dropdown-input-field"
type="search"
placeholder="Search projects"
/>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-search dropdown-input-search"
>
</i>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
</div>
</template>
Loading
Loading
@@ -24,7 +24,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
},
computed: {
updateUrl() {
return this.issueUpdate;
return this.issueUpdate.replace(':project_path', this.issue.project.path);
},
},
methods: {
Loading
Loading
@@ -32,17 +32,21 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
const issue = this.issue;
const lists = issue.getLists();
const listLabelIds = lists.map(list => list.label.id);
let labelIds = this.issue.labels
let labelIds = issue.labels
.map(label => label.id)
.filter(id => !listLabelIds.includes(id));
if (labelIds.length === 0) {
labelIds = [''];
}
const data = {
issue: {
label_ids: labelIds,
},
};
// Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => {
Flash(__('Failed to remove issue from board, please try again.'));
 
Loading
Loading
Loading
Loading
@@ -6,6 +6,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
super({
page: 'boards',
stateFiltersSelector: '.issues-state-filters',
});
 
this.store = store;
Loading
Loading
Loading
Loading
@@ -13,6 +13,7 @@ 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';
Loading
Loading
@@ -89,7 +90,7 @@ export default () => {
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
mounted () {
this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit);
this.filterManager.setup();
 
Store.disabled = this.disabled;
Loading
Loading
@@ -179,6 +180,7 @@ export default () => {
return {
modal: ModalStore.store,
store: Store.state,
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
};
},
computed: {
Loading
Loading
@@ -232,6 +234,7 @@ export default () => {
:class="{ 'disabled': disabled }"
:title="tooltipTitle"
:aria-disabled="disabled"
v-if="canAdminList"
@click="openModal">
Add issues
</button>
Loading
Loading
/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
/* global DocumentTouch */
 
import sortableConfig from '../../sortable/sortable_config';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
 
Loading
Loading
@@ -18,19 +20,14 @@ gl.issueBoards.onEnd = () => {
gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
 
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
const defaultSortOptions = {
animation: 200,
forceFallback: true,
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
const defaultSortOptions = Object.assign({}, sortableConfig, {
filter: '.board-delete, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
onEnd: gl.issueBoards.onEnd
};
onEnd: gl.issueBoards.onEnd,
});
 
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
return defaultSortOptions;
Loading
Loading
Loading
Loading
@@ -4,6 +4,7 @@
/* global ListAssignee */
 
import Vue from 'vue';
import IssueProject from './project';
 
class ListIssue {
constructor (obj, defaultAvatar) {
Loading
Loading
@@ -23,6 +24,12 @@ class ListIssue {
this.isLoading = {};
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id;
if (obj.project) {
this.project = new IssueProject(obj.project);
}
 
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
Loading
Loading
@@ -105,7 +112,8 @@ class ListIssue {
data.issue.label_ids = [''];
}
 
return Vue.http.patch(url, data);
const projectPath = this.project ? this.project.path : '';
return Vue.http.patch(url.replace(':project_path', projectPath), data);
}
}
 
Loading
Loading
export default class IssueProject {
constructor(obj) {
this.id = obj.id;
this.path = obj.path;
}
}
import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/shortcuts_navigation';
import initBoards from '~/boards';
document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initBoards();
});
export default {
animation: 200,
forceFallback: true,
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
};
module Boards
class IssuesController < Boards::ApplicationController
include BoardsResponses
include ControllerWithCrossProjectAccessCheck
requires_cross_project_access if: -> { board&.group_board? }
 
before_action :whitelist_query_limiting, only: [:index, :update]
before_action :authorize_read_issue, only: [:index]
before_action :authorize_create_issue, only: [:create]
before_action :authorize_update_issue, only: [:update]
skip_before_action :authenticate_user!, only: [:index]
 
def index
issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute
Loading
Loading
@@ -64,11 +66,21 @@ module Boards
end
 
def issues_finder
IssuesFinder.new(current_user, project_id: board_parent.id)
if board.group_board?
IssuesFinder.new(current_user, group_id: board_parent.id)
else
IssuesFinder.new(current_user, project_id: board_parent.id)
end
end
 
def project
board_parent
@project ||= begin
if board.group_board?
Project.find(issue_params[:project_id])
else
board_parent
end
end
end
 
def move_params
Loading
Loading
module BoardsResponses
include Gitlab::Utils::StrongMemoize
def board_params
params.require(:board).permit(:name, :weight, :milestone_id, :assignee_id, label_ids: [])
end
def parent
strong_memoize(:parent) do
group? ? group : project
end
end
def boards_path
if group?
group_boards_path(parent)
else
project_boards_path(parent)
end
end
def board_path(board)
if group?
group_board_path(parent, board)
else
project_board_path(parent, board)
end
end
def group?
instance_variable_defined?(:@group)
end
def authorize_read_list
authorize_action_for!(board.parent, :read_list)
ability = board.group_board? ? :read_group : :read_list
authorize_action_for!(board.parent, ability)
end
 
def authorize_read_issue
authorize_action_for!(board.parent, :read_issue)
ability = board.group_board? ? :read_group : :read_issue
authorize_action_for!(board.parent, ability)
end
 
def authorize_update_issue
Loading
Loading
@@ -31,6 +67,10 @@ module BoardsResponses
respond_with(@board) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
 
def serialize_as_json(resource)
resource.as_json(only: [:id])
end
def respond_with(resource)
respond_to do |format|
format.html
Loading
Loading
class Groups::BoardsController < Groups::ApplicationController
include BoardsResponses
before_action :assign_endpoint_vars
def index
@boards = Boards::ListService.new(group, current_user).execute
respond_with_boards
end
def show
@board = group.boards.find(params[:id])
respond_with_board
end
def assign_endpoint_vars
@boards_endpoint = group_boards_url(group)
@namespace_path = group.to_param
@labels_endpoint = group_labels_url(group)
end
def serialize_as_json(resource)
resource.as_json(only: [:id])
end
end
Loading
Loading
@@ -35,10 +35,18 @@ class Groups::LabelsController < Groups::ApplicationController
def create
@label = Labels::CreateService.new(label_params).execute(group: group)
 
if @label.valid?
redirect_to group_labels_path(@group)
else
render :new
respond_to do |format|
format.html do
if @label.valid?
redirect_to group_labels_path(@group)
else
render :new
end
end
format.json do
render json: LabelSerializer.new.represent_appearance(@label)
end
end
end
 
Loading
Loading
Loading
Loading
@@ -17,23 +17,37 @@ module BoardsHelper
end
 
def build_issue_link_base
project_issues_path(@project)
if board.group_board?
"#{group_path(@board.group)}/:project_path/issues"
else
project_issues_path(@project)
end
end
 
def board_base_url
project_boards_path(@project)
if board.group_board?
group_boards_url(@group)
else
project_boards_path(@project)
end
end
 
def multiple_boards_available?
current_board_parent.multiple_issue_boards_available?(current_user)
current_board_parent.multiple_issue_boards_available?
end
 
def current_board_path(board)
@current_board_path ||= project_board_path(current_board_parent, board)
@current_board_path ||= begin
if board.group_board?
group_board_path(current_board_parent, board)
else
project_board_path(current_board_parent, board)
end
end
end
 
def current_board_parent
@current_board_parent ||= @project
@current_board_parent ||= @group || @project
end
 
def can_admin_issue?
Loading
Loading
@@ -47,7 +61,8 @@ module BoardsHelper
labels: labels_filter_path(true),
labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path,
project_path: @project&.try(:path)
project_path: @project&.path,
group_path: @group&.path
}
end
 
Loading
Loading
@@ -59,7 +74,8 @@ module BoardsHelper
field_name: 'issue[assignee_ids][]',
first_user: current_user&.username,
current_user: 'true',
project_id: @project&.try(:id),
project_id: @project&.id,
group_id: @group&.id,
null_user: 'true',
multi_select: 'true',
'dropdown-header': dropdown_options[:data][:'dropdown-header'],
Loading
Loading
Loading
Loading
@@ -27,7 +27,7 @@ module FormHelper
first_user: current_user&.username,
null_user: true,
current_user: true,
project_id: @project.id,
project_id: @project&.id,
field_name: 'issue[assignee_ids][]',
default_label: 'Unassigned',
'max-select': 1,
Loading
Loading
Loading
Loading
@@ -129,7 +129,7 @@ module GroupsHelper
links = [:overview, :group_members]
 
if can?(current_user, :read_cross_project)
links += [:activity, :issues, :labels, :milestones, :merge_requests]
links += [:activity, :issues, :labels, :milestones, :merge_requests, :boards]
end
 
if can?(current_user, :admin_group, @group)
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment