...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import { GlLabel } from '@gitlab/ui';
import Flash from '~/flash'; import Flash from '~/flash';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import Sidebar from '~/right_sidebar'; import Sidebar from '~/right_sidebar';
...@@ -22,6 +23,7 @@ export default Vue.extend({ ...@@ -22,6 +23,7 @@ export default Vue.extend({
components: { components: {
AssigneeTitle, AssigneeTitle,
Assignees, Assignees,
GlLabel,
SidebarEpicsSelect: () => SidebarEpicsSelect: () =>
import('ee_component/sidebar/components/sidebar_item_epics_select.vue'), import('ee_component/sidebar/components/sidebar_item_epics_select.vue'),
RemoveBtn, RemoveBtn,
...@@ -67,6 +69,9 @@ export default Vue.extend({ ...@@ -67,6 +69,9 @@ export default Vue.extend({
selectedLabels() { selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
}, },
helpLink() {
return boardsStore.scopedLabels.helpLink;
},
}, },
watch: { watch: {
detail: { detail: {
...@@ -147,8 +152,5 @@ export default Vue.extend({ ...@@ -147,8 +152,5 @@ export default Vue.extend({
showScopedLabels(label) { showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label); return boardsStore.scopedLabels.enabled && isScopedLabel(label);
}, },
helpLink() {
return boardsStore.scopedLabels.helpLink;
},
}, },
}); });
<script> <script>
import { throttle } from 'underscore'; import { throttle } from 'lodash';
import { import {
GlLoadingIcon, GlLoadingIcon,
GlSearchBoxByType, GlSearchBoxByType,
...@@ -10,6 +10,11 @@ import { ...@@ -10,6 +10,11 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import projectQuery from '../queries/project_boards.query.graphql';
import groupQuery from '../queries/group_boards.query.graphql';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import BoardForm from './board_form.vue'; import BoardForm from './board_form.vue';
...@@ -88,8 +93,9 @@ export default { ...@@ -88,8 +93,9 @@ export default {
}, },
data() { data() {
return { return {
loading: true,
hasScrollFade: false, hasScrollFade: false,
loadingBoards: 0,
loadingRecentBoards: false,
scrollFadeInitialized: false, scrollFadeInitialized: false,
boards: [], boards: [],
recentBoards: [], recentBoards: [],
...@@ -102,6 +108,12 @@ export default { ...@@ -102,6 +108,12 @@ export default {
}; };
}, },
computed: { computed: {
parentType() {
return this.groupId ? 'group' : 'project';
},
loading() {
return this.loadingRecentBoards && this.loadingBoards;
},
currentPage() { currentPage() {
return this.state.currentPage; return this.state.currentPage;
}, },
...@@ -110,14 +122,6 @@ export default { ...@@ -110,14 +122,6 @@ export default {
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()), board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
); );
}, },
reload: {
get() {
return this.state.reload;
},
set(newValue) {
this.state.reload = newValue;
},
},
board() { board() {
return this.state.currentBoard; return this.state.currentBoard;
}, },
...@@ -142,16 +146,6 @@ export default { ...@@ -142,16 +146,6 @@ export default {
this.scrollFadeInitialized = false; this.scrollFadeInitialized = false;
this.$nextTick(this.setScrollFade); this.$nextTick(this.setScrollFade);
}, },
reload() {
if (this.reload) {
this.boards = [];
this.recentBoards = [];
this.loading = true;
this.reload = false;
this.loadBoards(false);
}
},
}, },
created() { created() {
boardsStore.setCurrentBoard(this.currentBoard); boardsStore.setCurrentBoard(this.currentBoard);
...@@ -165,49 +159,71 @@ export default { ...@@ -165,49 +159,71 @@ export default {
return; return;
} }
const recentBoardsPromise = new Promise((resolve, reject) => this.$apollo.addSmartQuery('boards', {
variables() {
return { fullPath: this.state.endpoints.fullPath };
},
query() {
return this.groupId ? groupQuery : projectQuery;
},
loadingKey: 'loadingBoards',
update(data) {
if (!data?.[this.parentType]) {
return [];
}
return data[this.parentType].boards.edges.map(({ node }) => ({
id: getIdFromGraphQLId(node.id),
name: node.name,
}));
},
});
this.loadingRecentBoards = true;
boardsStore boardsStore
.recentBoards() .recentBoards()
.then(resolve) .then(res => {
this.recentBoards = res.data;
})
.catch(err => { .catch(err => {
/** /**
* If user is unauthorized we'd still want to resolve the * If user is unauthorized we'd still want to resolve the
* request to display all boards. * request to display all boards.
*/ */
if (err.response.status === httpStatusCodes.UNAUTHORIZED) { if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
resolve({ data: [] }); // recent boards are empty this.recentBoards = []; // recent boards are empty
return; return;
} }
reject(err); throw err;
}),
);
Promise.all([boardsStore.allBoards(), recentBoardsPromise])
.then(([allBoards, recentBoards]) => [allBoards.data, recentBoards.data])
.then(([allBoardsJson, recentBoardsJson]) => {
this.loading = false;
this.boards = allBoardsJson;
this.recentBoards = recentBoardsJson;
}) })
.then(() => this.$nextTick()) // Wait for boards list in DOM .then(() => this.$nextTick()) // Wait for boards list in DOM
.then(() => { .then(() => {
this.setScrollFade(); this.setScrollFade();
}) })
.catch(() => { .catch(() => {})
this.loading = false; .finally(() => {
this.loadingRecentBoards = false;
}); });
}, },
isScrolledUp() { isScrolledUp() {
const { content } = this.$refs; const { content } = this.$refs;
if (!content) {
return false;
}
const currentPosition = this.contentClientHeight + content.scrollTop; const currentPosition = this.contentClientHeight + content.scrollTop;
return content && currentPosition < this.maxPosition; return currentPosition < this.maxPosition;
}, },
initScrollFade() { initScrollFade() {
this.scrollFadeInitialized = true;
const { content } = this.$refs; const { content } = this.$refs;
if (!content) {
return;
}
this.scrollFadeInitialized = true;
this.contentClientHeight = content.clientHeight; this.contentClientHeight = content.clientHeight;
this.maxPosition = content.scrollHeight; this.maxPosition = content.scrollHeight;
}, },
... ...
......
<script> <script>
import _ from 'underscore'; import { sortBy } from 'lodash';
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlLabel, GlTooltipDirective } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner'; import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
...@@ -10,18 +10,17 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_ ...@@ -10,18 +10,17 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_
import IssueDueDate from './issue_due_date.vue'; import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue'; import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import IssueCardInnerScopedLabel from './issue_card_inner_scoped_label.vue';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
export default { export default {
components: { components: {
GlLabel,
Icon, Icon,
UserAvatarLink, UserAvatarLink,
TooltipOnTruncate, TooltipOnTruncate,
IssueDueDate, IssueDueDate,
IssueTimeEstimate, IssueTimeEstimate,
IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
IssueCardInnerScopedLabel,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -101,10 +100,7 @@ export default { ...@@ -101,10 +100,7 @@ export default {
return !groupId ? referencePath.split('#')[0] : null; return !groupId ? referencePath.split('#')[0] : null;
}, },
orderedLabels() { orderedLabels() {
return _.chain(this.issue.labels) return sortBy(this.issue.labels.filter(this.isNonListLabel), 'title');
.filter(this.isNonListLabel)
.sortBy('title')
.value();
}, },
helpLink() { helpLink() {
return boardsStore.scopedLabels.helpLink; return boardsStore.scopedLabels.helpLink;
...@@ -145,12 +141,6 @@ export default { ...@@ -145,12 +141,6 @@ export default {
boardsStore.toggleFilter(filter); boardsStore.toggleFilter(filter);
}, },
labelStyle(label) {
return {
backgroundColor: label.color,
color: label.textColor,
};
},
showScopedLabel(label) { showScopedLabel(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label); return boardsStore.scopedLabels.enabled && isScopedLabel(label);
}, },
...@@ -177,34 +167,23 @@ export default { ...@@ -177,34 +167,23 @@ export default {
class="confidential-icon append-right-4" class="confidential-icon append-right-4"
:aria-label="__('Confidential')" :aria-label="__('Confidential')"
/> />
<a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop> <a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>{{
{{ issue.title }} issue.title
</a> }}</a>
</h4> </h4>
</div> </div>
<div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap"> <div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap">
<template v-for="label in orderedLabels"> <template v-for="label in orderedLabels">
<issue-card-inner-scoped-label <gl-label
v-if="showScopedLabel(label)"
:key="label.id" :key="label.id"
:label="label" :background-color="label.color"
:label-style="labelStyle(label)" :title="label.title"
:description="label.description"
size="sm"
:scoped="showScopedLabel(label)"
:scoped-labels-documentation-link="helpLink" :scoped-labels-documentation-link="helpLink"
@scoped-label-click="filterByLabel($event)"
/>
<button
v-else
:key="label.id"
v-gl-tooltip
:style="labelStyle(label)"
:title="label.description"
class="badge color-label append-right-4 prepend-top-4"
type="button"
@click="filterByLabel(label)" @click="filterByLabel(label)"
> />
{{ label.title }}
</button>
</template> </template>
</div> </div>
<div class="board-card-footer d-flex justify-content-between align-items-end"> <div class="board-card-footer d-flex justify-content-between align-items-end">
...@@ -225,7 +204,7 @@ export default { ...@@ -225,7 +204,7 @@ export default {
#{{ issue.iid }} #{{ issue.iid }}
</span> </span>
<span class="board-info-items prepend-top-8 d-inline-block"> <span class="board-info-items prepend-top-8 d-inline-block">
<issue-due-date v-if="issue.dueDate" :date="issue.dueDate" /> <issue-due-date v-if="issue.dueDate" :date="issue.dueDate" :closed="issue.closed" />
<issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" /> <issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
<issue-card-weight <issue-card-weight
v-if="validIssueWeight" v-if="validIssueWeight"
... ...
......
<script>
import { GlLink, GlTooltip } from '@gitlab/ui';
export default {
components: {
GlTooltip,
GlLink,
},
props: {
label: {
type: Object,
required: true,
},
labelStyle: {
type: Object,
required: true,
},
scopedLabelsDocumentationLink: {
type: String,
required: true,
},
},
};
</script>
<template>
<span
class="d-inline-block position-relative scoped-label-wrapper append-right-4 prepend-top-4 board-label"
>
<a @click="$emit('scoped-label-click', label)">
<span :ref="'labelTitleRef'" :style="labelStyle" class="badge label color-label">
{{ label.title }}
</span>
<gl-tooltip :target="() => $refs.labelTitleRef" placement="top" boundary="viewport">
<span class="font-weight-bold scoped-label-tooltip-title">{{ __('Scoped label') }}</span
><br />
{{ label.description }}
</gl-tooltip>
</a>
<gl-link :href="scopedLabelsDocumentationLink" target="_blank" class="label scoped-label"
><i class="fa fa-question-circle" :style="labelStyle"></i
></gl-link>
</span>
</template>
...@@ -16,6 +16,11 @@ export default { ...@@ -16,6 +16,11 @@ export default {
GlTooltip, GlTooltip,
}, },
props: { props: {
closed: {
type: Boolean,
required: false,
default: false,
},
date: { date: {
type: String, type: String,
required: true, required: true,
...@@ -66,7 +71,7 @@ export default { ...@@ -66,7 +71,7 @@ export default {
return getDayDifference(today, this.issueDueDate); return getDayDifference(today, this.issueDueDate);
}, },
isPastDue() { isPastDue() {
if (this.timeDifference >= 0) return false; if (this.timeDifference >= 0 || this.closed) return false;
return true; return true;
}, },
standardDateFormat() { standardDateFormat() {
...@@ -92,7 +97,8 @@ export default { ...@@ -92,7 +97,8 @@ export default {
}}</time> }}</time>
</span> </span>
<gl-tooltip :target="() => $refs.issueDueDate" :placement="tooltipPlacement"> <gl-tooltip :target="() => $refs.issueDueDate" :placement="tooltipPlacement">
<span class="bold">{{ __('Due date') }}</span> <br /> <span class="bold">{{ __('Due date') }}</span>
<br />
<span :class="{ 'text-danger-muted': isPastDue }">{{ title }}</span> <span :class="{ 'text-danger-muted': isPastDue }">{{ title }}</span>
</gl-tooltip> </gl-tooltip>
</span> </span>
... ...
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import { escape } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -83,7 +83,7 @@ export default { ...@@ -83,7 +83,7 @@ export default {
}" data-project-name="${project.name}" data-project-name-with-namespace="${ }" data-project-name="${project.name}" data-project-name-with-namespace="${
project.name_with_namespace project.name_with_namespace
}"> }">
${_.escape(project.name_with_namespace)} ${escape(project.name_with_namespace)}
</a> </a>
</li> </li>
`; `;
... ...
......
...@@ -84,7 +84,6 @@ export default () => { ...@@ -84,7 +84,6 @@ export default () => {
rootPath: $boardApp.dataset.rootPath, rootPath: $boardApp.dataset.rootPath,
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: boardsStore.detail, detailIssue: boardsStore.detail,
defaultAvatar: $boardApp.dataset.defaultAvatar,
}, },
computed: { computed: {
detailIssueVisible() { detailIssueVisible() {
...@@ -98,6 +97,7 @@ export default () => { ...@@ -98,6 +97,7 @@ export default () => {
listsEndpoint: this.listsEndpoint, listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath, bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId, boardId: this.boardId,
fullPath: $boardApp.dataset.fullPath,
}); });
boardsStore.rootPath = this.boardsEndpoint; boardsStore.rootPath = this.boardsEndpoint;
...@@ -129,13 +129,10 @@ export default () => { ...@@ -129,13 +129,10 @@ export default () => {
position = -1; position = -1;
} }
boardsStore.addList( boardsStore.addList({
{
...listObj, ...listObj,
position, position,
}, });
this.defaultAvatar,
);
}); });
boardsStore.addBlankState(); boardsStore.addBlankState();
... ...
......
...@@ -26,7 +26,6 @@ export function getBoardSortableDefaultOptions(obj) { ...@@ -26,7 +26,6 @@ export function getBoardSortableDefaultOptions(obj) {
scrollSpeed: 20, scrollSpeed: 20,
onStart: sortableStart, onStart: sortableStart,
onEnd: sortableEnd, onEnd: sortableEnd,
fallbackTolerance: 1,
}); });
Object.keys(obj).forEach(key => { Object.keys(obj).forEach(key => {
... ...
......
export default class ListAssignee { export default class ListAssignee {
constructor(obj, defaultAvatar) { constructor(obj) {
this.id = obj.id; this.id = obj.id;
this.name = obj.name; this.name = obj.name;
this.username = obj.username; this.username = obj.username;
this.avatar = obj.avatar_url || obj.avatar || defaultAvatar; this.avatar = obj.avatar_url || obj.avatar || gon.default_avatar_url;
this.path = obj.path; this.path = obj.path;
this.state = obj.state; this.state = obj.state;
this.webUrl = obj.web_url || obj.webUrl; this.webUrl = obj.web_url || obj.webUrl;
... ...
......
...@@ -10,7 +10,7 @@ import IssueProject from './project'; ...@@ -10,7 +10,7 @@ import IssueProject from './project';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
class ListIssue { class ListIssue {
constructor(obj, defaultAvatar) { constructor(obj) {
this.subscribed = obj.subscribed; this.subscribed = obj.subscribed;
this.labels = []; this.labels = [];
this.assignees = []; this.assignees = [];
...@@ -19,42 +19,14 @@ class ListIssue { ...@@ -19,42 +19,14 @@ class ListIssue {
this.isFetching = { this.isFetching = {
subscriptions: true, subscriptions: true,
}; };
this.closed = obj.closed;
this.isLoading = {}; this.isLoading = {};
this.refreshData(obj, defaultAvatar); this.refreshData(obj);
} }
refreshData(obj, defaultAvatar) { refreshData(obj) {
this.id = obj.id; boardsStore.refreshIssueData(this, obj);
this.iid = obj.iid;
this.title = obj.title;
this.confidential = obj.confidential;
this.dueDate = obj.due_date;
this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
this.referencePath = obj.reference_path;
this.path = obj.real_path;
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.project_id = obj.project_id;
this.timeEstimate = obj.time_estimate;
this.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
this.blocked = obj.blocked;
if (obj.project) {
this.project = new IssueProject(obj.project);
}
if (obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
this.milestone_id = obj.milestone.id;
}
if (obj.labels) {
this.labels = obj.labels.map(label => new ListLabel(label));
}
if (obj.assignees) {
this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar));
}
} }
addLabel(label) { addLabel(label) {
... ...
......
...@@ -36,7 +36,7 @@ const TYPES = { ...@@ -36,7 +36,7 @@ const TYPES = {
}; };
class List { class List {
constructor(obj, defaultAvatar) { constructor(obj) {
this.id = obj.id; this.id = obj.id;
this._uid = this.guid(); this._uid = this.guid();
this.position = obj.position; this.position = obj.position;
...@@ -55,7 +55,6 @@ class List { ...@@ -55,7 +55,6 @@ class List {
this.maxIssueCount = Object.hasOwnProperty.call(obj, 'max_issue_count') this.maxIssueCount = Object.hasOwnProperty.call(obj, 'max_issue_count')
? obj.max_issue_count ? obj.max_issue_count
: 0; : 0;
this.defaultAvatar = defaultAvatar;
if (obj.label) { if (obj.label) {
this.label = new ListLabel(obj.label); this.label = new ListLabel(obj.label);
...@@ -156,7 +155,7 @@ class List { ...@@ -156,7 +155,7 @@ class List {
createIssues(data) { createIssues(data) {
data.forEach(issueObj => { data.forEach(issueObj => {
this.addIssue(new ListIssue(issueObj, this.defaultAvatar)); this.addIssue(new ListIssue(issueObj));
}); });
} }
... ...
......
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import BoardsSelector from '~/boards/components/boards_selector.vue'; import BoardsSelector from '~/boards/components/boards_selector.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => { export default () => {
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher'); const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
return new Vue({ return new Vue({
...@@ -9,6 +17,7 @@ export default () => { ...@@ -9,6 +17,7 @@ export default () => {
components: { components: {
BoardsSelector, BoardsSelector,
}, },
apolloProvider,
data() { data() {
const { dataset } = boardsSwitcherElement; const { dataset } = boardsSwitcherElement;
... ...
......
fragment BoardFragment on Board {
id,
name
}
#import "ee_else_ce/boards/queries/board.fragment.graphql"
query group_boards($fullPath: ID!) {
group(fullPath: $fullPath) {
boards {
edges {
node {
...BoardFragment
}
}
}
}
}
#import "ee_else_ce/boards/queries/board.fragment.graphql"
query project_boards($fullPath: ID!) {
project(fullPath: $fullPath) {
boards {
edges {
node {
...BoardFragment
}
}
}
}
}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/* global List */ /* global List */
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import { sortBy } from 'lodash';
import Vue from 'vue'; import Vue from 'vue';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee'; import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
...@@ -12,6 +12,10 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -12,6 +12,10 @@ import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import { ListType } from '../constants'; import { ListType } from '../constants';
import IssueProject from '../models/project';
import ListLabel from '../models/label';
import ListAssignee from '../models/assignee';
import ListMilestone from '../models/milestone';
const boardsStore = { const boardsStore = {
disabled: false, disabled: false,
...@@ -30,7 +34,6 @@ const boardsStore = { ...@@ -30,7 +34,6 @@ const boardsStore = {
labels: [], labels: [],
}, },
currentPage: '', currentPage: '',
reload: false,
endpoints: {}, endpoints: {},
}, },
detail: { detail: {
...@@ -42,7 +45,14 @@ const boardsStore = { ...@@ -42,7 +45,14 @@ const boardsStore = {
}, },
multiSelect: { list: [] }, multiSelect: { list: [] },
setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) { setEndpoints({
boardsEndpoint,
listsEndpoint,
bulkUpdatePath,
boardId,
recentBoardsEndpoint,
fullPath,
}) {
const listsEndpointGenerate = `${listsEndpoint}/generate.json`; const listsEndpointGenerate = `${listsEndpoint}/generate.json`;
this.state.endpoints = { this.state.endpoints = {
boardsEndpoint, boardsEndpoint,
...@@ -50,6 +60,7 @@ const boardsStore = { ...@@ -50,6 +60,7 @@ const boardsStore = {
listsEndpoint, listsEndpoint,
listsEndpointGenerate, listsEndpointGenerate,
bulkUpdatePath, bulkUpdatePath,
fullPath,
recentBoardsEndpoint: `${recentBoardsEndpoint}.json`, recentBoardsEndpoint: `${recentBoardsEndpoint}.json`,
}; };
}, },
...@@ -61,13 +72,11 @@ const boardsStore = { ...@@ -61,13 +72,11 @@ const boardsStore = {
}; };
}, },
showPage(page) { showPage(page) {
this.state.reload = false;
this.state.currentPage = page; this.state.currentPage = page;
}, },
addList(listObj, defaultAvatar) { addList(listObj) {
const list = new List(listObj, defaultAvatar); const list = new List(listObj);
this.state.lists = _.sortBy([...this.state.lists, list], 'position'); this.state.lists = sortBy([...this.state.lists, list], 'position');
return list; return list;
}, },
new(listObj) { new(listObj) {
...@@ -80,7 +89,7 @@ const boardsStore = { ...@@ -80,7 +89,7 @@ const boardsStore = {
// Remove any new issues from the backlog // Remove any new issues from the backlog
// as they will be visible in the new list // as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList)); list.issues.forEach(backlogList.removeIssue.bind(backlogList));
this.state.lists = _.sortBy(this.state.lists, 'position'); this.state.lists = sortBy(this.state.lists, 'position');
}) })
.catch(() => { .catch(() => {
// https://gitlab.com/gitlab-org/gitlab-foss/issues/30821 // https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
...@@ -184,10 +193,9 @@ const boardsStore = { ...@@ -184,10 +193,9 @@ const boardsStore = {
moveMultipleIssuesToList({ listFrom, listTo, issues, newIndex }) { moveMultipleIssuesToList({ listFrom, listTo, issues, newIndex }) {
const issueTo = issues.map(issue => listTo.findIssue(issue.id)); const issueTo = issues.map(issue => listTo.findIssue(issue.id));
const issueLists = _.flatten(issues.map(issue => issue.getLists())); const issueLists = issues.map(issue => issue.getLists()).flat();
const listLabels = issueLists.map(list => list.label); const listLabels = issueLists.map(list => list.label);
const hasMoveableIssues = issueTo.filter(Boolean).length > 0;
const hasMoveableIssues = _.compact(issueTo).length > 0;
if (!hasMoveableIssues) { if (!hasMoveableIssues) {
// Check if target list assignee is already present in this issue // Check if target list assignee is already present in this issue
...@@ -335,7 +343,8 @@ const boardsStore = { ...@@ -335,7 +343,8 @@ const boardsStore = {
return ( return (
(listTo.type !== 'label' && listFrom.type === 'assignee') || (listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label') || (listTo.type !== 'assignee' && listFrom.type === 'label') ||
listFrom.type === 'backlog' listFrom.type === 'backlog' ||
listFrom.type === 'closed'
); );
}, },
moveIssueInList(list, issue, oldIndex, newIndex, idArray) { moveIssueInList(list, issue, oldIndex, newIndex, idArray) {
...@@ -539,10 +548,6 @@ const boardsStore = { ...@@ -539,10 +548,6 @@ const boardsStore = {
return axios.post(endpoint); return axios.post(endpoint);
}, },
allBoards() {
return axios.get(this.generateBoardsPath());
},
recentBoards() { recentBoards() {
return axios.get(this.state.endpoints.recentBoardsEndpoint); return axios.get(this.state.endpoints.recentBoardsEndpoint);
}, },
...@@ -595,6 +600,38 @@ const boardsStore = { ...@@ -595,6 +600,38 @@ const boardsStore = {
clearMultiSelect() { clearMultiSelect() {
this.multiSelect.list = []; this.multiSelect.list = [];
}, },
refreshIssueData(issue, obj) {
issue.id = obj.id;
issue.iid = obj.iid;
issue.title = obj.title;
issue.confidential = obj.confidential;
issue.dueDate = obj.due_date;
issue.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
issue.referencePath = obj.reference_path;
issue.path = obj.real_path;
issue.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
issue.project_id = obj.project_id;
issue.timeEstimate = obj.time_estimate;
issue.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
issue.blocked = obj.blocked;
if (obj.project) {
issue.project = new IssueProject(obj.project);
}
if (obj.milestone) {
issue.milestone = new ListMilestone(obj.milestone);
issue.milestone_id = obj.milestone.id;
}
if (obj.labels) {
issue.labels = obj.labels.map(label => new ListLabel(label));
}
if (obj.assignees) {
issue.assignees = obj.assignees.map(a => new ListAssignee(a));
}
},
}; };
BoardsStoreEE.initEESpecific(boardsStore); BoardsStoreEE.initEESpecific(boardsStore);
... ...
......
...@@ -6,16 +6,14 @@ const handleOnDismiss = ({ currentTarget }) => { ...@@ -6,16 +6,14 @@ const handleOnDismiss = ({ currentTarget }) => {
dataset: { id }, dataset: { id },
} = currentTarget; } = currentTarget;
Cookies.set(`hide_broadcast_notification_message_${id}`, true); Cookies.set(`hide_broadcast_message_${id}`, true);
const notification = document.querySelector(`.js-broadcast-notification-${id}`); const notification = document.querySelector(`.js-broadcast-notification-${id}`);
notification.parentNode.removeChild(notification); notification.parentNode.removeChild(notification);
}; };
export default () => { export default () => {
const dismissButton = document.querySelector('.js-dismiss-current-broadcast-notification'); document
.querySelectorAll('.js-dismiss-current-broadcast-notification')
if (dismissButton) { .forEach(dismissButton => dismissButton.addEventListener('click', handleOnDismiss));
dismissButton.addEventListener('click', handleOnDismiss);
}
}; };
<script>
import { __ } from '~/locale';
import { mapActions, mapState } from 'vuex';
import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
import {
GlButton,
GlModal,
GlFormSelect,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlFormCheckbox,
GlLink,
GlIcon,
} from '@gitlab/ui';
export default {
modalId: ADD_CI_VARIABLE_MODAL_ID,
components: {
GlButton,
GlModal,
GlFormSelect,
GlFormGroup,
GlFormInput,
GlFormTextarea,
GlFormCheckbox,
GlLink,
GlIcon,
},
computed: {
...mapState([
'projectId',
'environments',
'typeOptions',
'variable',
'variableBeingEdited',
'isGroup',
'maskableRegex',
]),
canSubmit() {
if (this.variableData.masked && this.maskedState === false) {
return false;
}
return this.variableData.key !== '' && this.variableData.secret_value !== '';
},
canMask() {
const regex = RegExp(this.maskableRegex);
return regex.test(this.variableData.secret_value);
},
displayMaskedError() {
return !this.canMask && this.variableData.masked && this.variableData.secret_value !== '';
},
maskedState() {
if (this.displayMaskedError) {
return false;
}
return null;
},
variableData() {
return this.variableBeingEdited || this.variable;
},
modalActionText() {
return this.variableBeingEdited ? __('Update variable') : __('Add variable');
},
primaryAction() {
return {
text: this.modalActionText,
attributes: { variant: 'success', disabled: !this.canSubmit },
};
},
maskedFeedback() {
return __('This variable can not be masked');
},
},
methods: {
...mapActions([
'addVariable',
'updateVariable',
'resetEditing',
'displayInputValue',
'clearModal',
'deleteVariable',
]),
updateOrAddVariable() {
if (this.variableBeingEdited) {
this.updateVariable(this.variableBeingEdited);
} else {
this.addVariable();
}
this.hideModal();
},
resetModalHandler() {
if (this.variableBeingEdited) {
this.resetEditing();
} else {
this.clearModal();
}
},
hideModal() {
this.$refs.modal.hide();
},
deleteVarAndClose() {
this.deleteVariable(this.variableBeingEdited);
this.hideModal();
},
},
};
</script>
<template>
<gl-modal
ref="modal"
:modal-id="$options.modalId"
:title="modalActionText"
@hidden="resetModalHandler"
>
<form>
<gl-form-group :label="__('Key')" label-for="ci-variable-key">
<gl-form-input
id="ci-variable-key"
v-model="variableData.key"
data-qa-selector="variable_key"
/>
</gl-form-group>
<gl-form-group
:label="__('Value')"
label-for="ci-variable-value"
:state="maskedState"
:invalid-feedback="maskedFeedback"
>
<gl-form-textarea
id="ci-variable-value"
v-model="variableData.secret_value"
rows="3"
max-rows="6"
data-qa-selector="variable_value"
/>
</gl-form-group>
<div class="d-flex">
<gl-form-group
:label="__('Type')"
label-for="ci-variable-type"
class="w-50 append-right-15"
:class="{ 'w-100': isGroup }"
>
<gl-form-select
id="ci-variable-type"
v-model="variableData.variable_type"
:options="typeOptions"
/>
</gl-form-group>
<gl-form-group
v-if="!isGroup"
:label="__('Environment scope')"
label-for="ci-variable-env"
class="w-50"
>
<gl-form-select
id="ci-variable-env"
v-model="variableData.environment_scope"
:options="environments"
/>
</gl-form-group>
</div>
<gl-form-group :label="__('Flags')" label-for="ci-variable-flags">
<gl-form-checkbox v-model="variableData.protected" class="mb-0">
{{ __('Protect variable') }}
<gl-link href="/help/ci/variables/README#protected-environment-variables">
<gl-icon name="question" :size="12" />
</gl-link>
<p class="prepend-top-4 text-secondary">
{{ __('Export variable to pipelines running on protected branches and tags only.') }}
</p>
</gl-form-checkbox>
<gl-form-checkbox
ref="masked-ci-variable"
v-model="variableData.masked"
data-qa-selector="variable_masked"
>
{{ __('Mask variable') }}
<gl-link href="/help/ci/variables/README#masked-variables">
<gl-icon name="question" :size="12" />
</gl-link>
<p class="prepend-top-4 append-bottom-0 text-secondary">
{{ __('Variable will be masked in job logs.') }}
<span
:class="{
'bold text-plain': displayMaskedError,
}"
>
{{ __('Requires values to meet regular expression requirements.') }}</span
>
<gl-link href="/help/ci/variables/README#masked-variables">{{
__('More information')
}}</gl-link>
</p>
</gl-form-checkbox>
</gl-form-group>
</form>
<template #modal-footer>
<gl-button @click="hideModal">{{ __('Cancel') }}</gl-button>
<gl-button
v-if="variableBeingEdited"
ref="deleteCiVariable"
category="secondary"
variant="danger"
@click="deleteVarAndClose"
>{{ __('Delete variable') }}</gl-button
>
<gl-button
ref="updateOrAddVariable"
:disabled="!canSubmit"
variant="success"
@click="updateOrAddVariable"
>{{ modalActionText }}
</gl-button>
</template>
</gl-modal>
</template>
<script>
import { GlPopover, GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
export default {
maxTextLength: 95,
components: {
GlPopover,
GlIcon,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
target: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
tooltipText: {
type: String,
required: true,
},
},
computed: {
displayValue() {
if (this.value.length > this.$options.maxTextLength) {
return `${this.value.substring(0, this.$options.maxTextLength)}...`;
}
return this.value;
},
},
};
</script>
<template>
<div id="popover-container">
<gl-popover :target="target" triggers="hover" placement="top" container="popover-container">
<div class="d-flex justify-content-between position-relative">
<div class="pr-5 w-100 ci-popover-value">{{ displayValue }}</div>
<gl-button
v-gl-tooltip
class="btn-transparent btn-clipboard position-absolute position-top-0 position-right-0"
:title="tooltipText"
:data-clipboard-text="value"
>
<gl-icon name="copy-to-clipboard" />
</gl-button>
</div>
</gl-popover>
</div>
</template>
<script>
import CiVariableModal from './ci_variable_modal.vue';
import CiVariableTable from './ci_variable_table.vue';
import { mapState, mapActions } from 'vuex';
export default {
components: {
CiVariableModal,
CiVariableTable,
},
computed: {
...mapState(['isGroup']),
},
mounted() {
if (!this.isGroup) {
this.fetchEnvironments();
}
},
methods: {
...mapActions(['fetchEnvironments']),
},
};
</script>
<template>
<div class="row">
<div class="col-lg-12">
<ci-variable-table />
<ci-variable-modal />
</div>
</div>
</template>