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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-08 18:08:30 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-08 18:08:30 +0300
commitec9dd96cd876d8778bb757a1e1e0252a58fdcbbb (patch)
tree434606041cb42bcc922a02efe52a156b792e123b /app
parent473b876fe3d7e0b36eb6268cc44a4fe0d94f4422 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/reported_content.vue9
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue151
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue6
-rw-r--r--app/assets/javascripts/labels/components/promote_label_modal.vue2
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js9
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/truncated_text/constants.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/truncated_text/truncated_text.stories.js26
-rw-r--r--app/assets/javascripts/vue_shared/components/truncated_text/truncated_text.vue81
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_award_emoji.vue175
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue4
-rw-r--r--app/assets/javascripts/work_items/graphql/update_award_emoji.mutation.graphql6
-rw-r--r--app/assets/stylesheets/components/content_editor.scss20
-rw-r--r--app/assets/stylesheets/utilities.scss18
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/graphql_controller.rb2
-rw-r--r--app/models/broadcast_message.rb2
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb10
-rw-r--r--app/views/explore/projects/_head.html.haml2
-rw-r--r--app/views/groups/settings/_permissions.html.haml1
-rw-r--r--app/views/shared/_broadcast_message.html.haml3
31 files changed, 267 insertions, 313 deletions
diff --git a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
index b5ffba26360..f4f0fcac58f 100644
--- a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
@@ -1,10 +1,9 @@
<script>
-import { GlButton, GlModal, GlCard, GlLink, GlAvatar } from '@gitlab/ui';
+import { GlButton, GlModal, GlCard, GlLink, GlAvatar, GlTruncateText } from '@gitlab/ui';
import { __ } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import TruncatedText from '~/vue_shared/components/truncated_text/truncated_text.vue';
import { REPORTED_CONTENT_I18N } from '../constants';
export default {
@@ -15,8 +14,8 @@ export default {
GlCard,
GlLink,
GlAvatar,
+ GlTruncateText,
TimeAgoTooltip,
- TruncatedText,
},
modalId: 'abuse-report-screenshot-modal',
directives: {
@@ -109,13 +108,13 @@ export default {
footer-class="gl-bg-white js-test-card-footer"
>
<template v-if="report.content" #header>
- <truncated-text>
+ <gl-truncate-text>
<div
ref="gfmContent"
v-safe-html:[$options.safeHtmlConfig]="report.content"
class="md"
></div>
- </truncated-text>
+ </gl-truncate-text>
</template>
{{ $options.i18n.reportedBy }}
<template #footer>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
index 5624bae34c2..5abacf44cf3 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
@@ -1,22 +1,71 @@
<script>
-import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
import { selectedRect as getSelectedRect } from '@tiptap/pm/tables';
-import { __ } from '~/locale';
+import { __, n__ } from '~/locale';
const TABLE_CELL_HEADER = 'th';
const TABLE_CELL_BODY = 'td';
+function getDropdownItems({ selectedRect, cellType, rowspan = 1, colspan = 1 }) {
+ const totalRows = selectedRect?.map.height;
+ const totalCols = selectedRect?.map.width;
+ const isTableBodyCell = cellType === TABLE_CELL_BODY;
+ const selectedRows = selectedRect ? selectedRect.bottom - selectedRect.top : 0;
+ const selectedCols = selectedRect ? selectedRect.right - selectedRect.left : 0;
+ const showSplitCellOption =
+ selectedRows === rowspan && selectedCols === colspan && (rowspan > 1 || colspan > 1);
+ const showMergeCellsOption = selectedRows !== rowspan || selectedCols !== colspan;
+ const numCellsToMerge = (selectedRows - rowspan + 1) * (selectedCols - colspan + 1);
+ const showDeleteRowOption = totalRows > selectedRows + 1 && isTableBodyCell;
+ const showDeleteColumnOption = totalCols > selectedCols;
+
+ return [
+ {
+ items: [
+ { text: __('Insert column before'), value: 'addColumnBefore' },
+ { text: __('Insert column after'), value: 'addColumnAfter' },
+ isTableBodyCell && { text: __('Insert row before'), value: 'addRowBefore' },
+ { text: __('Insert row after'), value: 'addRowAfter' },
+ ].filter(Boolean),
+ },
+ {
+ items: [
+ showSplitCellOption && { text: __('Split cell'), value: 'splitCell' },
+ showMergeCellsOption && {
+ text: n__('Merge %d cell', 'Merge %d cells', numCellsToMerge),
+ value: 'mergeCells',
+ },
+ ].filter(Boolean),
+ },
+ {
+ items: [
+ showDeleteRowOption && {
+ text: n__('Delete row', 'Delete %d rows', selectedRows),
+ value: 'deleteRow',
+ },
+ showDeleteColumnOption && {
+ text: n__('Delete column', 'Delete %d columns', selectedCols),
+ value: 'deleteColumn',
+ },
+ { text: __('Delete table'), value: 'deleteTable' },
+ ].filter(Boolean),
+ },
+ ].filter(({ items }) => items.length);
+}
+
export default {
name: 'TableCellBaseWrapper',
components: {
NodeViewWrapper,
NodeViewContent,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
+ GlDisclosureDropdown,
},
props: {
+ getPos: {
+ type: Function,
+ required: true,
+ },
cellType: {
type: String,
validator: (type) => [TABLE_CELL_HEADER, TABLE_CELL_BODY].includes(type),
@@ -34,19 +83,17 @@ export default {
data() {
return {
displayActionsDropdown: false,
- preventHide: true,
selectedRect: null,
};
},
computed: {
- totalRows() {
- return this.selectedRect?.map.height;
- },
- totalCols() {
- return this.selectedRect?.map.width;
- },
- isTableBodyCell() {
- return this.cellType === TABLE_CELL_BODY;
+ dropdownItems() {
+ return getDropdownItems({
+ selectedRect: this.selectedRect,
+ cellType: this.cellType,
+ rowspan: this.node.attrs.rowspan,
+ colspan: this.node.attrs.colspan,
+ });
},
},
mounted() {
@@ -61,6 +108,8 @@ export default {
const { state } = this.editor;
const { $cursor } = state.selection;
+ this.selectedRect = getSelectedRect(state);
+
if (!$cursor) return;
this.displayActionsDropdown = false;
@@ -71,54 +120,34 @@ export default {
break;
}
}
-
- if (this.displayActionsDropdown) {
- this.selectedRect = getSelectedRect(state);
- }
},
- runCommand(command) {
- this.editor.chain()[command]().run();
+
+ runCommand({ value: command }) {
this.hideDropdown();
+ this.editor.chain()[command]().run();
},
- handleHide($event) {
- if (this.preventHide) {
- $event.preventDefault();
- }
- this.preventHide = true;
- },
+
hideDropdown() {
- this.preventHide = false;
- this.$refs.dropdown?.hide();
+ this.$refs.dropdown?.close();
},
},
- i18n: {
- insertColumnBefore: __('Insert column before'),
- insertColumnAfter: __('Insert column after'),
- insertRowBefore: __('Insert row before'),
- insertRowAfter: __('Insert row after'),
- deleteRow: __('Delete row'),
- deleteColumn: __('Delete column'),
- deleteTable: __('Delete table'),
- editTableActions: __('Edit table'),
- },
- dropdownPopperOpts: {
- positionFixed: true,
- },
};
</script>
<template>
<node-view-wrapper
- class="gl-relative gl-padding-5 gl-min-w-10"
:as="cellType"
+ :rowspan="node.attrs.rowspan || 1"
+ :colspan="node.attrs.colspan || 1"
dir="auto"
+ class="gl-m-0! gl-p-0! gl-relative"
@click="hideDropdown"
>
<span
v-if="displayActionsDropdown"
contenteditable="false"
- class="gl-absolute gl-right-0 gl-top-0"
+ class="gl-absolute gl-right-0 gl-top-0 gl-pr-1 gl-pt-1"
>
- <gl-dropdown
+ <gl-disclosure-dropdown
ref="dropdown"
dropup
icon="chevron-down"
@@ -127,34 +156,12 @@ export default {
boundary="viewport"
no-caret
text-sr-only
- :text="$options.i18n.editTableActions"
- :popper-opts="$options.dropdownPopperOpts"
- @hide="handleHide($event)"
- >
- <gl-dropdown-item @click="runCommand('addColumnBefore')">
- {{ $options.i18n.insertColumnBefore }}
- </gl-dropdown-item>
- <gl-dropdown-item @click="runCommand('addColumnAfter')">
- {{ $options.i18n.insertColumnAfter }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="isTableBodyCell" @click="runCommand('addRowBefore')">
- {{ $options.i18n.insertRowBefore }}
- </gl-dropdown-item>
- <gl-dropdown-item @click="runCommand('addRowAfter')">
- {{ $options.i18n.insertRowAfter }}
- </gl-dropdown-item>
- <gl-dropdown-divider />
- <gl-dropdown-item v-if="totalRows > 2 && isTableBodyCell" @click="runCommand('deleteRow')">
- {{ $options.i18n.deleteRow }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="totalCols > 1" @click="runCommand('deleteColumn')">
- {{ $options.i18n.deleteColumn }}
- </gl-dropdown-item>
- <gl-dropdown-item @click="runCommand('deleteTable')">
- {{ $options.i18n.deleteTable }}
- </gl-dropdown-item>
- </gl-dropdown>
+ :items="dropdownItems"
+ :toggle-text="__('Edit table')"
+ positioning-strategy="fixed"
+ @action="runCommand"
+ />
</span>
- <node-view-content />
+ <node-view-content as="div" class="gl-p-5 gl-min-w-10" />
</node-view-wrapper>
</template>
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index 8e169e8641b..d8734c8a2c4 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -451,7 +451,10 @@ export default {
},
showStickyHeader() {
- this.isStickyHeaderShowing = true;
+ // only if scrolled under the issue's title
+ if (this.$refs.title.$el.offsetTop < window.pageYOffset) {
+ this.isStickyHeaderShowing = true;
+ }
},
handleSaveDescription(description) {
@@ -501,6 +504,7 @@ export default {
</div>
<div v-else>
<title-component
+ ref="title"
:issuable-ref="issuableRef"
:can-update="canUpdate"
:title-html="state.titleHtml"
diff --git a/app/assets/javascripts/labels/components/promote_label_modal.vue b/app/assets/javascripts/labels/components/promote_label_modal.vue
index 752fda83d6b..ab50e6cdcd3 100644
--- a/app/assets/javascripts/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/labels/components/promote_label_modal.vue
@@ -3,8 +3,8 @@ import { GlSprintf, GlModal } from '@gitlab/ui';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
+import { stripQuotes } from '~/lib/utils/text_utility';
import { s__, __, sprintf } from '~/locale';
-import { stripQuotes } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import eventHub from '../event_hub';
export default {
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 963041dd5d0..42f481261a2 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -568,3 +568,12 @@ export const humanizeBranchValidationErrors = (invalidChars = []) => {
}
return '';
};
+
+/**
+ * Strips enclosing quotations from a string if it has one.
+ *
+ * @param {String} value String to strip quotes from
+ *
+ * @returns {String} String without any enclosure
+ */
+export const stripQuotes = (value) => value.replace(/^('|")(.*)('|")$/, '$2');
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index e2a10a1c1f3..a67928c387b 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -1,6 +1,7 @@
import { isEqual } from 'lodash';
import { STATUS_CLOSED, STATUS_REOPENED } from '~/issues/constants';
import { isInMRPage } from '~/lib/utils/common_utils';
+import { uuids } from '~/lib/utils/uuids';
import * as constants from '../constants';
import * as types from './mutation_types';
import * as utils from './utils';
@@ -185,6 +186,7 @@ export default {
}
notesArr.push({
+ id: uuids()[0],
individual_note: true,
isPlaceholderNote: true,
placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE,
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
index 6ea1fff9ef0..37fc326f902 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
@@ -81,7 +81,6 @@ export default {
const urlParams = new URLSearchParams(window.location.search);
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
if (showAlert) {
- // to be refactored to use gl-alert
createAlert({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, variant: VARIANT_INFO });
const cleanUrl = window.location.href.split('?')[0];
historyReplaceState(cleanUrl);
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
index 044ce4e6413..14d617a7a3c 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
@@ -114,7 +114,6 @@ export default {
const urlParams = new URLSearchParams(window.location.search);
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
if (showAlert) {
- // to be refactored to use gl-alert
createAlert({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, variant: VARIANT_INFO });
const cleanUrl = window.location.href.split('?')[0];
historyReplaceState(cleanUrl);
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 88062bf245f..042400d4340 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -13,10 +13,11 @@ import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searche
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import { createAlert } from '~/alert';
+import { stripQuotes } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import { SORT_DIRECTION } from './constants';
-import { filterEmptySearchTerm, stripQuotes, uniqueTokens } from './filtered_search_utils';
+import { filterEmptySearchTerm, uniqueTokens } from './filtered_search_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
index 5cc96471aef..65c783ada55 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
@@ -5,15 +5,6 @@ import { queryToObject } from '~/lib/utils/url_utility';
import { MAX_RECENT_TOKENS_SIZE, FILTERED_SEARCH_TERM } from './constants';
/**
- * Strips enclosing quotations from a string if it has one.
- *
- * @param {String} value String to strip quotes from
- *
- * @returns {String} String without any enclosure
- */
-export const stripQuotes = (value) => value.replace(/^('|")(.*)('|")$/, '$2');
-
-/**
* This method removes duplicate tokens from tokens array.
*
* @param {Array} tokens Array of tokens as defined by `GlFilteredSearch`
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index e2829d75ab1..5a7382bcd7c 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -9,12 +9,9 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
+import { stripQuotes } from '~/lib/utils/text_utility';
import { DEBOUNCE_DELAY, FILTERS_NONE_ANY, OPERATOR_NOT, OPERATOR_OR } from '../constants';
-import {
- getRecentlyUsedSuggestions,
- setTokenValueToRecentlyUsed,
- stripQuotes,
-} from '../filtered_search_utils';
+import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
index c69a2927ec9..0ce784fab1a 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
@@ -3,8 +3,8 @@ import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { stripQuotes } from '~/lib/utils/text_utility';
import { OPTIONS_NONE_ANY } from '../constants';
-import { stripQuotes } from '../filtered_search_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index 6a7dd6131e2..3dfdb15db31 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -3,10 +3,10 @@ import { GlToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { stripQuotes } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import { OPTIONS_NONE_ANY } from '../constants';
-import { stripQuotes } from '../filtered_search_utils';
import BaseToken from './base_token.vue';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index 81b8a6c78fc..8322fe92de4 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -4,8 +4,8 @@ import { createAlert } from '~/alert';
import { __ } from '~/locale';
import { sortMilestonesByDueDate } from '~/milestones/utils';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { stripQuotes } from '~/lib/utils/text_utility';
import { DEFAULT_MILESTONES } from '../constants';
-import { stripQuotes } from '../filtered_search_utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/truncated_text/constants.js b/app/assets/javascripts/vue_shared/components/truncated_text/constants.js
deleted file mode 100644
index c3b43d40adf..00000000000
--- a/app/assets/javascripts/vue_shared/components/truncated_text/constants.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { __ } from '~/locale';
-
-export const SHOW_MORE = __('Show more');
-export const SHOW_LESS = __('Show less');
-export const STATES = {
- INITIAL: 'initial',
- TRUNCATED: 'truncated',
- EXTENDED: 'extended',
-};
diff --git a/app/assets/javascripts/vue_shared/components/truncated_text/truncated_text.stories.js b/app/assets/javascripts/vue_shared/components/truncated_text/truncated_text.stories.js
deleted file mode 100644
index 6a7ac72c31e..00000000000
--- a/app/assets/javascripts/vue_shared/components/truncated_text/truncated_text.stories.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { escape } from 'lodash';
-import TruncatedText from './truncated_text.vue';
-
-export default {
- component: TruncatedText,
- title: 'vue_shared/truncated_text',
-};
-
-const Template = (args, { argTypes }) => ({
- components: { TruncatedText },
- props: Object.keys(argTypes),
- template: `
- <truncated-text v-bind="$props">
- <template v-if="${'default' in args}" v-slot>
- <span style="white-space: pre-line;">${escape(args.default)}</span>
- </template>
- </truncated-text>
- `,
-});
-
-export const Default = Template.bind({});
-Default.args = {
- lines: 3,
- mobileLines: 10,
- default: [...Array(15)].map((_, i) => `line ${i + 1}`).join('\n'),
-};
diff --git a/app/assets/javascripts/vue_shared/components/truncated_text/truncated_text.vue b/app/assets/javascripts/vue_shared/components/truncated_text/truncated_text.vue
deleted file mode 100644
index 96fc04ec825..00000000000
--- a/app/assets/javascripts/vue_shared/components/truncated_text/truncated_text.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<script>
-import { GlResizeObserverDirective, GlButton } from '@gitlab/ui';
-import { STATES, SHOW_MORE, SHOW_LESS } from './constants';
-
-export default {
- name: 'TruncatedText',
- components: {
- GlButton,
- },
- directives: {
- GlResizeObserver: GlResizeObserverDirective,
- },
- props: {
- lines: {
- type: Number,
- required: false,
- default: 3,
- },
- mobileLines: {
- type: Number,
- required: false,
- default: 10,
- },
- },
- data() {
- return {
- state: STATES.INITIAL,
- };
- },
- computed: {
- showTruncationToggle() {
- return this.state !== STATES.INITIAL;
- },
- truncationToggleText() {
- if (this.state === STATES.TRUNCATED) {
- return SHOW_MORE;
- }
- return SHOW_LESS;
- },
- styleObject() {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return { '--lines': this.lines, '--mobile-lines': this.mobileLines };
- },
- isTruncated() {
- return this.state === STATES.EXTENDED ? null : 'gl-truncate-text-by-line gl-overflow-hidden';
- },
- },
- methods: {
- onResize({ target }) {
- if (target.scrollHeight > target.offsetHeight) {
- this.state = STATES.TRUNCATED;
- } else if (this.state === STATES.TRUNCATED) {
- this.state = STATES.INITIAL;
- }
- },
- toggleTruncation() {
- if (this.state === STATES.TRUNCATED) {
- this.state = STATES.EXTENDED;
- } else if (this.state === STATES.EXTENDED) {
- this.state = STATES.TRUNCATED;
- }
- },
- },
-};
-</script>
-
-<template>
- <section>
- <article
- ref="content"
- v-gl-resize-observer="onResize"
- :class="isTruncated"
- :style="styleObject"
- >
- <slot></slot>
- </article>
- <gl-button v-if="showTruncationToggle" variant="link" @click="toggleTruncation">{{
- truncationToggleText
- }}</gl-button>
- </section>
-</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index 8ea5873f73a..9c6ce7ab702 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -146,6 +146,9 @@ export default {
this.track('click_toggle_work_item_confidentiality');
this.$emit('toggleWorkItemConfidentiality', !this.isConfidential);
},
+ handleDelete() {
+ this.$refs.modal.show();
+ },
handleDeleteWorkItem() {
this.track('click_delete_work_item');
this.$emit('deleteWorkItem');
@@ -288,13 +291,15 @@ export default {
</template>
<gl-dropdown-item
v-if="canDelete"
- v-gl-modal="'work-item-confirm-delete'"
:data-testid="$options.deleteActionTestId"
variant="danger"
- >{{ i18n.deleteWorkItem }}</gl-dropdown-item
+ @click="handleDelete"
>
+ {{ i18n.deleteWorkItem }}
+ </gl-dropdown-item>
</gl-dropdown>
<gl-modal
+ ref="modal"
modal-id="work-item-confirm-delete"
:title="i18n.deleteWorkItem"
:ok-title="i18n.deleteWorkItem"
diff --git a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
index 948258fbee6..144c29b8ec3 100644
--- a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
+++ b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
@@ -1,17 +1,15 @@
<script>
import * as Sentry from '@sentry/browser';
+import { produce } from 'immer';
+
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import AwardsList from '~/vue_shared/components/awards_list.vue';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
-import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
-import {
- EMOJI_ACTION_REMOVE,
- EMOJI_ACTION_ADD,
- WIDGET_TYPE_AWARD_EMOJI,
- EMOJI_THUMBSDOWN,
- EMOJI_THUMBSUP,
-} from '../constants';
+
+import updateAwardEmojiMutation from '../graphql/update_award_emoji.mutation.graphql';
+import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
+import { EMOJI_THUMBSDOWN, EMOJI_THUMBSUP, WIDGET_TYPE_AWARD_EMOJI } from '../constants';
export default {
defaultAwards: [EMOJI_THUMBSUP, EMOJI_THUMBSDOWN],
@@ -20,14 +18,23 @@ export default {
AwardsList,
},
props: {
- workItem: {
- type: Object,
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemFullpath: {
+ type: String,
required: true,
},
awardEmoji: {
type: Object,
required: true,
},
+ workItemIid: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
currentUserId() {
@@ -40,8 +47,7 @@ export default {
* Parse and convert award emoji list to a format that AwardsList can understand
*/
awards() {
- return this.awardEmoji.nodes.map((emoji, index) => ({
- id: index + 1,
+ return this.awardEmoji.nodes.map((emoji) => ({
name: emoji.name,
user: {
id: getIdFromGraphQLId(emoji.user.id),
@@ -51,34 +57,107 @@ export default {
},
},
methods: {
- handleAward(name) {
- // Decide action based on emoji given by current user.
- const action =
+ getAwards() {
+ return this.awardEmoji.nodes.map((emoji) => ({
+ name: emoji.name,
+ user: {
+ id: getIdFromGraphQLId(emoji.user.id),
+ name: emoji.user.name,
+ },
+ }));
+ },
+ isEmojiPresentForCurrentUser(name) {
+ return (
this.awards.findIndex(
(emoji) => emoji.name === name && emoji.user.id === this.currentUserId,
) > -1
- ? EMOJI_ACTION_REMOVE
- : EMOJI_ACTION_ADD;
- const inputVariables = {
- id: this.workItem.id,
- awardEmojiWidget: {
- action,
+ );
+ },
+ /**
+ * Prepare award emoji nodes based on emoji name
+ * and whether the user has toggled the emoji off or on
+ */
+ getAwardEmojiNodes(name, toggledOn) {
+ // If the emoji toggled on, add the emoji
+ if (toggledOn) {
+ // If emoji is already present in award list, no action is needed
+ if (this.isEmojiPresentForCurrentUser(name)) {
+ return this.awardEmoji.nodes;
+ }
+
+ // else make a copy of unmutable list and return the list after adding the new emoji
+ const awardEmojiNodes = [...this.awardEmoji.nodes];
+ awardEmojiNodes.push({
name,
- },
+ __typename: 'AwardEmoji',
+ user: {
+ id: convertToGraphQLId(TYPENAME_USER, this.currentUserId),
+ name: this.currentUserFullName,
+ __typename: 'UserCore',
+ },
+ });
+
+ return awardEmojiNodes;
+ }
+
+ // else just filter the emoji
+ return this.awardEmoji.nodes.filter(
+ (emoji) =>
+ !(emoji.name === name && getIdFromGraphQLId(emoji.user.id) === this.currentUserId),
+ );
+ },
+ updateWorkItemAwardEmojiWidgetCache({ cache, name, toggledOn }) {
+ const query = {
+ query: workItemByIidQuery,
+ variables: { fullPath: this.workItemFullpath, iid: this.workItemIid },
+ };
+
+ const sourceData = cache.readQuery(query);
+
+ const newData = produce(sourceData, (draftState) => {
+ const { widgets } = draftState.workspace.workItems.nodes[0];
+ const widgetAwardEmoji = widgets.find((widget) => widget.type === WIDGET_TYPE_AWARD_EMOJI);
+
+ widgetAwardEmoji.awardEmoji.nodes = this.getAwardEmojiNodes(name, toggledOn);
+ });
+
+ cache.writeQuery({ ...query, data: newData });
+ },
+ handleAward(name) {
+ // Decide action based on emoji is already present
+ const inputVariables = {
+ awardableId: this.workItemId,
+ name,
};
this.$apollo
.mutate({
- mutation: updateWorkItemMutation,
+ mutation: updateAwardEmojiMutation,
variables: {
input: inputVariables,
},
- optimisticResponse: this.getOptimisticResponse({ name, action }),
+ optimisticResponse: {
+ awardEmojiToggle: {
+ errors: [],
+ toggledOn: !this.isEmojiPresentForCurrentUser(name),
+ },
+ },
+ update: (
+ cache,
+ {
+ data: {
+ awardEmojiToggle: { toggledOn },
+ },
+ },
+ ) => {
+ // update the cache of award emoji widget object
+ this.updateWorkItemAwardEmojiWidgetCache({ cache, name, toggledOn });
+ },
})
.then(
({
data: {
- workItemUpdate: { errors },
+ awardEmojiToggle: { errors },
},
}) => {
if (errors?.length) {
@@ -91,52 +170,6 @@ export default {
Sentry.captureException(error);
});
},
- /**
- * Prepare workItemUpdate for optimistic response
- */
- getOptimisticResponse({ name, action }) {
- let awardEmojiNodes = [
- ...this.awardEmoji.nodes,
- {
- name,
- __typename: 'AwardEmoji',
- user: {
- id: convertToGraphQLId(TYPENAME_USER, this.currentUserId),
- name: this.currentUserFullName,
- __typename: 'UserCore',
- },
- },
- ];
- // Exclude the award emoji node in case of remove action
- if (action === EMOJI_ACTION_REMOVE) {
- awardEmojiNodes = [
- ...this.awardEmoji.nodes.filter(
- (emoji) =>
- !(emoji.name === name && getIdFromGraphQLId(emoji.user.id) === this.currentUserId),
- ),
- ];
- }
- return {
- workItemUpdate: {
- errors: [],
- workItem: {
- ...this.workItem,
- widgets: [
- {
- type: WIDGET_TYPE_AWARD_EMOJI,
- awardEmoji: {
- nodes: awardEmojiNodes,
- __typename: 'AwardEmojiConnection',
- },
- __typename: 'WorkItemWidgetAwardEmoji',
- },
- ],
- __typename: 'WorkItem',
- },
- __typename: 'WorkItemUpdatePayload',
- },
- };
- },
},
};
</script>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 0b7d9edd765..65e8999ae03 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -663,8 +663,10 @@ export default {
/>
<work-item-award-emoji
v-if="workItemAwardEmoji"
- :work-item="workItem"
+ :work-item-id="workItem.id"
+ :work-item-fullpath="workItem.project.fullPath"
:award-emoji="workItemAwardEmoji.awardEmoji"
+ :work-item-iid="workItemIid"
@error="updateError = $event"
/>
<work-item-tree
diff --git a/app/assets/javascripts/work_items/graphql/update_award_emoji.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_award_emoji.mutation.graphql
new file mode 100644
index 00000000000..1506d13d2da
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/update_award_emoji.mutation.graphql
@@ -0,0 +1,6 @@
+mutation updateWorkItemAwardEmojiWidget($input: AwardEmojiToggleInput!) {
+ awardEmojiToggle(input: $input) {
+ errors
+ toggledOn
+ }
+}
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index 7f66d335f41..680b24be442 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -33,6 +33,17 @@
outline-offset: -3px;
}
+ .selectedCell::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba($blue-200, 0.25);
+ pointer-events: none;
+ }
+
video {
max-width: 400px;
}
@@ -115,6 +126,15 @@
display: inherit;
}
}
+
+ .gl-new-dropdown-inner li {
+ margin-left: 0 !important;
+
+ &.gl-new-dropdown-item {
+ padding-left: $gl-spacing-scale-2;
+ padding-right: $gl-spacing-scale-2;
+ }
+ }
}
.table-creator-grid-item {
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index fd378dc7008..08c4efce542 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -153,21 +153,3 @@
.gl-fill-red-500 {
fill: $red-500;
}
-
-/**
- Note: used by app/assets/javascripts/vue_shared/components/truncated_text/truncated_text.vue
- Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab/-/issues/408643
-
- Although this solution uses vendor-prefixes, it is supported by all browsers and it is
- currently the only way to truncate text by lines. See https://caniuse.com/css-line-clamp
-**/
-.gl-truncate-text-by-line {
- // stylelint-disable-next-line value-no-vendor-prefix
- display: -webkit-box;
- -webkit-line-clamp: var(--lines);
- -webkit-box-orient: vertical;
-
- @include gl-media-breakpoint-down(sm) {
- -webkit-line-clamp: var(--mobile-lines);
- }
-}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 9749af08dca..08e4f4956df 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -110,7 +110,7 @@ class ApplicationController < ActionController::Base
rescue_from Gitlab::Git::ResourceExhaustedError do |e|
response.headers.merge!(e.headers)
- render plain: e.message, status: :too_many_requests
+ render plain: e.message, status: :service_unavailable
end
content_security_policy do |p|
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 11ac641419f..617a8aa1508 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -88,7 +88,7 @@ class GraphqlController < ApplicationController
log_exception(exception)
response.headers.merge!(exception.headers)
- render_error(exception.message, status: :too_many_requests)
+ render_error(exception.message, status: :service_unavailable)
end
rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index fd7a8708683..bf25ea7367c 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -81,7 +81,7 @@ class BroadcastMessage < MainClusterwide::ApplicationRecord
def cache
::Gitlab::SafeRequestStore.fetch(:broadcast_message_json_cache) do
- Gitlab::JsonCache.new
+ Gitlab::Cache::JsonCaches::JsonKeyed.new
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 65e7f734233..5c91f2460c4 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -169,7 +169,9 @@ module Noteable
def expire_note_etag_cache
return unless discussions_rendered_on_frontend?
return unless etag_caching_enabled?
- return unless project.present?
+
+ # TODO: We need to figure out a way to make ETag caching work for group-level work items
+ return if is_a?(Issue) && project.nil?
Gitlab::EtagCaching::Store.new.touch(note_etag_key)
end
diff --git a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
index d4b42b4bfc3..02c762faf14 100644
--- a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
+++ b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
@@ -15,6 +15,7 @@ module Ci
# rubocop: disable CodeReuse/ActiveRecord
def execute
+ return if service_disabled?
return if pipeline.parent_pipeline? # skip if child pipeline
return unless project.auto_cancel_pending_pipelines?
@@ -99,6 +100,15 @@ module Ci
)
end
end
+
+ # Finding the pipelines to cancel is an expensive task that is not well
+ # covered by indexes for all project use-cases and sometimes it might
+ # harm other services. See https://gitlab.com/gitlab-com/gl-infra/production/-/issues/14758
+ # This feature flag is in place to disable this feature for rogue projects.
+ #
+ def service_disabled?
+ Feature.enabled?(:disable_cancel_redundant_pipelines_service, project, type: :ops)
+ end
end
end
end
diff --git a/app/views/explore/projects/_head.html.haml b/app/views/explore/projects/_head.html.haml
index 605d85f49e0..c1d37965cd6 100644
--- a/app/views/explore/projects/_head.html.haml
+++ b/app/views/explore/projects/_head.html.haml
@@ -3,7 +3,7 @@
= render_dashboard_ultimate_trial(current_user)
-.page-title-holder.gl-display-flex.gl-align-items-center
+.page-title-holder.gl-display-flex.gl-align-items-center{ data: { testid: 'explore-projects-title' } }
%h1.page-title.gl-font-size-h-display= page_title
.page-title-controls
- if current_user&.can_create_project?
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 8fec5600780..f4749617463 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -38,6 +38,7 @@
= render 'groups/settings/lfs', f: f
= render_if_exists 'groups/settings/code_suggestions', f: f, group: @group
= render_if_exists 'groups/settings/ai_related_settings', f: f, group: @group
+ = render_if_exists 'groups/settings/ai_third_party_settings', f: f, group: @group
= render 'groups/settings/git_access_protocols', f: f, group: @group
= render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/subgroup_creation_level', f: f, group: @group
diff --git a/app/views/shared/_broadcast_message.html.haml b/app/views/shared/_broadcast_message.html.haml
index b065d5d2dc9..2f470d5ef53 100644
--- a/app/views/shared/_broadcast_message.html.haml
+++ b/app/views/shared/_broadcast_message.html.haml
@@ -3,7 +3,8 @@
- preview = local_assigns.fetch(:preview, false)
- unless message.notification?
- .gl-broadcast-message.broadcast-banner-message.banner{ role: "alert", class: "js-broadcast-notification-#{message.id} #{message.theme}" }
+ .gl-broadcast-message.broadcast-banner-message.banner{ role: "alert",
+ class: "js-broadcast-notification-#{message.id} #{message.theme}", data: { testid: 'banner-broadcast-message' } }
.gl-broadcast-message-content
.gl-broadcast-message-icon
= sprite_icon(icon_name)