Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 21:42:06 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 21:42:06 +0300
commit6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch)
tree78be5963ec075d80116a932011d695dd33910b4e /app/assets/javascripts/projects
parent1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff)
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'app/assets/javascripts/projects')
-rw-r--r--app/assets/javascripts/projects/commits/store/actions.js2
-rw-r--r--app/assets/javascripts/projects/components/project_delete_button.vue52
-rw-r--r--app/assets/javascripts/projects/components/remove_modal.vue108
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_button.vue101
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue2
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue2
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue2
-rw-r--r--app/assets/javascripts/projects/project_delete_button.js (renamed from app/assets/javascripts/projects/project_remove_modal.js)9
-rw-r--r--app/assets/javascripts/projects/project_new.js2
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js524
-rw-r--r--app/assets/javascripts/projects/settings/constants.js13
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue10
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue2
13 files changed, 706 insertions, 123 deletions
diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js
index f0832bd36a5..927501748a5 100644
--- a/app/assets/javascripts/projects/commits/store/actions.js
+++ b/app/assets/javascripts/projects/commits/store/actions.js
@@ -1,7 +1,7 @@
import * as Sentry from '@sentry/browser';
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue
new file mode 100644
index 00000000000..4b27c5e3d30
--- /dev/null
+++ b/app/assets/javascripts/projects/components/project_delete_button.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlAlert, GlSprintf } from '@gitlab/ui';
+import { __ } from '~/locale';
+import SharedDeleteButton from './shared/delete_button.vue';
+
+export default {
+ components: {
+ GlSprintf,
+ GlAlert,
+ SharedDeleteButton,
+ },
+ props: {
+ confirmPhrase: {
+ type: String,
+ required: true,
+ },
+ formPath: {
+ type: String,
+ required: true,
+ },
+ },
+ strings: {
+ alertTitle: __('You are about to permanently delete this project'),
+ alertBody: __(
+ 'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its respositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.',
+ ),
+ modalBody: __(
+ "This action cannot be undone. You will lose the project's respository and all conent: issues, merge requests, etc.",
+ ),
+ },
+};
+</script>
+
+<template>
+ <shared-delete-button v-bind="{ confirmPhrase, formPath }">
+ <template #modal-body>
+ <gl-alert
+ class="gl-mb-5"
+ variant="danger"
+ :title="$options.strings.alertTitle"
+ :dismissible="false"
+ >
+ <gl-sprintf :message="$options.strings.alertBody">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+ <p>{{ $options.strings.modalBody }}</p>
+ </template>
+ </shared-delete-button>
+</template>
diff --git a/app/assets/javascripts/projects/components/remove_modal.vue b/app/assets/javascripts/projects/components/remove_modal.vue
deleted file mode 100644
index 37f58efcb30..00000000000
--- a/app/assets/javascripts/projects/components/remove_modal.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<script>
-import { GlModal, GlModalDirective, GlSprintf, GlFormInput, GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
-import { rstrip } from '~/lib/utils/common_utils';
-import csrf from '~/lib/utils/csrf';
-
-export default {
- components: {
- GlModal,
- GlSprintf,
- GlFormInput,
- GlButton,
- },
- directives: {
- GlModal: GlModalDirective,
- },
- props: {
- confirmPhrase: {
- type: String,
- required: true,
- },
- warningMessage: {
- type: String,
- required: true,
- },
- formPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- userInput: null,
- };
- },
- computed: {
- buttonDisabled() {
- return rstrip(this.userInput) !== this.confirmPhrase;
- },
- csrfToken() {
- return csrf.token;
- },
- },
- methods: {
- submitForm() {
- this.$refs.form.submit();
- },
- },
- strings: {
- removeProject: __('Remove project'),
- title: __('Confirmation required'),
- confirm: __('Confirm'),
- dataLoss: __(
- 'This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.',
- ),
- confirmText: __('Please type %{phrase_code} to proceed or close this modal to cancel.'),
- },
- modalId: 'remove-project-modal',
-};
-</script>
-
-<template>
- <form ref="form" :action="formPath" method="post">
- <input type="hidden" name="_method" value="delete" />
- <input :value="csrfToken" type="hidden" name="authenticity_token" />
- <gl-button v-gl-modal="$options.modalId" category="primary" variant="danger">{{
- $options.strings.removeProject
- }}</gl-button>
- <gl-modal
- ref="removeModal"
- :modal-id="$options.modalId"
- size="sm"
- ok-variant="danger"
- footer-class="bg-gray-light gl-p-5"
- >
- <template #modal-title>{{ $options.strings.title }}</template>
- <template #modal-footer>
- <div class="gl-w-full gl-display-flex gl-just-content-start gl-m-0">
- <gl-button
- :disabled="buttonDisabled"
- category="primary"
- variant="danger"
- @click="submitForm"
- >
- {{ $options.strings.confirm }}
- </gl-button>
- </div>
- </template>
- <div>
- <p class="gl-text-red-500 gl-font-weight-bold">{{ warningMessage }}</p>
- <p class="gl-mb-0">{{ $options.strings.dataLoss }}</p>
- <p>
- <gl-sprintf :message="$options.strings.confirmText">
- <template #phrase_code>
- <code>{{ confirmPhrase }}</code>
- </template>
- </gl-sprintf>
- </p>
- <gl-form-input
- id="confirm_name_input"
- v-model="userInput"
- name="confirm_name_input"
- type="text"
- />
- </div>
- </gl-modal>
- </form>
-</template>
diff --git a/app/assets/javascripts/projects/components/shared/delete_button.vue b/app/assets/javascripts/projects/components/shared/delete_button.vue
new file mode 100644
index 00000000000..e3f4500d404
--- /dev/null
+++ b/app/assets/javascripts/projects/components/shared/delete_button.vue
@@ -0,0 +1,101 @@
+<script>
+import { uniqueId } from 'lodash';
+import { GlModal, GlModalDirective, GlFormInput, GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import csrf from '~/lib/utils/csrf';
+
+export default {
+ components: {
+ GlModal,
+ GlFormInput,
+ GlButton,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ confirmPhrase: {
+ type: String,
+ required: true,
+ },
+ formPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ userInput: null,
+ modalId: uniqueId('delete-project-modal-'),
+ };
+ },
+ computed: {
+ confirmDisabled() {
+ return this.userInput !== this.confirmPhrase;
+ },
+ csrfToken() {
+ return csrf.token;
+ },
+ modalActionProps() {
+ return {
+ primary: {
+ text: __('Yes, delete project'),
+ attributes: [{ variant: 'danger' }, { disabled: this.confirmDisabled }],
+ },
+ cancel: {
+ text: __('Cancel, keep project'),
+ },
+ };
+ },
+ },
+ methods: {
+ submitForm() {
+ this.$refs.form.submit();
+ },
+ },
+ strings: {
+ deleteProject: __('Delete project'),
+ title: __('Delete project. Are you ABSOLUTELY SURE?'),
+ confirmText: __('Please type the following to confirm:'),
+ },
+};
+</script>
+
+<template>
+ <form ref="form" :action="formPath" method="post">
+ <input type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
+
+ <gl-button v-gl-modal="modalId" category="primary" variant="danger">{{
+ $options.strings.deleteProject
+ }}</gl-button>
+
+ <gl-modal
+ ref="removeModal"
+ :modal-id="modalId"
+ size="sm"
+ ok-variant="danger"
+ footer-class="gl-bg-gray-10 gl-p-5"
+ title-class="gl-text-red-500"
+ :action-primary="modalActionProps.primary"
+ :action-cancel="modalActionProps.cancel"
+ @ok="submitForm"
+ >
+ <template #modal-title>{{ $options.strings.title }}</template>
+ <div>
+ <slot name="modal-body"></slot>
+ <p class="gl-mb-1">{{ $options.strings.confirmText }}</p>
+ <p>
+ <code>{{ confirmPhrase }}</code>
+ </p>
+ <gl-form-input
+ id="confirm_name_input"
+ v-model="userInput"
+ name="confirm_name_input"
+ type="text"
+ />
+ <slot name="modal-footer"></slot>
+ </div>
+ </gl-modal>
+ </form>
+</template>
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
index e553599357c..ee4a00dbc75 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
+++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
@@ -1,7 +1,7 @@
<script>
+import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
import WelcomePage from './welcome.vue';
import LegacyContainer from './legacy_container.vue';
-import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import blankProjectIllustration from '../illustrations/blank-project.svg';
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
index ea22818da0e..cd9a72996cf 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
+++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
@@ -1,6 +1,6 @@
<script>
-import Tracking from '~/tracking';
import { GlPopover } from '@gitlab/ui';
+import Tracking from '~/tracking';
import LegacyContainer from './legacy_container.vue';
const trackingMixin = Tracking.mixin(gon.tracking_data);
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index cdf03a5013f..0777dddfc19 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -1,7 +1,7 @@
<script>
import dateFormat from 'dateformat';
-import { __, sprintf } from '~/locale';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import { __, sprintf } from '~/locale';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import StatisticsList from './statistics_list.vue';
import PipelinesAreaChart from './pipelines_area_chart.vue';
diff --git a/app/assets/javascripts/projects/project_remove_modal.js b/app/assets/javascripts/projects/project_delete_button.js
index dbdad1bf6f1..aa7fc31d307 100644
--- a/app/assets/javascripts/projects/project_remove_modal.js
+++ b/app/assets/javascripts/projects/project_delete_button.js
@@ -1,21 +1,20 @@
import Vue from 'vue';
-import RemoveProjectModal from './components/remove_modal.vue';
+import ProjectDeleteButton from './components/project_delete_button.vue';
-export default (selector = '#js-confirm-project-remove') => {
+export default (selector = '#js-project-delete-button') => {
const el = document.querySelector(selector);
if (!el) return;
- const { formPath, confirmPhrase, warningMessage } = el.dataset;
+ const { confirmPhrase, formPath } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
render(createElement) {
- return createElement(RemoveProjectModal, {
+ return createElement(ProjectDeleteButton, {
props: {
confirmPhrase,
- warningMessage,
formPath,
},
});
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index ebf745fd046..ec0a83b5736 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
+import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates';
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
import { convertToTitleCase, humanize, slugify } from '../lib/utils/text_utility';
-import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates';
let hasUserDefinedProjectPath = false;
let hasUserDefinedProjectName = false;
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
new file mode 100644
index 00000000000..4dbf6675357
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -0,0 +1,524 @@
+/* eslint-disable no-underscore-dangle, class-methods-use-this */
+import { escape, find, countBy } from 'lodash';
+import axios from '~/lib/utils/axios_utils';
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import { n__, s__, __ } from '~/locale';
+import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVEL_NONE } from './constants';
+
+export default class AccessDropdown {
+ constructor(options) {
+ const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options;
+ this.options = options;
+ this.hasLicense = hasLicense;
+ this.groups = [];
+ this.accessLevel = accessLevel;
+ this.accessLevelsData = accessLevelsData.roles;
+ this.$dropdown = $dropdown;
+ this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
+ this.usersPath = '/-/autocomplete/users.json';
+ this.groupsPath = '/-/autocomplete/project_groups.json';
+ this.defaultLabel = this.$dropdown.data('defaultLabel');
+
+ this.setSelectedItems([]);
+ this.persistPreselectedItems();
+
+ this.noOneObj = this.accessLevelsData.find(level => level.id === ACCESS_LEVEL_NONE);
+
+ this.initDropdown();
+ }
+
+ initDropdown() {
+ const { onSelect, onHide } = this.options;
+ this.$dropdown.glDropdown({
+ data: this.getData.bind(this),
+ selectable: true,
+ filterable: true,
+ filterRemote: true,
+ multiSelect: this.$dropdown.hasClass('js-multiselect'),
+ renderRow: this.renderRow.bind(this),
+ toggleLabel: this.toggleLabel.bind(this),
+ hidden() {
+ if (onHide) {
+ onHide();
+ }
+ },
+ clicked: options => {
+ const { $el, e } = options;
+ const item = options.selectedObj;
+
+ e.preventDefault();
+
+ if (!this.hasLicense) {
+ // We're not multiselecting quite yet with FOSS:
+ // remove all preselected items before selecting this item
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
+ this.accessLevelsData.forEach(level => {
+ this.removeSelectedItem(level);
+ });
+ }
+
+ if ($el.is('.is-active')) {
+ if (this.noOneObj) {
+ if (item.id === this.noOneObj.id && this.hasLicense) {
+ // remove all others selected items
+ this.accessLevelsData.forEach(level => {
+ if (level.id !== item.id) {
+ this.removeSelectedItem(level);
+ }
+ });
+
+ // remove selected item visually
+ this.$wrap.find(`.item-${item.type}`).removeClass('is-active');
+ } else {
+ const $noOne = this.$wrap.find(
+ `.is-active.item-${item.type}[data-role-id="${this.noOneObj.id}"]`,
+ );
+ if ($noOne.length) {
+ $noOne.removeClass('is-active');
+ this.removeSelectedItem(this.noOneObj);
+ }
+ }
+ }
+
+ // make element active right away
+ $el.addClass(`is-active item-${item.type}`);
+
+ // Add "No one"
+ this.addSelectedItem(item);
+ } else {
+ this.removeSelectedItem(item);
+ }
+
+ if (onSelect) {
+ onSelect(item, $el, this);
+ }
+ },
+ });
+
+ this.$dropdown.find('.dropdown-toggle-text').text(this.toggleLabel());
+ }
+
+ persistPreselectedItems() {
+ const itemsToPreselect = this.$dropdown.data('preselectedItems');
+
+ if (!itemsToPreselect || !itemsToPreselect.length) {
+ return;
+ }
+
+ const persistedItems = itemsToPreselect.map(item => {
+ const persistedItem = { ...item };
+ persistedItem.persisted = true;
+ return persistedItem;
+ });
+
+ this.setSelectedItems(persistedItems);
+ }
+
+ setSelectedItems(items = []) {
+ this.items = items;
+ }
+
+ getSelectedItems() {
+ return this.items.filter(item => !item._destroy);
+ }
+
+ getAllSelectedItems() {
+ return this.items;
+ }
+
+ // Return dropdown as input data ready to submit
+ getInputData() {
+ const selectedItems = this.getAllSelectedItems();
+
+ const accessLevels = selectedItems.map(item => {
+ const obj = {};
+
+ if (typeof item.id !== 'undefined') {
+ obj.id = item.id;
+ }
+
+ if (typeof item._destroy !== 'undefined') {
+ obj._destroy = item._destroy;
+ }
+
+ if (item.type === LEVEL_TYPES.ROLE) {
+ obj.access_level = item.access_level;
+ } else if (item.type === LEVEL_TYPES.USER) {
+ obj.user_id = item.user_id;
+ } else if (item.type === LEVEL_TYPES.GROUP) {
+ obj.group_id = item.group_id;
+ }
+
+ return obj;
+ });
+
+ return accessLevels;
+ }
+
+ addSelectedItem(selectedItem) {
+ let itemToAdd = {};
+
+ let index = -1;
+ let alreadyAdded = false;
+ const selectedItems = this.getAllSelectedItems();
+
+ // Compare IDs based on selectedItem.type
+ selectedItems.forEach((item, i) => {
+ let comparator;
+ switch (selectedItem.type) {
+ case LEVEL_TYPES.ROLE:
+ comparator = LEVEL_ID_PROP.ROLE;
+ // If the item already exists, just use it
+ if (item[comparator] === selectedItem.id) {
+ alreadyAdded = true;
+ }
+ break;
+ case LEVEL_TYPES.GROUP:
+ comparator = LEVEL_ID_PROP.GROUP;
+ break;
+ case LEVEL_TYPES.USER:
+ comparator = LEVEL_ID_PROP.USER;
+ break;
+ default:
+ break;
+ }
+
+ if (selectedItem.id === item[comparator]) {
+ index = i;
+ }
+ });
+
+ if (alreadyAdded) {
+ return;
+ }
+
+ if (index !== -1 && selectedItems[index]._destroy) {
+ delete selectedItems[index]._destroy;
+ return;
+ }
+
+ itemToAdd.type = selectedItem.type;
+
+ if (selectedItem.type === LEVEL_TYPES.USER) {
+ itemToAdd = {
+ user_id: selectedItem.id,
+ name: selectedItem.name || '_name1',
+ username: selectedItem.username || '_username1',
+ avatar_url: selectedItem.avatar_url || '_avatar_url1',
+ type: LEVEL_TYPES.USER,
+ };
+ } else if (selectedItem.type === LEVEL_TYPES.ROLE) {
+ itemToAdd = {
+ access_level: selectedItem.id,
+ type: LEVEL_TYPES.ROLE,
+ };
+ } else if (selectedItem.type === LEVEL_TYPES.GROUP) {
+ itemToAdd = {
+ group_id: selectedItem.id,
+ type: LEVEL_TYPES.GROUP,
+ };
+ }
+
+ this.items.push(itemToAdd);
+ }
+
+ removeSelectedItem(itemToDelete) {
+ let index = -1;
+ const selectedItems = this.getAllSelectedItems();
+
+ // To find itemToDelete on selectedItems, first we need the index
+ selectedItems.every((item, i) => {
+ if (item.type !== itemToDelete.type) {
+ return true;
+ }
+
+ if (item.type === LEVEL_TYPES.USER && item.user_id === itemToDelete.id) {
+ index = i;
+ } else if (item.type === LEVEL_TYPES.ROLE && item.access_level === itemToDelete.id) {
+ index = i;
+ } else if (item.type === LEVEL_TYPES.GROUP && item.group_id === itemToDelete.id) {
+ index = i;
+ }
+
+ // Break once we have index set
+ return !(index > -1);
+ });
+
+ // if ItemToDelete is not really selected do nothing
+ if (index === -1) {
+ return;
+ }
+
+ if (selectedItems[index].persisted) {
+ // If we toggle an item that has been already marked with _destroy
+ if (selectedItems[index]._destroy) {
+ delete selectedItems[index]._destroy;
+ } else {
+ selectedItems[index]._destroy = '1';
+ }
+ } else {
+ selectedItems.splice(index, 1);
+ }
+ }
+
+ toggleLabel() {
+ const currentItems = this.getSelectedItems();
+ const $dropdownToggleText = this.$dropdown.find('.dropdown-toggle-text');
+
+ if (currentItems.length === 0) {
+ $dropdownToggleText.addClass('is-default');
+ return this.defaultLabel;
+ }
+
+ $dropdownToggleText.removeClass('is-default');
+
+ if (currentItems.length === 1 && currentItems[0].type === LEVEL_TYPES.ROLE) {
+ const roleData = this.accessLevelsData.find(data => data.id === currentItems[0].access_level);
+ return roleData.text;
+ }
+
+ const labelPieces = [];
+ const counts = countBy(currentItems, item => item.type);
+
+ if (counts[LEVEL_TYPES.ROLE] > 0) {
+ labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
+ }
+
+ if (counts[LEVEL_TYPES.USER] > 0) {
+ labelPieces.push(n__('1 user', '%d users', counts[LEVEL_TYPES.USER]));
+ }
+
+ if (counts[LEVEL_TYPES.GROUP] > 0) {
+ labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
+ }
+
+ return labelPieces.join(', ');
+ }
+
+ getData(query, callback) {
+ if (this.hasLicense) {
+ Promise.all([
+ this.getUsers(query),
+ this.groupsData ? Promise.resolve(this.groupsData) : this.getGroups(),
+ ])
+ .then(([usersResponse, groupsResponse]) => {
+ this.groupsData = groupsResponse;
+ callback(this.consolidateData(usersResponse.data, groupsResponse.data));
+ })
+ .catch(() => Flash(__('Failed to load groups & users.')));
+ } else {
+ callback(this.consolidateData());
+ }
+ }
+
+ consolidateData(usersResponse = [], groupsResponse = []) {
+ let consolidatedData = [];
+
+ // ID property is handled differently locally from the server
+ //
+ // For Groups
+ // In dropdown: `id`
+ // For submit: `group_id`
+ //
+ // For Roles
+ // In dropdown: `id`
+ // For submit: `access_level`
+ //
+ // For Users
+ // In dropdown: `id`
+ // For submit: `user_id`
+
+ /*
+ * Build roles
+ */
+ const roles = this.accessLevelsData.map(level => {
+ /* eslint-disable no-param-reassign */
+ // This re-assignment is intentional as
+ // level.type property is being used in removeSelectedItem()
+ // for comparision, and accessLevelsData is provided by
+ // gon.create_access_levels which doesn't have `type` included.
+ // See this discussion https://gitlab.com/gitlab-org/gitlab/merge_requests/1629#note_31285823
+ level.type = LEVEL_TYPES.ROLE;
+ return level;
+ });
+
+ if (roles.length) {
+ consolidatedData = consolidatedData.concat(
+ [{ type: 'header', content: s__('AccessDropdown|Roles') }],
+ roles,
+ );
+ }
+
+ if (this.hasLicense) {
+ const map = [];
+ const selectedItems = this.getSelectedItems();
+ /*
+ * Build groups
+ */
+ const groups = groupsResponse.map(group => ({
+ ...group,
+ type: LEVEL_TYPES.GROUP,
+ }));
+
+ /*
+ * Build users
+ */
+ const users = selectedItems
+ .filter(item => item.type === LEVEL_TYPES.USER)
+ .map(item => {
+ // Save identifiers for easy-checking more later
+ map.push(LEVEL_TYPES.USER + item.user_id);
+
+ return {
+ id: item.user_id,
+ name: item.name,
+ username: item.username,
+ avatar_url: item.avatar_url,
+ type: LEVEL_TYPES.USER,
+ };
+ });
+
+ // Has to be checked against server response
+ // because the selected item can be in filter results
+ usersResponse.forEach(response => {
+ // Add is it has not been added
+ if (map.indexOf(LEVEL_TYPES.USER + response.id) === -1) {
+ const user = { ...response };
+ user.type = LEVEL_TYPES.USER;
+ users.push(user);
+ }
+ });
+
+ if (groups.length) {
+ if (roles.length) {
+ consolidatedData = consolidatedData.concat([{ type: 'divider' }]);
+ }
+
+ consolidatedData = consolidatedData.concat(
+ [{ type: 'header', content: s__('AccessDropdown|Groups') }],
+ groups,
+ );
+ }
+
+ if (users.length) {
+ consolidatedData = consolidatedData.concat(
+ [{ type: 'divider' }],
+ [{ type: 'header', content: s__('AccessDropdown|Users') }],
+ users,
+ );
+ }
+ }
+
+ return consolidatedData;
+ }
+
+ getUsers(query) {
+ return axios.get(this.buildUrl(gon.relative_url_root, this.usersPath), {
+ params: {
+ search: query,
+ per_page: 20,
+ active: true,
+ project_id: gon.current_project_id,
+ push_code: true,
+ },
+ });
+ }
+
+ getGroups() {
+ return axios.get(this.buildUrl(gon.relative_url_root, this.groupsPath), {
+ params: {
+ project_id: gon.current_project_id,
+ },
+ });
+ }
+
+ buildUrl(urlRoot, url) {
+ let newUrl;
+ if (urlRoot != null) {
+ newUrl = urlRoot.replace(/\/$/, '') + url;
+ }
+ return newUrl;
+ }
+
+ renderRow(item) {
+ let criteria = {};
+ let groupRowEl;
+
+ // Dectect if the current item is already saved so we can add
+ // the `is-active` class so the item looks as marked
+ switch (item.type) {
+ case LEVEL_TYPES.USER:
+ criteria = { user_id: item.id };
+ break;
+ case LEVEL_TYPES.ROLE:
+ criteria = { access_level: item.id };
+ break;
+ case LEVEL_TYPES.GROUP:
+ criteria = { group_id: item.id };
+ break;
+ default:
+ break;
+ }
+
+ const isActive = find(this.getSelectedItems(), criteria) ? 'is-active' : '';
+
+ switch (item.type) {
+ case LEVEL_TYPES.USER:
+ groupRowEl = this.userRowHtml(item, isActive);
+ break;
+ case LEVEL_TYPES.ROLE:
+ groupRowEl = this.roleRowHtml(item, isActive);
+ break;
+ case LEVEL_TYPES.GROUP:
+ groupRowEl = this.groupRowHtml(item, isActive);
+ break;
+ default:
+ groupRowEl = '';
+ break;
+ }
+
+ return groupRowEl;
+ }
+
+ userRowHtml(user, isActive) {
+ const isActiveClass = isActive || '';
+
+ return `
+ <li>
+ <a href="#" class="${isActiveClass}">
+ <img src="${user.avatar_url}" class="avatar avatar-inline" width="30">
+ <strong class="dropdown-menu-user-full-name">${escape(user.name)}</strong>
+ <span class="dropdown-menu-user-username">${user.username}</span>
+ </a>
+ </li>
+ `;
+ }
+
+ groupRowHtml(group, isActive) {
+ const isActiveClass = isActive || '';
+ const avatarEl = group.avatar_url
+ ? `<img src="${group.avatar_url}" class="avatar avatar-inline" width="30">`
+ : '';
+
+ return `
+ <li>
+ <a href="#" class="${isActiveClass}">
+ ${avatarEl}
+ <span class="dropdown-menu-group-groupname">${group.name}</span>
+ </a>
+ </li>
+ `;
+ }
+
+ roleRowHtml(role, isActive) {
+ const isActiveClass = isActive || '';
+
+ return `
+ <li>
+ <a href="#" class="${isActiveClass} item-${role.type}" data-role-id="${role.id}">
+ ${role.text}
+ </a>
+ </li>
+ `;
+ }
+}
diff --git a/app/assets/javascripts/projects/settings/constants.js b/app/assets/javascripts/projects/settings/constants.js
new file mode 100644
index 00000000000..fadb1f4f178
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/constants.js
@@ -0,0 +1,13 @@
+export const LEVEL_TYPES = {
+ ROLE: 'role',
+ USER: 'user',
+ GROUP: 'group',
+};
+
+export const LEVEL_ID_PROP = {
+ ROLE: 'access_level',
+ USER: 'user_id',
+ GROUP: 'group_id',
+};
+
+export const ACCESS_LEVEL_NONE = 0;
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index 43c20fea43e..0b7433d6aaa 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton, GlFormSelect, GlToggle, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlFormSelect, GlToggle, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -13,7 +13,7 @@ export default {
},
components: {
ClipboardButton,
- GlDeprecatedButton,
+ GlButton,
GlFormSelect,
GlToggle,
GlLoadingIcon,
@@ -157,12 +157,14 @@ export default {
}}
</span>
</template>
- <gl-deprecated-button
+ <gl-button
variant="success"
+ class="gl-mt-5"
:disabled="isTemplateSaving"
@click="onSaveTemplate"
- >{{ __('Save template') }}</gl-deprecated-button
>
+ {{ __('Save template') }}
+ </gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index 571d305a50c..e691f675e59 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -3,7 +3,7 @@ import Visibility from 'visibilityjs';
import { GlLoadingIcon } from '@gitlab/ui';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import Poll from '~/lib/utils/poll';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { __, s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import CommitPipelineService from '../services/commit_pipeline_service';