diff options
Diffstat (limited to 'app/assets/javascripts')
39 files changed, 598 insertions, 57 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_kroki.js b/app/assets/javascripts/behaviors/markdown/render_kroki.js index abe71694d73..241585c30f1 100644 --- a/app/assets/javascripts/behaviors/markdown/render_kroki.js +++ b/app/assets/javascripts/behaviors/markdown/render_kroki.js @@ -55,8 +55,8 @@ export function renderKroki(krokiImages) { // A single Kroki image is processed multiple times for some reason, // so this condition ensures we only create one alert per Kroki image - if (!parent.hasAttribute('data-kroki-processed')) { - parent.setAttribute('data-kroki-processed', 'true'); + if (!Object.hasOwn(parent.dataset, 'krokiProcessed')) { + parent.dataset.krokiProcessed = 'true'; parent.after(createAlert(krokiImage)); } }); diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index fd1a99acf99..af7aac4cf36 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -112,7 +112,7 @@ class SafeMathRenderer { try { displayContainer.innerHTML = this.katex.renderToString(text, { - displayMode: el.getAttribute('data-math-style') === 'display', + displayMode: el.dataset.mathStyle === 'display', throwOnError: true, maxSize: 20, maxExpand: 20, @@ -145,7 +145,7 @@ class SafeMathRenderer { this.elements.forEach((el) => { const placeholder = document.createElement('span'); placeholder.style.display = 'none'; - placeholder.setAttribute('data-math-style', el.getAttribute('data-math-style')); + placeholder.dataset.mathStyle = el.dataset.mathStyle; placeholder.textContent = el.textContent; el.parentNode.replaceChild(placeholder, el); this.queue.push(placeholder); diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js index a3dd241604d..0a5bcf326a1 100644 --- a/app/assets/javascripts/blob/blob_line_permalink_updater.js +++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js @@ -9,10 +9,11 @@ const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => { [].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => { const baseHref = - permalinkButton.getAttribute('data-original-href') || + permalinkButton.dataset.originalHref || (() => { const href = permalinkButton.getAttribute('href'); - permalinkButton.setAttribute('data-original-href', href); + // eslint-disable-next-line no-param-reassign + permalinkButton.dataset.originalHref = href; return href; })(); permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`); diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index a6eed4ecae3..a0d4f7ef4f2 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -36,19 +36,19 @@ const loadRichBlobViewer = (type) => { const loadViewer = (viewerParam) => { const viewer = viewerParam; - const url = viewer.getAttribute('data-url'); + const { url } = viewer.dataset; - if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) { + if (!url || viewer.dataset.loaded || viewer.dataset.loading) { return Promise.resolve(viewer); } - viewer.setAttribute('data-loading', 'true'); + viewer.dataset.loading = 'true'; return axios.get(url).then(({ data }) => { viewer.innerHTML = data.html; window.requestIdleCallback(() => { - viewer.removeAttribute('data-loading'); + delete viewer.dataset.loading; }); return viewer; @@ -108,7 +108,7 @@ export class BlobViewer { switchToInitialViewer() { const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)'); - let initialViewerName = initialViewer.getAttribute('data-type'); + let initialViewerName = initialViewer.dataset.type; if (this.switcher && window.location.hash.indexOf('#L') === 0) { initialViewerName = 'simple'; @@ -138,12 +138,12 @@ export class BlobViewer { e.preventDefault(); - this.switchToViewer(target.getAttribute('data-viewer')); + this.switchToViewer(target.dataset.viewer); } toggleCopyButtonState() { if (!this.copySourceBtn) return; - if (this.simpleViewer.getAttribute('data-loaded')) { + if (this.simpleViewer.dataset.loaded) { this.copySourceBtnTooltip.setAttribute('title', __('Copy file contents')); this.copySourceBtn.classList.remove('disabled'); } else if (this.activeViewer === this.simpleViewer) { @@ -199,7 +199,8 @@ export class BlobViewer { this.$fileHolder.trigger('highlight:line'); handleLocationHash(); - viewer.setAttribute('data-loaded', 'true'); + // eslint-disable-next-line no-param-reassign + viewer.dataset.loaded = 'true'; this.toggleCopyButtonState(); eventHub.$emit('showBlobInteractionZones', viewer.dataset.path); }); diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js index b9d3742974c..113840dbc52 100644 --- a/app/assets/javascripts/breadcrumb.js +++ b/app/assets/javascripts/breadcrumb.js @@ -5,7 +5,7 @@ export const addTooltipToEl = (el) => { if (textEl && textEl.scrollWidth > textEl.offsetWidth) { el.setAttribute('title', el.textContent); - el.setAttribute('data-container', 'body'); + el.dataset.container = 'body'; el.classList.add('has-tooltip'); } }; diff --git a/app/assets/javascripts/code_navigation/utils/index.js b/app/assets/javascripts/code_navigation/utils/index.js index 0d72153d8fe..46038df2f86 100644 --- a/app/assets/javascripts/code_navigation/utils/index.js +++ b/app/assets/javascripts/code_navigation/utils/index.js @@ -32,8 +32,8 @@ export const addInteractionClass = ({ path, d, wrapTextNodes }) => { }); if (el && !isTextNode(el)) { - el.setAttribute('data-char-index', d.start_char); - el.setAttribute('data-line-index', d.start_line); + el.dataset.charIndex = d.start_char; + el.dataset.lineIndex = d.start_line; el.classList.add('cursor-pointer', 'code-navigation', 'js-code-navigation'); el.closest('.line').classList.add('code-navigation-line'); } diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js index 37287b9d981..f10c2d82b61 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/render.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js @@ -107,10 +107,10 @@ function createLink(data, selected, options, index) { } if (options.trackSuggestionClickedLabel) { - link.setAttribute('data-track-action', 'click_text'); - link.setAttribute('data-track-label', options.trackSuggestionClickedLabel); - link.setAttribute('data-track-value', index); - link.setAttribute('data-track-property', slugify(data.category || 'no-category')); + link.dataset.trackAction = 'click_text'; + link.dataset.trackLabel = options.trackSuggestionClickedLabel; + link.dataset.trackValue = index; + link.dataset.trackProperty = slugify(data.category || 'no-category'); } link.classList.toggle('is-active', selected); diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index a12829f8420..47de7a76899 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -26,7 +26,7 @@ export default class Diff { FilesCommentButton.init($diffFile); const firstFile = $('.files').first().get(0); - const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note'); + const canCreateNote = firstFile && Object.hasOwn(firstFile.dataset, 'canCreateNote'); $diffFile.each((index, file) => initImageDiffHelper.initImageDiff(file, canCreateNote)); if (!isBound) { diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js index b57db73a86e..3913e4e8d81 100644 --- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js +++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js @@ -197,10 +197,10 @@ export default class AvailableDropdownMappings { } getGroupId() { - return this.filteredSearchInput.getAttribute('data-group-id') || ''; + return this.filteredSearchInput.dataset.groupId || ''; } getProjectId() { - return this.filteredSearchInput.getAttribute('data-project-id') || ''; + return this.filteredSearchInput.dataset.projectId || ''; } } diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 9d29782c9a7..93897b4ed6c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -25,9 +25,9 @@ export default class DropdownHint extends FilteredSearchDropdown { const { selected } = e.detail; if (selected.tagName === 'LI') { - if (selected.hasAttribute('data-value')) { + if (Object.hasOwn(selected.dataset, 'value')) { this.dismissDropdown(); - } else if (selected.getAttribute('data-action') === 'submit') { + } else if (selected.dataset.action === 'submit') { this.dismissDropdown(); this.dispatchFormSubmitEvent(); } else { diff --git a/app/assets/javascripts/filtered_search/dropdown_operator.js b/app/assets/javascripts/filtered_search/dropdown_operator.js index fb9f25a8c45..cd0f541b4fb 100644 --- a/app/assets/javascripts/filtered_search/dropdown_operator.js +++ b/app/assets/javascripts/filtered_search/dropdown_operator.js @@ -23,7 +23,7 @@ export default class DropdownOperator extends FilteredSearchDropdown { const { selected } = e.detail; if (selected.tagName === 'LI') { - if (selected.hasAttribute('data-value')) { + if (Object.hasOwn(selected.dataset, 'value')) { const name = FilteredSearchVisualTokens.getLastTokenPartial(); const operator = selected.dataset.value; diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index 9a23ff25eac..26507a85fa8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -31,11 +31,11 @@ export default class DropdownUser extends DropdownAjaxFilter { } getGroupId() { - return this.input.getAttribute('data-group-id'); + return this.input.dataset.groupId; } getProjectId() { - return this.input.getAttribute('data-project-id'); + return this.input.dataset.projectId; } projectOrGroupId() { diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index c98d1f8e064..22e1604871a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -87,6 +87,7 @@ export default class DropdownUtils { } static setDataValueIfSelected(filter, operator, selected) { + // eslint-disable-next-line unicorn/prefer-dom-node-dataset const dataValue = selected.getAttribute('data-value'); if (dataValue) { @@ -96,6 +97,7 @@ export default class DropdownUtils { tokenValue: dataValue, clicked: true, options: { + // eslint-disable-next-line unicorn/prefer-dom-node-dataset capitalizeTokenValue: selected.hasAttribute('data-capitalize'), }, }); diff --git a/app/assets/javascripts/filtered_search/droplab/drop_down.js b/app/assets/javascripts/filtered_search/droplab/drop_down.js index 05b741af191..398a7b26677 100644 --- a/app/assets/javascripts/filtered_search/droplab/drop_down.js +++ b/app/assets/javascripts/filtered_search/droplab/drop_down.js @@ -165,8 +165,8 @@ class DropDown { images.forEach((image) => { const img = image; - img.src = img.getAttribute('data-src'); - img.removeAttribute('data-src'); + img.src = img.dataset.src; + delete img.dataset.src; }); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 07f2c75f00a..ac2cf27e873 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -814,7 +814,7 @@ export default class FilteredSearchManager { getUsernameParams() { const usernamesById = {}; try { - const attribute = this.filteredSearchInput.getAttribute('data-username-params'); + const attribute = this.filteredSearchInput.dataset.usernameParams; JSON.parse(attribute).forEach((user) => { usernamesById[user.id] = user.username; }); diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js index 3468a629f5a..180e927a3e7 100644 --- a/app/assets/javascripts/image_diff/helpers/dom_helper.js +++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js @@ -6,7 +6,7 @@ export function setPositionDataAttribute(el, options) { const positionObject = { ...JSON.parse(position), x, y, width, height }; - el.setAttribute('data-position', JSON.stringify(positionObject)); + el.dataset.position = JSON.stringify(positionObject); } export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) { diff --git a/app/assets/javascripts/issues/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js index c5f31081625..edf3789e6dc 100644 --- a/app/assets/javascripts/issues/create_merge_request_dropdown.js +++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js @@ -82,10 +82,7 @@ export default class CreateMergeRequestDropdown { this.init(); if (isConfidentialIssue()) { - this.createMergeRequestButton.setAttribute( - 'data-dropdown-trigger', - '#create-merge-request-dropdown', - ); + this.createMergeRequestButton.dataset.dropdownTrigger = '#create-merge-request-dropdown'; initConfidentialMergeRequest(); } } diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index d6ea06d172e..90e039259da 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -378,7 +378,7 @@ export default { }, setActiveTask(el) { const { parentElement } = el; - const lineNumbers = parentElement.getAttribute('data-sourcepos').match(/\b\d+(?=:)/g); + const lineNumbers = parentElement.dataset.sourcepos.match(/\b\d+(?=:)/g); this.activeTask = { title: parentElement.innerText, lineNumberStart: lineNumbers[0], diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js index 2b4dd205cf1..ba801082377 100644 --- a/app/assets/javascripts/lazy_loader.js +++ b/app/assets/javascripts/lazy_loader.js @@ -127,7 +127,7 @@ export default class LazyLoader { // Loading Images which are in the current viewport or close to them this.lazyImages = this.lazyImages.filter((selectedImage) => { - if (selectedImage.getAttribute('data-src')) { + if (selectedImage.dataset.src) { const imgBoundRect = selectedImage.getBoundingClientRect(); const imgTop = scrollTop + imgBoundRect.top; const imgBound = imgTop + imgBoundRect.height; @@ -156,16 +156,17 @@ export default class LazyLoader { } static loadImage(img) { - if (img.getAttribute('data-src')) { + if (img.dataset.src) { img.setAttribute('loading', 'lazy'); - let imgUrl = img.getAttribute('data-src'); + let imgUrl = img.dataset.src; // Only adding width + height for avatars for now if (imgUrl.indexOf('/avatar/') > -1 && imgUrl.indexOf('?') === -1) { const targetWidth = img.getAttribute('width') || img.width; imgUrl += `?width=${targetWidth}`; } img.setAttribute('src', imgUrl); - img.removeAttribute('data-src'); + // eslint-disable-next-line no-param-reassign + delete img.dataset.src; img.classList.remove('lazy'); img.classList.add('js-lazy-loaded'); img.classList.add('qa-js-lazy-loaded'); diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js index 173116062c9..2dc479db80a 100644 --- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js @@ -56,7 +56,7 @@ export function confirmAction( export function confirmViaGlModal(message, element) { const primaryBtnConfig = {}; - const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant'); + const { confirmBtnVariant } = element.dataset; if (confirmBtnVariant) { primaryBtnConfig.primaryBtnVariant = confirmBtnVariant; diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue index fa895cf24c4..6cd8bf57313 100644 --- a/app/assets/javascripts/members/components/table/role_dropdown.vue +++ b/app/assets/javascripts/members/components/table/role_dropdown.vue @@ -41,7 +41,7 @@ export default { const dropdownToggle = this.$refs.glDropdown.$el.querySelector('.dropdown-toggle'); if (dropdownToggle) { - dropdownToggle.setAttribute('data-qa-selector', 'access_level_dropdown'); + dropdownToggle.dataset.qaSelector = 'access_level_dropdown'; } }, methods: { diff --git a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js index 79ce1a37d21..47aae36ecbb 100644 --- a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js +++ b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js @@ -1,6 +1,6 @@ function onSidebarLinkClick() { const setDataTrackAction = (element, action) => { - element.setAttribute('data-track-action', action); + element.dataset.trackAction = action; }; const setDataTrackExtra = (element, value) => { @@ -12,10 +12,10 @@ function onSidebarLinkClick() { ? SIDEBAR_COLLAPSED : SIDEBAR_EXPANDED; - element.setAttribute( - 'data-track-extra', - JSON.stringify({ sidebar_display: sidebarCollapsed, menu_display: value }), - ); + element.dataset.trackExtra = JSON.stringify({ + sidebar_display: sidebarCollapsed, + menu_display: value, + }); }; const EXPANDED = 'Expanded'; diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 996e12bc105..94506d33b33 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -298,7 +298,7 @@ export default class ActivityCalendar { .querySelector(this.activitiesContainer) .querySelectorAll('.js-localtime') .forEach((el) => { - el.setAttribute('title', formatDate(el.getAttribute('data-datetime'))); + el.setAttribute('title', formatDate(el.dataset.datetime)); }); }) .catch(() => diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue index c8a0a3417f3..884ef732144 100644 --- a/app/assets/javascripts/projects/commits/components/author_select.vue +++ b/app/assets/javascripts/projects/commits/components/author_select.vue @@ -57,7 +57,7 @@ export default { if (authorParam) { commitsSearchInput.setAttribute('disabled', true); - commitsSearchInput.setAttribute('data-toggle', 'tooltip'); + commitsSearchInput.dataset.toggle = 'tooltip'; commitsSearchInput.setAttribute('title', tooltipMessage); this.currentAuthor = authorParam; } diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index d4b1f7e57d8..0c4ce5dab65 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -17,6 +17,7 @@ export default { piplelinesTabEvent: 'p_analytics_ci_cd_pipelines', deploymentFrequencyTabEvent: 'p_analytics_ci_cd_deployment_frequency', leadTimeTabEvent: 'p_analytics_ci_cd_lead_time', + timeToRestoreServiceTabEvent: 'p_analytics_ci_cd_time_to_restore_service', inject: { shouldRenderDoraCharts: { type: Boolean, @@ -37,7 +38,7 @@ export default { const chartsToShow = ['pipelines']; if (this.shouldRenderDoraCharts) { - chartsToShow.push('deployment-frequency', 'lead-time'); + chartsToShow.push('deployment-frequency', 'lead-time', 'time-to-restore-service'); } if (this.shouldRenderQualitySummary) { diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 351bb50d941..81ed04c7ce6 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -119,7 +119,7 @@ function mountAssigneesComponentDeprecated(mediator) { issuableIid: String(iid), projectPath: fullPath, field: el.dataset.field, - signedIn: el.hasAttribute('data-signed-in'), + signedIn: Object.hasOwn(el.dataset, 'signedIn'), issuableType: isInIssuePage() || isInIncidentPage() || isInDesignPage() ? IssuableType.Issue @@ -149,7 +149,7 @@ function mountAssigneesComponent() { }, provide: { canUpdate: editable, - directlyInviteMembers: el.hasAttribute('data-directly-invite-members'), + directlyInviteMembers: Object.hasOwn(el.dataset, 'directlyInviteMembers'), }, render: (createElement) => createElement('sidebar-assignees-widget', { diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js index 571177986d2..1f851a4376a 100644 --- a/app/assets/javascripts/terraform/index.js +++ b/app/assets/javascripts/terraform/index.js @@ -39,7 +39,7 @@ export default () => { return createElement(TerraformList, { props: { emptyStateImage, - terraformAdmin: el.hasAttribute('data-terraform-admin'), + terraformAdmin: Object.hasOwn(el.dataset, 'terraformAdmin'), }, }); }, diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue new file mode 100644 index 00000000000..92817d5fa70 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_item.vue @@ -0,0 +1,25 @@ +<script> +export default { + props: { + color: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div> + <span + class="dropdown-label-box gl-flex-shrink-0 gl-top-1 gl-mr-0" + data-testid="color-item" + :style="{ backgroundColor: color }" + ></span> + <span class="hide-collapsed">{{ title }}</span> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue new file mode 100644 index 00000000000..6b79883d76b --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue @@ -0,0 +1,214 @@ +<script> +import createFlash from '~/flash'; +import { s__ } from '~/locale'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import { DEFAULT_COLOR, COLOR_WIDGET_COLOR, DROPDOWN_VARIANT, ISSUABLE_COLORS } from './constants'; +import DropdownContents from './dropdown_contents.vue'; +import DropdownValue from './dropdown_value.vue'; +import { isDropdownVariantSidebar, isDropdownVariantEmbedded } from './utils'; +import epicColorQuery from './graphql/epic_color.query.graphql'; +import updateEpicColorMutation from './graphql/epic_update_color.mutation.graphql'; + +export default { + i18n: { + assignColor: s__('ColorWidget|Assign epic color'), + dropdownButtonText: COLOR_WIDGET_COLOR, + fetchingError: s__('ColorWidget|Error fetching epic color.'), + updatingError: s__('ColorWidget|An error occurred while updating color.'), + widgetTitle: COLOR_WIDGET_COLOR, + }, + components: { + DropdownValue, + DropdownContents, + SidebarEditableItem, + }, + props: { + allowEdit: { + type: Boolean, + required: false, + default: false, + }, + iid: { + type: String, + required: false, + default: '', + }, + fullPath: { + type: String, + required: true, + }, + variant: { + type: String, + required: false, + default: DROPDOWN_VARIANT.Sidebar, + }, + dropdownButtonText: { + type: String, + required: false, + default: COLOR_WIDGET_COLOR, + }, + dropdownTitle: { + type: String, + required: false, + default: s__('ColorWidget|Assign epic color'), + }, + }, + data() { + return { + issuableColor: { + color: '', + title: '', + }, + colorUpdateInProgress: false, + oldIid: null, + sidebarExpandedOnClick: false, + }; + }, + apollo: { + issuableColor: { + query: epicColorQuery, + skip() { + return !isDropdownVariantSidebar(this.variant); + }, + variables() { + return { + iid: this.iid, + fullPath: this.fullPath, + }; + }, + update(data) { + const issuableColor = data.workspace?.issuable?.color; + + if (issuableColor) { + return ISSUABLE_COLORS.find((color) => color.color === issuableColor) ?? DEFAULT_COLOR; + } + + return DEFAULT_COLOR; + }, + error() { + createFlash({ + message: this.$options.i18n.fetchingError, + captureError: true, + }); + }, + }, + }, + computed: { + isLoading() { + return this.colorUpdateInProgress || this.$apollo.queries.issuableColor.loading; + }, + }, + watch: { + iid(_, oldVal) { + this.oldIid = oldVal; + }, + }, + methods: { + handleDropdownClose(color) { + if (this.iid !== '') { + this.updateSelectedColor(this.getUpdateVariables(color)); + } else { + this.$emit('updateSelectedColor', color); + } + + this.collapseEditableItem(); + }, + collapseEditableItem() { + this.$refs.editable?.collapse(); + if (this.sidebarExpandedOnClick) { + this.sidebarExpandedOnClick = false; + this.$emit('toggleCollapse'); + } + }, + getUpdateVariables(color) { + const currentIid = this.oldIid || this.iid; + + return { + iid: currentIid, + groupPath: this.fullPath, + color: color.color, + }; + }, + updateSelectedColor(inputVariables) { + this.colorUpdateInProgress = true; + + this.$apollo + .mutate({ + mutation: updateEpicColorMutation, + variables: { input: inputVariables }, + }) + .then(({ data }) => { + if (data.updateIssuableColor?.errors?.length) { + throw new Error(); + } + + this.$emit('updateSelectedColor', { + id: data.updateIssuableColor?.issuable?.id, + color: data.updateIssuableColor?.issuable?.color, + }); + }) + .catch((error) => + createFlash({ + message: this.$options.i18n.updatingError, + captureError: true, + error, + }), + ) + .finally(() => { + this.colorUpdateInProgress = false; + }); + }, + isDropdownVariantSidebar, + isDropdownVariantEmbedded, + }, +}; +</script> + +<template> + <div + class="labels-select-wrapper gl-relative" + :class="{ + 'is-embedded': isDropdownVariantEmbedded(variant), + }" + > + <template v-if="isDropdownVariantSidebar(variant)"> + <sidebar-editable-item + ref="editable" + :title="$options.i18n.widgetTitle" + :loading="isLoading" + :can-edit="allowEdit" + @open="oldIid = null" + > + <template #collapsed> + <dropdown-value :selected-color="issuableColor"> + <slot></slot> + </dropdown-value> + </template> + <template #default="{ edit }"> + <dropdown-value :selected-color="issuableColor" class="gl-mb-2"> + <slot></slot> + </dropdown-value> + <dropdown-contents + ref="dropdownContents" + :dropdown-button-text="dropdownButtonText" + :dropdown-title="dropdownTitle" + :selected-color="issuableColor" + :variant="variant" + :is-visible="edit" + @setColor="handleDropdownClose" + @closeDropdown="collapseEditableItem" + /> + </template> + </sidebar-editable-item> + </template> + <dropdown-contents + v-else + ref="dropdownContents" + :dropdown-button-text="dropdownButtonText" + :dropdown-title="dropdownTitle" + :selected-color="issuableColor" + :variant="variant" + @setColor="handleDropdownClose" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js b/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js new file mode 100644 index 00000000000..c70785abd1e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/constants.js @@ -0,0 +1,30 @@ +import { __, s__ } from '~/locale'; + +export const COLOR_WIDGET_COLOR = s__('ColorWidget|Color'); + +export const DROPDOWN_VARIANT = { + Sidebar: 'sidebar', + Embedded: 'embedded', +}; + +export const DEFAULT_COLOR = { title: __('SuggestedColors|Blue'), color: '#1068bf' }; + +export const ISSUABLE_COLORS = [ + DEFAULT_COLOR, + { + title: s__('SuggestedColors|Green'), + color: '#217645', + }, + { + title: s__('SuggestedColors|Red'), + color: '#c91c00', + }, + { + title: s__('SuggestedColors|Orange'), + color: '#9e5400', + }, + { + title: s__('SuggestedColors|Purple'), + color: '#694cc0', + }, +]; diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue new file mode 100644 index 00000000000..4eb1d3d08ca --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents.vue @@ -0,0 +1,109 @@ +<script> +import { GlDropdown } from '@gitlab/ui'; +import DropdownContentsColorView from './dropdown_contents_color_view.vue'; +import DropdownHeader from './dropdown_header.vue'; +import { isDropdownVariantSidebar } from './utils'; + +export default { + components: { + DropdownContentsColorView, + DropdownHeader, + GlDropdown, + }, + props: { + dropdownTitle: { + type: String, + required: true, + }, + selectedColor: { + type: Object, + required: true, + }, + dropdownButtonText: { + type: String, + required: true, + }, + variant: { + type: String, + required: true, + }, + isVisible: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + showDropdownContentsCreateView: false, + localSelectedColor: this.selectedColor, + isDirty: false, + }; + }, + computed: { + buttonText() { + if (!this.localSelectedColor?.title) { + return this.dropdownButtonText; + } + + return this.localSelectedColor.title; + }, + }, + watch: { + localSelectedColor: { + handler() { + this.isDirty = true; + }, + deep: true, + }, + isVisible(newVal) { + if (newVal) { + this.$refs.dropdown.show(); + this.isDirty = false; + this.localSelectedColor = this.selectedColor; + } else { + this.$refs.dropdown.hide(); + this.setColor(); + } + }, + selectedColor(newVal) { + if (!this.isDirty) { + this.localSelectedColor = newVal; + } + }, + }, + methods: { + setColor() { + if (!this.isDirty) { + return; + } + this.$emit('setColor', this.localSelectedColor); + }, + handleDropdownHide() { + this.$emit('closeDropdown'); + if (!isDropdownVariantSidebar(this.variant)) { + this.setColor(); + } + this.$refs.dropdown.hide(); + }, + }, +}; +</script> + +<template> + <gl-dropdown ref="dropdown" :text="buttonText" class="gl-w-full" @hide="handleDropdownHide"> + <template #header> + <dropdown-header + ref="header" + :dropdown-title="dropdownTitle" + @closeDropdown="handleDropdownHide" + /> + </template> + <template #default> + <dropdown-contents-color-view + v-model="localSelectedColor" + @closeDropdown="handleDropdownHide" + /> + </template> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue new file mode 100644 index 00000000000..62f4cf59c14 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue @@ -0,0 +1,53 @@ +<script> +import { GlDropdownForm, GlDropdownItem } from '@gitlab/ui'; +import ColorItem from './color_item.vue'; +import { ISSUABLE_COLORS } from './constants'; + +export default { + components: { + GlDropdownForm, + GlDropdownItem, + ColorItem, + }, + model: { + prop: 'selectedColor', + }, + props: { + selectedColor: { + type: Object, + required: true, + }, + }, + data() { + return { + colors: ISSUABLE_COLORS, + }; + }, + methods: { + isColorSelected(color) { + return this.selectedColor.color === color.color; + }, + handleColorClick(color) { + this.$emit('input', color); + this.$emit('closeDropdown', this.selectedColor); + }, + }, +}; +</script> + +<template> + <gl-dropdown-form> + <div> + <gl-dropdown-item + v-for="color in colors" + :key="color.color" + :is-checked="isColorSelected(color)" + :is-check-centered="true" + :is-check-item="true" + @click.native.capture.stop="handleColorClick(color)" + > + <color-item :color="color.color" :title="color.title" /> + </gl-dropdown-item> + </div> + </gl-dropdown-form> +</template> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue new file mode 100644 index 00000000000..a32b1570f5f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_header.vue @@ -0,0 +1,31 @@ +<script> +import { GlButton } from '@gitlab/ui'; + +export default { + components: { + GlButton, + }, + props: { + dropdownTitle: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div> + <div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"> + <span class="gl-flex-grow-1">{{ dropdownTitle }}</span> + <gl-button + :aria-label="__('Close')" + variant="link" + size="small" + class="dropdown-header-button gl-p-0!" + icon="close" + @click="$emit('closeDropdown')" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue new file mode 100644 index 00000000000..4cba66eefd2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/dropdown_value.vue @@ -0,0 +1,43 @@ +<script> +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { COLOR_WIDGET_COLOR } from './constants'; +import ColorItem from './color_item.vue'; + +export default { + i18n: { + dropdownTitle: COLOR_WIDGET_COLOR, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + GlIcon, + ColorItem, + }, + props: { + selectedColor: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <div class="value js-value"> + <div + v-gl-tooltip.left.viewport + :title="$options.i18n.dropdownTitle" + class="sidebar-collapsed-icon" + > + <gl-icon name="appearance" /> + <color-item + :color="selectedColor.color" + :title="selectedColor.title" + class="gl-font-base gl-line-height-24" + /> + </div> + + <color-item class="hide-collapsed" :color="selectedColor.color" :title="selectedColor.title" /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql new file mode 100644 index 00000000000..959e0f8c1a5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql @@ -0,0 +1,9 @@ +query epicColor($fullPath: ID!, $iid: ID) { + workspace: group(fullPath: $fullPath) { + id + issuable: epic(iid: $iid) { + id + color + } + } +} diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql new file mode 100644 index 00000000000..2975b42253f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql @@ -0,0 +1,9 @@ +mutation updateEpicColor($input: UpdateEpicInput!) { + updateIssuableColor: updateEpic(input: $input) { + issuable: epic { + id + color + } + errors + } +} diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js b/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js new file mode 100644 index 00000000000..46196e793b3 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/utils.js @@ -0,0 +1,15 @@ +import { DROPDOWN_VARIANT } from './constants'; + +/** + * Returns boolean representing whether dropdown variant + * is `sidebar` + * @param {string} variant + */ +export const isDropdownVariantSidebar = (variant) => variant === DROPDOWN_VARIANT.Sidebar; + +/** + * Returns boolean representing whether dropdown variant + * is `embedded` + * @param {string} variant + */ +export const isDropdownVariantEmbedded = (variant) => variant === DROPDOWN_VARIANT.Embedded; diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue index b74dba686ad..0c55cc2f8a6 100644 --- a/app/assets/javascripts/whats_new/components/app.vue +++ b/app/assets/javascripts/whats_new/components/app.vue @@ -33,7 +33,7 @@ export default { this.fetchFreshItems(); const body = document.querySelector('body'); - const namespaceId = body.getAttribute('data-namespace-id'); + const { namespaceId } = body.dataset; this.track('click_whats_new_drawer', { label: 'namespace_id', value: namespaceId }); }, diff --git a/app/assets/javascripts/whats_new/utils/notification.js b/app/assets/javascripts/whats_new/utils/notification.js index 66ee3b1a971..41aff202f48 100644 --- a/app/assets/javascripts/whats_new/utils/notification.js +++ b/app/assets/javascripts/whats_new/utils/notification.js @@ -1,6 +1,6 @@ export const STORAGE_KEY = 'display-whats-new-notification'; -export const getVersionDigest = (appEl) => appEl.getAttribute('data-version-digest'); +export const getVersionDigest = (appEl) => appEl.dataset.versionDigest; export const setNotification = (appEl) => { const versionDigest = getVersionDigest(appEl); |