diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-14 18:08:42 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-14 18:08:42 +0300 |
commit | 99670fc6a027caee34a6537c8def2e998d1ac5c2 (patch) | |
tree | a2ea3ec131d3cb155e13140c8486f1be2a5822b4 | |
parent | c9ca178ba4c9a3e48d9d069f7d7486a29827cc61 (diff) |
Add latest changes from gitlab-org/gitlab@master
77 files changed, 753 insertions, 185 deletions
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js index 48bcba7bcca..a492b95d1d9 100644 --- a/app/assets/javascripts/behaviors/copy_to_clipboard.js +++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js @@ -1,19 +1,24 @@ import $ from 'jquery'; import Clipboard from 'clipboard'; import { sprintf, __ } from '~/locale'; +import { fixTitle, show } from '~/tooltips'; function showTooltip(target, title) { - const $target = $(target); - const originalTitle = $target.data('originalTitle'); + const { originalTitle } = target.dataset; + const hideTooltip = () => { + target.removeEventListener('mouseout', hideTooltip); + setTimeout(() => { + target.setAttribute('title', originalTitle); + fixTitle(target); + }, 300); + }; - if (!$target.data('hideTooltip')) { - $target - .attr('title', title) - .tooltip('_fixTitle') - .tooltip('show') - .attr('title', originalTitle) - .tooltip('_fixTitle'); - } + target.setAttribute('title', title); + + fixTitle(target); + show(target); + + target.addEventListener('mouseout', hideTooltip); } function genericSuccess(e) { diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js index b03da311c43..c06ecb3a8d9 100644 --- a/app/assets/javascripts/emoji/index.js +++ b/app/assets/javascripts/emoji/index.js @@ -66,12 +66,8 @@ export function isEmojiNameValid(name) { return validEmojiNames.indexOf(name) >= 0; } -export function getValidEmojiUnicodeValues() { - return Object.values(emojiMap).map(({ e }) => e); -} - -export function getValidEmojiDescriptions() { - return Object.values(emojiMap).map(({ d }) => d); +export function getAllEmoji() { + return emojiMap; } /** @@ -106,16 +102,43 @@ export function getEmoji(query, fallback = false) { } const searchMatchers = { - fuzzy: (value, query) => fuzzaldrinPlus.score(value, query) > 0, // Fuzzy matching compares using a fuzzy matching library - contains: (value, query) => value.indexOf(query.toLowerCase()) >= 0, // Contains matching compares by indexOf - exact: (value, query) => value === query.toLowerCase(), // Exact matching compares by equality + // Fuzzy matching compares using a fuzzy matching library + fuzzy: (value, query) => { + const score = fuzzaldrinPlus.score(value, query) > 0; + return { score, success: score > 0 }; + }, + // Contains matching compares by indexOf + contains: (value, query) => { + const index = value.indexOf(query.toLowerCase()); + return { index, success: index >= 0 }; + }, + // Exact matching compares by equality + exact: (value, query) => { + return { success: value === query.toLowerCase() }; + }, }; const searchPredicates = { - name: (matcher, query) => emoji => matcher(emoji.name, query), // Search by name - alias: (matcher, query) => emoji => emoji.aliases.some(v => matcher(v, query)), // Search by alias - description: (matcher, query) => emoji => matcher(emoji.d, query), // Search by description - unicode: (matcher, query) => emoji => emoji.e === query, // Search by unicode value (always exact) + // Search by name + name: (matcher, query) => emoji => { + const m = matcher(emoji.name, query); + return [{ ...m, emoji, field: emoji.name }]; + }, + // Search by alias + alias: (matcher, query) => emoji => + emoji.aliases.map(alias => { + const m = matcher(alias, query); + return { ...m, emoji, field: alias }; + }), + // Search by description + description: (matcher, query) => emoji => { + const m = matcher(emoji.d, query); + return [{ ...m, emoji, field: emoji.d }]; + }, + // Search by unicode value (always exact) + unicode: (matcher, query) => emoji => { + return [{ emoji, field: emoji.e, success: emoji.e === query }]; + }, }; /** @@ -138,6 +161,8 @@ const searchPredicates = { * matching compares using a fuzzy matching library. * @param {Boolean} opts.fallback If true, a fallback emoji will be returned if * the result set is empty. Defaults to false. + * @param {Boolean} opts.raw Returns the raw match data instead of just the + * matching emoji. * @returns {Object[]} A list of emoji that match the query. */ export function searchEmoji(query, opts) { @@ -150,6 +175,7 @@ export function searchEmoji(query, opts) { fields = ['name', 'alias', 'description', 'unicode'], match = 'exact', fallback = false, + raw = false, } = opts || {}; // optimization for an exact match in name and alias @@ -161,16 +187,22 @@ export function searchEmoji(query, opts) { const matcher = searchMatchers[match] || searchMatchers.exact; const predicates = fields.map(f => searchPredicates[f](matcher, query)); - const results = Object.values(emojiMap).filter(emoji => - predicates.some(predicate => predicate(emoji)), - ); + const results = Object.values(emojiMap) + .flatMap(emoji => predicates.flatMap(predicate => predicate(emoji))) + .filter(r => r.success); // Fallback to question mark for unknown emojis if (fallback && results.length === 0) { + if (raw) { + return [{ emoji: emojiMap.grey_question }]; + } return [emojiMap.grey_question]; } - return results; + if (raw) { + return results; + } + return results.map(r => r.emoji); } let emojiCategoryMap; diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 5b604cc2a05..4c92aa41c41 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -181,6 +181,9 @@ class GfmAutoComplete { } setupEmoji($input) { + const self = this; + const { filter, ...defaults } = this.getDefaultCallbacks(); + // Emoji $input.atwho({ at: ':', @@ -195,13 +198,43 @@ class GfmAutoComplete { skipSpecialCharacterTest: true, data: GfmAutoComplete.defaultLoadingData, callbacks: { - ...this.getDefaultCallbacks(), + ...defaults, matcher(flag, subtext) { const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi'); const match = regexp.exec(subtext); return match && match.length ? match[1] : null; }, + filter(query, items, searchKey) { + const filtered = filter.call(this, query, items, searchKey); + if (query.length === 0 || GfmAutoComplete.isLoading(items)) { + return filtered; + } + + // map from value to "<value> is <field> of <emoji>", arranged by emoji + const emojis = {}; + filtered.forEach(({ name: value }) => { + self.emojiLookup[value].forEach(({ emoji: { name }, kind }) => { + let entry = emojis[name]; + if (!entry) { + entry = {}; + emojis[name] = entry; + } + if (!(kind in entry) || value.localeCompare(entry[kind]) < 0) { + entry[kind] = value; + } + }); + }); + + // collate results to list, prefering name > unicode > alias > description + const results = []; + Object.values(emojis).forEach(({ name, unicode, alias, description }) => { + results.push(name || unicode || alias || description); + }); + + // return to the form atwho wants + return results.map(name => ({ name })); + }, }, }); } @@ -637,12 +670,33 @@ class GfmAutoComplete { async loadEmojiData($input, at) { await Emoji.initEmojiMap(); + // All the emoji + const emojis = Emoji.getAllEmoji(); + + // Add all of the fields to atwho's database this.loadData($input, at, [ - ...Emoji.getValidEmojiNames(), - ...Emoji.getValidEmojiDescriptions(), - ...Emoji.getValidEmojiUnicodeValues(), + ...Object.keys(emojis), // Names + ...Object.values(emojis).flatMap(({ aliases }) => aliases), // Aliases + ...Object.values(emojis).map(({ e }) => e), // Unicode values + ...Object.values(emojis).map(({ d }) => d), // Descriptions ]); + // Construct a lookup that can correlate a value to "<value> is the <field> of <emoji>" + const lookup = {}; + const add = (key, kind, emoji) => { + if (!(key in lookup)) { + lookup[key] = []; + } + lookup[key].push({ kind, emoji }); + }; + Object.values(emojis).forEach(emoji => { + add(emoji.name, 'name', emoji); + add(emoji.d, 'description', emoji); + add(emoji.e, 'unicode', emoji); + emoji.aliases.forEach(a => add(a, 'alias', emoji)); + }); + this.emojiLookup = lookup; + GfmAutoComplete.glEmojiTag = Emoji.glEmojiTag; } @@ -711,19 +765,36 @@ GfmAutoComplete.atTypeMap = { GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities']; +function findEmoji(name) { + return Emoji.searchEmoji(name, { match: 'contains', raw: true }).sort((a, b) => { + if (a.index !== b.index) { + return a.index - b.index; + } + return a.field.localeCompare(b.field); + }); +} + // Emoji GfmAutoComplete.glEmojiTag = null; GfmAutoComplete.Emoji = { insertTemplateFunction(value) { - const { name = value.name } = Emoji.searchEmoji(value.name, { match: 'contains' })[0] || {}; - return `:${name}:`; + const results = findEmoji(value.name); + if (results.length) { + return `:${results[0].emoji.name}:`; + } + return `:${value.name}:`; }, templateFunction(name) { // glEmojiTag helper is loaded on-demand in fetchData() if (!GfmAutoComplete.glEmojiTag) return `<li>${name}</li>`; - const emoji = Emoji.searchEmoji(name, { match: 'contains' })[0]; - return `<li>${name} ${GfmAutoComplete.glEmojiTag(emoji?.name || name)}</li>`; + const results = findEmoji(name); + if (!results.length) { + return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`; + } + + const { field, emoji } = results[0]; + return `<li>${field} ${GfmAutoComplete.glEmojiTag(emoji.name)}</li>`; }, }; // Team Members diff --git a/app/assets/javascripts/issuable_show/components/issuable_body.vue b/app/assets/javascripts/issuable_show/components/issuable_body.vue new file mode 100644 index 00000000000..e6a05c1ab8b --- /dev/null +++ b/app/assets/javascripts/issuable_show/components/issuable_body.vue @@ -0,0 +1,103 @@ +<script> +import { GlLink } from '@gitlab/ui'; + +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +import IssuableTitle from './issuable_title.vue'; +import IssuableDescription from './issuable_description.vue'; +import IssuableEditForm from './issuable_edit_form.vue'; + +export default { + components: { + GlLink, + TimeAgoTooltip, + IssuableTitle, + IssuableDescription, + IssuableEditForm, + }, + props: { + issuable: { + type: Object, + required: true, + }, + statusBadgeClass: { + type: String, + required: true, + }, + statusIcon: { + type: String, + required: true, + }, + enableEdit: { + type: Boolean, + required: true, + }, + enableAutocomplete: { + type: Boolean, + required: true, + }, + editFormVisible: { + type: Boolean, + required: true, + }, + descriptionPreviewPath: { + type: String, + required: true, + }, + descriptionHelpPath: { + type: String, + required: true, + }, + }, + computed: { + isUpdated() { + return Boolean(this.issuable.updatedAt); + }, + updatedBy() { + return this.issuable.updatedBy; + }, + }, +}; +</script> + +<template> + <div class="issue-details issuable-details"> + <div class="detail-page-description content-block"> + <issuable-edit-form + v-if="editFormVisible" + :issuable="issuable" + :enable-autocomplete="enableAutocomplete" + :description-preview-path="descriptionPreviewPath" + :description-help-path="descriptionHelpPath" + > + <template #edit-form-actions="issuableMeta"> + <slot name="edit-form-actions" v-bind="issuableMeta"></slot> + </template> + </issuable-edit-form> + <template v-else> + <issuable-title + :issuable="issuable" + :status-badge-class="statusBadgeClass" + :status-icon="statusIcon" + :enable-edit="enableEdit" + @edit-issuable="$emit('edit-issuable', $event)" + > + <template #status-badge> + <slot name="status-badge"></slot> + </template> + </issuable-title> + <issuable-description v-if="issuable.descriptionHtml" :issuable="issuable" /> + <small v-if="isUpdated" class="edited-text gl-font-sm!"> + {{ __('Edited') }} + <time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" /> + <span v-if="updatedBy"> + {{ __('by') }} + <gl-link :href="updatedBy.webUrl" class="author-link gl-font-sm!"> + <span>{{ updatedBy.name }}</span> + </gl-link> + </span> + </small> + </template> + </div> + </div> +</template> diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js index debb36dc53f..9f5dce4183c 100644 --- a/app/assets/javascripts/tooltips/index.js +++ b/app/assets/javascripts/tooltips/index.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import jQuery from 'jquery'; -import { toArray, isFunction } from 'lodash'; +import { toArray, isFunction, isElement } from 'lodash'; import Tooltips from './components/tooltips.vue'; let app; @@ -54,7 +54,11 @@ const handleTooltipEvent = (rootTarget, e, selector, config = {}) => { } }; -const applyToElements = (elements, handler) => toArray(elements).forEach(handler); +const applyToElements = (elements, handler) => { + const iterable = isElement(elements) ? [elements] : toArray(elements); + + toArray(iterable).forEach(handler); +}; const invokeBootstrapApi = (elements, method) => { if (isFunction(elements.tooltip)) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 543d70cbdbe..17cd740ddd9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -1,8 +1,7 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ -import { GlLoadingIcon, GlButton } from '@gitlab/ui'; +import { GlLoadingIcon, GlButton, GlTooltipDirective } from '@gitlab/ui'; import { deprecatedCreateFlash as Flash } from '~/flash'; -import tooltip from '~/vue_shared/directives/tooltip'; import { s__, __ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import MrWidgetAuthorTime from '../mr_widget_author_time.vue'; @@ -12,7 +11,7 @@ import eventHub from '../../event_hub'; export default { name: 'MRWidgetMerged', directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, components: { MrWidgetAuthorTime, @@ -115,7 +114,7 @@ export default { /> <gl-button v-if="mr.canRevertInCurrentMR" - v-tooltip + v-gl-tooltip.hover :title="revertTitle" size="small" category="secondary" @@ -128,7 +127,7 @@ export default { </gl-button> <gl-button v-else-if="mr.revertInForkPath" - v-tooltip + v-gl-tooltip.hover :href="mr.revertInForkPath" :title="revertTitle" size="small" @@ -140,7 +139,7 @@ export default { </gl-button> <gl-button v-if="mr.canCherryPickInCurrentMR" - v-tooltip + v-gl-tooltip.hover :title="cherryPickTitle" size="small" href="#modal-cherry-pick-commit" @@ -151,7 +150,7 @@ export default { </gl-button> <gl-button v-else-if="mr.cherryPickInForkPath" - v-tooltip + v-gl-tooltip.hover :href="mr.cherryPickInForkPath" :title="cherryPickTitle" size="small" diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue index a0c161a335a..f2e9c4a4fbb 100644 --- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue +++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue @@ -1,11 +1,11 @@ <script> +import { GlTooltipDirective } from '@gitlab/ui'; import { __ } from '~/locale'; import { roundOffFloat } from '~/lib/utils/common_utils'; -import tooltip from '~/vue_shared/directives/tooltip'; export default { directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, props: { cssClass: { @@ -112,7 +112,7 @@ export default { <span v-if="!totalCount" class="status-unavailable">{{ unavailableLabel }}</span> <span v-if="successPercent" - v-tooltip + v-gl-tooltip :title="successTooltip" :style="successBarStyle" class="status-green" @@ -122,7 +122,7 @@ export default { </span> <span v-if="neutralPercent" - v-tooltip + v-gl-tooltip :title="neutralTooltip" :style="neutralBarStyle" class="status-neutral" @@ -132,7 +132,7 @@ export default { </span> <span v-if="failurePercent" - v-tooltip + v-gl-tooltip :title="failureTooltip" :style="failureBarStyle" class="status-red" diff --git a/app/models/blob_viewer/balsamiq.rb b/app/models/blob_viewer/balsamiq.rb index 1af6c5474d7..6ab73730222 100644 --- a/app/models/blob_viewer/balsamiq.rb +++ b/app/models/blob_viewer/balsamiq.rb @@ -8,7 +8,7 @@ module BlobViewer self.partial_name = 'balsamiq' self.extensions = %w(bmpr) self.binary = true - self.switcher_icon = 'file-image-o' + self.switcher_icon = 'doc-image' self.switcher_title = 'preview' end end diff --git a/app/models/blob_viewer/pdf.rb b/app/models/blob_viewer/pdf.rb index 2cf7752585c..e3542b91d5c 100644 --- a/app/models/blob_viewer/pdf.rb +++ b/app/models/blob_viewer/pdf.rb @@ -8,7 +8,7 @@ module BlobViewer self.partial_name = 'pdf' self.extensions = %w(pdf) self.binary = true - self.switcher_icon = 'file-pdf-o' + self.switcher_icon = 'document' self.switcher_title = 'PDF' end end diff --git a/app/models/blob_viewer/sketch.rb b/app/models/blob_viewer/sketch.rb index 659ab11f30b..90bc9be29f4 100644 --- a/app/models/blob_viewer/sketch.rb +++ b/app/models/blob_viewer/sketch.rb @@ -8,7 +8,7 @@ module BlobViewer self.partial_name = 'sketch' self.extensions = %w(sketch) self.binary = true - self.switcher_icon = 'file-image-o' + self.switcher_icon = 'doc-image' self.switcher_title = 'preview' end end diff --git a/app/models/diff_viewer/rich.rb b/app/models/diff_viewer/rich.rb index 5caefa2031c..0d94d8f773b 100644 --- a/app/models/diff_viewer/rich.rb +++ b/app/models/diff_viewer/rich.rb @@ -6,7 +6,7 @@ module DiffViewer included do self.type = :rich - self.switcher_icon = 'file-text-o' + self.switcher_icon = 'doc-text' self.switcher_title = _('rendered diff') end end diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml index 6ffeea81f28..c77615f9040 100644 --- a/app/views/admin/application_settings/_abuse.html.haml +++ b/app/views/admin/application_settings/_abuse.html.haml @@ -8,4 +8,4 @@ .form-text.text-muted Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index 1701eb5b6e4..f46eb84ce8e 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -60,4 +60,4 @@ = render_if_exists 'admin/application_settings/updating_name_disabled_for_users', form: f = render_if_exists 'admin/application_settings/availability_on_namespace_setting', form: f - = f.submit _('Save changes'), class: 'btn btn-success qa-save-changes-button' + = f.submit _('Save changes'), class: 'gl-button btn btn-success qa-save-changes-button' diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index b9cce6c8085..9f384519c3a 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -60,4 +60,4 @@ = _("The default CI configuration path for new projects.").html_safe = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'custom-ci-configuration-path'), target: '_blank' - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_diff_limits.html.haml b/app/views/admin/application_settings/_diff_limits.html.haml index 734640c16a1..6811c1e10d6 100644 --- a/app/views/admin/application_settings/_diff_limits.html.haml +++ b/app/views/admin/application_settings/_diff_limits.html.haml @@ -12,4 +12,4 @@ = link_to sprite_icon('question-o'), help_page_path('user/admin_area/diff_limits', anchor: 'maximum-diff-patch-size') - = f.submit _('Save changes'), class: 'btn btn-success' + = f.submit _('Save changes'), class: 'gl-button btn btn-success' diff --git a/app/views/admin/application_settings/_eks.html.haml b/app/views/admin/application_settings/_eks.html.haml index d74afcd3e64..68324425ef9 100644 --- a/app/views/admin/application_settings/_eks.html.haml +++ b/app/views/admin/application_settings/_eks.html.haml @@ -28,4 +28,4 @@ = f.label :eks_secret_access_key, 'Secret access key', class: 'label-bold' = f.password_field :eks_secret_access_key, autocomplete: 'off', class: 'form-control' - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml index 49747f2bfd4..dd1be876505 100644 --- a/app/views/admin/application_settings/_email.html.haml +++ b/app/views/admin/application_settings/_email.html.haml @@ -25,4 +25,4 @@ = render_if_exists 'admin/application_settings/email_additional_text_setting', form: f - = f.submit _('Save changes'), class: "btn btn-success", data: { qa_selector: 'save_changes_button' } + = f.submit _('Save changes'), class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' } diff --git a/app/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml index 08620bdde35..c8c1f3e6214 100644 --- a/app/views/admin/application_settings/_external_authorization_service_form.html.haml +++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml @@ -47,4 +47,4 @@ .form-group = f.label :external_authorization_service_default_label, _('Default classification label'), class: 'label-bold' = f.text_field :external_authorization_service_default_label, class: 'form-control' - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml index fac2de8811f..a0cd70b4d7c 100644 --- a/app/views/admin/application_settings/_gitaly.html.haml +++ b/app/views/admin/application_settings/_gitaly.html.haml @@ -24,4 +24,4 @@ .form-text.text-muted Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout. - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml index cf40eb7b108..f0a1fd5e763 100644 --- a/app/views/admin/application_settings/_gitpod.html.haml +++ b/app/views/admin/application_settings/_gitpod.html.haml @@ -26,4 +26,4 @@ = f.text_field :gitpod_url, class: 'form-control', placeholder: s_('Gitpod|e.g. https://gitpod.example.com') .form-text.text-muted = s_('Gitpod|Add the URL to your Gitpod instance configured to read your GitLab projects.') - = f.submit s_('Save changes'), class: 'btn btn-success' + = f.submit s_('Save changes'), class: 'gl-button btn btn-success' diff --git a/app/views/admin/application_settings/_grafana.html.haml b/app/views/admin/application_settings/_grafana.html.haml index 80ff5a298b4..bd2b2094311 100644 --- a/app/views/admin/application_settings/_grafana.html.haml +++ b/app/views/admin/application_settings/_grafana.html.haml @@ -14,4 +14,4 @@ = f.label :grafana_url, _('Grafana URL'), class: 'label-bold' = f.text_field :grafana_url, class: 'form-control', placeholder: '/-/grafana' - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml index 776bc9d821a..fc31f612b8c 100644 --- a/app/views/admin/application_settings/_help_page.html.haml +++ b/app/views/admin/application_settings/_help_page.html.haml @@ -23,4 +23,4 @@ = f.label :help_page_documentation_base_url, _('Documentation pages URL'), class: 'label-bold' = f.text_field :help_page_documentation_base_url, class: 'form-control', placeholder: 'https://docs.gitlab.com' - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_import_export_limits.html.haml b/app/views/admin/application_settings/_import_export_limits.html.haml index d26c3376391..58218a41282 100644 --- a/app/views/admin/application_settings/_import_export_limits.html.haml +++ b/app/views/admin/application_settings/_import_export_limits.html.haml @@ -31,4 +31,4 @@ = f.label :group_download_export_limit, _('Max Group Export Download requests per minute per user'), class: 'label-bold' = f.number_field :group_download_export_limit, class: 'form-control' - = f.submit 'Save changes', class: "btn btn-success", data: { qa_selector: 'save_changes_button' } + = f.submit 'Save changes', class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' } diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml index 9512c1837bf..c1565cf42e1 100644 --- a/app/views/admin/application_settings/_ip_limits.html.haml +++ b/app/views/admin/application_settings/_ip_limits.html.haml @@ -42,4 +42,4 @@ = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'label-bold' = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' - = f.submit 'Save changes', class: "btn btn-success", data: { qa_selector: 'save_changes_button' } + = f.submit 'Save changes', class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' } diff --git a/app/views/admin/application_settings/_issue_limits.html.haml b/app/views/admin/application_settings/_issue_limits.html.haml index b0bdc204f64..200ea3a8ec1 100644 --- a/app/views/admin/application_settings/_issue_limits.html.haml +++ b/app/views/admin/application_settings/_issue_limits.html.haml @@ -6,4 +6,4 @@ = f.label :issues_create_limit, 'Max requests per minute per user', class: 'label-bold' = f.number_field :issues_create_limit, class: 'form-control' - = f.submit 'Save changes', class: "btn btn-success", data: { qa_selector: 'save_changes_button' } + = f.submit 'Save changes', class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' } diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml index e01c123d1db..5ad7080b22b 100644 --- a/app/views/admin/application_settings/_localization.html.haml +++ b/app/views/admin/application_settings/_localization.html.haml @@ -15,4 +15,4 @@ = f.label :time_tracking_limit_to_hours, class: 'form-check-label' do = _('Limit display of time tracking units to hours.') - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml index b0593b3bfa2..4f38ab3ab7a 100644 --- a/app/views/admin/application_settings/_outbound.html.haml +++ b/app/views/admin/application_settings/_outbound.html.haml @@ -27,4 +27,4 @@ %span.form-text.text-muted = _('Resolves IP addresses once and uses them to submit requests') - = f.submit 'Save changes', class: "btn btn-success", data: { qa_selector: 'save_changes_button' } + = f.submit 'Save changes', class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' } diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml index 2ee7f3edc97..d42987eb7d8 100644 --- a/app/views/admin/application_settings/_pages.html.haml +++ b/app/views/admin/application_settings/_pages.html.haml @@ -41,4 +41,4 @@ - terms_of_service_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: lets_encrypt_terms_of_service_admin_application_settings_path } = _("I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end} (PDF)").html_safe % { link_start: terms_of_service_link_start, link_end: '</a>'.html_safe } - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml index 3473c185dbe..2d27bceef10 100644 --- a/app/views/admin/application_settings/_performance.html.haml +++ b/app/views/admin/application_settings/_performance.html.haml @@ -31,4 +31,4 @@ .form-text.text-muted = _('Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value.') - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml index f8bc29048f2..1036cc94bd0 100644 --- a/app/views/admin/application_settings/_performance_bar.html.haml +++ b/app/views/admin/application_settings/_performance_bar.html.haml @@ -11,4 +11,4 @@ = f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'label-bold' = f.text_field :performance_bar_allowed_group_path, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path - = f.submit 'Save changes', class: 'btn btn-success qa-save-changes-button' + = f.submit 'Save changes', class: 'gl-button btn btn-success qa-save-changes-button' diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml index f2011257b8c..324f544a108 100644 --- a/app/views/admin/application_settings/_plantuml.html.haml +++ b/app/views/admin/application_settings/_plantuml.html.haml @@ -24,4 +24,4 @@ = link_to "PlantUML", "http://plantuml.com" diagrams in Asciidoc documents using an external PlantUML service. - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml index 49f58449d29..c571ec1c1b0 100644 --- a/app/views/admin/application_settings/_prometheus.html.haml +++ b/app/views/admin/application_settings/_prometheus.html.haml @@ -30,4 +30,4 @@ A method call is only tracked when it takes longer to complete than the given amount of milliseconds. - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_protected_paths.html.haml b/app/views/admin/application_settings/_protected_paths.html.haml index 0220570daa9..fce64369f17 100644 --- a/app/views/admin/application_settings/_protected_paths.html.haml +++ b/app/views/admin/application_settings/_protected_paths.html.haml @@ -28,4 +28,4 @@ = _('All paths are relative to the GitLab URL. Do not include %{relative_url_link_start}relative URL%{relative_url_link_end}.').html_safe % { relative_url_link_start: relative_url_link_start, relative_url_link_end: '</a>'.html_safe } = f.text_area :protected_paths_raw, placeholder: '/users/sign_in,/users/password', class: 'form-control', rows: 10 - = f.submit 'Save changes', class: 'btn btn-success' + = f.submit 'Save changes', class: 'gl-button btn btn-success' diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml index 0e9731b1c70..cf0b2b53eff 100644 --- a/app/views/admin/application_settings/_realtime.html.haml +++ b/app/views/admin/application_settings/_realtime.html.haml @@ -14,4 +14,4 @@ installations. Set to 0 to completely disable polling. = link_to sprite_icon('question-o'), help_page_path('administration/polling') - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml index 7ff2b6e841d..dd64d0ae419 100644 --- a/app/views/admin/application_settings/_registry.html.haml +++ b/app/views/admin/application_settings/_registry.html.haml @@ -21,4 +21,4 @@ .form-text.text-muted = _("Tags are deleted until the timeout is reached. Any remaining tags are included the next time the policy runs. To remove the time limit, set it to 0.") - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml index 985dee6f594..b9c2e406b78 100644 --- a/app/views/admin/application_settings/_repository_check.html.haml +++ b/app/views/admin/application_settings/_repository_check.html.haml @@ -55,4 +55,4 @@ .form-text.text-muted Number of Git pushes after which 'git gc' is run. - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml index 86f8ea8821e..125fa48bbc3 100644 --- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml +++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml @@ -14,4 +14,4 @@ = render_if_exists 'admin/application_settings/mirror_settings', form: f - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_repository_static_objects.html.haml b/app/views/admin/application_settings/_repository_static_objects.html.haml index 03aa48b2282..00b9b4b8964 100644 --- a/app/views/admin/application_settings/_repository_static_objects.html.haml +++ b/app/views/admin/application_settings/_repository_static_objects.html.haml @@ -15,4 +15,4 @@ %span.form-text.text-muted#static_objects_external_storage_auth_token_help_block = _('A secure token that identifies an external storage request.') - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml index 71c957b0bea..0862d1bf0b6 100644 --- a/app/views/admin/application_settings/_repository_storage.html.haml +++ b/app/views/admin/application_settings/_repository_storage.html.haml @@ -22,4 +22,4 @@ = f.text_field attribute[:name], class: 'form-text-input', value: attribute[:value] = f.label attribute[:label], attribute[:label], class: 'label-bold form-check-label' %br - = f.submit _('Save changes'), class: "btn btn-success qa-save-changes-button" + = f.submit _('Save changes'), class: "gl-button btn btn-success qa-save-changes-button" diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml index f2ff3891ace..4a8616beff6 100644 --- a/app/views/admin/application_settings/_signin.html.haml +++ b/app/views/admin/application_settings/_signin.html.haml @@ -57,4 +57,4 @@ = f.label :sign_in_text, class: 'label-bold' = f.text_area :sign_in_text, class: 'form-control', rows: 4 .form-text.text-muted Markdown enabled - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml index adc585125a6..d614912b2e4 100644 --- a/app/views/admin/application_settings/_signup.html.haml +++ b/app/views/admin/application_settings/_signup.html.haml @@ -75,4 +75,4 @@ = f.label :after_sign_up_text, class: 'label-bold' = f.text_area :after_sign_up_text, class: 'form-control', rows: 4 .form-text.text-muted Markdown enabled - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml index c339d6df363..7c2c5e0b3dc 100644 --- a/app/views/admin/application_settings/_snowplow.html.haml +++ b/app/views/admin/application_settings/_snowplow.html.haml @@ -26,4 +26,4 @@ = f.label :snowplow_cookie_domain, _('Cookie domain'), class: 'label-light' = f.text_field :snowplow_cookie_domain, class: 'form-control' - = f.submit _('Save changes'), class: 'btn btn-success' + = f.submit _('Save changes'), class: 'gl-button btn btn-success' diff --git a/app/views/admin/application_settings/_sourcegraph.html.haml b/app/views/admin/application_settings/_sourcegraph.html.haml index 7650526dfc0..2a4e8f87c31 100644 --- a/app/views/admin/application_settings/_sourcegraph.html.haml +++ b/app/views/admin/application_settings/_sourcegraph.html.haml @@ -35,4 +35,4 @@ = f.text_field :sourcegraph_url, class: 'form-control', placeholder: s_('SourcegraphAdmin|e.g. https://sourcegraph.example.com') .form-text.text-muted = s_('SourcegraphAdmin|Configure the URL to a Sourcegraph instance which can read your GitLab projects.') - = f.submit s_('SourcegraphAdmin|Save changes'), class: 'btn btn-success' + = f.submit s_('SourcegraphAdmin|Save changes'), class: 'gl-button btn btn-success' diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml index ab9368e723e..b54f1d7c829 100644 --- a/app/views/admin/application_settings/_spam.html.haml +++ b/app/views/admin/application_settings/_spam.html.haml @@ -71,4 +71,4 @@ = f.label :spam_check_endpoint_url, _('URL of the external Spam Check endpoint'), class: 'label-bold' = f.text_field :spam_check_endpoint_url, class: 'form-control' - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml index 60d5ca1ee0f..7bc5b2405e8 100644 --- a/app/views/admin/application_settings/_terminal.html.haml +++ b/app/views/admin/application_settings/_terminal.html.haml @@ -8,4 +8,4 @@ .form-text.text-muted Maximum time for web terminal websocket connection (in seconds). 0 for unlimited. - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml index 07d372882b9..10db1e23d7b 100644 --- a/app/views/admin/application_settings/_terms.html.haml +++ b/app/views/admin/application_settings/_terms.html.haml @@ -15,4 +15,4 @@ = f.text_area :terms, class: 'form-control', rows: 8 .form-text.text-muted = _("Markdown enabled") - = f.submit _("Save changes"), class: "btn btn-success" + = f.submit _("Save changes"), class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_third_party_offers.html.haml b/app/views/admin/application_settings/_third_party_offers.html.haml index 0ed7341986d..7e3e063118e 100644 --- a/app/views/admin/application_settings/_third_party_offers.html.haml +++ b/app/views/admin/application_settings/_third_party_offers.html.haml @@ -17,4 +17,4 @@ = f.check_box :hide_third_party_offers, class: 'form-check-input' = f.label :hide_third_party_offers, _('Do not display offers from third parties within GitLab'), class: 'form-check-label' - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index 88c88b96a91..2ba7dcefd44 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -37,4 +37,4 @@ - deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path } = s_('For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}.').html_safe % { deactivating_usage_ping_link_start: deactivating_usage_ping_link_start, deactivating_usage_ping_link_end: '</a>'.html_safe } - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index d44a1524f2e..46d8a8ac9c7 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -66,4 +66,4 @@ .form-group = f.label field_name, "#{type.upcase} SSH keys", class: 'label-bold' = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control' - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "gl-button btn btn-success" diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml index 493c995dd91..2d336bebc8d 100644 --- a/app/views/admin/application_settings/general.html.haml +++ b/app/views/admin/application_settings/general.html.haml @@ -101,7 +101,7 @@ = s_('IDE|Live Preview') %span.form-text.text-muted = s_('IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview.') - = f.submit _('Save changes'), class: "btn btn-success" + = f.submit _('Save changes'), class: "gl-button btn btn-success" - if Feature.enabled?(:maintenance_mode) %section.settings.no-animate#js-maintenance-mode-toggle{ class: ('expanded' if expanded_by_default?) } diff --git a/changelogs/unreleased/229327-migrate-bootstrap-button-to-gitlab-ui-glbutton-in-ee-app-assets-ja.yml b/changelogs/unreleased/229327-migrate-bootstrap-button-to-gitlab-ui-glbutton-in-ee-app-assets-ja.yml new file mode 100644 index 00000000000..52666825ae1 --- /dev/null +++ b/changelogs/unreleased/229327-migrate-bootstrap-button-to-gitlab-ui-glbutton-in-ee-app-assets-ja.yml @@ -0,0 +1,5 @@ +--- +title: Replace button component +merge_request: 42716 +author: +type: changed diff --git a/changelogs/unreleased/mw-replace-switcher-icons.yml b/changelogs/unreleased/mw-replace-switcher-icons.yml new file mode 100644 index 00000000000..d09bf1abc98 --- /dev/null +++ b/changelogs/unreleased/mw-replace-switcher-icons.yml @@ -0,0 +1,5 @@ +--- +title: Replace switcher fa- icons in blob viewer models +merge_request: 45124 +author: +type: changed diff --git a/doc/administration/auth/ldap/ldap-troubleshooting.md b/doc/administration/auth/ldap/ldap-troubleshooting.md index 3d3ac124ac4..c6558bf1791 100644 --- a/doc/administration/auth/ldap/ldap-troubleshooting.md +++ b/doc/administration/auth/ldap/ldap-troubleshooting.md @@ -682,7 +682,7 @@ The rails console is a valuable tool to help debug LDAP problems. It allows you directly interact with the application by running commands and seeing how GitLab responds to them. -Please refer to [this guide](../../troubleshooting/debug.md#starting-a-rails-console-session) +Please refer to [this guide](../../operations/rails_console.md#starting-a-rails-console-session) for instructions on how to use the rails console. #### Enable debug output diff --git a/doc/administration/feature_flags.md b/doc/administration/feature_flags.md index 14ac4d12a2e..4129677f134 100644 --- a/doc/administration/feature_flags.md +++ b/doc/administration/feature_flags.md @@ -65,7 +65,7 @@ For installations from the source: sudo -u git -H bundle exec rails console -e production ``` -For details, see [starting a Rails console session](troubleshooting/debug.md#starting-a-rails-console-session). +For details, see [starting a Rails console session](operations/rails_console.md#starting-a-rails-console-session). ### Enable or disable the feature diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index e647c020e01..75a87d46109 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -163,7 +163,7 @@ There is a limit when embedding metrics in GFM for performance reasons. On GitLab.com, the [maximum number of webhooks and their size](../user/gitlab_com/index.md#webhooks) per project, and per group, is limited. To set this limit on a self-managed installation, run the following in the -[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session): +[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): ```ruby # If limits don't exist for the default plan, you can create one with: @@ -203,7 +203,7 @@ support keyset-based pagination. More information about pagination options can b found in the [API docs section on pagination](../api/README.md#pagination). To set this limit on a self-managed installation, run the following in the -[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session): +[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): ```ruby # If limits don't exist for the default plan, you can create one with: @@ -238,7 +238,7 @@ will fail with a `job_activity_limit_exceeded` error. This limit is disabled by default. To set this limit on a self-managed installation, run the following in the -[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session): +[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): ```ruby # If limits don't exist for the default plan, you can create one with: @@ -264,7 +264,7 @@ limit, the subscription will be considered invalid. - On [GitLab Starter](https://about.gitlab.com/pricing/#self-managed) tier or higher self-managed installations, this limit is defined for the `default` plan that affects all projects. To set this limit on a self-managed installation, run the following in the -[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session): +[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): ```ruby Plan.default.actual_limits.update!(ci_project_subscriptions: 500) @@ -290,7 +290,7 @@ or higher tiers), this limit is defined for the `default` plan that affects all projects. By default, there is no limit. To set this limit on a self-managed installation, run the following in the -[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session): +[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): ```ruby Plan.default.actual_limits.update!(ci_pipeline_schedules: 100) @@ -308,7 +308,7 @@ On self-managed instances this limit is defined for the `default` plan. By defau this limit is set to `25`. To update this limit to a new value on a self-managed installation, run the following in the -[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session): +[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): ```ruby Plan.default.actual_limits.update!(ci_instance_level_variables: 30) @@ -361,7 +361,7 @@ setting is used: | `ci_max_artifact_size_trace` | 0 | For example, to set the `ci_max_artifact_size_junit` limit to 10MB on a self-managed -installation, run the following in the [GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session): +installation, run the following in the [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): ```ruby Plan.default.actual_limits.update!(ci_max_artifact_size_junit: 10) @@ -538,7 +538,7 @@ On GitLab.com, the maximum file size for a package that's uploaded to the [GitLa - PyPI: 3GB To set this limit on a self-managed installation, run the following in the -[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session): +[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session): ```ruby # File size limit is stored in bytes diff --git a/doc/administration/operations/rails_console.md b/doc/administration/operations/rails_console.md new file mode 100644 index 00000000000..9db9a885baf --- /dev/null +++ b/doc/administration/operations/rails_console.md @@ -0,0 +1,144 @@ +# The Rails Console + +The [Rails console](https://guides.rubyonrails.org/command_line.html#rails-console). +provides a way to interact with your GitLab instance from the command line. + +CAUTION: **Caution:** +The Rails console interacts directly with GitLab. In many cases, +there are no handrails to prevent you from permanently modifying, corrupting +or destroying production data. If you would like to explore the Rails console +with no consequences, you are strongly advised to do so in a test environment. + +The Rails console is for GitLab system administrators who are troubleshooting +a problem or need to retrieve some data that can only be done through direct +access of the GitLab application. + +## Starting a Rails console session + +**For Omnibus installations** + +```shell +sudo gitlab-rails console +``` + +**For installations from source** + +```shell +sudo -u git -H bundle exec rails console -e production +``` + +**For Kubernetes deployments** + +The console is in the task-runner pod. Refer to our [Kubernetes cheat sheet](../troubleshooting/kubernetes_cheat_sheet.md#gitlab-specific-kubernetes-information) for details. + +To exit the console, type: `quit`. + +## Output Rails console session history + +Enter the following command on the rails console to display +your command history. + +```ruby +puts Readline::HISTORY.to_a +``` + +You can then copy it to your clipboard and save for future reference. + +## Using the Rails Runner + +If you need to run some Ruby code in the context of your GitLab production +environment, you can do so using the [Rails Runner](https://guides.rubyonrails.org/command_line.html#rails-runner). +When executing a script file, the script must be accessible by the `git` user. + +When the command or script completes, the Rails Runner process finishes. +It is useful for running within other scripts or cron jobs for example. + +**For Omnibus installations** + +```shell +sudo gitlab-rails runner "RAILS_COMMAND" + +# Example with a two-line Ruby script +sudo gitlab-rails runner "user = User.first; puts user.username" + +# Example with a ruby script file (make sure to use the full path) +sudo gitlab-rails runner /path/to/script.rb +``` + +**For installations from source** + +```shell +sudo -u git -H bundle exec rails runner -e production "RAILS_COMMAND" + +# Example with a two-line Ruby script +sudo -u git -H bundle exec rails runner -e production "user = User.first; puts user.username" + +# Example with a ruby script file (make sure to use the full path) +sudo -u git -H bundle exec rails runner -e production /path/to/script.rb +``` + +Rails Runner does not produce the same output as the console. + +If you set a variable on the console, the console will generate useful debug output +such as the variable contents or properties of referenced entity: + +```ruby +irb(main):001:0> user = User.first +=> #<User id:1 @root> +``` + +Rails Runner does not do this: you have to be explicit about generating +output: + +```shell +$ sudo gitlab-rails runner "user = User.first" +$ sudo gitlab-rails runner "user = User.first; puts user.username ; puts user.id" +root +1 +``` + +Some basic knowledge of Ruby will be very useful. Try [this +30-minute tutorial](https://try.ruby-lang.org/) for a quick introduction. +Rails experience is helpful but not essential. + +### Troubleshooting Rails Runner + +The `gitlab-rails` command executes Rails Runner using a non-root account and group, by default: `git:git`. + +If the non-root account cannot find the Ruby script filename passed to `gitlab-rails runner` +you may get a syntax error, not an error that the file couldn't be accessed. + +A common reason for this is that the script has been put in the root account's home directory. + +`runner` tries to parse the path and file parameter as Ruby code. + +For example: + +```plaintext +[root ~]# echo 'puts "hello world"' > ./helloworld.rb +[root ~]# sudo gitlab-rails runner ./helloworld.rb +Please specify a valid ruby command or the path of a script to run. +Run 'rails runner -h' for help. + +/opt/gitlab/..../runner_command.rb:45: syntax error, unexpected '.' +./helloworld.rb +^ +[root ~]# sudo gitlab-rails runner /root/helloworld.rb +Please specify a valid ruby command or the path of a script to run. +Run 'rails runner -h' for help. + +/opt/gitlab/..../runner_command.rb:45: unknown regexp options - hllwrld +[root ~]# mv ~/helloworld.rb /tmp +[root ~]# sudo gitlab-rails runner /tmp/helloworld.rb +hello world +``` + +A meaningful error should be generated if the directory can be accessed, but the file cannot: + +```plaintext +[root ~]# chmod 400 /tmp/helloworld.rb +[root ~]# sudo gitlab-rails runner /tmp/helloworld.rb +Traceback (most recent call last): + [traceback removed] +/opt/gitlab/..../runner_command.rb:42:in `load': cannot load such file -- /tmp/helloworld.rb (LoadError) +``` diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md index fdf342fd420..bdccd6d5fe9 100644 --- a/doc/administration/raketasks/check.md +++ b/doc/administration/raketasks/check.md @@ -148,7 +148,7 @@ above. for the affected project(s). If the issue persists, try triggering `gc` via the -[Rails Console](../troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session): +[Rails Console](../operations/rails_console.md#starting-a-rails-console-session): ```ruby p = Project.find_by_path("project-name") @@ -171,7 +171,7 @@ Checking integrity of Uploads Done! ``` -To delete these references to remote uploads that were deleted externally, open the [GitLab Rails Console](../troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session) and run: +To delete these references to remote uploads that were deleted externally, open the [GitLab Rails Console](../operations/rails_console.md#starting-a-rails-console-session) and run: ```ruby uploads_deleted=0 diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md index 1c036cfe2ac..b272c4b463e 100644 --- a/doc/administration/repository_storage_types.md +++ b/doc/administration/repository_storage_types.md @@ -83,7 +83,7 @@ The "Gitaly relative path" is shown there, for example: This is the path under `/var/opt/gitlab/git-data/repositories/` on a default Omnibus installation. -In a [Rails console](troubleshooting/debug.md#starting-a-rails-console-session), +In a [Rails console](operations/rails_console.md#starting-a-rails-console-session), get this information using either the numeric project ID or the full path: ```ruby @@ -95,7 +95,7 @@ Project.find_by_full_path('group/project').disk_path To translate from a hashed storage path to a project name: -1. Start a [Rails console](troubleshooting/debug.md#starting-a-rails-console-session). +1. Start a [Rails console](operations/rails_console.md#starting-a-rails-console-session). 1. Run the following: ```ruby diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md index a1b4df9b94e..ef95bdc8602 100644 --- a/doc/administration/troubleshooting/debug.md +++ b/doc/administration/troubleshooting/debug.md @@ -5,27 +5,15 @@ in production. ## Starting a Rails console session -Troubleshooting and debugging your GitLab instance often requires a -[Rails console](https://guides.rubyonrails.org/command_line.html#rails-console). +Troubleshooting and debugging your GitLab instance often requires a Rails console. + +Your type of GitLab installation determines how +[to start a rails console](../operations/rails_console.md). See also: - [GitLab Rails Console Cheat Sheet](gitlab_rails_cheat_sheet.md). - [Navigating GitLab via Rails console](navigating_gitlab_via_rails_console.md). -**For Omnibus installations** - -```shell -sudo gitlab-rails console -``` - -**For installations from source** - -```shell -sudo -u git -H bundle exec rails console -e production -``` - -Kubernetes: the console is in the task-runner pod, refer to our [Kubernetes cheat sheet](kubernetes_cheat_sheet.md#gitlab-specific-kubernetes-information) for details. - ### Enabling Active Record logging You can enable output of Active Record debug logging in the Rails console @@ -98,7 +86,7 @@ sudo -u git -H bundle exec rails runner -e production /path/to/script.rb A common problem is that mails are not being sent for some reason. Suppose you configured an SMTP server, but you're not seeing mail delivered. Here's how to check the settings: -1. Run a [Rails console](#starting-a-rails-console-session). +1. Run a [Rails console](../operations/rails_console.md#starting-a-rails-console-session). 1. Look at the ActionMailer `delivery_method` to make sure it matches what you intended. If you configured SMTP, it should say `:smtp`. If you're using @@ -238,7 +226,7 @@ separate Rails process to debug the issue: 1. Log in to your GitLab account. 1. Copy the URL that is causing problems (e.g. `https://gitlab.com/ABC`). 1. Create a Personal Access Token for your user (Profile Settings -> Access Tokens). -1. Bring up the [GitLab Rails console.](#starting-a-rails-console-session) +1. Bring up the [GitLab Rails console.](../operations/rails_console.md#starting-a-rails-console-session) 1. At the Rails console, run: ```ruby diff --git a/doc/administration/troubleshooting/navigating_gitlab_via_rails_console.md b/doc/administration/troubleshooting/navigating_gitlab_via_rails_console.md index 571973c12d9..a1485249b0e 100644 --- a/doc/administration/troubleshooting/navigating_gitlab_via_rails_console.md +++ b/doc/administration/troubleshooting/navigating_gitlab_via_rails_console.md @@ -3,7 +3,7 @@ At the heart of GitLab is a web application [built using the Ruby on Rails framework](https://about.gitlab.com/blog/2018/10/29/why-we-use-rails-to-build-gitlab/). Thanks to this, we also get access to the amazing tools built right into Rails. -In this guide, we'll introduce the [Rails console](debug.md#starting-a-rails-console-session) +In this guide, we'll introduce the [Rails console](../operations/rails_console.md#starting-a-rails-console-session) and the basics of interacting with your GitLab instance from the command line. CAUTION: **Caution:** @@ -20,20 +20,10 @@ Rails experience is helpful to have but not a must. ## Starting a Rails console session -Omnibus GitLab comes with a convenient wrapper command which automatically loads -the production GitLab environment: +Your type of GitLab installation determines how +[to start a rails console](../operations/rails_console.md). -```shell -sudo gitlab-rails console -``` - -For source installations, you'll have to instead run: - -```shell -sudo -u git -H bundle exec rails console -e production -``` - -Further code examples will all take place inside the Rails console and also +The following code examples will all take place inside the Rails console and also assume an Omnibus GitLab installation. ## Active Record objects diff --git a/doc/administration/troubleshooting/ssl.md b/doc/administration/troubleshooting/ssl.md index dc3c08b61ac..4d4b9755fa9 100644 --- a/doc/administration/troubleshooting/ssl.md +++ b/doc/administration/troubleshooting/ssl.md @@ -23,7 +23,7 @@ After configuring a GitLab instance with an internal CA certificate, you might n More details here: https://curl.haxx.se/docs/sslcerts.html ``` -- Testing via the [rails console](debug.md#starting-a-rails-console-session) also fails: +- Testing via the [rails console](../operations/rails_console.md#starting-a-rails-console-session) also fails: ```ruby uri = URI.parse("https://gitlab.domain.tld") diff --git a/doc/integration/github.md b/doc/integration/github.md index 4444db46853..ce2b50acc54 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -150,7 +150,7 @@ via Omnibus, or [restart GitLab](../administration/restart_gitlab.md#installatio Check the [`production.log`](../administration/logs.md#productionlog) on your GitLab server to obtain further details. If you are getting the error like `Faraday::ConnectionFailed (execution expired)` in the log, there may be a connectivity issue -between your GitLab instance and GitHub Enterprise. To verify it, [start the rails console](../administration/troubleshooting/debug.md#starting-a-rails-console-session) +between your GitLab instance and GitHub Enterprise. To verify it, [start the rails console](../administration/operations/rails_console.md#starting-a-rails-console-session) and run the commands below replacing `<github_url>` with the URL of your GitHub Enterprise instance: ```ruby diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md index c8ca92b4bae..9d1560ccfd5 100644 --- a/doc/raketasks/import.md +++ b/doc/raketasks/import.md @@ -131,7 +131,7 @@ Bare repositories are **not** importable by GitLab 10.4 to GitLab 11.6, if all t bare repositories are importable. To manually migrate repositories yourself (for GitLab 10.4 to GitLab 11.6), you can use the -[Rails console](../administration/troubleshooting/debug.md#starting-a-rails-console-session) +[Rails console](../administration/operations/rails_console.md#starting-a-rails-console-session) to do so. In a Rails console session, run the following to migrate a project: ```ruby diff --git a/doc/security/project_import_decompressed_archive_size_limits.md b/doc/security/project_import_decompressed_archive_size_limits.md index dd67db23d6b..16821e1f192 100644 --- a/doc/security/project_import_decompressed_archive_size_limits.md +++ b/doc/security/project_import_decompressed_archive_size_limits.md @@ -17,7 +17,7 @@ If you have a project with decompressed size exceeding this limit, it is possible to disable the validation by turning off the `validate_import_decompressed_archive_size` feature flag. -Start a [Rails console](../administration/troubleshooting/debug.md#starting-a-rails-console-session). +Start a [Rails console](../administration/operations/rails_console.md#starting-a-rails-console-session). ```ruby # Disable diff --git a/doc/update/README.md b/doc/update/README.md index a7f7aaf5887..b5e99671278 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -123,7 +123,7 @@ If using GitLab 12.9 and newer, run: sudo gitlab-rails runner -e production 'puts Gitlab::BackgroundMigration.remaining' ``` -If using GitLab 12.8 and older, run the following using a [Rails console](../administration/troubleshooting/debug.md#starting-a-rails-console-session): +If using GitLab 12.8 and older, run the following using a [Rails console](../administration/operations/rails_console.md#starting-a-rails-console-session): ```ruby puts Sidekiq::Queue.new("background_migration").size @@ -141,7 +141,7 @@ cd /home/git/gitlab sudo -u git -H bundle exec rails runner -e production 'puts Gitlab::BackgroundMigration.remaining' ``` -If using GitLab 12.8 and older, run the following using a [Rails console](../administration/troubleshooting/debug.md#starting-a-rails-console-session): +If using GitLab 12.8 and older, run the following using a [Rails console](../administration/operations/rails_console.md#starting-a-rails-console-session): ```ruby puts Sidekiq::Queue.new("background_migration").size diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index 33235eef7b8..3152966a6b3 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -461,7 +461,7 @@ Cleanup policies can be run on all projects, with these exceptions: for all projects (even those created before 12.8) in [GitLab application settings](../../../api/settings.md#change-application-settings) by setting `container_expiration_policies_enable_historic_entries` to true. - Alternatively, you can execute the following command in the [Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session): + Alternatively, you can execute the following command in the [Rails console](../../../administration/operations/rails_console.md#starting-a-rails-console-session): ```ruby ApplicationSetting.last.update(container_expiration_policies_enable_historic_entries: true) diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 1c0c1255ee3..911be117716 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -71,7 +71,7 @@ the following table. You can programmatically create a predetermined personal access token for use in automation or tests. You need sufficient access to run a -[Rails console session](../../administration/troubleshooting/debug.md#starting-a-rails-console-session) +[Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session) for your GitLab instance. To create a token belonging to a user with username `automation-bot`, run the @@ -101,7 +101,7 @@ The list of valid scopes and what they do can be found ## Programmatically revoking a personal access token You can programmatically revoke a personal access token. You need -sufficient access to run a [Rails console session](../../administration/troubleshooting/debug.md#starting-a-rails-console-session) +sufficient access to run a [Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session) for your GitLab instance. To revoke a known token `token-string-here123`, run the following in the Rails diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md index b89dd734042..bfc37977c9b 100644 --- a/doc/user/project/labels.md +++ b/doc/user/project/labels.md @@ -57,7 +57,7 @@ and edit labels. ### Project labels -View the project labels list by going to the project and clicking **Issues > Labels**. +To view the project labels list, navigate to the project and click **Issues > Labels**. The list includes all labels that are defined at the project level, as well as all labels inherited from the immediate parent group. For each label, you can see the project or group path from where it was created. @@ -89,19 +89,19 @@ a label by clicking the three dots (**{ellipsis_v}**) next to the **Subscribe** and selecting **Delete**. CAUTION: **Caution:** -If you delete a label, it is permanently deleted. You will not be able to undo the deletion, and all references to the label will be removed from the system. +If you delete a label, it is permanently deleted. All references to the label are removed from the system and you cannot undo the deletion. #### Promote a project label to a group label If you previously created a project label and now want to make it available for other projects within the same group, you can promote it to a group label. -If other projects in the same group have a label with the same title, they will all be -merged with the new group label. If a group label with the same title exists, it will -also be merged. +If other projects in the same group have a label with the same title, they are all +merged with the new group label. If a group label with the same title exists, it is +also merged. All issues, merge requests, issue board lists, issue board filters, and label subscriptions -with the old labels will be assigned to the new group label. +with the old labels are assigned to the new group label. CAUTION: **Caution:** Promoting a label is a permanent action, and cannot be reversed. @@ -114,7 +114,7 @@ To promote a project label to a group label: ### Group labels -View the group labels list by going to the group and clicking **Issues > Labels**. +To view the group labels list, navigate to the group and click **Issues > Labels**. The list includes all labels that are defined at the group level only. It does not list any labels that are defined in projects. You can filter the list by entering a search query at the top and clicking search (**{search}**). @@ -124,15 +124,15 @@ follow the same process as [creating a project label](#project-labels). #### Create group labels from epics **(ULTIMATE)** -You can create group labels from the Epic sidebar. The labels you create will +You can create group labels from the epic sidebar. The labels you create belong to the immediate group to which the epic belongs. The process is the same as creating a [project label from an issue or merge request](#project-labels). ### Generate default labels If a project or group has no labels, you can generate a default set of project or group -labels from the label list page. The page will show a **Generate a default set of labels** -button if the list is empty, and clicking it will add the following default labels +labels from the label list page. The page shows a **Generate a default set of labels** +button if the list is empty. Select the button to add the following default labels to the project: - `bug` @@ -158,10 +158,10 @@ title, for example: ![Scoped labels](img/labels_key_value_v13_5.png) An issue, merge request or epic cannot have two scoped labels, of the form `key::value`, -with the same `key`. Adding a new label with the same `key`, but a different `value` will -cause the previous `key` label to be replaced with the new label. +with the same `key`. Adding a new label with the same `key`, but a different `value` +causes the previous `key` label to be replaced with the new label. -Example use case: +For example: 1. An issue is identified as being low priority, and a `priority::low` project label is added to it. @@ -194,7 +194,7 @@ This functionality is demonstrated in a video regarding ### Scoped labels with nested scopes You can create a label with a nested scope by using multiple double colons `::` when creating -it. In this case, everything before the last `::` will be the scope. +it. In this case, everything before the last `::` is the scope. For example, `workflow::backend::review` and `workflow::backend::development` are valid scoped labels, but they **can't** exist on the same issue at the same time, as they @@ -208,7 +208,7 @@ both have different scopes, `workflow::frontend` and `workflow::backend`. From the project label list page and the group label list page, you can click **Subscribe** to the right of any label to enable [notifications](../profile/notifications.md) for that -label. You will be notified whenever the label is assigned to an epic, +label. You are notified whenever the label is assigned to an epic, issue, or merge request. If you are subscribing to a group label from within a project, you can select to subscribe diff --git a/doc/user/upgrade_email_bypass.md b/doc/user/upgrade_email_bypass.md index 027f7337228..35fddc4a1d4 100644 --- a/doc/user/upgrade_email_bypass.md +++ b/doc/user/upgrade_email_bypass.md @@ -82,7 +82,7 @@ As an administrator, you may also confirm a user in the [Admin Area](admin_area/ ## What do I do if I am an administrator and I am locked out? If you are an administrator and cannot otherwise verify your email address, sign in to your GitLab -instance with a [Rails console session](../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session). +instance with a [Rails console session](../administration/operations/rails_console.md#starting-a-rails-console-session). Once connected, run the following commands to confirm your administrator account: ```ruby @@ -94,7 +94,7 @@ admin.save! ## How do I force-confirm all users on my self-managed instance? If you are an administrator and would like to force-confirm all users on your system, sign in to your GitLab -instance with a [Rails console session](../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session). +instance with a [Rails console session](../administration/operations/rails_console.md#starting-a-rails-console-session). Once connected, run the following commands to confirm all user accounts: ```ruby diff --git a/lib/gitlab/database/reindexing/concurrent_reindex.rb b/lib/gitlab/database/reindexing/concurrent_reindex.rb index c4da82c2241..fd3dca88567 100644 --- a/lib/gitlab/database/reindexing/concurrent_reindex.rb +++ b/lib/gitlab/database/reindexing/concurrent_reindex.rb @@ -111,7 +111,7 @@ module Gitlab end def set_statement_timeout - execute("SET statement_timeout TO #{STATEMENT_TIMEOUT}") + execute("SET statement_timeout TO '%ds'" % STATEMENT_TIMEOUT) yield ensure execute('RESET statement_timeout') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index adc1743d1d5..662bae4cf43 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9545,6 +9545,9 @@ msgstr "" msgid "Edit your most recent comment in a thread (from an empty textarea)" msgstr "" +msgid "Edited" +msgstr "" + msgid "Edited %{timeago}" msgstr "" @@ -21110,6 +21113,9 @@ msgstr "" msgid "ProtectedBranch|Code owner approval" msgstr "" +msgid "ProtectedBranch|Does not apply to users allowed to merge or push." +msgstr "" + msgid "ProtectedBranch|Protect" msgstr "" diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index ce59574ef61..5a50cc063f9 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -705,12 +705,12 @@ describe('GfmAutoComplete', () => { }); describe('emoji', () => { - const { atom } = emojiFixtureMap; + const { atom, heart, star } = emojiFixtureMap; const assertInserted = ({ input, subject, emoji }) => expect(subject).toBe(`:${emoji?.name || input}:`); - const assertTemplated = ({ input, subject, emoji }) => + const assertTemplated = ({ input, subject, emoji, field }) => expect(subject.replace(/\s+/g, ' ')).toBe( - `<li>${input} <gl-emoji data-name="${emoji?.name || input}"></gl-emoji> </li>`, + `<li>${field || input} <gl-emoji data-name="${emoji?.name || input}"></gl-emoji> </li>`, ); let mock; @@ -731,33 +731,85 @@ describe('GfmAutoComplete', () => { ${'insertTemplateFunction'} | ${name => ({ name })} | ${assertInserted} ${'templateFunction'} | ${name => name} | ${assertTemplated} `('Emoji.$name', ({ name, inputFormat, assert }) => { - const execute = (input, emoji) => + const execute = (accessor, input, emoji) => assert({ input, emoji, + field: accessor && accessor(emoji), subject: GfmAutoComplete.Emoji[name](inputFormat(input)), }); describeEmojiFields('for $field', ({ accessor }) => { it('should work with lowercase', () => { - execute(accessor(atom), atom); + execute(accessor, accessor(atom), atom); }); it('should work with uppercase', () => { - execute(accessor(atom).toUpperCase(), atom); + execute(accessor, accessor(atom).toUpperCase(), atom); }); it('should work with partial value', () => { - execute(accessor(atom).slice(1), atom); + execute(accessor, accessor(atom).slice(1), atom); }); }); it('should work with unicode value', () => { - execute(atom.moji, atom); + execute(null, atom.moji, atom); }); it('should pass through unknown value', () => { - execute('foo bar baz'); + execute(null, 'foo bar baz'); + }); + }); + + const expectEmojiOrder = (first, second) => { + const keys = Object.keys(emojiFixtureMap); + const firstIndex = keys.indexOf(first); + const secondIndex = keys.indexOf(second); + expect(firstIndex).toBeGreaterThanOrEqual(0); + expect(secondIndex).toBeGreaterThanOrEqual(0); + expect(firstIndex).toBeLessThan(secondIndex); + }; + + describe('Emoji.insertTemplateFunction', () => { + it('should map ":heart" to :heart: [regression]', () => { + // the bug mapped heart to black_heart because the latter sorted first + expectEmojiOrder('black_heart', 'heart'); + + const item = GfmAutoComplete.Emoji.insertTemplateFunction({ name: 'heart' }); + expect(item).toEqual(`:${heart.name}:`); + }); + + it('should map ":star" to :star: [regression]', () => { + // the bug mapped star to custard because the latter sorted first + expectEmojiOrder('custard', 'star'); + + const item = GfmAutoComplete.Emoji.insertTemplateFunction({ name: 'star' }); + expect(item).toEqual(`:${star.name}:`); + }); + }); + + describe('Emoji.templateFunction', () => { + it('should map ":heart" to ❤ [regression]', () => { + // the bug mapped heart to black_heart because the latter sorted first + expectEmojiOrder('black_heart', 'heart'); + + const item = GfmAutoComplete.Emoji.templateFunction('heart') + .replace(/(<gl-emoji)\s+(data-name)/, '$1 $2') + .replace(/>\s+|\s+</g, s => s.trim()); + expect(item).toEqual( + `<li>${heart.name}<gl-emoji data-name="${heart.name}"></gl-emoji></li>`, + ); + }); + + it('should map ":star" to ⭐ [regression]', () => { + // the bug mapped star to custard because the latter sorted first + expectEmojiOrder('custard', 'star'); + + const item = GfmAutoComplete.Emoji.templateFunction('star') + .replace(/(<gl-emoji)\s+(data-name)/, '$1 $2') + .replace(/>\s+|\s+</g, s => s.trim()); + expect(item).toEqual(`<li>${star.name}<gl-emoji data-name="${star.name}"></gl-emoji></li>`); }); }); }); diff --git a/spec/frontend/helpers/emoji.js b/spec/frontend/helpers/emoji.js index 0f257c32a68..e8a93e21818 100644 --- a/spec/frontend/helpers/emoji.js +++ b/spec/frontend/helpers/emoji.js @@ -4,42 +4,64 @@ import { initEmojiMap, EMOJI_VERSION } from '~/emoji'; export const emojiFixtureMap = { atom: { - name: 'atom', moji: '⚛', description: 'atom symbol', unicodeVersion: '4.1', aliases: ['atom_symbol'], }, bomb: { - name: 'bomb', moji: '💣', unicodeVersion: '6.0', description: 'bomb', - aliases: [], }, construction_worker_tone5: { - name: 'construction_worker_tone5', moji: '👷🏿', unicodeVersion: '8.0', description: 'construction worker tone 5', - aliases: [], }, five: { - name: 'five', moji: '5️⃣', unicodeVersion: '3.0', description: 'keycap digit five', - aliases: [], }, grey_question: { - name: 'grey_question', moji: '❔', unicodeVersion: '6.0', description: 'white question mark ornament', - aliases: [], + }, + + // used for regression tests + // black_heart MUST come before heart + // custard MUST come before star + black_heart: { + moji: '🖤', + unicodeVersion: '1.1', + description: 'black heart', + }, + heart: { + moji: '❤', + unicodeVersion: '1.1', + description: 'heavy black heart', + }, + custard: { + moji: '🍮', + unicodeVersion: '6.0', + description: 'custard', + }, + star: { + moji: '⭐', + unicodeVersion: '5.1', + description: 'white medium star', }, }; +Object.keys(emojiFixtureMap).forEach(k => { + emojiFixtureMap[k].name = k; + if (!emojiFixtureMap[k].aliases) { + emojiFixtureMap[k].aliases = []; + } +}); + export async function initEmojiMock() { const emojiData = Object.fromEntries( Object.values(emojiFixtureMap).map(m => { diff --git a/spec/frontend/issuable_show/components/issuable_body_spec.js b/spec/frontend/issuable_show/components/issuable_body_spec.js new file mode 100644 index 00000000000..0e4475e8103 --- /dev/null +++ b/spec/frontend/issuable_show/components/issuable_body_spec.js @@ -0,0 +1,140 @@ +import { shallowMount } from '@vue/test-utils'; + +import IssuableBody from '~/issuable_show/components/issuable_body.vue'; + +import IssuableTitle from '~/issuable_show/components/issuable_title.vue'; +import IssuableDescription from '~/issuable_show/components/issuable_description.vue'; +import IssuableEditForm from '~/issuable_show/components/issuable_edit_form.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +import { mockIssuableShowProps, mockIssuable } from '../mock_data'; + +jest.mock('~/autosave'); + +const issuableBodyProps = { + ...mockIssuableShowProps, + issuable: mockIssuable, +}; + +const createComponent = (propsData = issuableBodyProps) => + shallowMount(IssuableBody, { + propsData, + stubs: { + IssuableTitle, + IssuableDescription, + IssuableEditForm, + TimeAgoTooltip, + }, + slots: { + 'status-badge': 'Open', + 'edit-form-actions': ` + <button class="js-save">Save changes</button> + <button class="js-cancel">Cancel</button> + `, + }, + }); + +describe('IssuableBody', () => { + let wrapper; + + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('computed', () => { + describe('isUpdated', () => { + it.each` + updatedAt | returnValue + ${mockIssuable.updatedAt} | ${true} + ${null} | ${false} + ${''} | ${false} + `( + 'returns $returnValue when value of `updateAt` prop is `$updatedAt`', + async ({ updatedAt, returnValue }) => { + wrapper.setProps({ + issuable: { + ...mockIssuable, + updatedAt, + }, + }); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.isUpdated).toBe(returnValue); + }, + ); + }); + + describe('updatedBy', () => { + it('returns value of `issuable.updatedBy`', () => { + expect(wrapper.vm.updatedBy).toBe(mockIssuable.updatedBy); + }); + }); + }); + + describe('template', () => { + it('renders issuable-title component', () => { + const titleEl = wrapper.find(IssuableTitle); + + expect(titleEl.exists()).toBe(true); + expect(titleEl.props()).toMatchObject({ + issuable: issuableBodyProps.issuable, + statusBadgeClass: issuableBodyProps.statusBadgeClass, + statusIcon: issuableBodyProps.statusIcon, + enableEdit: issuableBodyProps.enableEdit, + }); + }); + + it('renders issuable-description component', () => { + const descriptionEl = wrapper.find(IssuableDescription); + + expect(descriptionEl.exists()).toBe(true); + expect(descriptionEl.props('issuable')).toEqual(issuableBodyProps.issuable); + }); + + it('renders issuable edit info', () => { + const editedEl = wrapper.find('small'); + const sanitizedText = editedEl + .text() + .replace(/\n/g, ' ') + .replace(/\s+/g, ' '); + + expect(sanitizedText).toContain('Edited'); + expect(sanitizedText).toContain('ago'); + expect(sanitizedText).toContain(`by ${mockIssuable.updatedBy.name}`); + }); + + it('renders issuable-edit-form when `editFormVisible` prop is true', async () => { + wrapper.setProps({ + editFormVisible: true, + }); + + await wrapper.vm.$nextTick(); + + const editFormEl = wrapper.find(IssuableEditForm); + expect(editFormEl.exists()).toBe(true); + expect(editFormEl.props()).toMatchObject({ + issuable: issuableBodyProps.issuable, + enableAutocomplete: issuableBodyProps.enableAutocomplete, + descriptionPreviewPath: issuableBodyProps.descriptionPreviewPath, + descriptionHelpPath: issuableBodyProps.descriptionHelpPath, + }); + expect(editFormEl.find('button.js-save').exists()).toBe(true); + expect(editFormEl.find('button.js-cancel').exists()).toBe(true); + }); + + describe('events', () => { + it('component emits `edit-issuable` event bubbled via issuable-title', () => { + const issuableTitle = wrapper.find(IssuableTitle); + + issuableTitle.vm.$emit('edit-issuable'); + + expect(wrapper.emitted('edit-issuable')).toBeTruthy(); + }); + }); + }); +}); diff --git a/spec/frontend/issuable_show/mock_data.js b/spec/frontend/issuable_show/mock_data.js index 0a4c0880856..14e5febdc6b 100644 --- a/spec/frontend/issuable_show/mock_data.js +++ b/spec/frontend/issuable_show/mock_data.js @@ -11,6 +11,7 @@ export const mockIssuable = { state: 'opened', blocked: false, confidential: false, + updatedBy: issuable.author, currentUserTodos: { nodes: [ { diff --git a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb index 2dae4a65eeb..364be9cdc97 100644 --- a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb +++ b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb @@ -96,6 +96,7 @@ RSpec.describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMent # this not does not have actual mentions let!(:note4) { notes.create!(commit_id: commit.id, noteable_type: 'Commit', project_id: project.id, author_id: author.id, note: 'note for an email@somesite.com and some other random @ ref' ) } + # this should have pointed to an innexisted commit record in a commits table # but because commit is not an AR we'll just make it so that it does not have mentions let!(:note5) { notes.create!(commit_id: 'abc', noteable_type: 'Commit', project_id: project.id, author_id: author.id, note: 'note for an email@somesite.com and some other random @ ref') } diff --git a/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb b/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb index 843d91005f8..2d6765aac2e 100644 --- a/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb +++ b/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb @@ -111,7 +111,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do end it 'replaces the existing index with an identical index' do - expect(connection).to receive(:execute).with('SET statement_timeout TO 21600').twice + expect(connection).to receive(:execute).with('SET statement_timeout TO \'21600s\'').twice expect_to_execute_concurrently_in_order(create_index) @@ -136,7 +136,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do end it 'replaces the existing index with an identical index' do - expect(connection).to receive(:execute).with('SET statement_timeout TO 21600').exactly(3).times + expect(connection).to receive(:execute).with('SET statement_timeout TO \'21600s\'').exactly(3).times expect_to_execute_concurrently_in_order(drop_index) expect_to_execute_concurrently_in_order(create_index) |