diff options
Diffstat (limited to 'app/assets/javascripts/boards')
22 files changed, 313 insertions, 179 deletions
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index ac02c229a07..517a13ceb27 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -113,9 +113,6 @@ export default Vue.extend({ // eslint-disable-next-line @gitlab/require-i18n-strings return `boards.${this.boardId}.${this.list.type}.${this.list.id}`; }, - helpLink() { - return boardsStore.scopedLabels.helpLink; - }, }, watch: { filter: { diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue index 10c855675db..fb854616a04 100644 --- a/app/assets/javascripts/boards/components/board_column.vue +++ b/app/assets/javascripts/boards/components/board_column.vue @@ -113,9 +113,6 @@ export default { // eslint-disable-next-line @gitlab/require-i18n-strings return `boards.${this.boardId}.${this.list.type}.${this.list.id}`; }, - helpLink() { - return boardsStore.scopedLabels.helpLink; - }, }, watch: { filter: { @@ -286,7 +283,6 @@ export default { :background-color="list.label.color" :description="list.label.description" :scoped="showScopedLabels(list.label)" - :scoped-labels-documentation-link="helpLink" :size="!list.isExpanded ? 'sm' : ''" :title="list.label.title" tooltip-placement="bottom" @@ -353,7 +349,7 @@ export default { v-if="isSettingsShown" ref="settingsBtn" :aria-label="__(`List settings`)" - class="no-drag rounded-right" + class="no-drag rounded-right js-board-settings-button" title="List settings" type="button" @click="openSidebarSettings" diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index c0df8b72095..fbe221041c1 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -58,11 +58,6 @@ export default { required: false, default: false, }, - scopedLabelsDocumentationLink: { - type: String, - required: false, - default: '#', - }, }, data() { return { @@ -182,7 +177,7 @@ export default { @cancel="cancel" @submit="submit" > - <template slot="body"> + <template #body> <p v-if="isDeleteForm">{{ __('Are you sure you want to delete this board?') }}</p> <form v-else class="js-board-config-modal" @submit.prevent> <div v-if="!readonly" class="append-bottom-20"> @@ -208,7 +203,6 @@ export default { :can-admin-board="canAdminBoard" :milestone-path="milestonePath" :labels-path="labelsPath" - :scoped-labels-documentation-link="scopedLabelsDocumentationLink" :enable-scoped-labels="enableScopedLabels" :project-id="projectId" :group-id="groupId" diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 66a5e134205..c8953158811 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -51,7 +51,7 @@ export default Vue.extend({ return Object.keys(this.issue).length; }, milestoneTitle() { - return this.issue.milestone ? this.issue.milestone.title : __('No Milestone'); + return this.issue.milestone ? this.issue.milestone.title : __('No milestone'); }, canRemove() { return !this.list.preset; @@ -70,9 +70,6 @@ export default Vue.extend({ selectedLabels() { return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; }, - helpLink() { - return boardsStore.scopedLabels.helpLink; - }, }, watch: { detail: { diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index f2c976be7ae..80db9930259 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -86,11 +86,6 @@ export default { required: false, default: false, }, - scopedLabelsDocumentationLink: { - type: String, - required: false, - default: '#', - }, }, data() { return { @@ -348,7 +343,6 @@ export default { :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled" :weights="weights" :enable-scoped-labels="enabledScopedLabels" - :scoped-labels-documentation-link="scopedLabelsDocumentationLink" /> </span> </div> diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index daaa12c096b..a589fb325b2 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -102,9 +102,6 @@ export default { orderedLabels() { return sortBy(this.issue.labels.filter(this.isNonListLabel), 'title'); }, - helpLink() { - return boardsStore.scopedLabels.helpLink; - }, }, methods: { isIndexLessThanlimit(index) { @@ -181,7 +178,6 @@ export default { :description="label.description" size="sm" :scoped="showScopedLabel(label)" - :scoped-labels-documentation-link="helpLink" @click="filterByLabel(label)" /> </template> diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js index dcecfe5e1bb..f577a168e75 100644 --- a/app/assets/javascripts/boards/constants.js +++ b/app/assets/javascripts/boards/constants.js @@ -1,3 +1,8 @@ +export const BoardType = { + project: 'project', + group: 'group', +}; + export const ListType = { assignee: 'assignee', milestone: 'milestone', @@ -8,6 +13,9 @@ export const ListType = { blank: 'blank', }; +export const inactiveListId = 0; + export default { + BoardType, ListType, }; diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index b1b4b1c5508..ca85e54eb89 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -1,6 +1,6 @@ import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; import FilteredSearchContainer from '../filtered_search/container'; -import FilteredSearchManager from '../filtered_search/filtered_search_manager'; +import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager'; import boardsStore from './stores/boards_store'; export default class FilteredSearchBoards extends FilteredSearchManager { diff --git a/app/assets/javascripts/boards/icons/fullscreen_collapse.svg b/app/assets/javascripts/boards/icons/fullscreen_collapse.svg new file mode 100644 index 00000000000..6bd773dc4c5 --- /dev/null +++ b/app/assets/javascripts/boards/icons/fullscreen_collapse.svg @@ -0,0 +1 @@ +<svg width="17" height="17" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg"><path d="M.147 15.496l2.146-2.146-1.286-1.286a.55.55 0 0 1-.125-.616c.101-.238.277-.357.527-.357h4a.55.55 0 0 1 .402.17.55.55 0 0 1 .17.401v4c0 .25-.12.426-.358.527-.232.101-.437.06-.616-.125l-1.286-1.286-2.146 2.146-1.428-1.428zM14.996.646l1.428 1.43-2.146 2.145 1.286 1.286c.185.179.226.384.125.616-.101.238-.277.357-.527.357h-4a.55.55 0 0 1-.402-.17.55.55 0 0 1-.17-.401v-4c0-.25.12-.426.358-.527a.553.553 0 0 1 .616.125l1.286 1.286L14.996.647zm-13.42 0L3.72 2.794l1.286-1.286a.55.55 0 0 1 .616-.125c.238.101.357.277.357.527v4a.55.55 0 0 1-.17.402.55.55 0 0 1-.401.17h-4c-.25 0-.426-.12-.527-.358-.101-.232-.06-.437.125-.616l1.286-1.286L.147 2.075 1.575.647zm14.848 14.85l-1.428 1.428-2.146-2.146-1.286 1.286c-.179.185-.384.226-.616.125-.238-.101-.357-.277-.357-.527v-4a.55.55 0 0 1 .17-.402.55.55 0 0 1 .401-.17h4c.25 0 .426.12.527.358a.553.553 0 0 1-.125.616l-1.286 1.286 2.146 2.146z" fill-rule="evenodd"/></svg> diff --git a/app/assets/javascripts/boards/icons/fullscreen_expand.svg b/app/assets/javascripts/boards/icons/fullscreen_expand.svg new file mode 100644 index 00000000000..306073b8af2 --- /dev/null +++ b/app/assets/javascripts/boards/icons/fullscreen_expand.svg @@ -0,0 +1 @@ +<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"><path d="M8.591 5.056l2.147-2.146-1.286-1.286a.55.55 0 0 1-.125-.616c.101-.238.277-.357.527-.357h4a.55.55 0 0 1 .402.17.55.55 0 0 1 .17.401v4c0 .25-.12.426-.358.527-.232.101-.437.06-.616-.125l-1.286-1.286-2.146 2.147-1.429-1.43zM5.018 8.553l1.429 1.43L4.3 12.127l1.286 1.286c.185.179.226.384.125.616-.101.238-.277.357-.527.357h-4a.55.55 0 0 1-.402-.17.55.55 0 0 1-.17-.401v-4c0-.25.12-.426.358-.527a.553.553 0 0 1 .616.125L2.872 10.7l2.146-2.147zm4.964 0l2.146 2.147 1.286-1.286a.55.55 0 0 1 .616-.125c.238.101.357.277.357.527v4a.55.55 0 0 1-.17.402.55.55 0 0 1-.401.17h-4c-.25 0-.426-.12-.527-.358-.101-.232-.06-.437.125-.616l1.286-1.286-2.147-2.146 1.43-1.429zM6.447 5.018l-1.43 1.429L2.873 4.3 1.586 5.586c-.179.185-.384.226-.616.125-.238-.101-.357-.277-.357-.527v-4a.55.55 0 0 1 .17-.402.55.55 0 0 1 .401-.17h4c.25 0 .426.12.527.358a.553.553 0 0 1-.125.616L4.3 2.872l2.147 2.146z" fill-rule="evenodd"/></svg> diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index a12db7a5f1a..9ff7575ae09 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -6,7 +6,6 @@ import 'ee_else_ce/boards/models/list'; import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar'; import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown'; import boardConfigToggle from 'ee_else_ce/boards/config_toggle'; -import toggleFocusMode from 'ee_else_ce/boards/toggle_focus'; import toggleLabels from 'ee_else_ce/boards/toggle_labels'; import { setPromotionState, @@ -16,11 +15,15 @@ import { getBoardsModalData, } from 'ee_else_ce/boards/ee_functions'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; import Flash from '~/flash'; import { __ } from '~/locale'; import './models/label'; import './models/assignee'; +import { BoardType } from './constants'; +import toggleFocusMode from '~/boards/toggle_focus'; import FilteredSearchBoards from '~/boards/filtered_search_boards'; import eventHub from '~/boards/eventhub'; import sidebarEventHub from '~/sidebar/event_hub'; @@ -37,7 +40,16 @@ import { convertObjectPropsToCamelCase, parseBoolean, } from '~/lib/utils/common_utils'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher'; +import projectBoardQuery from './queries/project_board.query.graphql'; +import groupQuery from './queries/group_board.query.graphql'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); let issueBoardsApp; @@ -79,18 +91,22 @@ export default () => { import('ee_component/boards/components/board_settings_sidebar.vue'), }, store, - data: { - state: boardsStore.state, - loading: true, - boardsEndpoint: $boardApp.dataset.boardsEndpoint, - recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint, - listsEndpoint: $boardApp.dataset.listsEndpoint, - boardId: $boardApp.dataset.boardId, - disabled: parseBoolean($boardApp.dataset.disabled), - issueLinkBase: $boardApp.dataset.issueLinkBase, - rootPath: $boardApp.dataset.rootPath, - bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, - detailIssue: boardsStore.detail, + apolloProvider, + data() { + return { + state: boardsStore.state, + loading: 0, + boardsEndpoint: $boardApp.dataset.boardsEndpoint, + recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint, + listsEndpoint: $boardApp.dataset.listsEndpoint, + boardId: $boardApp.dataset.boardId, + disabled: parseBoolean($boardApp.dataset.disabled), + issueLinkBase: $boardApp.dataset.issueLinkBase, + rootPath: $boardApp.dataset.rootPath, + bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, + detailIssue: boardsStore.detail, + parent: $boardApp.dataset.parent, + }; }, computed: { detailIssueVisible() { @@ -124,31 +140,56 @@ export default () => { this.filterManager.setup(); boardsStore.disabled = this.disabled; - boardsStore - .all() - .then(res => res.data) - .then(lists => { - lists.forEach(listObj => { - let { position } = listObj; - if (listObj.list_type === 'closed') { - position = Infinity; - } else if (listObj.list_type === 'backlog') { - position = -1; + + if (gon.features.graphqlBoardLists) { + this.$apollo.addSmartQuery('lists', { + query() { + return this.parent === BoardType.group ? groupQuery : projectBoardQuery; + }, + variables() { + return { + fullPath: this.state.endpoints.fullPath, + boardId: `gid://gitlab/Board/${this.boardId}`, + }; + }, + update(data) { + return this.getNodes(data); + }, + result({ data, error }) { + if (error) { + throw error; } - boardsStore.addList({ - ...listObj, - position, - }); - }); + const lists = this.getNodes(data); + + lists.forEach(list => + boardsStore.addList({ + ...list, + id: getIdFromGraphQLId(list.id), + }), + ); - boardsStore.addBlankState(); - setPromotionState(boardsStore); - this.loading = false; - }) - .catch(() => { - Flash(__('An error occurred while fetching the board lists. Please try again.')); + boardsStore.addBlankState(); + setPromotionState(boardsStore); + }, + error() { + Flash(__('An error occurred while fetching the board lists. Please try again.')); + }, }); + } else { + boardsStore + .all() + .then(res => res.data) + .then(lists => { + lists.forEach(list => boardsStore.addList(list)); + boardsStore.addBlankState(); + setPromotionState(boardsStore); + this.loading = false; + }) + .catch(() => { + Flash(__('An error occurred while fetching the board lists. Please try again.')); + }); + } }, methods: { updateTokens() { @@ -233,6 +274,9 @@ export default () => { }); } }, + getNodes(data) { + return data[this.parent]?.board?.lists.nodes; + }, }, }); @@ -261,7 +305,7 @@ export default () => { return { modal: ModalStore.store, store: boardsStore.state, - ...getBoardsModalData($boardApp), + ...getBoardsModalData(), canAdminList: this.$options.el.hasAttribute('data-can-admin-list'), }; }, @@ -325,7 +369,7 @@ export default () => { }); } - toggleFocusMode(ModalStore, boardsStore, $boardApp); + toggleFocusMode(ModalStore, boardsStore); toggleLabels(); mountMultipleBoardsSwitcher(); }; diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js index 68ea28e68d9..fceb8c9d48e 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js @@ -19,14 +19,15 @@ export function getBoardSortableDefaultOptions(obj) { const touchEnabled = 'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch); - const defaultSortOptions = Object.assign({}, sortableConfig, { + const defaultSortOptions = { + ...sortableConfig, filter: '.no-drag', delay: touchEnabled ? 100 : 0, scrollSensitivity: touchEnabled ? 60 : 100, scrollSpeed: 20, onStart: sortableStart, onEnd: sortableEnd, - }); + }; Object.keys(obj).forEach(key => { defaultSortOptions[key] = obj[key]; diff --git a/app/assets/javascripts/boards/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js index 5f5758583bb..1e822d06bfd 100644 --- a/app/assets/javascripts/boards/models/assignee.js +++ b/app/assets/javascripts/boards/models/assignee.js @@ -3,7 +3,7 @@ export default class ListAssignee { this.id = obj.id; this.name = obj.name; this.username = obj.username; - this.avatar = obj.avatar_url || obj.avatar || gon.default_avatar_url; + this.avatar = obj.avatarUrl || obj.avatar_url || obj.avatar || gon.default_avatar_url; this.path = obj.path; this.state = obj.state; this.webUrl = obj.web_url || obj.webUrl; diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index d099c4b930c..878f49cc6be 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -15,7 +15,7 @@ class ListIssue { this.labels = []; this.assignees = []; this.selected = false; - this.position = obj.relative_position || Infinity; + this.position = obj.position || obj.relative_position || Infinity; this.isFetching = { subscriptions: true, }; @@ -99,31 +99,7 @@ class ListIssue { } update() { - const data = { - issue: { - milestone_id: this.milestone ? this.milestone.id : null, - due_date: this.dueDate, - assignee_ids: this.assignees.length > 0 ? this.assignees.map(u => u.id) : [0], - label_ids: this.labels.map(label => label.id), - }, - }; - - if (!data.issue.label_ids.length) { - data.issue.label_ids = ['']; - } - - const projectPath = this.project ? this.project.path : ''; - return axios.patch(`${this.path}.json`, data).then(({ data: body = {} } = {}) => { - /** - * Since post implementation of Scoped labels, server can reject - * same key-ed labels. To keep the UI and server Model consistent, - * we're just assigning labels that server echo's back to us when we - * PATCH the said object. - */ - if (body) { - this.labels = convertObjectPropsToCamelCase(body.labels, { deep: true }); - } - }); + return boardsStore.updateIssue(this); } } diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 990b648190a..31c372b7a75 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -1,10 +1,9 @@ -/* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow */ +/* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return */ import ListIssue from 'ee_else_ce/boards/models/issue'; import { __ } from '~/locale'; import ListLabel from './label'; import ListAssignee from './assignee'; -import { urlParamsToObject } from '~/lib/utils/common_utils'; import flash from '~/flash'; import boardsStore from '../stores/boards_store'; import ListMilestone from './milestone'; @@ -40,8 +39,8 @@ class List { this.id = obj.id; this._uid = this.guid(); this.position = obj.position; - this.title = obj.list_type === 'backlog' ? __('Open') : obj.title; - this.type = obj.list_type; + this.title = (obj.list_type || obj.listType) === 'backlog' ? __('Open') : obj.title; + this.type = obj.list_type || obj.listType; const typeInfo = this.getTypeInfo(this.type); this.preset = Boolean(typeInfo.isPreset); @@ -52,14 +51,12 @@ class List { this.loadingMore = false; this.issues = obj.issues || []; this.issuesSize = obj.issuesSize ? obj.issuesSize : 0; - this.maxIssueCount = Object.hasOwnProperty.call(obj, 'max_issue_count') - ? obj.max_issue_count - : 0; + this.maxIssueCount = obj.maxIssueCount || obj.max_issue_count || 0; if (obj.label) { this.label = new ListLabel(obj.label); - } else if (obj.user) { - this.assignee = new ListAssignee(obj.user); + } else if (obj.user || obj.assignee) { + this.assignee = new ListAssignee(obj.user || obj.assignee); this.title = this.assignee.name; } else if (IS_EE && obj.milestone) { this.milestone = new ListMilestone(obj.milestone); @@ -113,34 +110,7 @@ class List { } getIssues(emptyIssues = true) { - const data = { - ...urlParamsToObject(boardsStore.filter.path), - page: this.page, - }; - - if (this.label && data.label_name) { - data.label_name = data.label_name.filter(label => label !== this.label.title); - } - - if (emptyIssues) { - this.loading = true; - } - - return boardsStore - .getIssuesForList(this.id, data) - .then(res => res.data) - .then(data => { - this.loading = false; - this.issuesSize = data.size; - - if (emptyIssues) { - this.issues = []; - } - - this.createIssues(data.issues); - - return data; - }); + return boardsStore.getListIssues(this, emptyIssues); } newIssue(issue) { @@ -164,48 +134,7 @@ class List { } addIssue(issue, listFrom, newIndex) { - let moveBeforeId = null; - let moveAfterId = null; - - if (!this.findIssue(issue.id)) { - if (newIndex !== undefined) { - this.issues.splice(newIndex, 0, issue); - - if (this.issues[newIndex - 1]) { - moveBeforeId = this.issues[newIndex - 1].id; - } - - if (this.issues[newIndex + 1]) { - moveAfterId = this.issues[newIndex + 1].id; - } - } else { - this.issues.push(issue); - } - - if (this.label) { - issue.addLabel(this.label); - } - - if (this.assignee) { - if (listFrom && listFrom.type === 'assignee') { - issue.removeAssignee(listFrom.assignee); - } - issue.addAssignee(this.assignee); - } - - if (IS_EE && this.milestone) { - if (listFrom && listFrom.type === 'milestone') { - issue.removeMilestone(listFrom.milestone); - } - issue.addMilestone(this.milestone); - } - - if (listFrom) { - this.issuesSize += 1; - - this.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId); - } - } + boardsStore.addListIssue(this, issue, listFrom, newIndex); } moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) { diff --git a/app/assets/javascripts/boards/queries/board_list.fragment.graphql b/app/assets/javascripts/boards/queries/board_list.fragment.graphql new file mode 100644 index 00000000000..bbf3314377e --- /dev/null +++ b/app/assets/javascripts/boards/queries/board_list.fragment.graphql @@ -0,0 +1,5 @@ +#import "./board_list_shared.fragment.graphql" + +fragment BoardListFragment on BoardList { + ...BoardListShared +} diff --git a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql new file mode 100644 index 00000000000..6ba6c05d6d9 --- /dev/null +++ b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql @@ -0,0 +1,15 @@ +fragment BoardListShared on BoardList { + id, + title, + position, + listType, + collapsed, + label { + id, + title, + color, + textColor, + description, + descriptionHtml + } +} diff --git a/app/assets/javascripts/boards/queries/group_board.query.graphql b/app/assets/javascripts/boards/queries/group_board.query.graphql new file mode 100644 index 00000000000..cb42cb3f73d --- /dev/null +++ b/app/assets/javascripts/boards/queries/group_board.query.graphql @@ -0,0 +1,13 @@ +#import "ee_else_ce/boards/queries/board_list.fragment.graphql" + +query GroupBoard($fullPath: ID!, $boardId: ID!) { + group(fullPath: $fullPath) { + board(id: $boardId) { + lists { + nodes { + ...BoardListFragment + } + } + } + } +} diff --git a/app/assets/javascripts/boards/queries/project_board.query.graphql b/app/assets/javascripts/boards/queries/project_board.query.graphql new file mode 100644 index 00000000000..4620a7e0fd5 --- /dev/null +++ b/app/assets/javascripts/boards/queries/project_board.query.graphql @@ -0,0 +1,13 @@ +#import "ee_else_ce/boards/queries/board_list.fragment.graphql" + +query ProjectBoard($fullPath: ID!, $boardId: ID!) { + project(fullPath: $fullPath) { + board(id: $boardId) { + lists { + nodes { + ...BoardListFragment + } + } + } + } +} diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index e5447080e37..fdbd7e89bfb 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -6,7 +6,12 @@ import { sortBy } from 'lodash'; import Vue from 'vue'; import Cookies from 'js-cookie'; import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee'; -import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils'; +import { + urlParamsToObject, + getUrlParamsArray, + parseBoolean, + convertObjectPropsToCamelCase, +} from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; @@ -23,7 +28,6 @@ const boardsStore = { limitToHours: false, }, scopedLabels: { - helpLink: '', enabled: false, }, filter: { @@ -75,7 +79,15 @@ const boardsStore = { this.state.currentPage = page; }, addList(listObj) { - const list = new List(listObj); + const listType = listObj.listType || listObj.list_type; + let { position } = listObj; + if (listType === ListType.closed) { + position = Infinity; + } else if (listType === ListType.backlog) { + position = -1; + } + + const list = new List({ ...listObj, position }); this.state.lists = sortBy([...this.state.lists, list], 'position'); return list; }, @@ -121,6 +133,50 @@ const boardsStore = { path: '', }); }, + addListIssue(list, issue, listFrom, newIndex) { + let moveBeforeId = null; + let moveAfterId = null; + + if (!list.findIssue(issue.id)) { + if (newIndex !== undefined) { + list.issues.splice(newIndex, 0, issue); + + if (list.issues[newIndex - 1]) { + moveBeforeId = list.issues[newIndex - 1].id; + } + + if (list.issues[newIndex + 1]) { + moveAfterId = list.issues[newIndex + 1].id; + } + } else { + list.issues.push(issue); + } + + if (list.label) { + issue.addLabel(list.label); + } + + if (list.assignee) { + if (listFrom && listFrom.type === 'assignee') { + issue.removeAssignee(listFrom.assignee); + } + issue.addAssignee(list.assignee); + } + + if (IS_EE && list.milestone) { + if (listFrom && listFrom.type === 'milestone') { + issue.removeMilestone(listFrom.milestone); + } + issue.addMilestone(list.milestone); + } + + if (listFrom) { + list.issuesSize += 1; + + list.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId); + } + } + }, welcomeIsHidden() { return parseBoolean(Cookies.get('issue_board_welcome_hidden')); }, @@ -487,6 +543,36 @@ const boardsStore = { }); }, + getListIssues(list, emptyIssues = true) { + const data = { + ...urlParamsToObject(this.filter.path), + page: list.page, + }; + + if (list.label && data.label_name) { + data.label_name = data.label_name.filter(label => label !== list.label.title); + } + + if (emptyIssues) { + list.loading = true; + } + + return this.getIssuesForList(list.id, data) + .then(res => res.data) + .then(data => { + list.loading = false; + list.issuesSize = data.size; + + if (emptyIssues) { + list.issues = []; + } + + list.createIssues(data.issues); + + return data; + }); + }, + getIssuesForList(id, filter = {}) { const data = { id }; Object.keys(filter).forEach(key => { @@ -632,6 +718,28 @@ const boardsStore = { issue.assignees = obj.assignees.map(a => new ListAssignee(a)); } }, + updateIssue(issue) { + const data = { + issue: { + milestone_id: issue.milestone ? issue.milestone.id : null, + due_date: issue.dueDate, + assignee_ids: issue.assignees.length > 0 ? issue.assignees.map(({ id }) => id) : [0], + label_ids: issue.labels.length > 0 ? issue.labels.map(({ id }) => id) : [''], + }, + }; + + return axios.patch(`${issue.path}.json`, data).then(({ data: body = {} } = {}) => { + /** + * Since post implementation of Scoped labels, server can reject + * same key-ed labels. To keep the UI and server Model consistent, + * we're just assigning labels that server echo's back to us when we + * PATCH the said object. + */ + if (body) { + issue.labels = convertObjectPropsToCamelCase(body.labels, { deep: true }); + } + }); + }, }; BoardsStoreEE.initEESpecific(boardsStore); diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js index 731aea996fb..10aac2f649e 100644 --- a/app/assets/javascripts/boards/stores/state.js +++ b/app/assets/javascripts/boards/stores/state.js @@ -1,4 +1,6 @@ +import { inactiveListId } from '~/boards/constants'; + export default () => ({ isShowingLabels: true, - activeListId: 0, + activeListId: inactiveListId, }); diff --git a/app/assets/javascripts/boards/toggle_focus.js b/app/assets/javascripts/boards/toggle_focus.js index 2d1ec238274..a437a34c948 100644 --- a/app/assets/javascripts/boards/toggle_focus.js +++ b/app/assets/javascripts/boards/toggle_focus.js @@ -1 +1,45 @@ -export default () => {}; +import $ from 'jquery'; +import Vue from 'vue'; +import collapseIcon from './icons/fullscreen_collapse.svg'; +import expandIcon from './icons/fullscreen_expand.svg'; + +export default (ModalStore, boardsStore) => { + const issueBoardsContent = document.querySelector('.content-wrapper > .js-focus-mode-board'); + + return new Vue({ + el: document.getElementById('js-toggle-focus-btn'), + data: { + modal: ModalStore.store, + store: boardsStore.state, + isFullscreen: false, + }, + methods: { + toggleFocusMode() { + $(this.$refs.toggleFocusModeButton).tooltip('hide'); + issueBoardsContent.classList.toggle('is-focused'); + + this.isFullscreen = !this.isFullscreen; + }, + }, + template: ` + <div class="board-extra-actions"> + <a + href="#" + class="btn btn-default has-tooltip prepend-left-10 js-focus-mode-btn" + data-qa-selector="focus_mode_button" + role="button" + aria-label="Toggle focus mode" + title="Toggle focus mode" + ref="toggleFocusModeButton" + @click="toggleFocusMode"> + <span v-show="isFullscreen"> + ${collapseIcon} + </span> + <span v-show="!isFullscreen"> + ${expandIcon} + </span> + </a> + </div> + `, + }); +}; |