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:
Diffstat (limited to 'app/assets/javascripts/projects')
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue15
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue5
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue156
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/constants.js2
-rw-r--r--app/assets/javascripts/projects/project_star_button.js46
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js611
-rw-r--r--app/assets/javascripts/projects/settings/api/access_dropdown_api.js16
-rw-r--r--app/assets/javascripts/projects/settings/components/access_dropdown.vue126
-rw-r--r--app/assets/javascripts/projects/settings/constants.js7
-rw-r--r--app/assets/javascripts/projects/settings/init_access_dropdown.js25
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue33
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue1
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js2
-rw-r--r--app/assets/javascripts/projects/star.js43
14 files changed, 215 insertions, 873 deletions
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
index 6ff9bd7390f..b7355b909a1 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
@@ -2,16 +2,13 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
-import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
-import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
+import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
+import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
+import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
-import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
+import getLinkedPipelinesQuery from '~/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from '~/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stages.query.graphql';
import { formatStages } from '../utils';
import { COMMIT_BOX_POLL_INTERVAL } from '../constants';
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
index 71f53613a3b..ccecc914cf1 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
@@ -2,10 +2,7 @@
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createAlert } from '~/alert';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
+import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import getLatestPipelineStatusQuery from '../graphql/queries/get_latest_pipeline_status.query.graphql';
import { COMMIT_BOX_POLL_INTERVAL, PIPELINE_STATUS_FETCH_ERROR } from '../constants';
diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
deleted file mode 100644
index 034bae3066d..00000000000
--- a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
+++ /dev/null
@@ -1,156 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { s__ } from '~/locale';
-
-export default {
- components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- },
- props: {
- refsProjectPath: {
- type: String,
- required: true,
- },
- revisionText: {
- type: String,
- required: true,
- },
- paramsName: {
- type: String,
- required: true,
- },
- paramsBranch: {
- type: String,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- branches: [],
- tags: [],
- loading: true,
- searchTerm: '',
- selectedRevision: this.getDefaultBranch(),
- };
- },
- computed: {
- filteredBranches() {
- return this.branches.filter((branch) =>
- branch.toLowerCase().includes(this.searchTerm.toLowerCase()),
- );
- },
- hasFilteredBranches() {
- return this.filteredBranches.length;
- },
- filteredTags() {
- return this.tags.filter((tag) => tag.toLowerCase().includes(this.searchTerm.toLowerCase()));
- },
- hasFilteredTags() {
- return this.filteredTags.length;
- },
- },
- watch: {
- paramsBranch(newBranch) {
- this.setSelectedRevision(newBranch);
- },
- },
- mounted() {
- this.fetchBranchesAndTags();
- },
- methods: {
- fetchBranchesAndTags() {
- const endpoint = this.refsProjectPath;
-
- return axios
- .get(endpoint)
- .then(({ data }) => {
- this.branches = data.Branches || [];
- this.tags = data.Tags || [];
- })
- .catch(() => {
- createAlert({
- message: `${s__(
- 'CompareRevisions|There was an error while updating the branch/tag list. Please try again.',
- )}`,
- });
- })
- .finally(() => {
- this.loading = false;
- });
- },
- getDefaultBranch() {
- return this.paramsBranch || s__('CompareRevisions|Select branch/tag');
- },
- onClick(revision) {
- this.setSelectedRevision(revision);
- },
- onSearchEnter() {
- this.setSelectedRevision(this.searchTerm);
- },
- setSelectedRevision(revision) {
- this.selectedRevision = revision || s__('CompareRevisions|Select branch/tag');
- this.$emit('selectRevision', { direction: this.paramsName, revision });
- },
- },
-};
-</script>
-
-<template>
- <div class="form-group compare-form-group" :class="`js-compare-${paramsName}-dropdown`">
- <div class="input-group inline-input-group">
- <span class="input-group-prepend">
- <div class="input-group-text">
- {{ revisionText }}
- </div>
- </span>
- <input type="hidden" :name="paramsName" :value="selectedRevision" />
- <gl-dropdown
- class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-0 gl-font-monospace"
- toggle-class="form-control compare-dropdown-toggle gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!"
- :text="selectedRevision"
- header-text="Select Git revision"
- :loading="loading"
- >
- <template #header>
- <gl-search-box-by-type
- v-model.trim="searchTerm"
- :placeholder="s__('CompareRevisions|Filter by Git revision')"
- @keyup.enter="onSearchEnter"
- />
- </template>
- <gl-dropdown-section-header v-if="hasFilteredBranches">
- {{ s__('CompareRevisions|Branches') }}
- </gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="(branch, index) in filteredBranches"
- :key="`branch${index}`"
- is-check-item
- :is-checked="selectedRevision === branch"
- data-testid="branches-dropdown-item"
- @click="onClick(branch)"
- >
- {{ branch }}
- </gl-dropdown-item>
- <gl-dropdown-section-header v-if="hasFilteredTags">
- {{ s__('CompareRevisions|Tags') }}
- </gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="(tag, index) in filteredTags"
- :key="`tag${index}`"
- is-check-item
- :is-checked="selectedRevision === tag"
- data-testid="tags-dropdown-item"
- @click="onClick(tag)"
- >
- {{ tag }}
- </gl-dropdown-item>
- </gl-dropdown>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/constants.js b/app/assets/javascripts/projects/pipelines/charts/constants.js
index c13824a9952..dcec77ac6a4 100644
--- a/app/assets/javascripts/projects/pipelines/charts/constants.js
+++ b/app/assets/javascripts/projects/pipelines/charts/constants.js
@@ -21,5 +21,5 @@ export const LOAD_PIPELINES_FAILURE = 'load_analytics_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
export const SNOWPLOW_LABEL = 'redis_hll_counters.analytics.analytics_total_unique_counts_monthly';
-export const SNOWPLOW_SCHEMA = 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-0';
+export const SNOWPLOW_SCHEMA = 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-1';
export const SNOWPLOW_DATA_SOURCE = 'redis_hll';
diff --git a/app/assets/javascripts/projects/project_star_button.js b/app/assets/javascripts/projects/project_star_button.js
new file mode 100644
index 00000000000..06f982b500d
--- /dev/null
+++ b/app/assets/javascripts/projects/project_star_button.js
@@ -0,0 +1,46 @@
+import { createAlert } from '~/alert';
+import axios from '~/lib/utils/axios_utils';
+import { spriteIcon } from '~/lib/utils/common_utils';
+import { __, s__ } from '~/locale';
+
+export function initStarButton(containerSelector = '.project-home-panel') {
+ const container = document.querySelector(containerSelector);
+ const starToggle = container?.querySelector('.toggle-star');
+
+ if (!starToggle) {
+ return;
+ }
+
+ starToggle.addEventListener('click', function toggleStarClickCallback() {
+ const starSpan = starToggle.querySelector('span');
+ const starIcon = starToggle.querySelector('svg');
+ const iconClasses = Array.from(starIcon.classList.values());
+
+ axios
+ .post(starToggle.dataset.endpoint)
+ .then(({ data }) => {
+ const isStarred = starSpan.classList.contains('starred');
+ starToggle.parentNode.querySelector('.count').textContent = data.star_count;
+
+ if (isStarred) {
+ starSpan.classList.remove('starred');
+ starSpan.textContent = s__('StarProject|Star');
+ starIcon.remove();
+ // eslint-disable-next-line no-unsanitized/method
+ starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star-o', iconClasses));
+ } else {
+ starSpan.classList.add('starred');
+ starSpan.textContent = __('Unstar');
+ starIcon.remove();
+
+ // eslint-disable-next-line no-unsanitized/method
+ starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star', iconClasses));
+ }
+ })
+ .catch(() =>
+ createAlert({
+ message: __('Star toggle failed. Try again later.'),
+ }),
+ );
+ });
+}
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
deleted file mode 100644
index 75d72f719e5..00000000000
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ /dev/null
@@ -1,611 +0,0 @@
-/* eslint-disable no-underscore-dangle, class-methods-use-this */
-import { escape, find, countBy } from 'lodash';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { createAlert } from '~/alert';
-import { n__, s__, __, sprintf } from '~/locale';
-import { renderAvatar } from '~/helpers/avatar_helper';
-import { getUsers, getGroups, getDeployKeys } from './api/access_dropdown_api';
-import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, 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.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;
- initDeprecatedJQueryDropdown(this.$dropdown, {
- 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;
- const fossWithMergeAccess = !this.hasLicense && this.accessLevel === ACCESS_LEVELS.MERGE;
-
- e.preventDefault();
-
- if (fossWithMergeAccess) {
- // We're not multiselecting quite yet in "Merge" access dropdown, on 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 && !fossWithMergeAccess) {
- // 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.DEPLOY_KEY) {
- obj.deploy_key_id = item.deploy_key_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.DEPLOY_KEY:
- comparator = LEVEL_ID_PROP.DEPLOY_KEY;
- 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,
- };
- } else if (selectedItem.type === LEVEL_TYPES.DEPLOY_KEY) {
- itemToAdd = {
- deploy_key_id: selectedItem.id,
- type: LEVEL_TYPES.DEPLOY_KEY,
- };
- }
-
- 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) ||
- (item.type === LEVEL_TYPES.ROLE && item.access_level === itemToDelete.id) ||
- (item.type === LEVEL_TYPES.DEPLOY_KEY && item.deploy_key_id === itemToDelete.id) ||
- (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.DEPLOY_KEY] > 0) {
- labelPieces.push(n__('1 deploy key', '%d deploy keys', counts[LEVEL_TYPES.DEPLOY_KEY]));
- }
-
- 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([
- getDeployKeys(query),
- getUsers(query),
- this.groupsData ? Promise.resolve(this.groupsData) : getGroups(),
- ])
- .then(([deployKeysResponse, usersResponse, groupsResponse]) => {
- this.groupsData = groupsResponse;
- callback(
- this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data),
- );
- })
- .catch(() => {
- createAlert({ message: __('Failed to load groups, users and deploy keys.') });
- });
- } else {
- getDeployKeys(query)
- .then((deployKeysResponse) => callback(this.consolidateData(deployKeysResponse.data)))
- .catch(() => createAlert({ message: __('Failed to load deploy keys.') }));
- }
- }
-
- consolidateData(deployKeysResponse, 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`
- //
- // For Deploy Keys
- // In dropdown: `id`
- // For submit: `deploy_key_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
- if (gon.current_project_id) {
- 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,
- );
- }
- }
-
- const deployKeys = deployKeysResponse.map((response) => {
- const {
- id,
- fingerprint,
- fingerprint_sha256: fingerprintSha256,
- title,
- owner: { avatar_url, name, username },
- } = response;
-
- const availableFingerprint = fingerprintSha256 || fingerprint;
- const shortFingerprint = `(${availableFingerprint.substring(0, 14)}...)`;
-
- return {
- id,
- title: title.concat(' ', shortFingerprint),
- avatar_url,
- fullname: name,
- username,
- type: LEVEL_TYPES.DEPLOY_KEY,
- };
- });
-
- if (this.accessLevel === ACCESS_LEVELS.PUSH) {
- if (deployKeys.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
- deployKeys,
- );
- }
- }
-
- if (this.accessLevel === ACCESS_LEVELS.CREATE && deployKeys.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
- deployKeys,
- );
- }
-
- return consolidatedData;
- }
-
- 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.DEPLOY_KEY:
- criteria = { deploy_key_id: 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.DEPLOY_KEY:
- groupRowEl =
- this.accessLevel === ACCESS_LEVELS.PUSH || this.accessLevel === ACCESS_LEVELS.CREATE
- ? this.deployKeyRowHtml(item, isActive)
- : '';
-
- break;
- case LEVEL_TYPES.GROUP:
- groupRowEl = this.groupRowHtml(item, isActive);
- break;
- default:
- groupRowEl = '';
- break;
- }
-
- return groupRowEl;
- }
-
- userRowHtml(user, isActive) {
- const isActiveClass = isActive || '';
- const avatarEl = renderAvatar(user, {
- sizeClass: 's32',
- });
-
- return `
- <li>
- <a href="#" class="${isActiveClass}">
- <div class="gl-avatar-labeled">
- ${avatarEl}
- <div>
- <strong class="dropdown-menu-user-full-name">${escape(user.name)}</strong>
- <span class="gl-avatar-labeled-sublabel dropdown-menu-user-username">@${
- user.username
- }</span>
- </div>
- </div>
- </a>
- </li>
- `;
- }
-
- deployKeyRowHtml(key, isActive) {
- const isActiveClass = isActive || '';
-
- return `
- <li>
- <a href="#" class="${isActiveClass}">
- <strong>${escape(key.title)}</strong>
- <p>
- ${sprintf(
- __('Owned by %{image_tag}'),
- {
- image_tag: `<img src="${key.avatar_url}" class="avatar avatar-inline s26" width="30">`,
- },
- false,
- )}
- <strong class="dropdown-menu-user-full-name gl-display-inline">${escape(
- key.fullname,
- )}</strong>
- <span class="dropdown-menu-user-username gl-display-inline">${key.username}</span>
- </p>
- </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}">
- ${escape(role.text)}
- </a>
- </li>
- `;
- }
-}
diff --git a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
index df99aac6b9e..b886bf43b57 100644
--- a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
+++ b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
@@ -1,7 +1,9 @@
+import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
+import { ACCESS_LEVEL_DEVELOPER_INTEGER } from '~/access_level/constants';
-const USERS_PATH = '/-/autocomplete/users.json';
const GROUPS_PATH = '/-/autocomplete/project_groups.json';
+const USERS_PATH = '/-/autocomplete/users.json';
const DEPLOY_KEYS_PATH = '/-/autocomplete/deploy_keys_with_owners.json';
const buildUrl = (urlRoot, url) => {
@@ -26,10 +28,14 @@ export const getUsers = (query, states) => {
};
export const getGroups = () => {
- return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH), {
- params: {
- project_id: gon.current_project_id,
- },
+ if (gon.current_project_id) {
+ return Api.projectGroups(gon.current_project_id, {
+ with_shared: true,
+ shared_min_access_level: ACCESS_LEVEL_DEVELOPER_INTEGER,
+ });
+ }
+ return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH)).then(({ data }) => {
+ return data;
});
};
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
index a2e4827cbfa..ca24e948f69 100644
--- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -12,13 +12,14 @@ import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } fro
import { createAlert } from '~/alert';
import { __, s__, n__ } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from '../api/access_dropdown_api';
-import { LEVEL_TYPES, ACCESS_LEVELS } from '../constants';
+import { LEVEL_TYPES, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from '../constants';
export const i18n = {
- selectUsers: s__('ProtectedEnvironment|Select users'),
+ defaultLabel: s__('AccessDropdown|Select'),
rolesSectionHeader: s__('AccessDropdown|Roles'),
groupsSectionHeader: s__('AccessDropdown|Groups'),
usersSectionHeader: s__('AccessDropdown|Users'),
+ noRole: s__('AccessDropdown|No role'),
deployKeysSectionHeader: s__('AccessDropdown|Deploy Keys'),
ownedBy: __('Owned by %{image_tag}'),
};
@@ -51,7 +52,7 @@ export default {
label: {
type: String,
required: false,
- default: i18n.selectUsers,
+ default: i18n.defaultLabel,
},
disabled: {
type: Boolean,
@@ -68,6 +69,31 @@ export default {
required: false,
default: () => [],
},
+ toggleClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ searchEnabled: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ block: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ testId: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ showUsers: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -96,6 +122,9 @@ export default {
this.deployKeys.length
);
},
+ isAccessesLevelNoneSelected() {
+ return this.selected.role[0].id === ACCESS_LEVEL_NONE;
+ },
toggleLabel() {
const counts = Object.entries(this.selected).reduce((acc, [key, value]) => {
acc[key] = value.length;
@@ -115,7 +144,11 @@ export default {
const labelPieces = [];
if (counts[LEVEL_TYPES.ROLE] > 0) {
- labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
+ if (this.isAccessesLevelNoneSelected) {
+ labelPieces.push(this.$options.i18n.noRole);
+ } else {
+ labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
+ }
}
if (counts[LEVEL_TYPES.USER] > 0) {
@@ -132,8 +165,14 @@ export default {
return labelPieces.join(', ') || this.label;
},
- toggleClass() {
- return this.toggleLabel === this.label ? 'gl-text-gray-500!' : '';
+ fossWithMergeAccess() {
+ return !this.hasLicense && this.accessLevel === ACCESS_LEVELS.MERGE;
+ },
+ dropdownToggleClass() {
+ return {
+ 'gl-text-gray-500!': this.toggleLabel === this.label,
+ [this.toggleClass]: true,
+ };
},
selection() {
return [
@@ -180,7 +219,7 @@ export default {
);
},
focusInput() {
- this.$refs.search.focusInput();
+ this.$refs.search?.focusInput();
},
getData({ initial = false } = {}) {
this.initialLoading = initial;
@@ -190,10 +229,10 @@ export default {
Promise.all([
getDeployKeys(this.query),
getUsers(this.query),
- this.groups.length ? Promise.resolve({ data: this.groups }) : getGroups(),
+ this.groups.length ? Promise.resolve(this.groups) : getGroups(),
])
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
- this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data);
+ this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse);
this.setSelected({ initial });
})
.catch(() =>
@@ -224,13 +263,18 @@ export default {
if (this.hasLicense) {
this.groups = groupsResponse.map((group) => ({ ...group, type: LEVEL_TYPES.GROUP }));
- this.users = usersResponse.map(({ id, name, username, avatar_url }) => ({
- id,
- name,
- username,
- avatar_url,
- type: LEVEL_TYPES.USER,
- }));
+
+ // Has to be checked against server response
+ // because the selected item can be in filter results
+ if (this.showUsers) {
+ this.users = usersResponse.map(({ id, name, username, avatar_url }) => ({
+ id,
+ name,
+ username,
+ avatar_url,
+ type: LEVEL_TYPES.USER,
+ }));
+ }
}
this.deployKeys = deployKeysResponse.map((response) => {
@@ -328,14 +372,38 @@ export default {
return [...added, ...removed, ...preserved];
},
onItemClick(item) {
- this.toggleSelection(this.selected[item.type], item);
+ this.toggleSelection(item);
this.emitUpdate();
},
- toggleSelection(arr, item) {
- const itemIndex = arr.findIndex(({ id }) => id === item.id);
- if (itemIndex > -1) {
- arr.splice(itemIndex, 1);
- } else arr.push(item);
+ toggleSelection(item) {
+ if (item.id === ACCESS_LEVEL_NONE) {
+ this.selected[LEVEL_TYPES.ROLE] = [item];
+ return;
+ }
+
+ const itemSelected = this.isSelected(item);
+ if (itemSelected) {
+ this.selected[item.type] = this.selected[item.type].filter(({ id }) => id !== item.id);
+ return;
+ }
+
+ // We're not multiselecting quite yet in "Merge" access dropdown, on FOSS:
+ // remove all preselected items before selecting this item
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
+ if (this.fossWithMergeAccess) this.clearSelection();
+ else if (item.type === LEVEL_TYPES.ROLE) this.unselectNone();
+
+ this.selected[item.type].push(item);
+ },
+ unselectNone() {
+ this.selected[LEVEL_TYPES.ROLE] = this.selected[LEVEL_TYPES.ROLE].filter(
+ ({ id }) => id !== ACCESS_LEVEL_NONE,
+ );
+ },
+ clearSelection() {
+ Object.values(LEVEL_TYPES).forEach((level) => {
+ this.selected[level] = [];
+ });
},
isSelected(item) {
return this.selected[item.type].some((selected) => selected.id === item.id);
@@ -346,6 +414,10 @@ export default {
onHide() {
this.$emit('hidden', this.selection);
},
+ onShown() {
+ this.$emit('shown');
+ this.focusInput();
+ },
},
};
</script>
@@ -354,13 +426,15 @@ export default {
<gl-dropdown
:disabled="disabled || initialLoading"
:text="toggleLabel"
- class="gl-min-w-20"
- :toggle-class="toggleClass"
+ :block="block"
+ class="gl-min-w-20 gl-p-0!"
+ :toggle-class="dropdownToggleClass"
aria-labelledby="allowed-users-label"
- @shown="focusInput"
+ :data-testid="testId"
+ @shown="onShown"
@hidden="onHide"
>
- <template #header>
+ <template v-if="searchEnabled" #header>
<gl-search-box-by-type ref="search" v-model.trim="query" :is-loading="loading" />
</template>
<template v-if="roles.length">
diff --git a/app/assets/javascripts/projects/settings/constants.js b/app/assets/javascripts/projects/settings/constants.js
index 595cbc9c991..37a9fe0c741 100644
--- a/app/assets/javascripts/projects/settings/constants.js
+++ b/app/assets/javascripts/projects/settings/constants.js
@@ -7,13 +7,6 @@ export const LEVEL_TYPES = {
GROUP: 'group',
};
-export const LEVEL_ID_PROP = {
- ROLE: 'access_level',
- USER: 'user_id',
- DEPLOY_KEY: 'deploy_key_id',
- GROUP: 'group_id',
-};
-
export const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
diff --git a/app/assets/javascripts/projects/settings/init_access_dropdown.js b/app/assets/javascripts/projects/settings/init_access_dropdown.js
index 941efaef3bc..67afbee3854 100644
--- a/app/assets/javascripts/projects/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/init_access_dropdown.js
@@ -7,8 +7,8 @@ export const initAccessDropdown = (el, options) => {
return null;
}
- const { accessLevelsData, accessLevel } = options;
- const { label, disabled, preselectedItems } = el.dataset;
+ const { accessLevelsData, ...props } = options;
+ const { label, disabled, preselectedItems } = el.dataset || {};
let preselected = [];
try {
preselected = JSON.parse(preselectedItems);
@@ -18,20 +18,35 @@ export const initAccessDropdown = (el, options) => {
return new Vue({
el,
+ name: 'AccessDropdownRoot',
+ data() {
+ return { preselected };
+ },
+ methods: {
+ setPreselectedItems(items) {
+ this.preselected = items;
+ },
+ },
render(createElement) {
const vm = this;
return createElement(AccessDropdown, {
props: {
- accessLevel,
- accessLevelsData: accessLevelsData.roles,
- preselectedItems: preselected,
label,
disabled,
+ accessLevelsData: accessLevelsData.roles,
+ preselectedItems: this.preselected,
+ ...props,
},
on: {
select(selected) {
vm.$emit('select', selected);
},
+ shown() {
+ vm.$emit('shown');
+ },
+ hidden() {
+ vm.$emit('hidden');
+ },
},
});
},
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
index 4affcd926d4..09bc275cbd4 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
@@ -1,5 +1,14 @@
<script>
-import { GlButton, GlForm, GlFormGroup, GlFormInputGroup, GlFormInput } from '@gitlab/ui';
+import {
+ GlButton,
+ GlForm,
+ GlFormGroup,
+ GlFormInputGroup,
+ GlFormInput,
+ GlLink,
+ GlSprintf,
+} from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { isEmptyValue, hasMinimumLength, isIntegerGreaterThan, isEmail } from '~/lib/utils/forms';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
@@ -23,6 +32,9 @@ import {
} from '../custom_email_constants';
export default {
+ customEmailHelpUrl: helpPagePath('user/project/service_desk/configure.html', {
+ anchor: 'custom-email-address',
+ }),
components: {
ClipboardButton,
GlButton,
@@ -30,6 +42,8 @@ export default {
GlFormGroup,
GlFormInputGroup,
GlFormInput,
+ GlLink,
+ GlSprintf,
},
I18N_FORM_INTRODUCTION_PARAGRAPH,
I18N_FORM_CUSTOM_EMAIL_LABEL,
@@ -137,7 +151,19 @@ export default {
<template>
<div>
- <p>{{ $options.I18N_FORM_INTRODUCTION_PARAGRAPH }}</p>
+ <p>
+ <gl-sprintf :message="$options.I18N_FORM_INTRODUCTION_PARAGRAPH">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.customEmailHelpUrl"
+ class="gl-display-inline-block"
+ target="_blank"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
<gl-form class="js-quick-submit" @submit.prevent="onSubmit">
<gl-form-group
:label="$options.I18N_FORM_FORWARDING_LABEL"
@@ -149,7 +175,6 @@ export default {
id="custom-email-form-forwarding"
ref="service-desk-incoming-email"
type="text"
- data-testid="custom-email-form-forwarding"
:aria-label="$options.I18N_FORM_FORWARDING_LABEL"
:value="incomingEmail"
:disabled="true"
@@ -167,7 +192,6 @@ export default {
<gl-form-group
:label="$options.I18N_FORM_CUSTOM_EMAIL_LABEL"
label-for="custom-email-form-custom-email"
- data-testid="form-group-custom-email"
:invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL"
class="gl-mt-3"
:description="$options.I18N_FORM_CUSTOM_EMAIL_DESCRIPTION"
@@ -191,7 +215,6 @@ export default {
<gl-form-group
:label="$options.I18N_FORM_SMTP_ADDRESS_LABEL"
label-for="custom-email-form-smtp-address"
- data-testid="form-group-smtp-address"
:invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS"
class="gl-mt-3"
>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
index 7e040e6001a..03ba99bcf71 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
@@ -233,6 +233,7 @@ export default {
<gl-link
:href="$options.FEEDBACK_ISSUE_URL"
target="_blank"
+ data-testid="feedback-link"
class="gl-text-blue-600 font-size-inherit"
>{{ content }}
</gl-link>
diff --git a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
index cdf2e53982e..aafd77bd25e 100644
--- a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
+++ b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
@@ -17,7 +17,7 @@ export const I18N_TOAST_ENABLED = s__('ServiceDesk|Custom email enabled.');
export const I18N_TOAST_DISABLED = s__('ServiceDesk|Custom email disabled.');
export const I18N_FORM_INTRODUCTION_PARAGRAPH = s__(
- 'ServiceDesk|Connect a custom email address your customers can use to create Service Desk issues. Forward all emails from your custom email address to the Service Desk email address of this project. GitLab will send Service Desk emails from the custom address on your behalf using your SMTP credentials.',
+ 'ServiceDesk|Connect a custom email address your customers can use to create Service Desk issues. Forward all emails from your custom email address to the Service Desk email address of this project. GitLab will send Service Desk emails from the custom address on your behalf using your SMTP credentials. %{linkStart}Learn more about prerequisites and the verification process%{linkEnd}.',
);
export const I18N_FORM_FORWARDING_LABEL = s__(
'ServiceDesk|Service Desk email address to forward emails to',
diff --git a/app/assets/javascripts/projects/star.js b/app/assets/javascripts/projects/star.js
deleted file mode 100644
index f294811dfff..00000000000
--- a/app/assets/javascripts/projects/star.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { spriteIcon } from '~/lib/utils/common_utils';
-import { __, s__ } from '~/locale';
-
-export default class Star {
- constructor(containerSelector = '.project-home-panel') {
- const container = document.querySelector(containerSelector);
- const starToggle = container.querySelector('.toggle-star');
- starToggle?.addEventListener('click', function toggleStarClickCallback() {
- const starSpan = starToggle.querySelector('span');
- const starIcon = starToggle.querySelector('svg');
- const iconClasses = Array.from(starIcon.classList.values());
-
- axios
- .post(starToggle.dataset.endpoint)
- .then(({ data }) => {
- const isStarred = starSpan.classList.contains('starred');
- starToggle.parentNode.querySelector('.count').textContent = data.star_count;
-
- if (isStarred) {
- starSpan.classList.remove('starred');
- starSpan.textContent = s__('StarProject|Star');
- starIcon.remove();
- // eslint-disable-next-line no-unsanitized/method
- starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star-o', iconClasses));
- } else {
- starSpan.classList.add('starred');
- starSpan.textContent = __('Unstar');
- starIcon.remove();
-
- // eslint-disable-next-line no-unsanitized/method
- starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star', iconClasses));
- }
- })
- .catch(() =>
- createAlert({
- message: __('Star toggle failed. Try again later.'),
- }),
- );
- });
- }
-}