summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-27 18:09:54 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-27 18:09:54 +0000
commit03fbe61813666e96cd9b2dec9a22ab39f358f542 (patch)
tree786d9e6ad0e3ddf56709dad4dfabe963be378ebd /app/assets
parente69aae81ead38d4740771a5c8f0f33a4f248a312 (diff)
downloadgitlab-ce-03fbe61813666e96cd9b2dec9a22ab39f358f542.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.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/protected_branches/constants.js18
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js28
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js109
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js171
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit_list.js1
-rw-r--r--app/assets/javascripts/static_site_editor/components/app.vue12
-rw-r--r--app/assets/javascripts/static_site_editor/components/saved_changes_message.vue79
-rw-r--r--app/assets/javascripts/static_site_editor/index.js15
-rw-r--r--app/assets/javascripts/static_site_editor/pages/success.vue72
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js13
13 files changed, 870 insertions, 187 deletions
diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js
index 3e02893f24c..eff45bad603 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/form.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/form.js
@@ -14,7 +14,7 @@ export default () => {
new ProtectedTagEditList();
initDeployKeys();
initSettingsPanels();
- new ProtectedBranchCreate();
+ new ProtectedBranchCreate({ hasLicense: false });
new ProtectedBranchEditList();
new DueDateSelectors();
fileUpload('.js-choose-file', '.js-object-map-input');
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..b9d75340f8b
--- /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 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/protected_branches/constants.js b/app/assets/javascripts/protected_branches/constants.js
new file mode 100644
index 00000000000..a17ae6811b7
--- /dev/null
+++ b/app/assets/javascripts/protected_branches/constants.js
@@ -0,0 +1,18 @@
+export const ACCESS_LEVELS = {
+ MERGE: 'merge_access_levels',
+ PUSH: 'push_access_levels',
+};
+
+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/protected_branches/protected_branch_access_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
deleted file mode 100644
index 41e295387ae..00000000000
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { __ } from '~/locale';
-
-export default class ProtectedBranchAccessDropdown {
- constructor(options) {
- this.options = options;
- this.initDropdown();
- }
-
- initDropdown() {
- const { $dropdown, data, onSelect } = this.options;
- $dropdown.glDropdown({
- data,
- selectable: true,
- inputId: $dropdown.data('inputId'),
- fieldName: $dropdown.data('fieldName'),
- toggleLabel(item, $el) {
- if ($el.is('.is-active')) {
- return item.text;
- }
- return __('Select');
- },
- clicked(options) {
- options.e.preventDefault();
- onSelect();
- },
- });
- }
-}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 16ecd5523d6..af1d1f1b31a 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -1,41 +1,62 @@
import $ from 'jquery';
-import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
-import CreateItemDropdown from '../create_item_dropdown';
-import AccessorUtilities from '../lib/utils/accessor';
+import AccessDropdown from '~/projects/settings/access_dropdown';
+import axios from '~/lib/utils/axios_utils';
+import AccessorUtilities from '~/lib/utils/accessor';
+import Flash from '~/flash';
+import CreateItemDropdown from '~/create_item_dropdown';
+import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import { __ } from '~/locale';
export default class ProtectedBranchCreate {
- constructor() {
+ constructor(options) {
+ this.hasLicense = options.hasLicense;
+
this.$form = $('.js-new-protected-branch');
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.currentProjectUserDefaults = {};
this.buildDropdowns();
+ this.$codeOwnerToggle = this.$form.find('.js-code-owner-toggle');
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ if (this.hasLicense) {
+ this.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this));
+ }
+ this.$form.on('submit', this.onFormSubmit.bind(this));
+ }
+
+ onCodeOwnerToggleClick() {
+ this.$codeOwnerToggle.toggleClass('is-checked');
}
buildDropdowns() {
const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
- const $protectedBranchDropdown = this.$form.find('.js-protected-branch-select');
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Merge dropdown
- this.protectedBranchMergeAccessDropdown = new ProtectedBranchAccessDropdown({
+ this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
$dropdown: $allowedToMergeDropdown,
- data: gon.merge_access_levels,
+ accessLevelsData: gon.merge_access_levels,
onSelect: this.onSelectCallback,
+ accessLevel: ACCESS_LEVELS.MERGE,
+ hasLicense: this.hasLicense,
});
// Allowed to Push dropdown
- this.protectedBranchPushAccessDropdown = new ProtectedBranchAccessDropdown({
+ this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
$dropdown: $allowedToPushDropdown,
- data: gon.push_access_levels,
+ accessLevelsData: gon.push_access_levels,
onSelect: this.onSelectCallback,
+ accessLevel: ACCESS_LEVELS.PUSH,
+ hasLicense: this.hasLicense,
});
this.createItemDropdown = new CreateItemDropdown({
- $dropdown: $protectedBranchDropdown,
+ $dropdown: this.$form.find('.js-protected-branch-select'),
defaultToggleLabel: __('Protected Branch'),
fieldName: 'protected_branch[name]',
onSelect: this.onSelectCallback,
@@ -43,26 +64,66 @@ export default class ProtectedBranchCreate {
});
}
- // This will run after clicked callback
+ // Enable submit button after selecting an option
onSelect() {
- // Enable submit button
- const $branchInput = this.$form.find('input[name="protected_branch[name]"]');
- const $allowedToMergeInput = this.$form.find(
- 'input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]',
- );
- const $allowedToPushInput = this.$form.find(
- 'input[name="protected_branch[push_access_levels_attributes][0][access_level]"]',
- );
- const completedForm = !(
- $branchInput.val() &&
- $allowedToMergeInput.length &&
- $allowedToPushInput.length
+ const $allowedToMerge = this[`${ACCESS_LEVELS.MERGE}_dropdown`].getSelectedItems();
+ const $allowedToPush = this[`${ACCESS_LEVELS.PUSH}_dropdown`].getSelectedItems();
+ const toggle = !(
+ this.$form.find('input[name="protected_branch[name]"]').val() &&
+ $allowedToMerge.length &&
+ $allowedToPush.length
);
- this.$form.find('input[type="submit"]').prop('disabled', completedForm);
+ this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
static getProtectedBranches(term, callback) {
callback(gon.open_branches);
}
+
+ getFormData() {
+ const formData = {
+ authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
+ protected_branch: {
+ name: this.$form.find('input[name="protected_branch[name]"]').val(),
+ code_owner_approval_required: this.$codeOwnerToggle.hasClass('is-checked'),
+ },
+ };
+
+ Object.keys(ACCESS_LEVELS).forEach(level => {
+ const accessLevel = ACCESS_LEVELS[level];
+ const selectedItems = this[`${accessLevel}_dropdown`].getSelectedItems();
+ const levelAttributes = [];
+
+ selectedItems.forEach(item => {
+ if (item.type === LEVEL_TYPES.USER) {
+ levelAttributes.push({
+ user_id: item.user_id,
+ });
+ } else if (item.type === LEVEL_TYPES.ROLE) {
+ levelAttributes.push({
+ access_level: item.access_level,
+ });
+ } else if (item.type === LEVEL_TYPES.GROUP) {
+ levelAttributes.push({
+ group_id: item.group_id,
+ });
+ }
+ });
+
+ formData.protected_branch[`${accessLevel}_attributes`] = levelAttributes;
+ });
+
+ return formData;
+ }
+
+ onFormSubmit(e) {
+ e.preventDefault();
+
+ axios[this.$form.attr('method')](this.$form.attr('action'), this.getFormData())
+ .then(() => {
+ window.location.reload();
+ })
+ .catch(() => Flash(__('Failed to protect the branch')));
+ }
}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 08d8c9919dd..239bd8e543a 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -1,78 +1,165 @@
-import flash from '../flash';
-import axios from '../lib/utils/axios_utils';
-import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
+import { find } from 'lodash';
+import AccessDropdown from '~/projects/settings/access_dropdown';
+import axios from '~/lib/utils/axios_utils';
+import Flash from '~/flash';
+import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import { __ } from '~/locale';
export default class ProtectedBranchEdit {
constructor(options) {
+ this.hasLicense = options.hasLicense;
+
+ this.$wraps = {};
+ this.hasChanges = false;
this.$wrap = options.$wrap;
this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
- this.onSelectCallback = this.onSelect.bind(this);
+ this.$codeOwnerToggle = this.$wrap.find('.js-code-owner-toggle');
+
+ this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest(
+ `.${ACCESS_LEVELS.MERGE}-container`,
+ );
+ this.$wraps[ACCESS_LEVELS.PUSH] = this.$allowedToPushDropdown.closest(
+ `.${ACCESS_LEVELS.PUSH}-container`,
+ );
this.buildDropdowns();
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ if (this.hasLicense) {
+ this.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this));
+ }
+ }
+
+ onCodeOwnerToggleClick() {
+ this.$codeOwnerToggle.toggleClass('is-checked');
+ this.$codeOwnerToggle.prop('disabled', true);
+
+ const formData = {
+ code_owner_approval_required: this.$codeOwnerToggle.hasClass('is-checked'),
+ };
+
+ this.updateCodeOwnerApproval(formData);
+ }
+
+ updateCodeOwnerApproval(formData) {
+ axios
+ .patch(this.$wrap.data('url'), {
+ protected_branch: formData,
+ })
+ .then(() => {
+ this.$codeOwnerToggle.prop('disabled', false);
+ })
+ .catch(() => {
+ Flash(__('Failed to update branch!'));
+ });
}
buildDropdowns() {
// Allowed to merge dropdown
- this.protectedBranchAccessDropdown = new ProtectedBranchAccessDropdown({
+ this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
+ accessLevel: ACCESS_LEVELS.MERGE,
+ accessLevelsData: gon.merge_access_levels,
$dropdown: this.$allowedToMergeDropdown,
- data: gon.merge_access_levels,
- onSelect: this.onSelectCallback,
+ onSelect: this.onSelectOption.bind(this),
+ onHide: this.onDropdownHide.bind(this),
+ hasLicense: this.hasLicense,
});
// Allowed to push dropdown
- this.protectedBranchAccessDropdown = new ProtectedBranchAccessDropdown({
+ this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
+ accessLevel: ACCESS_LEVELS.PUSH,
+ accessLevelsData: gon.push_access_levels,
$dropdown: this.$allowedToPushDropdown,
- data: gon.push_access_levels,
- onSelect: this.onSelectCallback,
+ onSelect: this.onSelectOption.bind(this),
+ onHide: this.onDropdownHide.bind(this),
+ hasLicense: this.hasLicense,
});
}
- onSelect() {
- const $allowedToMergeInput = this.$wrap.find(
- `input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`,
- );
- const $allowedToPushInput = this.$wrap.find(
- `input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`,
- );
+ onSelectOption() {
+ this.hasChanges = true;
+ }
- // Do not update if one dropdown has not selected any option
- if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
+ onDropdownHide() {
+ if (!this.hasChanges) {
+ return;
+ }
- this.$allowedToMergeDropdown.disable();
- this.$allowedToPushDropdown.disable();
+ this.hasChanges = true;
+ this.updatePermissions();
+ }
+
+ updatePermissions() {
+ const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
+ const accessLevelName = ACCESS_LEVELS[level];
+ const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
+ acc[`${accessLevelName}_attributes`] = inputData;
+
+ return acc;
+ }, {});
axios
.patch(this.$wrap.data('url'), {
- protected_branch: {
- merge_access_levels_attributes: [
- {
- id: this.$allowedToMergeDropdown.data('accessLevelId'),
- access_level: $allowedToMergeInput.val(),
- },
- ],
- push_access_levels_attributes: [
- {
- id: this.$allowedToPushDropdown.data('accessLevelId'),
- access_level: $allowedToPushInput.val(),
- },
- ],
- },
+ protected_branch: formData,
})
- .then(() => {
+ .then(({ data }) => {
+ this.hasChanges = false;
+
+ Object.keys(ACCESS_LEVELS).forEach(level => {
+ const accessLevelName = ACCESS_LEVELS[level];
+
+ // The data coming from server will be the new persisted *state* for each dropdown
+ this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`);
+ });
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
})
.catch(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
-
- flash(
- __('Failed to update branch!'),
- 'alert',
- document.querySelector('.js-protected-branches-list'),
- );
+ Flash(__('Failed to update branch!'));
});
}
+
+ setSelectedItemsToDropdown(items = [], dropdownName) {
+ const itemsToAdd = items.map(currentItem => {
+ if (currentItem.user_id) {
+ // Do this only for users for now
+ // get the current data for selected items
+ const selectedItems = this[dropdownName].getSelectedItems();
+ const currentSelectedItem = find(selectedItems, {
+ user_id: currentItem.user_id,
+ });
+
+ return {
+ id: currentItem.id,
+ user_id: currentItem.user_id,
+ type: LEVEL_TYPES.USER,
+ persisted: true,
+ name: currentSelectedItem.name,
+ username: currentSelectedItem.username,
+ avatar_url: currentSelectedItem.avatar_url,
+ };
+ } else if (currentItem.group_id) {
+ return {
+ id: currentItem.id,
+ group_id: currentItem.group_id,
+ type: LEVEL_TYPES.GROUP,
+ persisted: true,
+ };
+ }
+
+ return {
+ id: currentItem.id,
+ access_level: currentItem.access_level,
+ type: LEVEL_TYPES.ROLE,
+ persisted: true,
+ };
+ });
+
+ this[dropdownName].setSelectedItems(itemsToAdd);
+ }
}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js
index 10253c0febc..6ab9a126e76 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js
@@ -13,6 +13,7 @@ export default class ProtectedBranchEditList {
this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => {
new ProtectedBranchEdit({
$wrap: $(el),
+ hasLicense: false,
});
});
}
diff --git a/app/assets/javascripts/static_site_editor/components/app.vue b/app/assets/javascripts/static_site_editor/components/app.vue
index 98240aef810..365fc7ce6e9 100644
--- a/app/assets/javascripts/static_site_editor/components/app.vue
+++ b/app/assets/javascripts/static_site_editor/components/app.vue
@@ -1,3 +1,13 @@
+<script>
+export default {
+ props: {
+ mergeRequestsIllustrationPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
<template>
- <router-view />
+ <router-view :merge-requests-illustration-path="mergeRequestsIllustrationPath" />
</template>
diff --git a/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue b/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue
deleted file mode 100644
index dd907570114..00000000000
--- a/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-<script>
-import { isString } from 'lodash';
-
-import { GlLink, GlButton } from '@gitlab/ui';
-
-const validateUrlAndLabel = value => isString(value.label) && isString(value.url);
-
-export default {
- components: {
- GlLink,
- GlButton,
- },
- props: {
- branch: {
- type: Object,
- required: true,
- validator: validateUrlAndLabel,
- },
- commit: {
- type: Object,
- required: true,
- validator: validateUrlAndLabel,
- },
- mergeRequest: {
- type: Object,
- required: true,
- validator: validateUrlAndLabel,
- },
- returnUrl: {
- type: String,
- required: false,
- default: '',
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div class="border-bottom pb-4">
- <h3>{{ s__('StaticSiteEditor|Success!') }}</h3>
- <p>
- {{
- s__(
- 'StaticSiteEditor|Your changes have been submitted and a merge request has been created. The changes won’t be visible on the site until the merge request has been accepted.',
- )
- }}
- </p>
- <div class="d-flex justify-content-end">
- <gl-button v-if="returnUrl" ref="returnToSiteButton" :href="returnUrl">{{
- s__('StaticSiteEditor|Return to site')
- }}</gl-button>
- <gl-button ref="mergeRequestButton" class="ml-2" :href="mergeRequest.url" variant="success">
- {{ s__('StaticSiteEditor|View merge request') }}
- </gl-button>
- </div>
- </div>
-
- <div class="pt-2">
- <h4>{{ s__('StaticSiteEditor|Summary of changes') }}</h4>
- <ul>
- <li>
- {{ s__('StaticSiteEditor|You created a new branch:') }}
- <gl-link ref="branchLink" :href="branch.url">{{ branch.label }}</gl-link>
- </li>
- <li>
- {{ s__('StaticSiteEditor|You created a merge request:') }}
- <gl-link ref="mergeRequestLink" :href="mergeRequest.url">{{
- mergeRequest.label
- }}</gl-link>
- </li>
- <li>
- {{ s__('StaticSiteEditor|You added a commit:') }}
- <gl-link ref="commitLink" :href="commit.url">{{ commit.label }}</gl-link>
- </li>
- </ul>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js
index 12aa301e02f..b7e5ea4eee3 100644
--- a/app/assets/javascripts/static_site_editor/index.js
+++ b/app/assets/javascripts/static_site_editor/index.js
@@ -5,7 +5,14 @@ import createRouter from './router';
import createApolloProvider from './graphql';
const initStaticSiteEditor = el => {
- const { isSupportedContent, path: sourcePath, baseUrl, namespace, project } = el.dataset;
+ const {
+ isSupportedContent,
+ path: sourcePath,
+ baseUrl,
+ namespace,
+ project,
+ mergeRequestsIllustrationPath,
+ } = el.dataset;
const { current_username: username } = window.gon;
const returnUrl = el.dataset.returnUrl || null;
@@ -26,7 +33,11 @@ const initStaticSiteEditor = el => {
App,
},
render(createElement) {
- return createElement('app');
+ return createElement('app', {
+ props: {
+ mergeRequestsIllustrationPath,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/static_site_editor/pages/success.vue b/app/assets/javascripts/static_site_editor/pages/success.vue
index 123683b2833..f0d597d7c9b 100644
--- a/app/assets/javascripts/static_site_editor/pages/success.vue
+++ b/app/assets/javascripts/static_site_editor/pages/success.vue
@@ -1,12 +1,21 @@
<script>
+import { GlEmptyState, GlButton } from '@gitlab/ui';
+import { s__, __, sprintf } from '~/locale';
+
import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql';
import appDataQuery from '../graphql/queries/app_data.query.graphql';
-import SavedChangesMessage from '../components/saved_changes_message.vue';
import { HOME_ROUTE } from '../router/constants';
export default {
components: {
- SavedChangesMessage,
+ GlEmptyState,
+ GlButton,
+ },
+ props: {
+ mergeRequestsIllustrationPath: {
+ type: String,
+ required: true,
+ },
},
apollo: {
savedContentMeta: {
@@ -16,20 +25,65 @@ export default {
query: appDataQuery,
},
},
+ computed: {
+ updatedFileDescription() {
+ const { sourcePath } = this.appData;
+
+ return sprintf(s__('Update %{sourcePath} file'), { sourcePath });
+ },
+ },
created() {
if (!this.savedContentMeta) {
this.$router.push(HOME_ROUTE);
}
},
+ title: s__('StaticSiteEditor|Your merge request has been created'),
+ primaryButtonText: __('View merge request'),
+ returnToSiteBtnText: s__('StaticSiteEditor|Return to site'),
+ mergeRequestInstructionsHeading: s__(
+ 'StaticSiteEditor|To see your changes live you will need to do the following things:',
+ ),
+ addTitleInstruction: s__('StaticSiteEditor|1. Add a clear title to describe the change.'),
+ addDescriptionInstruction: s__(
+ 'StaticSiteEditor|2. Add a description to explain why the change is being made.',
+ ),
+ assignMergeRequestInstruction: s__(
+ 'StaticSiteEditor|3. Assign a person to review and accept the merge request.',
+ ),
};
</script>
<template>
- <div v-if="savedContentMeta" class="container">
- <saved-changes-message
- :branch="savedContentMeta.branch"
- :commit="savedContentMeta.commit"
- :merge-request="savedContentMeta.mergeRequest"
- :return-url="appData.returnUrl"
- />
+ <div
+ v-if="savedContentMeta"
+ class="container gl-flex-grow-1 gl-display-flex gl-flex-direction-column"
+ >
+ <div class="gl-fixed gl-left-0 gl-right-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100">
+ <div class="container gl-py-4">
+ <gl-button
+ v-if="appData.returnUrl"
+ ref="returnToSiteButton"
+ class="gl-mr-5"
+ :href="appData.returnUrl"
+ >{{ $options.returnToSiteBtnText }}</gl-button
+ >
+ <strong>
+ {{ updatedFileDescription }}
+ </strong>
+ </div>
+ </div>
+ <gl-empty-state
+ class="gl-my-9"
+ :primary-button-text="$options.primaryButtonText"
+ :title="$options.title"
+ :primary-button-link="savedContentMeta.mergeRequest.url"
+ :svg-path="mergeRequestsIllustrationPath"
+ >
+ <template #description>
+ <p>{{ $options.mergeRequestInstructionsHeading }}</p>
+ <p>{{ $options.addTitleInstruction }}</p>
+ <p>{{ $options.addDescriptionInstruction }}</p>
+ <p>{{ $options.assignMergeRequestInstruction }}</p>
+ </template>
+ </gl-empty-state>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
index 9ecae87c1a9..b70f093e930 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
+++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
@@ -586,5 +586,16 @@ const fileNameIcons = {
};
export default function getIconForFile(name) {
- return fileNameIcons[name] || fileExtensionIcons[name ? name.split('.').pop() : ''] || '';
+ return (
+ fileNameIcons[name] ||
+ fileExtensionIcons[
+ name
+ ? name
+ .split('.')
+ .pop()
+ .toLowerCase()
+ : ''
+ ] ||
+ ''
+ );
}