Skip to content
Snippets Groups Projects
Commit 1d20d436 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@12-9-stable-ee

parent 2774ddc3
No related branches found
No related tags found
No related merge requests found
Showing
with 516 additions and 206 deletions
Loading
Loading
@@ -2,6 +2,7 @@
 
import $ from 'jquery';
import Vue from 'vue';
import { GlLabel } from '@gitlab/ui';
import Flash from '~/flash';
import { sprintf, __ } from '~/locale';
import Sidebar from '~/right_sidebar';
Loading
Loading
@@ -22,6 +23,7 @@ export default Vue.extend({
components: {
AssigneeTitle,
Assignees,
GlLabel,
SidebarEpicsSelect: () =>
import('ee_component/sidebar/components/sidebar_item_epics_select.vue'),
RemoveBtn,
Loading
Loading
@@ -67,6 +69,9 @@ export default Vue.extend({
selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
},
helpLink() {
return boardsStore.scopedLabels.helpLink;
},
},
watch: {
detail: {
Loading
Loading
@@ -147,8 +152,5 @@ export default Vue.extend({
showScopedLabels(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
helpLink() {
return boardsStore.scopedLabels.helpLink;
},
},
});
<script>
import { throttle } from 'underscore';
import { throttle } from 'lodash';
import {
GlLoadingIcon,
GlSearchBoxByType,
Loading
Loading
@@ -10,6 +10,11 @@ import {
} from '@gitlab/ui';
 
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 BoardForm from './board_form.vue';
 
Loading
Loading
@@ -88,8 +93,9 @@ export default {
},
data() {
return {
loading: true,
hasScrollFade: false,
loadingBoards: 0,
loadingRecentBoards: false,
scrollFadeInitialized: false,
boards: [],
recentBoards: [],
Loading
Loading
@@ -102,6 +108,12 @@ export default {
};
},
computed: {
parentType() {
return this.groupId ? 'group' : 'project';
},
loading() {
return this.loadingRecentBoards && this.loadingBoards;
},
currentPage() {
return this.state.currentPage;
},
Loading
Loading
@@ -110,14 +122,6 @@ export default {
board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
);
},
reload: {
get() {
return this.state.reload;
},
set(newValue) {
this.state.reload = newValue;
},
},
board() {
return this.state.currentBoard;
},
Loading
Loading
@@ -142,16 +146,6 @@ export default {
this.scrollFadeInitialized = false;
this.$nextTick(this.setScrollFade);
},
reload() {
if (this.reload) {
this.boards = [];
this.recentBoards = [];
this.loading = true;
this.reload = false;
this.loadBoards(false);
}
},
},
created() {
boardsStore.setCurrentBoard(this.currentBoard);
Loading
Loading
@@ -165,49 +159,71 @@ export default {
return;
}
 
const recentBoardsPromise = new Promise((resolve, reject) =>
boardsStore
.recentBoards()
.then(resolve)
.catch(err => {
/**
* If user is unauthorized we'd still want to resolve the
* request to display all boards.
*/
if (err.response.status === httpStatusCodes.UNAUTHORIZED) {
resolve({ data: [] }); // recent boards are empty
return;
}
reject(err);
}),
);
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,
}));
},
});
 
Promise.all([boardsStore.allBoards(), recentBoardsPromise])
.then(([allBoards, recentBoards]) => [allBoards.data, recentBoards.data])
.then(([allBoardsJson, recentBoardsJson]) => {
this.loading = false;
this.boards = allBoardsJson;
this.recentBoards = recentBoardsJson;
this.loadingRecentBoards = true;
boardsStore
.recentBoards()
.then(res => {
this.recentBoards = res.data;
})
.catch(err => {
/**
* If user is unauthorized we'd still want to resolve the
* request to display all boards.
*/
if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
this.recentBoards = []; // recent boards are empty
return;
}
throw err;
})
.then(() => this.$nextTick()) // Wait for boards list in DOM
.then(() => {
this.setScrollFade();
})
.catch(() => {
this.loading = false;
.catch(() => {})
.finally(() => {
this.loadingRecentBoards = false;
});
},
isScrolledUp() {
const { content } = this.$refs;
if (!content) {
return false;
}
const currentPosition = this.contentClientHeight + content.scrollTop;
 
return content && currentPosition < this.maxPosition;
return currentPosition < this.maxPosition;
},
initScrollFade() {
this.scrollFadeInitialized = true;
const { content } = this.$refs;
 
if (!content) {
return;
}
this.scrollFadeInitialized = true;
this.contentClientHeight = content.clientHeight;
this.maxPosition = content.scrollHeight;
},
Loading
Loading
<script>
import _ from 'underscore';
import { sortBy } from 'lodash';
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 { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
Loading
Loading
@@ -10,18 +10,17 @@ import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store';
import IssueCardInnerScopedLabel from './issue_card_inner_scoped_label.vue';
import { isScopedLabel } from '~/lib/utils/common_utils';
 
export default {
components: {
GlLabel,
Icon,
UserAvatarLink,
TooltipOnTruncate,
IssueDueDate,
IssueTimeEstimate,
IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
IssueCardInnerScopedLabel,
},
directives: {
GlTooltip: GlTooltipDirective,
Loading
Loading
@@ -101,10 +100,7 @@ export default {
return !groupId ? referencePath.split('#')[0] : null;
},
orderedLabels() {
return _.chain(this.issue.labels)
.filter(this.isNonListLabel)
.sortBy('title')
.value();
return sortBy(this.issue.labels.filter(this.isNonListLabel), 'title');
},
helpLink() {
return boardsStore.scopedLabels.helpLink;
Loading
Loading
@@ -145,12 +141,6 @@ export default {
 
boardsStore.toggleFilter(filter);
},
labelStyle(label) {
return {
backgroundColor: label.color,
color: label.textColor,
};
},
showScopedLabel(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
Loading
Loading
@@ -177,34 +167,23 @@ export default {
class="confidential-icon append-right-4"
:aria-label="__('Confidential')"
/>
<a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>
{{ issue.title }}
</a>
<a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>{{
issue.title
}}</a>
</h4>
</div>
<div v-if="showLabelFooter" class="board-card-labels prepend-top-4 d-flex flex-wrap">
<template v-for="label in orderedLabels">
<issue-card-inner-scoped-label
v-if="showScopedLabel(label)"
<gl-label
:key="label.id"
:label="label"
:label-style="labelStyle(label)"
:background-color="label.color"
:title="label.title"
:description="label.description"
size="sm"
:scoped="showScopedLabel(label)"
: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)"
>
{{ label.title }}
</button>
/>
</template>
</div>
<div class="board-card-footer d-flex justify-content-between align-items-end">
Loading
Loading
@@ -225,7 +204,7 @@ export default {
#{{ issue.iid }}
</span>
<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-card-weight
v-if="validIssueWeight"
Loading
Loading
<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>
Loading
Loading
@@ -16,6 +16,11 @@ export default {
GlTooltip,
},
props: {
closed: {
type: Boolean,
required: false,
default: false,
},
date: {
type: String,
required: true,
Loading
Loading
@@ -66,7 +71,7 @@ export default {
return getDayDifference(today, this.issueDueDate);
},
isPastDue() {
if (this.timeDifference >= 0) return false;
if (this.timeDifference >= 0 || this.closed) return false;
return true;
},
standardDateFormat() {
Loading
Loading
@@ -92,7 +97,8 @@ export default {
}}</time>
</span>
<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>
</gl-tooltip>
</span>
Loading
Loading
<script>
import $ from 'jquery';
import _ from 'underscore';
import { escape } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
Loading
Loading
@@ -83,7 +83,7 @@ export default {
}" data-project-name="${project.name}" data-project-name-with-namespace="${
project.name_with_namespace
}">
${_.escape(project.name_with_namespace)}
${escape(project.name_with_namespace)}
</a>
</li>
`;
Loading
Loading
Loading
Loading
@@ -84,7 +84,6 @@ export default () => {
rootPath: $boardApp.dataset.rootPath,
bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: boardsStore.detail,
defaultAvatar: $boardApp.dataset.defaultAvatar,
},
computed: {
detailIssueVisible() {
Loading
Loading
@@ -98,6 +97,7 @@ export default () => {
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
boardId: this.boardId,
fullPath: $boardApp.dataset.fullPath,
});
boardsStore.rootPath = this.boardsEndpoint;
 
Loading
Loading
@@ -129,13 +129,10 @@ export default () => {
position = -1;
}
 
boardsStore.addList(
{
...listObj,
position,
},
this.defaultAvatar,
);
boardsStore.addList({
...listObj,
position,
});
});
 
boardsStore.addBlankState();
Loading
Loading
Loading
Loading
@@ -26,7 +26,6 @@ export function getBoardSortableDefaultOptions(obj) {
scrollSpeed: 20,
onStart: sortableStart,
onEnd: sortableEnd,
fallbackTolerance: 1,
});
 
Object.keys(obj).forEach(key => {
Loading
Loading
export default class ListAssignee {
constructor(obj, defaultAvatar) {
constructor(obj) {
this.id = obj.id;
this.name = obj.name;
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.state = obj.state;
this.webUrl = obj.web_url || obj.webUrl;
Loading
Loading
Loading
Loading
@@ -10,7 +10,7 @@ import IssueProject from './project';
import boardsStore from '../stores/boards_store';
 
class ListIssue {
constructor(obj, defaultAvatar) {
constructor(obj) {
this.subscribed = obj.subscribed;
this.labels = [];
this.assignees = [];
Loading
Loading
@@ -19,42 +19,14 @@ class ListIssue {
this.isFetching = {
subscriptions: true,
};
this.closed = obj.closed;
this.isLoading = {};
 
this.refreshData(obj, defaultAvatar);
}
refreshData(obj, defaultAvatar) {
this.id = obj.id;
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));
}
this.refreshData(obj);
}
 
if (obj.assignees) {
this.assignees = obj.assignees.map(a => new ListAssignee(a, defaultAvatar));
}
refreshData(obj) {
boardsStore.refreshIssueData(this, obj);
}
 
addLabel(label) {
Loading
Loading
Loading
Loading
@@ -36,7 +36,7 @@ const TYPES = {
};
 
class List {
constructor(obj, defaultAvatar) {
constructor(obj) {
this.id = obj.id;
this._uid = this.guid();
this.position = obj.position;
Loading
Loading
@@ -55,7 +55,6 @@ class List {
this.maxIssueCount = Object.hasOwnProperty.call(obj, 'max_issue_count')
? obj.max_issue_count
: 0;
this.defaultAvatar = defaultAvatar;
 
if (obj.label) {
this.label = new ListLabel(obj.label);
Loading
Loading
@@ -156,7 +155,7 @@ class List {
 
createIssues(data) {
data.forEach(issueObj => {
this.addIssue(new ListIssue(issueObj, this.defaultAvatar));
this.addIssue(new ListIssue(issueObj));
});
}
 
Loading
Loading
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import BoardsSelector from '~/boards/components/boards_selector.vue';
 
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => {
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
return new Vue({
Loading
Loading
@@ -9,6 +17,7 @@ export default () => {
components: {
BoardsSelector,
},
apolloProvider,
data() {
const { dataset } = boardsSwitcherElement;
 
Loading
Loading
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
}
}
}
}
}
Loading
Loading
@@ -2,7 +2,7 @@
/* global List */
 
import $ from 'jquery';
import _ from 'underscore';
import { sortBy } from 'lodash';
import Vue from 'vue';
import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
Loading
Loading
@@ -12,6 +12,10 @@ import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../eventhub';
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 = {
disabled: false,
Loading
Loading
@@ -30,7 +34,6 @@ const boardsStore = {
labels: [],
},
currentPage: '',
reload: false,
endpoints: {},
},
detail: {
Loading
Loading
@@ -42,7 +45,14 @@ const boardsStore = {
},
multiSelect: { list: [] },
 
setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
setEndpoints({
boardsEndpoint,
listsEndpoint,
bulkUpdatePath,
boardId,
recentBoardsEndpoint,
fullPath,
}) {
const listsEndpointGenerate = `${listsEndpoint}/generate.json`;
this.state.endpoints = {
boardsEndpoint,
Loading
Loading
@@ -50,6 +60,7 @@ const boardsStore = {
listsEndpoint,
listsEndpointGenerate,
bulkUpdatePath,
fullPath,
recentBoardsEndpoint: `${recentBoardsEndpoint}.json`,
};
},
Loading
Loading
@@ -61,13 +72,11 @@ const boardsStore = {
};
},
showPage(page) {
this.state.reload = false;
this.state.currentPage = page;
},
addList(listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
this.state.lists = _.sortBy([...this.state.lists, list], 'position');
addList(listObj) {
const list = new List(listObj);
this.state.lists = sortBy([...this.state.lists, list], 'position');
return list;
},
new(listObj) {
Loading
Loading
@@ -80,7 +89,7 @@ const boardsStore = {
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
this.state.lists = _.sortBy(this.state.lists, 'position');
this.state.lists = sortBy(this.state.lists, 'position');
})
.catch(() => {
// https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
Loading
Loading
@@ -184,10 +193,9 @@ const boardsStore = {
 
moveMultipleIssuesToList({ listFrom, listTo, issues, newIndex }) {
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 hasMoveableIssues = _.compact(issueTo).length > 0;
const hasMoveableIssues = issueTo.filter(Boolean).length > 0;
 
if (!hasMoveableIssues) {
// Check if target list assignee is already present in this issue
Loading
Loading
@@ -335,7 +343,8 @@ const boardsStore = {
return (
(listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label') ||
listFrom.type === 'backlog'
listFrom.type === 'backlog' ||
listFrom.type === 'closed'
);
},
moveIssueInList(list, issue, oldIndex, newIndex, idArray) {
Loading
Loading
@@ -539,10 +548,6 @@ const boardsStore = {
return axios.post(endpoint);
},
 
allBoards() {
return axios.get(this.generateBoardsPath());
},
recentBoards() {
return axios.get(this.state.endpoints.recentBoardsEndpoint);
},
Loading
Loading
@@ -595,6 +600,38 @@ const boardsStore = {
clearMultiSelect() {
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);
Loading
Loading
Loading
Loading
@@ -6,16 +6,14 @@ const handleOnDismiss = ({ currentTarget }) => {
dataset: { id },
} = currentTarget;
 
Cookies.set(`hide_broadcast_notification_message_${id}`, true);
Cookies.set(`hide_broadcast_message_${id}`, true);
 
const notification = document.querySelector(`.js-broadcast-notification-${id}`);
notification.parentNode.removeChild(notification);
};
 
export default () => {
const dismissButton = document.querySelector('.js-dismiss-current-broadcast-notification');
if (dismissButton) {
dismissButton.addEventListener('click', handleOnDismiss);
}
document
.querySelectorAll('.js-dismiss-current-broadcast-notification')
.forEach(dismissButton => 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>
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