diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-15 18:15:01 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-15 18:15:01 +0300 |
commit | b616fd825faac3e7f194e1f942ef30730021e463 (patch) | |
tree | 8e187d885200ee5dd7958d7ef32383918ba8e99f /app | |
parent | 1a129420d6bd3e5223e8ba4a5b7749764118a885 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
36 files changed, 371 insertions, 299 deletions
diff --git a/app/assets/javascripts/header_search/init.js b/app/assets/javascripts/header_search/init.js new file mode 100644 index 00000000000..2b9fac0b395 --- /dev/null +++ b/app/assets/javascripts/header_search/init.js @@ -0,0 +1,48 @@ +import * as Sentry from '@sentry/browser'; + +async function eventHandler(callback = () => {}) { + if (this.newHeaderSearchFeatureFlag) { + const { initHeaderSearchApp } = await import( + /* webpackChunkName: 'globalSearch' */ '~/header_search' + ).catch((error) => Sentry.captureException(error)); + + // In case the user started searching before we bootstrapped, + // let's pass the search along. + const initialSearchValue = this.searchInputBox.value; + initHeaderSearchApp(initialSearchValue); + + // this is new #search input element. We need to re-find it. + // And re-focus in it. + document.querySelector('#search').focus(); + callback(); + return; + } + + const { default: initSearchAutocomplete } = await import( + /* webpackChunkName: 'globalSearch' */ '../search_autocomplete' + ).catch((error) => Sentry.captureException(error)); + + const searchDropdown = initSearchAutocomplete(); + searchDropdown.onSearchInputFocus(); + callback(); +} + +function cleanEventListeners() { + document.querySelector('#search').removeEventListener('focus', eventHandler); +} + +function initHeaderSearch() { + const searchInputBox = document.querySelector('#search'); + + searchInputBox?.addEventListener( + 'focus', + eventHandler.bind( + { searchInputBox, newHeaderSearchFeatureFlag: gon?.features?.newHeaderSearch }, + cleanEventListeners, + ), + { once: true }, + ); +} + +export default initHeaderSearch; +export { eventHandler }; diff --git a/app/assets/javascripts/issues/new/components/type_popover.vue b/app/assets/javascripts/issues/new/components/type_popover.vue index a70e79b70f9..9c43e527f8b 100644 --- a/app/assets/javascripts/issues/new/components/type_popover.vue +++ b/app/assets/javascripts/issues/new/components/type_popover.vue @@ -18,8 +18,9 @@ export default { </script> <template> - <span id="popovercontainer"> - <gl-icon id="issue-type-info" name="question-o" class="gl-ml-5 gl-text-gray-500" /> + <span id="popovercontainer" class="gl-ml-2"> + <gl-icon id="issue-type-info" name="question-o" class="gl-text-blue-600" /> + <gl-popover target="issue-type-info" container="popovercontainer" diff --git a/app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue b/app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue new file mode 100644 index 00000000000..6a0deb41fd1 --- /dev/null +++ b/app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue @@ -0,0 +1,75 @@ +<script> +import { GlFormGroup, GlButton, GlFormInput } from '@gitlab/ui'; +import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; +import { resourceLinksFormI18n } from '../constants'; + +export default { + name: 'AddIssuableResourceLinkForm', + components: { + GlFormGroup, + GlButton, + GlFormInput, + }, + i18n: resourceLinksFormI18n, + directives: { + autofocusonshow, + }, + props: { + isSubmitting: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + linkTextValue: '', + linkValue: '', + }; + }, + computed: { + isSubmitButtonDisabled() { + return this.linkValue.length === 0 || this.isSubmitting; + }, + }, + methods: { + onFormCancel() { + this.linkValue = ''; + this.linkTextValue = ''; + this.$emit('add-issuable-resource-link-form-cancel'); + }, + }, +}; +</script> + +<template> + <form @submit.prevent> + <gl-form-group :label="$options.i18n.linkTextLabel"> + <gl-form-input + v-model="linkTextValue" + v-autofocusonshow + data-testid="link-text-input" + type="text" + /> + </gl-form-group> + <gl-form-group :label="$options.i18n.linkValueLabel"> + <gl-form-input v-model="linkValue" data-testid="link-value-input" type="text" /> + </gl-form-group> + <div class="gl-mt-5 gl-clearfix"> + <gl-button + category="primary" + variant="confirm" + data-testid="add-button" + :disabled="isSubmitButtonDisabled" + :loading="isSubmitting" + type="submit" + class="gl-float-left" + > + {{ $options.i18n.submitButtonText }} + </gl-button> + <gl-button class="gl-float-right" @click="onFormCancel"> + {{ $options.i18n.cancelButtonText }} + </gl-button> + </div> + </form> +</template> diff --git a/app/assets/javascripts/linked_resources/components/resource_links_block.vue b/app/assets/javascripts/linked_resources/components/resource_links_block.vue index 3bfee61df15..46c4fc7f632 100644 --- a/app/assets/javascripts/linked_resources/components/resource_links_block.vue +++ b/app/assets/javascripts/linked_resources/components/resource_links_block.vue @@ -1,10 +1,7 @@ <script> import { GlLink, GlIcon, GlButton } from '@gitlab/ui'; -import { - LINKED_RESOURCES_HEADER_TEXT, - LINKED_RESOURCES_HELP_TEXT, - LINKED_RESOURCES_ADD_BUTTON_TEXT, -} from '../constants'; +import { resourceLinksI18n } from '../constants'; +import AddIssuableResourceLinkForm from './add_issuable_resource_link_form.vue'; export default { name: 'ResourceLinksBlock', @@ -12,7 +9,9 @@ export default { GlLink, GlButton, GlIcon, + AddIssuableResourceLinkForm, }, + i18n: resourceLinksI18n, props: { helpPath: { type: String, @@ -25,18 +24,26 @@ export default { default: false, }, }, + data() { + return { + isFormVisible: false, + isSubmitting: false, + }; + }, computed: { - helpLinkText() { - return LINKED_RESOURCES_HELP_TEXT; - }, badgeLabel() { return 0; }, - resourceLinkAddButtonText() { - return LINKED_RESOURCES_ADD_BUTTON_TEXT; + hasBody() { + return this.isFormVisible; + }, + }, + methods: { + async toggleResourceLinkForm() { + this.isFormVisible = !this.isFormVisible; }, - resourceLinkHeaderText() { - return LINKED_RESOURCES_HEADER_TEXT; + hideResourceLinkForm() { + this.isFormVisible = false; }, }, }; @@ -46,7 +53,7 @@ export default { <div id="resource-links" class="gl-mt-5"> <div class="card card-slim gl-overflow-hidden"> <div - :class="{ 'panel-empty-heading border-bottom-0': true }" + :class="{ 'panel-empty-heading border-bottom-0': !hasBody }" class="card-header gl-display-flex gl-justify-content-space-between" > <h3 @@ -58,13 +65,13 @@ export default { href="#resource-links" aria-hidden="true" /> - <slot name="header-text">{{ resourceLinkHeaderText }}</slot> + <slot name="header-text">{{ $options.i18n.headerText }}</slot> <gl-link :href="helpPath" target="_blank" class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500" data-testid="help-link" - :aria-label="helpLinkText" + :aria-label="$options.i18n.helpText" > <gl-icon name="question" :size="12" /> </gl-link> @@ -79,11 +86,26 @@ export default { <gl-button v-if="canAddResourceLinks" icon="plus" - :aria-label="resourceLinkAddButtonText" + :aria-label="$options.i18n.addButtonText" + @click="toggleResourceLinkForm" /> </div> </h3> </div> + <div + class="linked-issues-card-body bg-gray-light" + :class="{ + 'gl-p-5': isFormVisible, + }" + > + <div v-show="isFormVisible" class="card-body bordered-box gl-bg-white"> + <add-issuable-resource-link-form + ref="resourceLinkForm" + :is-submitting="isSubmitting" + @add-issuable-resource-link-form-cancel="hideResourceLinkForm" + /> + </div> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/linked_resources/constants.js b/app/assets/javascripts/linked_resources/constants.js index 358de326830..1b11cfc5f88 100644 --- a/app/assets/javascripts/linked_resources/constants.js +++ b/app/assets/javascripts/linked_resources/constants.js @@ -1,5 +1,14 @@ -import { __ } from '~/locale'; +import { s__ } from '~/locale'; -export const LINKED_RESOURCES_HEADER_TEXT = __('Linked resources'); -export const LINKED_RESOURCES_HELP_TEXT = __('Read more about linked resources'); -export const LINKED_RESOURCES_ADD_BUTTON_TEXT = __('Add a resource link'); +export const resourceLinksI18n = Object.freeze({ + headerText: s__('LinkedResources|Linked resources'), + helpText: s__('LinkedResources|Read more about linked resources'), + addButtonText: s__('LinkedResources|Add a resource link'), +}); + +export const resourceLinksFormI18n = Object.freeze({ + linkTextLabel: s__('LinkedResources|Text (Optional)'), + linkValueLabel: s__('LinkedResources|Link'), + submitButtonText: s__('LinkedResources|Add'), + cancelButtonText: s__('LinkedResources|Cancel'), +}); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 21d5decb15b..349a28ace52 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -36,6 +36,7 @@ import initUserPopovers from './user_popovers'; import initBroadcastNotifications from './broadcast_notification'; import { initTopNav } from './nav'; import { initCopyCodeButton } from './behaviors/copy_code'; +import initHeaderSearch from './header_search/init'; import 'ee_else_ce/main_ee'; import 'jh_else_ce/main_jh'; @@ -141,35 +142,10 @@ function deferredInitialisation() { } } +// header search vue component bootstrap // loading this inside requestIdleCallback is causing issues // see https://gitlab.com/gitlab-org/gitlab/-/issues/365746 -const searchInputBox = document.querySelector('#search'); -if (searchInputBox) { - searchInputBox.addEventListener( - 'focus', - () => { - if (gon.features?.newHeaderSearch) { - import(/* webpackChunkName: 'globalSearch' */ '~/header_search') - .then(async ({ initHeaderSearchApp }) => { - // In case the user started searching before we bootstrapped, let's pass the search along. - const initialSearchValue = searchInputBox.value; - await initHeaderSearchApp(initialSearchValue); - // this is new #search input element. We need to re-find it. - document.querySelector('#search').focus(); - }) - .catch(() => {}); - } else { - import(/* webpackChunkName: 'globalSearch' */ './search_autocomplete') - .then(({ default: initSearchAutocomplete }) => { - const searchDropdown = initSearchAutocomplete(); - searchDropdown.onSearchInputFocus(); - }) - .catch(() => {}); - } - }, - { once: true }, - ); -} +initHeaderSearch(); const $body = $('body'); const $document = $(document); diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js index f37a2987685..7db34816cfe 100644 --- a/app/assets/javascripts/pages/projects/init_blob.js +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -11,7 +11,7 @@ export default () => { // eslint-disable-next-line no-new new BlobLinePermalinkUpdater( document.querySelector('#blob-content-holder'), - '.file-line-num[data-line-number], .file-line-num[data-line-number] *', + '.diff-line-num[data-line-number], .diff-line-num[data-line-number] *', document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), ); diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql index 45a7793e559..8baee80e5d6 100644 --- a/app/assets/javascripts/repository/queries/blob_info.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql @@ -27,7 +27,6 @@ query getBlobInfo( fileType language path - blamePath editBlobPath gitpodBlobUrl ideEditPath diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue index 9683288f937..6babbca58c3 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue @@ -51,10 +51,6 @@ export default { required: false, default: null, }, - blamePath: { - type: String, - required: true, - }, }, computed: { lines() { @@ -80,7 +76,6 @@ export default { :number="startingFrom + index + 1" :content="line" :language="language" - :blame-path="blamePath" /> </div> <div v-else class="gl-display-flex"> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue index b6854ee0375..7b62f0cdb7d 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue @@ -1,5 +1,5 @@ <script> -import { GlLink, GlSafeHtmlDirective, GlTooltipDirective } from '@gitlab/ui'; +import { GlLink, GlSafeHtmlDirective } from '@gitlab/ui'; import { setAttributes } from '~/lib/utils/dom_utils'; import { BIDI_CHARS, BIDI_CHARS_CLASS_LIST, BIDI_CHAR_TOOLTIP } from '../constants'; @@ -9,7 +9,6 @@ export default { }, directives: { SafeHtml: GlSafeHtmlDirective, - GlTooltip: GlTooltipDirective, }, props: { number: { @@ -24,10 +23,6 @@ export default { type: String, required: true, }, - blamePath: { - type: String, - required: true, - }, }, computed: { formattedContent() { @@ -63,35 +58,21 @@ export default { }; </script> <template> - <div class="gl-display-flex line-links-wrapper"> - <div - class="gl-p-0! gl-absolute gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers" - :class="firstLineClass" - > - <gl-link - v-gl-tooltip="__('View blame')" - class="gl-user-select-none gl-ml-3 gl-shadow-none! file-line-blame" - :href="`${blamePath}#L${number}`" - data-track-action="click_link" - data-track-label="file_line_action" - data-track-property="blame" - /> - + <div class="gl-display-flex"> + <div class="gl-p-0! gl-absolute gl-z-index-3 gl-border-r diff-line-num line-numbers"> <gl-link :id="`L${number}`" - class="gl-user-select-none gl-flex-grow-1 gl-justify-content-end gl-pr-3 gl-shadow-none! file-line-num" + class="gl-user-select-none gl-ml-5 gl-pr-3 gl-shadow-none! file-line-num diff-line-num" + :class="firstLineClass" :to="`#L${number}`" :data-line-number="number" - data-track-action="click_link" - data-track-label="file_line_action" - data-track-property="link" > {{ number }} </gl-link> </div> <pre - class="gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-normal" + class="gl-p-0! gl-w-full gl-overflow-visible! gl-ml-11! gl-border-none! code highlight gl-line-height-normal" :class="firstLineClass" ><code><span :id="`LC${number}`" v-safe-html="formattedContent" :lang="language" class="line" data-testid="content"></span></code></pre> </div> diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index ccc8b44942a..1bdae40332f 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -199,7 +199,6 @@ export default { :starting-from="firstChunk.startingFrom" :is-highlighted="firstChunk.isHighlighted" :language="firstChunk.language" - :blame-path="blob.blamePath" /> <gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" /> @@ -214,7 +213,6 @@ export default { :is-highlighted="chunk.isHighlighted" :chunk-index="index" :language="chunk.language" - :blame-path="blob.blamePath" @appear="highlightChunk" /> </div> 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 40467205d0d..7314b0afc54 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -1,6 +1,7 @@ <script> import { GlAlert, GlSkeletonLoader, GlIcon, GlButton } from '@gitlab/ui'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import { i18n, WIDGET_TYPE_ASSIGNEES, @@ -8,6 +9,7 @@ import { WIDGET_TYPE_DESCRIPTION, WIDGET_TYPE_WEIGHT, WIDGET_TYPE_HIERARCHY, + WORK_ITEM_VIEWED_STORAGE_KEY, } from '../constants'; import workItemQuery from '../graphql/work_item.query.graphql'; import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql'; @@ -18,6 +20,7 @@ import WorkItemDescription from './work_item_description.vue'; import WorkItemAssignees from './work_item_assignees.vue'; import WorkItemLabels from './work_item_labels.vue'; import WorkItemWeight from './work_item_weight.vue'; +import WorkItemInformation from './work_item_information.vue'; export default { i18n, @@ -33,6 +36,8 @@ export default { WorkItemTitle, WorkItemState, WorkItemWeight, + WorkItemInformation, + LocalStorageSync, }, mixins: [glFeatureFlagMixin()], props: { @@ -56,6 +61,7 @@ export default { return { error: undefined, workItem: {}, + showInfoBanner: true, }; }, apollo: { @@ -120,6 +126,17 @@ export default { return `../../issues/${this.parentWorkItem?.iid}`; }, }, + beforeDestroy() { + /** make sure that if the user has not even dismissed the alert , + * should no be able to see the information next time and update the local storage * */ + this.dismissBanner(); + }, + methods: { + dismissBanner() { + this.showInfoBanner = false; + }, + }, + WORK_ITEM_VIEWED_STORAGE_KEY, }; </script> @@ -174,6 +191,16 @@ export default { @click="$emit('close')" /> </div> + <local-storage-sync + v-model="showInfoBanner" + :storage-key="$options.WORK_ITEM_VIEWED_STORAGE_KEY" + > + <work-item-information + v-if="showInfoBanner" + :show-info-banner="showInfoBanner" + @work-item-banner-dismissed="dismissBanner" + /> + </local-storage-sync> <work-item-title :work-item-id="workItem.id" :work-item-title="workItem.title" diff --git a/app/assets/javascripts/work_items/components/work_item_information.vue b/app/assets/javascripts/work_items/components/work_item_information.vue new file mode 100644 index 00000000000..2ff7ba169ea --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_information.vue @@ -0,0 +1,57 @@ +<script> +import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { helpPagePath } from '~/helpers/help_page_helper'; + +export default { + i18n: { + learnTasksButtonText: s__('WorkItem|Learn about tasks'), + workItemsText: s__('WorkItem|work items'), + tasksInformationTitle: s__('WorkItem|Introducing tasks'), + tasksInformationBody: s__( + 'WorkItem|A task provides the ability to break down your work into smaller pieces tied to an issue. Tasks are the first items using our new %{workItemsLink} objects. Additional work item types will be coming soon.', + ), + }, + helpPageLinks: { + tasksDocLinkPath: helpPagePath('user/tasks'), + workItemsLinkPath: helpPagePath(`development/work_items`), + }, + components: { + GlAlert, + GlSprintf, + GlLink, + }, + props: { + showInfoBanner: { + type: Boolean, + required: false, + default: true, + }, + }, + emits: ['work-item-banner-dismissed'], +}; +</script> + +<template> + <section class="gl-display-block gl-mb-2"> + <gl-alert + v-if="showInfoBanner" + variant="tip" + :title="$options.i18n.tasksInformationTitle" + :primary-button-link="$options.helpPageLinks.tasksDocLinkPath" + :primary-button-text="$options.i18n.learnTasksButtonText" + data-testid="work-item-information" + class="gl-mt-3" + @dismiss="$emit('work-item-banner-dismissed')" + > + <gl-sprintf :message="$options.i18n.tasksInformationBody"> + <template #workItemsLink> + <gl-link :href="$options.helpPageLinks.workItemsLinkPath">{{ + $options.i18n.workItemsText + }}</gl-link> + </template> + ></gl-sprintf + > + </gl-alert> + </section> +</template> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index c9ccbd48ba1..2140b418e6d 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -20,6 +20,7 @@ export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION'; export const WIDGET_TYPE_LABELS = 'LABELS'; export const WIDGET_TYPE_WEIGHT = 'WEIGHT'; export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY'; +export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner'; export const WIDGET_TYPE_TASK_ICON = 'task-done'; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index d59ca14ee84..f322c6c8929 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -202,10 +202,6 @@ float: none; border-left: 1px solid $gray-100; - .file-line-num { - @include gl-min-w-9; - } - i { float: none; margin-right: 0; diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index f1f43e55921..1c43212f501 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -48,9 +48,8 @@ a { font-family: $monospace-font; + display: block; white-space: nowrap; - @include gl-display-flex; - @include gl-justify-content-end; i, svg { @@ -91,44 +90,3 @@ td.line-numbers { cursor: pointer; text-decoration: underline wavy $red-500; } - -.blob-viewer { - .line-numbers { - // for server-side-rendering - .line-links { - min-width: 6.5rem; - - &:first-child { - margin-top: 10px; - } - - &:last-child { - margin-bottom: 10px; - } - } - - // for client - &.line-links { - min-width: 6.5rem; - border-bottom-left-radius: 0; - - + pre { - margin-left: 6.5rem; - } - } - } - - .line-links { - &:hover .file-line-blame::before, - &:hover .file-line-num::before, - &:focus-within .file-line-blame::before, - &:focus-within .file-line-num::before { - @include gl-visibility-visible; - } - } - - .file-line-num, - .file-line-blame { - @include gl-align-items-center; - } -} diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss index 8e4f4600d5c..fcbd05141b9 100644 --- a/app/assets/stylesheets/highlight/common.scss +++ b/app/assets/stylesheets/highlight/common.scss @@ -98,50 +98,32 @@ } } +@mixin line-number-link($color) { + min-width: $gl-spacing-scale-9; -@mixin line-link($color, $icon) { &::before { - @include gl-visibility-hidden; + @include gl-display-none; @include gl-align-self-center; - @include gl-mr-1; - @include gl-w-5; - @include gl-h-5; - background-color: rgba($color, 0.3); - mask-image: asset_url('icons-stacked.svg##{$icon}'); + @include gl-mt-2; + @include gl-mr-2; + @include gl-w-4; + @include gl-h-4; + @include gl-absolute; + @include gl-left-3; + background-color: $color; + mask-image: asset_url('icons-stacked.svg#link'); mask-repeat: no-repeat; mask-size: cover; mask-position: center; content: ''; } - &:hover { - &::before { - background-color: rgba($color, 0.6); - } - } -} - -@mixin line-hover-bg($color: $white-normal) { - &:hover, - &:focus-within { - background-color: darken($color, 10); + &:hover::before { + @include gl-display-inline-block; } -} -@mixin first-line-top-space($bg-color: $gray-light, $border-color: $white-normal) { - &:first-child { - .line-links { - &::before { - @include gl-absolute; - @include gl-h-3; - content: ''; - bottom: 100%; - left: 0; - width: 6.5rem; - background-color: $bg-color; - border-right: 1px solid $border-color; - } - } + &:focus::before { + @include gl-display-inline-block; } } diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss index f7f3c8964bf..709e7f5ae18 100644 --- a/app/assets/stylesheets/highlight/themes/dark.scss +++ b/app/assets/stylesheets/highlight/themes/dark.scss @@ -127,19 +127,7 @@ $dark-il: #de935f; .code.dark { // Line numbers .file-line-num { - @include line-link($white, 'link'); - } - - .file-line-blame { - @include line-link($white, 'git'); - } - - .line-links { - @include line-hover-bg($dark-main-bg); - } - - .line-links-wrapper { - @include first-line-top-space($dark-main-bg, $dark-code-border); + @include line-number-link($dark-line-num-color); } .line-numbers, diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss index 9dfe48f5609..0ed9c209417 100644 --- a/app/assets/stylesheets/highlight/themes/monokai.scss +++ b/app/assets/stylesheets/highlight/themes/monokai.scss @@ -120,19 +120,7 @@ $monokai-gh: #75715e; // Line numbers .file-line-num { - @include line-link($white, 'link'); - } - - .file-line-blame { - @include line-link($white, 'git'); - } - - .line-links { - @include line-hover-bg($monokai-bg); - } - - .line-links-wrapper { - @include first-line-top-space($monokai-bg, $monokai-border); + @include line-number-link($monokai-line-num-color); } .line-numbers, diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss index 7fd81353363..868e466b1f8 100644 --- a/app/assets/stylesheets/highlight/themes/none.scss +++ b/app/assets/stylesheets/highlight/themes/none.scss @@ -25,19 +25,7 @@ // Line numbers .file-line-num { - @include line-link($black, 'link'); - } - - .file-line-blame { - @include line-link($black, 'git'); - } - - .line-links { - @include line-hover-bg; - } - - .line-links-wrapper { - @include first-line-top-space; + @include line-number-link($black-transparent); } .line-numbers, diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss index 95d3c8feb54..6260339a48d 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss @@ -123,19 +123,7 @@ $solarized-dark-il: #2aa198; // Line numbers .file-line-num { - @include line-link($white, 'link'); - } - - .file-line-blame { - @include line-link($white, 'git'); - } - - .line-links { - @include line-hover-bg($solarized-dark-pre-bg); - } - - .line-links-wrapper { - @include first-line-top-space($solarized-dark-pre-bg, $solarized-dark-pre-border); + @include line-number-link($solarized-dark-line-color); } .line-numbers, diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss index 47f5e0bd3da..e6f098f4cdf 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-light.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss @@ -109,19 +109,7 @@ $solarized-light-il: #2aa198; @include hljs-override('title.class_.inherited__', $solarized-light-no); // Line numbers .file-line-num { - @include line-link($black, 'link'); - } - - .file-line-blame { - @include line-link($black, 'git'); - } - - .line-links { - @include line-hover-bg($solarized-light-pre-bg); - } - - .line-links-wrapper { - @include first-line-top-space($solarized-light-pre-bg, $solarized-light-border); + @include line-number-link($solarized-light-line-color); } .line-numbers, diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss index 336c2b739f1..770a90bbc57 100644 --- a/app/assets/stylesheets/highlight/white_base.scss +++ b/app/assets/stylesheets/highlight/white_base.scss @@ -95,15 +95,7 @@ $white-gc-bg: #eaf2f5; // Line numbers .file-line-num { - @include line-link($black, 'link'); -} - -.file-line-blame { - @include line-link($black, 'git'); -} - -.line-links { - @include line-hover-bg; + @include line-number-link($black-transparent); } .line-numbers, @@ -134,10 +126,6 @@ pre.code, border-color: $white-normal; } -.line-links-wrapper { - @include first-line-top-space; -} - &, pre.code, .line_holder .line_content { diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index 802cb09b36a..42b55f47f92 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -70,6 +70,8 @@ module Types description: 'Downstream pipeline for a bridge.' field :manual_job, GraphQL::Types::Boolean, null: true, description: 'Whether the job has a manual action.' + field :manual_variables, VariableType.connection_type, null: true, + description: 'Variables added to a manual job when the job is triggered.' field :playable, GraphQL::Types::Boolean, null: false, method: :playable?, description: 'Indicates the job can be played.' field :previous_stage_jobs_or_needs, Types::Ci::JobNeedUnion.connection_type, null: true, @@ -190,6 +192,14 @@ module Types def triggered object.try(:trigger_request) end + + def manual_variables + if object.manual? && object.respond_to?(:job_variables) + object.job_variables + else + [] + end + end end end end diff --git a/app/graphql/types/ci/variable_type.rb b/app/graphql/types/ci/variable_type.rb index 5d2acfb9c9f..63f89b6d207 100644 --- a/app/graphql/types/ci/variable_type.rb +++ b/app/graphql/types/ci/variable_type.rb @@ -26,6 +26,15 @@ module Types field :raw, GraphQL::Types::Boolean, null: true, description: 'Indicates whether the variable is raw.' + + field :environment_scope, GraphQL::Types::String, null: true, + description: 'Scope defining the environments in which the variable can be used.' + + def environment_scope + if object.respond_to?(:environment_scope) + object.environment_scope + end + end end end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 795def55306..1c01a81ee41 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -338,8 +338,8 @@ module Ci scope :for_id, -> (id) { where(id: id) } scope :for_iid, -> (iid) { where(iid: iid) } scope :for_project, -> (project_id) { where(project_id: project_id) } - scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) } - scope :created_before_id, -> (id) { where('ci_pipelines.id < ?', id) } + scope :created_after, -> (time) { where(arel_table[:created_at].gt(time)) } + scope :created_before_id, -> (id) { where(arel_table[:id].lt(id)) } scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) } scope :with_pipeline_source, -> (source) { where(source: source) } diff --git a/app/policies/work_item_policy.rb b/app/policies/work_item_policy.rb index ea7559592e1..2f3561f1135 100644 --- a/app/policies/work_item_policy.rb +++ b/app/policies/work_item_policy.rb @@ -13,4 +13,8 @@ class WorkItemPolicy < IssuePolicy # need to make sure we also prevent this rule if read_issue # is prevented rule { ~can?(:read_issue) }.prevent :read_work_item + + rule { can?(:reporter_access) }.policy do + enable :admin_parent_link + end end diff --git a/app/services/work_items/create_from_task_service.rb b/app/services/work_items/create_from_task_service.rb index d5fa5fca772..ef1d47c560d 100644 --- a/app/services/work_items/create_from_task_service.rb +++ b/app/services/work_items/create_from_task_service.rb @@ -27,6 +27,7 @@ module WorkItems replacement_result = TaskListReferenceReplacementService.new( work_item: @work_item, + current_user: @current_user, work_item_reference: create_and_link_result[:work_item].to_reference, line_number_start: @work_item_params[:line_number_start], line_number_end: @work_item_params[:line_number_end], diff --git a/app/services/work_items/parent_links/create_service.rb b/app/services/work_items/parent_links/create_service.rb index 87995cc6550..78013f081c8 100644 --- a/app/services/work_items/parent_links/create_service.rb +++ b/app/services/work_items/parent_links/create_service.rb @@ -20,7 +20,7 @@ module WorkItems def linkable_issuables(work_items) @linkable_issuables ||= begin - return [] unless can?(current_user, :read_work_item, issuable.project) + return [] unless can?(current_user, :admin_parent_link, issuable) work_items.select do |work_item| linkable?(work_item) @@ -29,7 +29,7 @@ module WorkItems end def linkable?(work_item) - can?(current_user, :update_work_item, work_item) && + can?(current_user, :admin_parent_link, work_item) && !previous_related_issuables.include?(work_item) end @@ -42,8 +42,8 @@ module WorkItems ::WorkItem.find(id) rescue ActiveRecord::RecordNotFound @errors << _("Task with ID: %{id} could not be found.") % { id: id } - nil - end + next + end.compact end # TODO: Create system notes when work item's parent or children are updated diff --git a/app/services/work_items/parent_links/destroy_service.rb b/app/services/work_items/parent_links/destroy_service.rb index deca24159d3..55870d44db9 100644 --- a/app/services/work_items/parent_links/destroy_service.rb +++ b/app/services/work_items/parent_links/destroy_service.rb @@ -25,7 +25,7 @@ module WorkItems end def permission_to_remove_relation? - can?(current_user, :update_work_item, child) && can?(current_user, :update_work_item, parent) + can?(current_user, :admin_parent_link, child) && can?(current_user, :admin_parent_link, parent) end end end diff --git a/app/services/work_items/task_list_reference_removal_service.rb b/app/services/work_items/task_list_reference_removal_service.rb index e7ec73a96e0..9152580bef0 100644 --- a/app/services/work_items/task_list_reference_removal_service.rb +++ b/app/services/work_items/task_list_reference_removal_service.rb @@ -11,6 +11,7 @@ module WorkItems @line_number_end = line_number_end @lock_version = lock_version @current_user = current_user + @task_reference = /#{Regexp.escape(@task.to_reference)}(?!\d)\+/ end def execute @@ -26,7 +27,9 @@ module WorkItems line_matches_reference = (@line_number_start..@line_number_end).any? do |line_number| markdown_line = source_lines[line_number - 1] - /#{Regexp.escape(@task.to_reference)}(?!\d)/.match?(markdown_line) + if @task_reference.match?(markdown_line) + markdown_line.sub!(@task_reference, @task.title) + end end unless line_matches_reference @@ -35,8 +38,6 @@ module WorkItems ) end - remove_task_lines!(source_lines) - ::WorkItems::UpdateService.new( project: @work_item.project, current_user: @current_user, @@ -51,13 +52,5 @@ module WorkItems rescue ActiveRecord::StaleObjectError ::ServiceResponse.error(message: STALE_OBJECT_MESSAGE) end - - private - - def remove_task_lines!(source_lines) - source_lines.delete_if.each_with_index do |_line, index| - index >= @line_number_start - 1 && index < @line_number_end - end - end end end diff --git a/app/services/work_items/task_list_reference_replacement_service.rb b/app/services/work_items/task_list_reference_replacement_service.rb index 1044a4feb88..b098d67561b 100644 --- a/app/services/work_items/task_list_reference_replacement_service.rb +++ b/app/services/work_items/task_list_reference_replacement_service.rb @@ -4,8 +4,9 @@ module WorkItems class TaskListReferenceReplacementService STALE_OBJECT_MESSAGE = 'Stale work item. Check lock version' - def initialize(work_item:, work_item_reference:, line_number_start:, line_number_end:, title:, lock_version:) + def initialize(work_item:, current_user:, work_item_reference:, line_number_start:, line_number_end:, title:, lock_version:) @work_item = work_item + @current_user = current_user @work_item_reference = work_item_reference @line_number_start = line_number_start @line_number_end = line_number_end @@ -32,7 +33,11 @@ module WorkItems source_lines[@line_number_start - 1] = markdown_task_first_line remove_additional_lines!(source_lines) - @work_item.update!(description: source_lines.join("\n")) + ::WorkItems::UpdateService.new( + project: @work_item.project, + current_user: @current_user, + params: { description: source_lines.join("\n"), lock_version: @lock_version } + ).execute(@work_item) ::ServiceResponse.success rescue ActiveRecord::StaleObjectError diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml index ba0935fff7d..e257117a32e 100644 --- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml @@ -4,7 +4,7 @@ .card-header = _('Protect a tag') .card-body - = form_errors(@protected_tag) + = form_errors(@protected_tag, pajamas_alert: true) .form-group.row = f.label :name, _('Tag:'), class: 'col-md-2 text-left text-md-right' .col-md-10.protected-tags-dropdown diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index 96564e44cf2..64f45ec89d1 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -16,7 +16,7 @@ .row .col-lg-12 = gitlab_ui_form_for @project, url: project_settings_ci_cd_path(@project, anchor: 'autodevops-settings') do |f| - = form_errors(@project) + = form_errors(@project, pajamas_alert: true) %fieldset.builds-feature.js-auto-devops-settings .form-group = f.fields_for :auto_devops_attributes, @auto_devops do |form| diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 453c6438edf..f8ac3832a77 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -1,16 +1,13 @@ #blob-content.file-content.code.js-syntax-highlight - offset = defined?(first_line_number) ? first_line_number : 1 - .line-numbers{ class: "gl-p-0\!" } + .line-numbers - if blob.data.present? - link = blob_link if defined?(blob_link) - - blame_link = project_blame_path(@project, tree_join(@ref, blob.path)) - blob.data.each_line.each_with_index do |_, index| - i = index + offset -# We're not using `link_to` because it is too slow once we get to thousands of lines. - .gl-display-flex.line-links.diff-line-num - %a.file-line-blame.gl-display-flex.has-tooltip.gl-ml-3{ href: "#{blame_link}#L#{i}", title: _('View blame'), data: { track_action: "click_link", track_label: "file_line_action", track_property: "blame" } } - %a.file-line-num.gl-display-flex.gl-justify-content-end.flex-grow-1.gl-pr-3{ href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i, data: { track_action: "click_link", track_label: "file_line_action", track_property: "link" } } - = i + %a.file-line-num.diff-line-num{ href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i } + = i - highlight = defined?(highlight_line) && highlight_line ? highlight_line - offset : nil .blob-content{ data: { blob_id: blob.id, path: blob.path, highlight_line: highlight, qa_selector: 'file_content' } } %pre.code.highlight diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml index d5c696b1698..a94ef70b2d5 100644 --- a/app/views/shared/issuable/form/_type_selector.html.haml +++ b/app/views/shared/issuable/form/_type_selector.html.haml @@ -1,35 +1,35 @@ - return unless issuable.supports_issue_type? && can?(current_user, :create_issue, @project) .form-group - = form.label :type, _('Type') - .gl-display-flex.gl-align-items-center - .issuable-form-select-holder.selectbox.form-group.gl-mb-0 - .dropdown.js-issuable-type-filter-dropdown-wrap - %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.dropdown-toggle-text.is-default - = issuable.issue_type.capitalize || _("Select type") - = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3") - .dropdown-menu.dropdown-menu-selectable.dropdown-select - .dropdown-title.gl-display-flex - %span.gl-ml-auto - = _("Select type") - %button.dropdown-title-button.dropdown-menu-close.gl-ml-auto{ type: 'button', "aria-label" => _('Close') } - = sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon') - .dropdown-content{ data: { testid: 'issue-type-select-dropdown' } } - %ul - - if create_issue_type_allowed?(@project, :issue) - %li.js-filter-issuable-type - = link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do - #{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_('Issue')} - - if create_issue_type_allowed?(@project, :incident) - %li.js-filter-issuable-type{ data: { track: { action: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } } - = link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do - #{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_('Incident')} - + = form.label :type do + = _('Type') #js-type-popover - - if issuable.incident? - %p.form-text.text-muted - - incident_docs_url = help_page_path('operations/incident_management/incidents.md') - - incident_docs_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: incident_docs_url) - = format(_('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.'), incident_docs_start: incident_docs_start, incident_docs_end: '</a>').html_safe + .issuable-form-select-holder.selectbox.form-group.gl-mb-0.gl-display-block + .dropdown.js-issuable-type-filter-dropdown-wrap + %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } + %span.dropdown-toggle-text.is-default + = issuable.issue_type.capitalize || _("Select type") + = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3") + .dropdown-menu.dropdown-menu-selectable.dropdown-select + .dropdown-title.gl-display-flex + %span.gl-ml-auto + = _("Select type") + %button.dropdown-title-button.dropdown-menu-close.gl-ml-auto{ type: 'button', "aria-label" => _('Close') } + = sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon') + .dropdown-content{ data: { testid: 'issue-type-select-dropdown' } } + %ul + - if create_issue_type_allowed?(@project, :issue) + %li.js-filter-issuable-type + = link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do + #{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_('Issue')} + - if create_issue_type_allowed?(@project, :incident) + %li.js-filter-issuable-type{ data: { track: { action: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } } + = link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do + #{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_('Incident')} + + - if issuable.incident? + %p.form-text.text-muted + - incident_docs_url = help_page_path('operations/incident_management/incidents.md') + - incident_docs_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: incident_docs_url) + = format(_('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.'), incident_docs_start: incident_docs_start, incident_docs_end: '</a>').html_safe |