diff options
Diffstat (limited to 'app')
80 files changed, 859 insertions, 380 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index bc2e71b99f2..aacfa0d87e6 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -142,8 +142,7 @@ export default class Clusters { addListeners() { if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); eventHub.$on('installApplication', this.installApplication); - eventHub.$on('upgradeApplication', data => this.upgradeApplication(data)); - eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId)); + eventHub.$on('updateApplication', data => this.updateApplication(data)); eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data)); eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data)); eventHub.$on('uninstallApplication', data => this.uninstallApplication(data)); @@ -155,8 +154,7 @@ export default class Clusters { removeListeners() { if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); eventHub.$off('installApplication', this.installApplication); - eventHub.$off('upgradeApplication', this.upgradeApplication); - eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess); + eventHub.$off('updateApplication', this.updateApplication); eventHub.$off('saveKnativeDomain'); eventHub.$off('setKnativeHostname'); eventHub.$off('uninstallApplication'); @@ -331,19 +329,13 @@ export default class Clusters { }); } - upgradeApplication(data) { - const appId = data.id; - + updateApplication({ id: appId, params }) { this.store.updateApplication(appId); - this.service.installApplication(appId, data.params).catch(() => { + this.service.installApplication(appId, params).catch(() => { this.store.notifyUpdateFailure(appId); }); } - dismissUpgradeSuccess(appId) { - this.store.acknowledgeSuccessfulUpdate(appId); - } - toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) { if (externalIp !== newExternalIp) { this.ingressDomainHelpText.classList.toggle('hide', !newExternalIp); diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 7b173be599a..4771090aa7e 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -2,7 +2,7 @@ /* eslint-disable vue/require-default-prop */ import { GlLink, GlModalDirective } from '@gitlab/ui'; import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; -import { s__, sprintf } from '../../locale'; +import { s__, __, sprintf } from '~/locale'; import eventHub from '../event_hub'; import identicon from '../../vue_shared/components/identicon.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue'; @@ -85,7 +85,7 @@ export default { type: String, required: false, }, - upgradeAvailable: { + updateAvailable: { type: Boolean, required: false, }, @@ -113,11 +113,6 @@ export default { required: false, default: false, }, - updateAcknowledged: { - type: Boolean, - required: false, - default: true, - }, installApplicationRequestParams: { type: Object, required: false, @@ -174,11 +169,11 @@ export default { installButtonLabel() { let label; if (this.canInstall) { - label = s__('ClusterIntegration|Install'); + label = __('Install'); } else if (this.isInstalling) { - label = s__('ClusterIntegration|Installing'); + label = __('Installing'); } else if (this.installed) { - label = s__('ClusterIntegration|Installed'); + label = __('Installed'); } return label; @@ -187,7 +182,7 @@ export default { return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED; }, manageButtonLabel() { - return s__('ClusterIntegration|Manage'); + return __('Manage'); }, hasError() { return this.installFailed || this.uninstallFailed; @@ -207,42 +202,42 @@ export default { }, versionLabel() { if (this.updateFailed) { - return s__('ClusterIntegration|Upgrade failed'); - } else if (this.isUpgrading) { - return s__('ClusterIntegration|Upgrading'); + return __('Update failed'); + } else if (this.isUpdating) { + return __('Updating'); } - return s__('ClusterIntegration|Upgraded'); + return __('Updated'); }, - upgradeFailureDescription() { + updateFailureDescription() { return s__('ClusterIntegration|Update failed. Please check the logs and try again.'); }, - upgradeSuccessDescription() { - return sprintf(s__('ClusterIntegration|%{title} upgraded successfully.'), { + updateSuccessDescription() { + return sprintf(s__('ClusterIntegration|%{title} updated successfully.'), { title: this.title, }); }, - upgradeButtonLabel() { + updateButtonLabel() { let label; - if (this.upgradeAvailable && !this.updateFailed && !this.isUpgrading) { - label = s__('ClusterIntegration|Upgrade'); - } else if (this.isUpgrading) { - label = s__('ClusterIntegration|Updating'); + if (this.updateAvailable && !this.updateFailed && !this.isUpdating) { + label = __('Update'); + } else if (this.isUpdating) { + label = __('Updating'); } else if (this.updateFailed) { - label = s__('ClusterIntegration|Retry update'); + label = __('Retry update'); } return label; }, - isUpgrading() { + isUpdating() { // Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend return this.status === APPLICATION_STATUS.UPDATING; }, - shouldShowUpgradeDetails() { + shouldShowUpdateDetails() { // This method only returns true when; - // Upgrade was successful OR Upgrade failed - // AND new upgrade is unavailable AND version information is present. - return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version; + // Update was successful OR Update failed + // AND new update is unavailable AND version information is present. + return (this.updateSuccessful || this.updateFailed) && !this.updateAvailable && this.version; }, uninstallSuccessDescription() { return sprintf(s__('ClusterIntegration|%{title} uninstalled successfully.'), { @@ -253,7 +248,7 @@ export default { watch: { updateSuccessful(updateSuccessful) { if (updateSuccessful) { - this.$toast.show(this.upgradeSuccessDescription); + this.$toast.show(this.updateSuccessDescription); } }, uninstallSuccessful(uninstallSuccessful) { @@ -269,8 +264,8 @@ export default { params: this.installApplicationRequestParams, }); }, - upgradeClicked() { - eventHub.$emit('upgradeApplication', { + updateClicked() { + eventHub.$emit('updateApplication', { id: this.id, params: this.installApplicationRequestParams, }); @@ -332,8 +327,8 @@ export default { <div v-if="updateable"> <div - v-if="shouldShowUpgradeDetails" - class="form-text text-muted label p-0 js-cluster-application-upgrade-details" + v-if="shouldShowUpdateDetails" + class="form-text text-muted label p-0 js-cluster-application-update-details" > {{ versionLabel }} <span v-if="updateSuccessful">to</span> @@ -342,24 +337,24 @@ export default { v-if="updateSuccessful" :href="chartRepo" target="_blank" - class="js-cluster-application-upgrade-version" + class="js-cluster-application-update-version" >chart v{{ version }}</gl-link > </div> <div - v-if="updateFailed && !isUpgrading" - class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message" + v-if="updateFailed && !isUpdating" + class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-update-details" > - {{ upgradeFailureDescription }} + {{ updateFailureDescription }} </div> <loading-button - v-if="upgradeAvailable || updateFailed || isUpgrading" - class="btn btn-primary js-cluster-application-upgrade-button mt-2" - :loading="isUpgrading" - :disabled="isUpgrading" - :label="upgradeButtonLabel" - @click="upgradeClicked" + v-if="updateAvailable || updateFailed || isUpdating" + class="btn btn-primary js-cluster-application-update-button mt-2" + :loading="isUpdating" + :disabled="isUpdating" + :label="updateButtonLabel" + @click="updateClicked" /> </div> </div> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 2d129245d37..970f5a7b297 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -376,7 +376,7 @@ export default { :request-reason="applications.runner.requestReason" :version="applications.runner.version" :chart-repo="applications.runner.chartRepo" - :upgrade-available="applications.runner.upgradeAvailable" + :update-available="applications.runner.updateAvailable" :installed="applications.runner.installed" :install-failed="applications.runner.installFailed" :update-successful="applications.runner.updateSuccessful" diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js index 14b80a116a7..17ea4d77795 100644 --- a/app/assets/javascripts/clusters/services/application_state_machine.js +++ b/app/assets/javascripts/clusters/services/application_state_machine.js @@ -123,7 +123,6 @@ const applicationStateMachine = { target: INSTALLED, effects: { updateSuccessful: true, - updateAcknowledged: false, }, }, [UPDATE_ERRORED]: { diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 89e61c10a46..f64f0ca616f 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -56,8 +56,7 @@ export default class ClusterStore { title: s__('ClusterIntegration|GitLab Runner'), version: null, chartRepo: 'https://gitlab.com/charts/gitlab-runner', - upgradeAvailable: null, - updateAcknowledged: true, + updateAvailable: null, updateSuccessful: false, updateFailed: false, }, @@ -136,10 +135,6 @@ export default class ClusterStore { this.state.applications[appId] = transitionApplicationState(currentAppState, event); } - acknowledgeSuccessfulUpdate(appId) { - this.state.applications[appId].updateAcknowledged = true; - } - updateAppProperty(appId, prop, value) { this.state.applications[appId][prop] = value; } @@ -154,7 +149,7 @@ export default class ClusterStore { status, status_reason: statusReason, version, - update_available: upgradeAvailable, + update_available: updateAvailable, can_uninstall: uninstallable, } = serverAppEntry; const currentApplicationState = this.state.applications[appId] || {}; @@ -191,7 +186,7 @@ export default class ClusterStore { serverAppEntry.external_hostname || this.state.applications.knative.externalHostname; } else if (appId === RUNNER) { this.state.applications.runner.version = version; - this.state.applications.runner.upgradeAvailable = upgradeAvailable; + this.state.applications.runner.updateAvailable = updateAvailable; } }); } diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index 2b3d6d1a3fa..d59b1136677 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -8,6 +8,7 @@ import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_d import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue'; import InlineDiffView from './inline_diff_view.vue'; import ParallelDiffView from './parallel_diff_view.vue'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import NoteForm from '../../notes/components/note_form.vue'; import ImageDiffOverlay from './image_diff_overlay.vue'; import DiffDiscussions from './diff_discussions.vue'; @@ -26,6 +27,7 @@ export default { ImageDiffOverlay, NotDiffableViewer, NoPreviewViewer, + userAvatarLink, DiffFileDrafts: () => import('ee_component/batch_comments/components/diff_file_drafts.vue'), }, mixins: [diffLineNoteFormMixin, draftCommentsMixin], @@ -47,7 +49,7 @@ export default { }), ...mapGetters('diffs', ['isInlineView', 'isParallelView']), ...mapGetters('diffs', ['getCommentFormForDiffFile']), - ...mapGetters(['getNoteableData', 'noteableType']), + ...mapGetters(['getNoteableData', 'noteableType', 'getUserData']), diffMode() { return getDiffMode(this.diffFile); }, @@ -72,6 +74,9 @@ export default { diffFileHash() { return this.diffFile.file_hash; }, + author() { + return this.getUserData; + }, }, methods: { ...mapActions('diffs', ['saveDiffDiscussion', 'closeDiffFileCommentForm']), @@ -134,6 +139,14 @@ export default { :can-comment="getNoteableData.current_user.can_create_note" /> <div v-if="showNotesContainer" class="note-container"> + <user-avatar-link + v-if="diffFileCommentForm && author" + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" + class="d-none d-sm-block new-comment" + /> <diff-discussions v-if="diffFile.discussions.length" class="diff-file-discussions" diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index 41670b45798..c209b857652 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -4,11 +4,13 @@ import { s__ } from '~/locale'; import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form'; import noteForm from '../../notes/components/note_form.vue'; import autosave from '../../notes/mixins/autosave'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import { DIFF_NOTE_TYPE } from '../constants'; export default { components: { noteForm, + userAvatarLink, }, mixins: [autosave, diffLineNoteFormMixin], props: { @@ -41,7 +43,16 @@ export default { diffViewType: state => state.diffs.diffViewType, }), ...mapGetters('diffs', ['getDiffFileByHash']), - ...mapGetters(['isLoggedIn', 'noteableType', 'getNoteableData', 'getNotesDataByProp']), + ...mapGetters([ + 'isLoggedIn', + 'noteableType', + 'getNoteableData', + 'getNotesDataByProp', + 'getUserData', + ]), + author() { + return this.getUserData; + }, formData() { return { noteableData: this.noteableData, @@ -99,6 +110,14 @@ export default { <template> <div class="content discussion-form discussion-form-container discussion-notes"> + <user-avatar-link + v-if="author" + :link-href="author.path" + :img-src="author.avatar_url" + :img-alt="author.name" + :img-size="40" + class="d-none d-sm-block" + /> <note-form ref="noteForm" :is-editing="true" diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index e15b2a6f76b..b0c4969c5e4 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -125,6 +125,7 @@ export default { 'setFileEOL', 'updateViewer', 'removePendingTab', + 'triggerFilesChange', ]), initEditor() { if (this.shouldHideEditor) return; @@ -256,6 +257,7 @@ export default { 'is-added': file.tempFile, }" class="multi-file-editor-holder" + @focusout="triggerFilesChange" ></div> <content-viewer v-if="showContentViewer" diff --git a/app/assets/javascripts/ide/lib/keymap.json b/app/assets/javascripts/ide/lib/keymap.json index 131abfebbed..2db87c07dde 100644 --- a/app/assets/javascripts/ide/lib/keymap.json +++ b/app/assets/javascripts/ide/lib/keymap.json @@ -7,5 +7,13 @@ "name": "toggleFileFinder", "params": true } + }, + { + "id": "save-files", + "label": "Save files", + "bindings": ["CtrlCmd+KEY_S"], + "action": { + "name": "triggerFilesChange" + } } ] diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index dc8ca732879..5429b834708 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -99,6 +99,7 @@ export const createTempEntry = ( commit(types.TOGGLE_FILE_OPEN, file.path); commit(types.ADD_FILE_TO_CHANGED, file.path); dispatch('setFileActive', file.path); + dispatch('triggerFilesChange'); } if (parentPath && !state.entries[parentPath].opened) { @@ -210,6 +211,8 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => { if (entry.parentPath && state.entries[entry.parentPath].tree.length === 0) { dispatch('deleteEntry', entry.parentPath); } + + dispatch('triggerFilesChange'); }; export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); @@ -240,6 +243,8 @@ export const renameEntry = ( if (!entryPath && !entry.tempFile) { dispatch('deleteEntry', path); } + + dispatch('triggerFilesChange'); }; export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) => diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index e7e8ac6d80b..dc40a1fa6a2 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -265,3 +265,8 @@ export const removePendingTab = ({ commit }, file) => { eventHub.$emit(`editor.update.model.dispose.${file.key}`); }; + +export const triggerFilesChange = () => { + // Used in EE for file mirroring + eventHub.$emit('ide.files.change'); +}; diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js index a24c71aeab1..28a7ebfdc69 100644 --- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js @@ -51,6 +51,7 @@ export default class LinkedTabs { this.defaultAction = this.options.defaultAction; this.action = this.options.action || this.defaultAction; + this.hashedTabs = this.options.hashedTabs || false; if (this.action === 'show') { this.action = this.defaultAction; @@ -58,6 +59,10 @@ export default class LinkedTabs { this.currentLocation = window.location; + if (this.hashedTabs) { + this.action = this.currentLocation.hash || this.action; + } + const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`; // since this is a custom event we need jQuery :( @@ -91,7 +96,9 @@ export default class LinkedTabs { copySource.replace(/\/+$/, ''); - const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; + const newState = this.hashedTabs + ? copySource + : `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; window.history.replaceState( { diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index d3e6851496b..d521c462ad8 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -81,7 +81,7 @@ export const getDayName = date => */ export const formatDate = datetime => { if (_.isString(datetime) && datetime.match(/\d+-\d+\d+ /)) { - throw new Error('Invalid date'); + throw new Error(__('Invalid date')); } return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); }; diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index 9ddfb4bca11..61c8b8803d7 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -100,3 +100,9 @@ export function numberToHumanSize(size) { * @returns {Float} The summed value */ export const sum = (a = 0, b = 0) => a + b; + +/** + * Checks if the provided number is odd + * @param {Int} number + */ +export const isOdd = (number = 0) => number % 2; diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 78744c0a0a9..2314f7b80cf 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -106,10 +106,6 @@ export default { type: String, required: true, }, - showTimeWindowDropdown: { - type: Boolean, - required: true, - }, customMetricsAvailable: { type: Boolean, required: false, @@ -248,7 +244,7 @@ export default { > </gl-dropdown> </div> - <div v-if="showTimeWindowDropdown" class="d-flex align-items-center prepend-left-8"> + <div class="d-flex align-items-center prepend-left-8"> <strong>{{ s__('Metrics|Show last') }}</strong> <gl-dropdown class="prepend-left-10 js-time-window-dropdown" diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 57771ccf4d9..62c0f44c1e6 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -16,7 +16,6 @@ export default (props = {}) => { props: { ...el.dataset, hasMetrics: parseBoolean(el.dataset.hasMetrics), - showTimeWindowDropdown: gon.features.metricsTimeWindow, ...props, }, }); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 1c9ca180100..a7156bd2406 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -990,6 +990,14 @@ export default class Notes { form.find('#note_position').val(dataHolder.attr('data-position')); form + .prepend( + `<div class="avatar-note-form-holder"><div class="content"><a href="${escape( + gon.current_username, + )}" class="user-avatar-link d-none d-sm-block"><img class="avatar s40" src="${encodeURI( + gon.current_user_avatar_url, + )}" alt="${escape(gon.current_user_fullname)}" /></a></div></div>`, + ) + .append('</div>') .find('.js-close-discussion-note-form') .show() .removeClass('hide'); @@ -1025,6 +1033,9 @@ export default class Notes { target: $link, lineType: link.dataset.lineType, showReplyInput, + currentUsername: gon.current_username, + currentUserAvatar: gon.current_user_avatar_url, + currentUserFullname: gon.current_user_fullname, }); } @@ -1053,7 +1064,15 @@ export default class Notes { this.setupDiscussionNoteForm($link, newForm); } - toggleDiffNote({ target, lineType, forceShow, showReplyInput = false }) { + toggleDiffNote({ + target, + lineType, + forceShow, + showReplyInput = false, + currentUsername, + currentUserAvatar, + currentUserFullname, + }) { var $link, addForm, hasNotes, @@ -1546,7 +1565,9 @@ export default class Notes { <div class="note-header"> <div class="note-header-info"> <a href="/${_.escape(currentUsername)}"> - <span class="d-none d-sm-inline-block">${_.escape(currentUsername)}</span> + <span class="d-none d-sm-inline-block bold">${_.escape( + currentUsername, + )}</span> <span class="note-headline-light">${_.escape(currentUsername)}</span> </a> </div> diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 688c06878ac..075c28e8d07 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -337,6 +337,8 @@ Please check your network connection and try again.`; v-if="hasWarning(getNoteableData)" :is-locked="isLocked(getNoteableData)" :is-confidential="isConfidential(getNoteableData)" + :locked-issue-docs-path="lockedIssueDocsPath" + :confidential-issue-docs-path="confidentialIssueDocsPath" /> <markdown-field diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 307e56708e0..efd84f5722c 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -49,8 +49,8 @@ export default { </script> <template> - <div v-if="resolvableDiscussionsCount > 0" class="line-resolve-all-container prepend-top-8"> - <div> + <div v-if="resolvableDiscussionsCount > 0" class="line-resolve-all-container full-width-mobile"> + <div class="full-width-mobile d-flex d-sm-block"> <div :class="{ 'has-next-btn': hasNextButton }" class="line-resolve-all"> <span :class="{ 'is-active': allResolved }" @@ -64,7 +64,11 @@ export default { {{ n__('discussion resolved', 'discussions resolved', resolvableDiscussionsCount) }} </span> </div> - <div v-if="resolveAllDiscussionsIssuePath && !allResolved" class="btn-group" role="group"> + <div + v-if="resolveAllDiscussionsIssuePath && !allResolved" + class="btn-group btn-group-sm" + role="group" + > <a v-gl-tooltip :href="resolveAllDiscussionsIssuePath" @@ -74,7 +78,7 @@ export default { <icon name="issue-new" /> </a> </div> - <div v-if="isLoggedIn && !allResolved" class="btn-group" role="group"> + <div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group"> <button v-gl-tooltip title="Jump to first unresolved discussion" diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 47951591e82..eb3fbbe1385 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -105,12 +105,12 @@ export default { <template> <div v-if="displayFilters" - class="discussion-filter-container js-discussion-filter-container d-inline-block align-bottom" + class="discussion-filter-container js-discussion-filter-container d-inline-block align-bottom full-width-mobile" > <button id="discussion-filter-dropdown" ref="dropdownToggle" - class="btn btn-default qa-discussion-filter" + class="btn btn-sm qa-discussion-filter" data-toggle="dropdown" aria-expanded="false" > diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue index c469a6b7bcd..53f509185a8 100644 --- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue @@ -1,12 +1,24 @@ <script> +import { GlLink } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; +import { __, sprintf } from '~/locale'; import Issuable from '~/vue_shared/mixins/issuable'; +import issuableStateMixin from '../mixins/issuable_state'; export default { components: { Icon, + GlLink, + }, + mixins: [Issuable, issuableStateMixin], + computed: { + lockedIssueWarning() { + return sprintf( + __('This %{issuableDisplayName} is locked. Only project members can comment.'), + { issuableDisplayName: this.issuableDisplayName }, + ); + }, }, - mixins: [Issuable], }; </script> @@ -15,7 +27,11 @@ export default { <span class="issuable-note-warning inline"> <icon :size="16" name="lock" class="icon" /> <span> - This {{ issuableDisplayName }} is locked. Only <b>project members</b> can comment. + {{ lockedIssueWarning }} + + <gl-link :href="lockedIssueDocsPath" target="_blank" class="learn-more"> + {{ __('Learn more') }} + </gl-link> </span> </span> </div> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index c9c40cb6acf..844d0c3e376 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -195,7 +195,7 @@ export default { </button> <ul class="dropdown-menu more-actions-dropdown dropdown-open-left"> <li v-if="canReportAsAbuse"> - <a :href="reportAbusePath">{{ __('Report abuse to GitLab') }}</a> + <a :href="reportAbusePath">{{ __('Report abuse to admin') }}</a> </li> <li v-if="noteUrl"> <button diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index acbb91ce7be..09ecb695214 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -234,6 +234,8 @@ export default { v-if="hasWarning(getNoteableData)" :is-locked="isLocked(getNoteableData)" :is-confidential="isConfidential(getNoteableData)" + :locked-issue-docs-path="lockedIssueDocsPath" + :confidential-issue-docs-path="confidentialIssueDocsPath" /> <markdown-field diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 5c59c0c32dd..fbf82fab9e9 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -82,7 +82,7 @@ export default { :data-username="author.username" > <slot name="note-header-info"></slot> - <span class="note-header-author-name">{{ author.name }}</span> + <span class="note-header-author-name bold">{{ author.name }}</span> <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span> <span class="note-headline-light">@{{ author.username }}</span> </a> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 2c549e7abdd..eb6a4a67fff 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -87,7 +87,11 @@ export default { 'unresolvedDiscussionsCount', 'hasUnresolvedDiscussions', 'showJumpToNextDiscussion', + 'getUserData', ]), + currentUser() { + return this.getUserData; + }, author() { return this.firstNote.author; }, @@ -377,6 +381,14 @@ Please check your network connection and try again.`; :class="{ 'is-replying': isReplying }" class="discussion-reply-holder" > + <user-avatar-link + v-if="!isReplying && currentUser" + :link-href="currentUser.path" + :img-src="currentUser.avatar_url" + :img-alt="currentUser.name" + :img-size="40" + class="d-none d-sm-block" + /> <discussion-actions v-if="!isReplying && userCanReply" :discussion="discussion" @@ -388,18 +400,27 @@ Please check your network connection and try again.`; @resolve="resolveHandler" @jumpToNextDiscussion="jumpToNextDiscussion" /> - <note-form - v-if="isReplying" - ref="noteForm" - :discussion="discussion" - :is-editing="false" - :line="diffLine" - save-button-title="Comment" - :autosave-key="autosaveKey" - @handleFormUpdateAddToReview="addReplyToReview" - @handleFormUpdate="saveReply" - @cancelForm="cancelReplyForm" - /> + <div v-if="isReplying" class="avatar-note-form-holder"> + <user-avatar-link + v-if="currentUser" + :link-href="currentUser.path" + :img-src="currentUser.avatar_url" + :img-alt="currentUser.name" + :img-size="40" + class="d-none d-sm-block" + /> + <note-form + ref="noteForm" + :discussion="discussion" + :is-editing="false" + :line="diffLine" + save-button-title="Comment" + :autosave-key="autosaveKey" + @handleFormUpdateAddToReview="addReplyToReview" + @handleFormUpdate="saveReply" + @cancelForm="cancelReplyForm" + /> + </div> <note-signed-out-widget v-if="!userCanReply" /> </div> </template> diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js index ded0ac3cfa9..d97d9f6850a 100644 --- a/app/assets/javascripts/notes/mixins/issuable_state.js +++ b/app/assets/javascripts/notes/mixins/issuable_state.js @@ -1,4 +1,15 @@ +import { mapGetters } from 'vuex'; + export default { + computed: { + ...mapGetters(['getNoteableDataByProp']), + lockedIssueDocsPath() { + return this.getNoteableDataByProp('locked_discussion_docs_path'); + }, + confidentialIssueDocsPath() { + return this.getNoteableDataByProp('confidential_issues_docs_path'); + }, + }, methods: { isConfidential(issue) { return Boolean(issue.confidential); diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index b288989b252..f0d529758d5 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -39,6 +39,11 @@ export default class Project { $label.text(activeText); }); + $('#modal-geo-info').data({ + cloneUrlSecondary: $this.attr('href'), + cloneUrlPrimary: $this.data('primaryUrl') || '', + }); + if (mobileCloneField) { mobileCloneField.dataset.clipboardText = url; } else { @@ -67,6 +72,13 @@ export default class Project { .remove(); return e.preventDefault(); }); + $('.hide-shared-runner-limit-message').on('click', function(e) { + var $alert = $(this).parents('.shared-runner-quota-message'); + var scope = $alert.data('scope'); + Cookies.set('hide_shared_runner_quota_message', 'false', { path: scope }); + $alert.remove(); + e.preventDefault(); + }); $('.hide-auto-devops-implicitly-enabled-banner').on('click', function(e) { const projectId = $(this).data('project-id'); const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`; diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index e92babc499b..e438ff16a41 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -1,9 +1,17 @@ <script> +import { GlLink } from '@gitlab/ui'; +import _ from 'underscore'; +import { sprintf } from '~/locale'; import icon from '../../../vue_shared/components/icon.vue'; +function buildDocsLinkStart(path) { + return `<a href="${_.escape(path)}" target="_blank" rel="noopener noreferrer">`; +} + export default { components: { icon, + GlLink, }, props: { isLocked: { @@ -16,6 +24,16 @@ export default { default: false, required: false, }, + lockedIssueDocsPath: { + type: String, + required: false, + default: '', + }, + confidentialIssueDocsPath: { + type: String, + required: false, + default: '', + }, }, computed: { warningIcon() { @@ -27,6 +45,17 @@ export default { isLockedAndConfidential() { return this.isConfidential && this.isLocked; }, + confidentialAndLockedDiscussionText() { + return sprintf( + 'This issue is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}.', + { + confidentialLinkStart: buildDocsLinkStart(this.confidentialIssueDocsPath), + lockedLinkStart: buildDocsLinkStart(this.lockedIssueDocsPath), + linkEnd: '</a>', + }, + false, + ); + }, }, }; </script> @@ -35,20 +64,26 @@ export default { <icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" /> <span v-if="isLockedAndConfidential"> - {{ __('This issue is confidential and locked.') }} + <span v-html="confidentialAndLockedDiscussionText"></span> {{ - __(`People without permission will never -get a notification and won't be able to comment.`) + __(`People without permission will never get a notification and won't be able to comment.`) }} </span> <span v-else-if="isConfidential"> {{ __('This is a confidential issue.') }} - {{ __('Your comment will not be visible to the public.') }} + {{ __('People without permission will never get a notification.') }} + <gl-link :href="confidentialIssueDocsPath" target="_blank"> + {{ __('Learn more') }} + </gl-link> </span> <span v-else-if="isLocked"> - {{ __('This issue is locked.') }} {{ __('Only project members can comment.') }} + {{ __('This issue is locked.') }} + {{ __('Only project members can comment.') }} + <gl-link :href="lockedIssueDocsPath" target="_blank"> + {{ __('Learn more') }} + </gl-link> </span> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index a50f49c1279..baed26a157c 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -51,7 +51,7 @@ export default { <div class="note-header"> <div class="note-header-info"> <a :href="getUserData.path"> - <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span> + <span class="d-none d-sm-inline-block bold">{{ getUserData.name }}</span> <span class="note-headline-light">@{{ getUserData.username }}</span> </a> </div> diff --git a/app/assets/stylesheets/components/popover.scss b/app/assets/stylesheets/components/popover.scss index d0aa6ec78aa..774be9ef588 100644 --- a/app/assets/stylesheets/components/popover.scss +++ b/app/assets/stylesheets/components/popover.scss @@ -39,6 +39,25 @@ } } +.onboarding-popover { + box-shadow: 0 2px 4px $dropdown-shadow-color; + + .popover-body { + font-size: $gl-font-size; + line-height: $gl-line-height; + padding: $gl-padding; + } + + .popover-header { + display: none; + } + + .accept-mr-label { + background-color: $accepting-mr-label-color; + color: $white-light; + } +} + .onboarding-welcome-page { .popover { min-width: auto; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 257d788873c..6f5a2e561af 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -268,3 +268,27 @@ $skeleton-line-widths: ( @include webkit-prefix(animation-duration, 1s); transform-origin: 50% 50%; } + +/* ---------------------------------------------- + * Generated by Animista on 2019-4-26 17:40:41 + * w: http://animista.net, t: @cssanimista + * ---------------------------------------------- */ +@keyframes slide-in-fwd-bottom { + 0% { + transform: translateZ(-1400px) translateY(800px); + opacity: 0; + } + + 100% { + transform: translateZ(0) translateY(0); + opacity: 1; + } +} + +.slide-in-fwd-bottom-enter-active { + animation: slide-in-fwd-bottom 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; +} + +.slide-in-fwd-bottom-leave-active { + animation: slide-in-fwd-bottom 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both reverse; +} diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 3aabb66f7a6..65c0ee74c60 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -199,6 +199,7 @@ &.user-cover-block { padding: 24px 0 0; + border-bottom: 1px solid $border-color; .nav-links { width: 100%; @@ -232,14 +233,6 @@ margin-top: -1px; } -.nav-block { - .controls { - float: right; - margin-top: 8px; - padding-bottom: 8px; - } -} - .content-block { padding: $gl-padding 0; border-bottom: 1px solid $white-dark; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 97a763671ba..767832e242c 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -249,7 +249,7 @@ padding: 6px 16px; border-color: $border-color; color: $gray-darkest; - background-color: $gray-light; + background-color: $white-light; &:hover, &:active, @@ -258,7 +258,6 @@ box-shadow: none; border-color: lighten($blue-300, 20%); color: $gray-darkest; - background-color: $gray-light; } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index fc488b85138..db09118ba15 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -489,3 +489,50 @@ img.emoji { .cursor-pointer { cursor: pointer; } + +// Make buttons/dropdowns full-width on mobile +.full-width-mobile { + @include media-breakpoint-down(xs) { + width: 100%; + + > .dropdown-menu, + > .btn { + width: 100%; + } + } +} + +.onboarding-helper-container { + bottom: 40px; + right: 40px; + font-size: $gl-font-size-small; + background: $gray-100; + width: 200px; + border-radius: 24px; + box-shadow: 0 2px 4px $issue-boards-card-shadow; + z-index: 10000; + + .collapsible { + max-height: 0; + transition: max-height 0.5s cubic-bezier(0, 1, 0, 1); + } + + &.expanded { + border-bottom-right-radius: $border-radius-default; + border-bottom-left-radius: $border-radius-default; + + .collapsible { + max-height: 1000px; + transition: max-height 1s ease-in-out; + } + } + + .avatar { + border-color: darken($gray-normal, 10%); + + img { + width: 32px; + height: 32px; + } + } +} diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index ef6f0633150..536a26a6ffe 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -453,6 +453,28 @@ span.idiff { } } + .note-container { + .user-avatar-link.new-comment { + position: absolute; + margin: 40px $gl-padding 0 116px; + + ~ .note-edit-form form.edit-note { + @include media-breakpoint-up(sm) { + margin-left: $note-icon-gutter-width; + } + } + } + } + + .diff-discussions:not(:last-child) .discussion .discussion-body { + padding-bottom: $gl-padding; + + .discussion-reply-holder { + border-bottom: 1px solid $gray-100; + border-radius: 0; + } + } + .md-previewer { padding: $gl-padding; } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 298610a0631..555a3fe0dc7 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -177,14 +177,6 @@ ul.content-list { } } - .member-controls { - float: none; - - @include media-breakpoint-up(sm) { - float: right; - } - } - // When dragging a list item &.ui-sortable-helper { border-bottom: 0; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index df40149f0a6..e7278554e6e 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -218,16 +218,22 @@ } } -@mixin build-trace-top-bar($height) { +// Used in EE for Web Terminal +@mixin build-trace-bar($height) { height: $height; min-height: $height; background: $gray-light; border: 1px solid $border-color; color: $gl-text-color; + padding: $grid-size; +} + +@mixin build-trace-top-bar($height) { + @include build-trace-bar($height); + position: -webkit-sticky; position: sticky; top: $header-height; - padding: $grid-size; .with-performance-bar & { top: $header-height + $performance-bar-height; diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index 31297b9d20c..ada8f2fe1a6 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -13,8 +13,8 @@ a, button { - padding: $gl-btn-padding; - padding-bottom: 11px; + padding: $gl-padding-8; + padding-bottom: $gl-padding-8 + 1; font-size: 14px; line-height: 28px; color: $gl-text-color-secondary; @@ -58,8 +58,12 @@ } .top-area { - @include clearfix; border-bottom: 1px solid $border-color; + display: flex; + + @include media-breakpoint-down(md) { + flex-flow: column-reverse wrap; + } .nav-text { padding-top: 16px; @@ -75,9 +79,8 @@ } .nav-links { - margin-bottom: 0; border-bottom: 0; - float: left; + flex: 1; &.wide { width: 100%; @@ -98,16 +101,23 @@ &.mobile-separator { border-bottom: 1px solid $border-color; + margin-bottom: $gl-padding-8; } } } .nav-controls { display: inline-block; - float: right; text-align: right; - padding: $gl-padding-8 0; - margin-bottom: 0; + + @include media-breakpoint-down(sm) { + margin-top: $gl-padding-8; + } + + @include media-breakpoint-up(md) { + display: flex; + align-items: center; + } > .btn, > .btn-container, @@ -115,8 +125,6 @@ > input, > form { margin-right: $gl-padding-top; - display: inline-block; - vertical-align: top; &:last-child { margin-right: 0; @@ -143,7 +151,7 @@ @include media-breakpoint-up(lg) { width: 250px; } } - @include media-breakpoint-down(xs) { + @include media-breakpoint-down(sm) { padding-bottom: 0; width: 100%; @@ -153,7 +161,7 @@ .dropdown-toggle, .dropdown-menu-toggle, .form-control { - margin: 0 0 10px; + margin: 0 0 $gl-padding-8; display: block; width: 100%; } @@ -165,7 +173,7 @@ form { display: block; height: auto; - margin-bottom: 14px; + margin-bottom: $gl-padding-8; input { width: 100%; @@ -236,20 +244,11 @@ width: 100%; } - @include media-breakpoint-down(xs) { - flex-flow: row wrap; - + @include media-breakpoint-down(md) { .nav-controls { $controls-margin: $btn-margin-5 - 2px; flex: 0 0 100%; - - &.controls-flex { - display: flex; - flex-flow: row wrap; - align-items: center; - justify-content: center; - padding: 0 0 $gl-padding-top; - } + margin-top: $gl-padding-8; .controls-item, .controls-item-full, @@ -326,8 +325,8 @@ .fade-right, .fade-left { - top: 16px; - bottom: auto; + bottom: $gl-padding; + top: auto; } &.is-smaller { @@ -367,6 +366,7 @@ display: flex; border-bottom: 1px solid $border-color; overflow: hidden; + align-items: center; .nav-links { border-bottom: 0; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index e8176e59c19..42a739e88f7 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -42,8 +42,8 @@ } } - .avatar { - margin-right: 15px; + img.avatar { + margin-right: $gl-padding; } .controls { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 0a3bd9bf4d1..dc451a97e17 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -641,6 +641,7 @@ $input-lg-width: 320px; */ $document-index-color: #888; $help-shortcut-header-color: #333; +$accepting-mr-label-color: #69d100; /* * Issues diff --git a/app/assets/stylesheets/page_bundles/_ide_mixins.scss b/app/assets/stylesheets/page_bundles/_ide_mixins.scss index 896a3466cb4..9465dd5bed6 100644 --- a/app/assets/stylesheets/page_bundles/_ide_mixins.scss +++ b/app/assets/stylesheets/page_bundles/_ide_mixins.scss @@ -2,17 +2,17 @@ display: flex; flex-direction: column; height: 100%; - margin-top: -$grid-size; - margin-bottom: -$grid-size; - &.build-page .top-bar { + .top-bar { + @include build-trace-bar(35px); + top: 0; - height: auto; font-size: 12px; border-top-right-radius: $border-radius-default; - } - - .top-bar { margin-left: -$gl-padding; + + .controllers { + @include build-controllers(15px, center, false, 0, inline, 0); + } } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 5e5d298f8f2..3b0d740def3 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1008,6 +1008,10 @@ table.code { display: block; } } + + .note-edit-form { + margin-left: $note-icon-gutter-width; + } } .discussion-body .image .frame { diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 0a07747e0d4..656202f4e58 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -35,9 +35,6 @@ } .group-nav-container .nav-controls { - align-items: flex-start; - padding: $gl-padding-top 0 0; - .group-filter-form { flex: 1 1 auto; margin-right: $gl-padding-8; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 4ba74d34664..79282f9043c 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -69,7 +69,11 @@ } .emoji-block { - padding: 10px 0; + padding: $gl-padding-4 0; + + @include media-breakpoint-down(md) { + padding: $gl-padding-8 0; + } } } @@ -132,6 +136,10 @@ z-index: 200; overflow: hidden; + @include media-breakpoint-down(sm) { + z-index: 251; + } + a:not(.btn) { color: inherit; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index c7d2369a6b8..48289c8f381 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -258,8 +258,15 @@ ul.related-merge-requests > li { } } -.discussion-reply-holder .note-edit-form { - display: block; +.discussion-reply-holder { + .avatar-note-form-holder .note-edit-form { + display: block; + margin-left: $note-icon-gutter-width; + + @include media-breakpoint-down(xs) { + margin-left: 0; + } + } } .issue-sort-dropdown { diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index f8e273a2735..68af01f9ccc 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -20,17 +20,6 @@ } } - .list-item-name { - @include media-breakpoint-up(sm) { - float: left; - width: 50%; - } - - strong { - font-weight: $gl-font-weight-bold; - } - } - .controls { @include media-breakpoint-up(sm) { display: flex; @@ -43,10 +32,11 @@ .form-group { margin-bottom: 0; + } - @include media-breakpoint-down(sm) { - display: block; - margin-left: 5px; + .member-controls { + .fa { + line-height: inherit; } } @@ -66,23 +56,12 @@ } .member-form-control { - @include media-breakpoint-down(sm) { - width: $dropdown-member-form-control-width; - margin-left: 0; - padding-bottom: 5px; - } - @include media-breakpoint-down(xs) { margin-right: 0; width: auto; } } -.member-access-text { - margin-left: auto; - line-height: 43px; -} - .member-search-form { position: relative; @@ -221,9 +200,6 @@ } .content-list.members-list li { - display: flex; - justify-content: space-between; - .list-item-name { float: none; display: flex; @@ -252,33 +228,24 @@ align-self: flex-start; } + @include media-breakpoint-down(sm) { + .member-access-text { + margin: 0 0 $gl-padding-4 ($grid-size * 6); + } + } + @include media-breakpoint-down(xs) { display: block; - .controls > .btn { - margin-left: 0; - margin-right: 0; + .controls > .btn, + .controls .member-form-control { + margin: 0 0 $gl-padding-8; display: block; } - .controls > .btn:last-child { - margin-left: 5px; - margin-right: 5px; - width: auto; - } - .form-control { width: 100%; } - - .member-access-text { - line-height: 0; - margin-left: 50px; - } - - .member-controls { - margin-top: 5px; - } } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 77b40fe2d30..8cb3fab74e0 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -846,15 +846,40 @@ display: flex; justify-content: space-between; - @include media-breakpoint-down(sm) { - flex-direction: column-reverse; + @include media-breakpoint-down(xs) { + .discussion-filter-container, + .line-resolve-all-container { + margin-bottom: $gl-padding-4; + } } .discussion-filter-container { - margin-top: $gl-padding-8; - &:not(:only-child) { - padding-right: $gl-padding-8; + margin: $gl-padding-4; + } + } + + .merge-request-tabs { + height: $grid-size * 6; + } +} + +// Wrap MR tabs/buttons so you don't have to scroll on desktop +@include media-breakpoint-down(md) { + .merge-request-tabs-container, + .epic-tabs-container { + flex-direction: column-reverse; + padding-top: $gl-padding-8; + } +} + +@include media-breakpoint-down(lg) { + .right-sidebar-expanded { + .merge-request-tabs-container, + .epic-tabs-container { + flex-direction: column-reverse; + align-items: flex-start; + padding-top: $gl-padding-8; } } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 8c7b124dd33..c6bac33e888 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -59,6 +59,7 @@ border-radius: $border-radius-base; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + background-color: $white-light; &.is-focused { @extend .form-control:focus; @@ -103,6 +104,11 @@ margin: auto; align-items: center; + a { + color: $orange-600; + text-decoration: underline; + } + .icon { margin-right: $issuable-warning-icon-margin; vertical-align: text-bottom; @@ -168,6 +174,16 @@ .discussion-form { background-color: $white-light; + + @include media-breakpoint-down(xs) { + .user-avatar-link { + display: none; + } + + .note-edit-form { + margin-left: 0; + } + } } table { @@ -234,13 +250,25 @@ table { .diff-file, .commit-diff { .discussion-reply-holder { - background-color: $white-light; + background-color: $gray-light; border-radius: 0 0 3px 3px; padding: $gl-padding; + border-top: 1px solid $gray-100; + + + .new-note { + background-color: $gray-light; + border-top: 1px solid $gray-100; + } &.is-replying { padding-bottom: $gl-padding; } + + .user-avatar-link { + img { + margin-top: -3px; + } + } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 32477c20db6..da341121087 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -80,21 +80,17 @@ $note-form-margin-left: 72px; } } - li.note { - border-bottom: 1px solid $border-color; - } - .replies-toggle { background-color: $gray-light; padding: $gl-padding-8 $gl-padding; + border-top: 1px solid $gray-100; + border-bottom: 1px solid $gray-100; .collapse-replies-btn:hover { color: $blue-600; } &.expanded { - border-bottom: 1px solid $border-color; - span { cursor: pointer; } @@ -211,8 +207,13 @@ $note-form-margin-left: 72px; display: none; } + .user-avatar-link img { + margin-top: $gl-padding-8; + } + .note-edit-form { display: block; + margin-left: 0; &.current-note-edit-form + .note-awards { display: none; @@ -519,12 +520,30 @@ $note-form-margin-left: 72px; } } -.commit-diff { - .notes-content { - background-color: $white-light; +.code-commit .notes-content, +.diff-viewer > .image ~ .note-container { + background-color: $white-light; + + .avatar-note-form-holder { + .user-avatar-link img { + margin: 13px $gl-padding $gl-padding; + } + + form, + ~ .discussion-form-container { + padding: $gl-padding; + + @include media-breakpoint-up(sm) { + margin-left: $note-icon-gutter-width; + } + } } } +.diff-viewer > .image ~ .note-container form.new-note { + margin-left: 0; +} + .discussion-header, .note-header-info { a { @@ -762,15 +781,13 @@ $note-form-margin-left: 72px; background-color: $white-light; } - a { + a:not(.learn-more) { color: $blue-600; } } .line-resolve-all-container { - @include notes-media('min', map-get($grid-breakpoints, sm)) { - margin-right: 0; - } + margin: $gl-padding-4; > div { white-space: nowrap; @@ -786,6 +803,8 @@ $note-form-margin-left: 72px; } .btn { + line-height: $gl-line-height; + svg { fill: $gray-darkest; } @@ -811,10 +830,11 @@ $note-form-margin-left: 72px; .line-resolve-all { vertical-align: middle; display: inline-block; - padding: 6px 10px; + padding: $gl-padding-4 10px; background-color: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; + font-size: $gl-btn-small-font-size; &.has-next-btn { border-top-right-radius: 0; @@ -830,6 +850,10 @@ $note-form-margin-left: 72px; vertical-align: middle; } } + + @include media-breakpoint-down(xs) { + flex: 1; + } } .line-resolve-btn { diff --git a/app/controllers/acme_challenges_controller.rb b/app/controllers/acme_challenges_controller.rb new file mode 100644 index 00000000000..67a39d8870b --- /dev/null +++ b/app/controllers/acme_challenges_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AcmeChallengesController < ActionController::Base + def show + if acme_order + render plain: acme_order.challenge_file_content, content_type: 'text/plain' + else + head :not_found + end + end + + private + + def acme_order + @acme_order ||= PagesDomainAcmeOrder.find_by_domain_and_token(params[:domain], params[:token]) + end +end diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index 73ebd4e0e42..80ee7c35906 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -12,9 +12,6 @@ class Clusters::ClustersController < Clusters::BaseController before_action :authorize_update_cluster!, only: [:update] before_action :authorize_admin_cluster!, only: [:destroy] before_action :update_applications_status, only: [:cluster_status] - before_action only: [:show] do - push_frontend_feature_flag(:metrics_time_window) - end helper_method :token_in_session diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index cb02581da37..98cd66cf6f9 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -4,6 +4,10 @@ class Projects::ClustersController < Clusters::ClustersController prepend_before_action :project before_action :repository + before_action do + push_frontend_feature_flag(:prometheus_computed_alerts) + end + layout 'project' private diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index c342e1c80b0..e002a4d349b 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -11,10 +11,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :expire_etag_cache, only: [:index] before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do - push_frontend_feature_flag(:metrics_time_window) push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint) push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards) push_frontend_feature_flag(:grafana_dashboard_link) + push_frontend_feature_flag(:prometheus_computed_alerts) end def index @@ -220,8 +220,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController end def metrics_params - return unless Feature.enabled?(:metrics_time_window, project) - params.require([:start, :end]) end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index a374851e835..dd0d9105df6 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -29,15 +29,18 @@ module Types # proc because we set complexity depending on arguments and number of # items which can be loaded. proc do |ctx, args, child_complexity| - page_size = @max_page_size || ctx.schema.default_max_page_size - limit_value = [args[:first], args[:last], page_size].compact.min - # Resolvers may add extra complexity depending on used arguments complexity = child_complexity + self.resolver&.try(:resolver_complexity, args, child_complexity: child_complexity).to_i - # Resolvers may add extra complexity depending on number of items being loaded. - multiplier = self.resolver&.try(:complexity_multiplier, args).to_f - complexity += complexity * limit_value * multiplier + field_defn = to_graphql + + if field_defn.connection? + # Resolvers may add extra complexity depending on number of items being loaded. + page_size = field_defn.connection_max_page_size || ctx.schema.default_max_page_size + limit_value = [args[:first], args[:last], page_size].compact.min + multiplier = self.resolver&.try(:complexity_multiplier, args).to_f + complexity += complexity * limit_value * multiplier + end complexity.to_i end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 59416fb4b51..4fcaac75655 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -984,21 +984,6 @@ class MergeRequest < ApplicationRecord end end - def reset_auto_merge - return unless auto_merge_enabled? - - self.auto_merge_enabled = false - self.merge_user = nil - if merge_params - merge_params.delete('should_remove_source_branch') - merge_params.delete('commit_message') - merge_params.delete('squash_commit_message') - merge_params.delete('auto_merge_strategy') - end - - self.save - end - # Return array of possible target branches # depends on target project of MR def target_branches diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 407d85b1520..524df30289e 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -5,6 +5,7 @@ class PagesDomain < ApplicationRecord VERIFICATION_THRESHOLD = 3.days.freeze belongs_to :project + has_many :acme_orders, class_name: "PagesDomainAcmeOrder" validates :domain, hostname: { allow_numeric_hostname: true } validates :domain, uniqueness: { case_sensitive: false } @@ -134,6 +135,14 @@ class PagesDomain < ApplicationRecord "#{VERIFICATION_KEY}=#{verification_code}" end + def certificate=(certificate) + super(certificate) + + # set nil, if certificate is nil + self.certificate_valid_not_before = x509&.not_before + self.certificate_valid_not_after = x509&.not_after + end + private def set_verification_code @@ -186,7 +195,7 @@ class PagesDomain < ApplicationRecord end def x509 - return unless certificate + return unless certificate.present? @x509 ||= OpenSSL::X509::Certificate.new(certificate) rescue OpenSSL::X509::CertificateError diff --git a/app/models/pages_domain_acme_order.rb b/app/models/pages_domain_acme_order.rb new file mode 100644 index 00000000000..63d7fbc8206 --- /dev/null +++ b/app/models/pages_domain_acme_order.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class PagesDomainAcmeOrder < ApplicationRecord + belongs_to :pages_domain + + scope :expired, -> { where("expires_at < ?", Time.now) } + + validates :pages_domain, presence: true + validates :expires_at, presence: true + validates :url, presence: true + validates :challenge_token, presence: true + validates :challenge_file_content, presence: true + validates :private_key, presence: true + + attr_encrypted :private_key, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm', + encode: true + + def self.find_by_domain_and_token(domain_name, challenge_token) + joins(:pages_domain).find_by(pages_domains: { domain: domain_name }, challenge_token: challenge_token) + end +end diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb index 957be685aea..175c2ebf197 100644 --- a/app/models/project_services/youtrack_service.rb +++ b/app/models/project_services/youtrack_service.rb @@ -5,12 +5,12 @@ class YoutrackService < IssueTrackerService prop_accessor :description, :project_url, :issues_url - # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1 + # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030 def self.reference_pattern(only_long: false) if only_long - /(?<issue>\b[A-Z][A-Za-z0-9_]*-\d+)/ + /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+)/ else - /(?<issue>\b[A-Z][A-Za-z0-9_]*-\d+)|(#{Issue.reference_prefix}(?<issue>\d+))/ + /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+)|(#{Issue.reference_prefix}(?<issue>\d+))/ end end diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index 914ad628a99..36e601f45c5 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -44,4 +44,12 @@ class IssueEntity < IssuableEntity expose :preview_note_path do |issue| preview_markdown_path(issue.project, target_type: 'Issue', target_id: issue.iid) end + + expose :confidential_issues_docs_path, if: -> (issue) { issue.confidential? } do |issue| + help_page_path('user/project/issues/confidential_issues.md') + end + + expose :locked_discussion_docs_path, if: -> (issue) { issue.discussion_locked? } do |issue| + help_page_path('user/discussions/index.md', anchor: 'lock-discussions') + end end diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb new file mode 100644 index 00000000000..058105db3a4 --- /dev/null +++ b/app/services/auto_merge/base_service.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module AutoMerge + class BaseService < ::BaseService + include Gitlab::Utils::StrongMemoize + + def execute(merge_request) + merge_request.merge_params.merge!(params) + merge_request.auto_merge_enabled = true + merge_request.merge_user = current_user + merge_request.auto_merge_strategy = strategy + + return :failed unless merge_request.save + + yield if block_given? + + strategy.to_sym + end + + def cancel(merge_request) + if cancel_auto_merge(merge_request) + yield if block_given? + + success + else + error("Can't cancel the automatic merge", 406) + end + end + + private + + def strategy + strong_memoize(:strategy) do + self.class.name.demodulize.remove('Service').underscore + end + end + + def cancel_auto_merge(merge_request) + merge_request.auto_merge_enabled = false + merge_request.merge_user = nil + + merge_request.merge_params&.except!( + 'should_remove_source_branch', + 'commit_message', + 'squash_commit_message', + 'auto_merge_strategy' + ) + + merge_request.save + end + end +end diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb index d0586468859..c41073a73e9 100644 --- a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb +++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb @@ -1,32 +1,12 @@ # frozen_string_literal: true module AutoMerge - class MergeWhenPipelineSucceedsService < BaseService + class MergeWhenPipelineSucceedsService < AutoMerge::BaseService def execute(merge_request) - return :failed unless merge_request.actual_head_pipeline - - if merge_request.actual_head_pipeline.active? - merge_request.merge_params.merge!(params) - - unless merge_request.auto_merge_enabled? - merge_request.auto_merge_enabled = true - merge_request.merge_user = @current_user - merge_request.auto_merge_strategy = AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS - - SystemNoteService.merge_when_pipeline_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit) + super do + if merge_request.saved_change_to_auto_merge_enabled? + SystemNoteService.merge_when_pipeline_succeeds(merge_request, project, current_user, merge_request.diff_head_commit) end - - return :failed unless merge_request.save - - :merge_when_pipeline_succeeds - elsif merge_request.actual_head_pipeline.success? - # This can be triggered when a user clicks the auto merge button while - # the tests finish at about the same time - merge_request.merge_async(current_user.id, merge_params) - - :success - else - :failed end end @@ -38,12 +18,8 @@ module AutoMerge end def cancel(merge_request) - if merge_request.reset_auto_merge + super do SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, @project, @current_user) - - success - else - error("Can't cancel the automatic merge", 406) end end diff --git a/app/services/pages_domains/create_acme_order_service.rb b/app/services/pages_domains/create_acme_order_service.rb new file mode 100644 index 00000000000..c600f497fa5 --- /dev/null +++ b/app/services/pages_domains/create_acme_order_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module PagesDomains + class CreateAcmeOrderService + attr_reader :pages_domain + + def initialize(pages_domain) + @pages_domain = pages_domain + end + + def execute + lets_encrypt_client = Gitlab::LetsEncrypt::Client.new + order = lets_encrypt_client.new_order(pages_domain.domain) + + challenge = order.new_challenge + + private_key = OpenSSL::PKey::RSA.new(4096) + saved_order = pages_domain.acme_orders.create!( + url: order.url, + expires_at: order.expires, + private_key: private_key.to_pem, + + challenge_token: challenge.token, + challenge_file_content: challenge.file_content + ) + + challenge.request_validation + saved_order + end + end +end diff --git a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb new file mode 100644 index 00000000000..2dfe1a3d8ca --- /dev/null +++ b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module PagesDomains + class ObtainLetsEncryptCertificateService + attr_reader :pages_domain + + def initialize(pages_domain) + @pages_domain = pages_domain + end + + def execute + pages_domain.acme_orders.expired.delete_all + acme_order = pages_domain.acme_orders.first + + unless acme_order + ::PagesDomains::CreateAcmeOrderService.new(pages_domain).execute + return + end + + api_order = ::Gitlab::LetsEncrypt::Client.new.load_order(acme_order.url) + + # https://tools.ietf.org/html/rfc8555#section-7.1.6 - statuses diagram + case api_order.status + when 'ready' + api_order.request_certificate(private_key: acme_order.private_key, domain: pages_domain.domain) + when 'valid' + save_certificate(acme_order.private_key, api_order) + acme_order.destroy! + # when 'invalid' + # TODO: implement error handling + end + end + + private + + def save_certificate(private_key, api_order) + certificate = api_order.certificate + pages_domain.update!(key: private_key, certificate: certificate) + end + end +end diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml index a161fbd064e..c6781e91cfd 100644 --- a/app/views/abuse_reports/new.html.haml +++ b/app/views/abuse_reports/new.html.haml @@ -1,10 +1,10 @@ -- page_title _("Report abuse to GitLab") +- page_title _("Report abuse to admin") %h3.page-title - = _("Report abuse to GitLab") + = _("Report abuse to admin") %p - = _("Please use this form to report users to GitLab who create spam issues, comments or behave inappropriately.") + = _("Please use this form to report to the admin users who create spam issues, comments or behave inappropriately.") %p - = _("A member of GitLab's abuse team will review your report as soon as possible.") + = _("A member of the abuse team will review your report as soon as possible.") %hr = form_for @abuse_report, html: { class: 'js-quick-submit js-requires-input'} do |f| = form_errors(@abuse_report) diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 214630d245a..8212fb8bb33 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -34,7 +34,7 @@ = icon('spinner spin') .todos-filters - .row-content-block.second-block + .issues-details-filters.row-content-block.second-block = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form d-sm-flex' do .filter-categories.flex-fill .filter-item.inline diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml index 30b00ca86b3..0a5541c3e82 100644 --- a/app/views/discussions/_notes.html.haml +++ b/app/views/discussions/_notes.html.haml @@ -19,20 +19,24 @@ .discussion-reply-holder - if can_create_note? + %a.user-avatar-link.d-none.d-sm-block{ href: user_path(current_user) } + = image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40' - if discussion.potentially_resolvable? - line_type = local_assigns.fetch(:line_type, nil) - .btn-group.discussion-with-resolve-btn{ role: "group" } - .btn-group{ role: "group" } - = link_to_reply_discussion(discussion, line_type) + .discussion-with-resolve-btn + .btn-group.discussion-with-resolve-btn{ role: "group" } + .btn-group{ role: "group" } + = link_to_reply_discussion(discussion, line_type) - = render "discussions/resolve_all", discussion: discussion + = render "discussions/resolve_all", discussion: discussion - .btn-group.discussion-actions - = render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable - = render "discussions/jump_to_next", discussion: discussion + .btn-group.discussion-actions + = render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable + = render "discussions/jump_to_next", discussion: discussion - else - = link_to_reply_discussion(discussion) + .discussion-with-resolve-btn + = link_to_reply_discussion(discussion) - elsif !current_user .disabled-comment.text-center Please diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 77fe88dacb7..255a9ad038c 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -9,7 +9,7 @@ = render 'groups/home_panel' .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } } - .top-area.group-nav-container + .top-area.group-nav-container.justify-content-between .scrolling-tabs-container.inner-page-scroll-tabs .fade-left= icon('angle-left') .fade-right= icon('angle-right') diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 47710b9e9e5..54028dc8554 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -19,17 +19,17 @@ - if dashboard_nav_link?(:activity) = nav_link(path: 'dashboard#activity', html_options: { class: ["d-none d-xl-block", ("d-lg-block" unless has_extra_nav_icons?)] }) do - = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: _('Activity') do + = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity' do = _('Activity') - if dashboard_nav_link?(:milestones) = nav_link(controller: 'dashboard/milestones', html_options: { class: ["d-none d-xl-block", ("d-lg-block" unless has_extra_nav_icons?)] }) do - = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: _('Milestones') do + = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones' do = _('Milestones') - if dashboard_nav_link?(:snippets) = nav_link(controller: 'dashboard/snippets', html_options: { class: ["d-none d-xl-block", ("d-lg-block" unless has_extra_nav_icons?)] }) do - = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets qa-snippets-link', title: _('Snippets') do + = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets qa-snippets-link' do = _('Snippets') - if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets]) @@ -41,47 +41,47 @@ %ul - if dashboard_nav_link?(:activity) = nav_link(path: 'dashboard#activity') do - = link_to activity_dashboard_path, title: _('Activity') do + = link_to activity_dashboard_path do = _('Activity') - if dashboard_nav_link?(:milestones) = nav_link(controller: 'dashboard/milestones') do - = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: _('Milestones') do + = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones' do = _('Milestones') - if dashboard_nav_link?(:snippets) = nav_link(controller: 'dashboard/snippets') do - = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: _('Snippets') do + = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets' do = _('Snippets') - - = render_if_exists 'dashboard/operations/nav_link' + %li.dropdown.d-lg-none + = render_if_exists 'dashboard/operations/nav_link_list' - if can?(current_user, :read_instance_statistics) - = nav_link(controller: [:conversational_development_index, :cohorts]) do - = link_to instance_statistics_root_path, title: _('Instance Statistics'), aria: { label: _('Instance Statistics') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = nav_link(controller: [:conversational_development_index, :cohorts], html_options: { class: 'd-lg-none' }) do + = link_to instance_statistics_root_path do = _('Instance Statistics') - if current_user.admin? = nav_link(controller: 'admin/dashboard') do - = link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to admin_root_path, class: 'd-lg-none admin-icon qa-admin-area-link' do = _('Admin Area') - if Gitlab::Sherlock.enabled? %li - = link_to sherlock_transactions_path, class: 'admin-icon', title: _('Sherlock Transactions'), - data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to sherlock_transactions_path, class: 'd-lg-none admin-icon' do = _('Sherlock Transactions') -# Shortcut to Dashboard > Projects - if dashboard_nav_link?(:projects) %li.hidden - = link_to dashboard_projects_path, title: _('Projects'), class: 'dashboard-shortcuts-projects' do + = link_to dashboard_projects_path, class: 'dashboard-shortcuts-projects' do = _('Projects') - if current_controller?('ide') %li.line-separator.d-none.d-sm-block = nav_link(controller: 'ide') do - = link_to '#', class: 'dashboard-shortcuts-web-ide', title: _('Web IDE') do + = link_to '#', class: 'dashboard-shortcuts-web-ide' do = _('Web IDE') - = render_if_exists 'dashboard/operations/nav_link' + %li.dropdown{ class: 'd-none d-lg-block' } + = render_if_exists 'dashboard/operations/nav_link' - if can?(current_user, :read_instance_statistics) = nav_link(controller: [:conversational_development_index, :cohorts], html_options: { class: "d-none d-lg-block d-xl-block"}) do = link_to instance_statistics_root_path, title: _('Instance Statistics'), aria: { label: _('Instance Statistics') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 311b0be19ab..9587ea4696b 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,7 +1,7 @@ / Side-by-side diff view .text-file{ data: diff_view_data } - %table.diff-wrap-lines.code.js-syntax-highlight + %table.diff-wrap-lines.code.code-commit.js-syntax-highlight - diff_file.parallel_diff_lines.each do |line| - left = line[:left] - right = line[:right] diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 018c5b38536..641a0689c26 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -3,7 +3,7 @@ .suppressed-container %a.show-suppressed-diff.cursor-pointer.js-show-suppressed-diff= _("Changes suppressed. Click to show.") -%table.text-file.diff-wrap-lines.code.js-syntax-highlight.commit-diff{ data: diff_view_data, class: too_big ? 'hide' : '' } +%table.text-file.diff-wrap-lines.code.code-commit.js-syntax-highlight.commit-diff{ data: diff_view_data, class: too_big ? 'hide' : '' } = render partial: "projects/diffs/line", collection: diff_file.highlighted_diff_lines, as: :line, diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index fbd70cd1906..457b2936278 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -8,18 +8,18 @@ - create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid) - refs_path = refs_namespace_project_path(@project.namespace, @project, search: '') - .create-mr-dropdown-wrap.d-inline-block{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } } - .btn-group.unavailable + .create-mr-dropdown-wrap.d-inline-block.full-width-mobile{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } } + .btn-group.btn-group-sm.unavailable %button.btn.btn-grouped{ type: 'button', disabled: 'disabled' } = icon('spinner', class: 'fa-spin') %span.text Checking branch availability… - .btn-group.available.hidden + .btn-group.btn-group-sm.available.hidden %button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } } = value - %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' }, display: 'static' } } + %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle.flex-grow-0{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' }, display: 'static' } } = icon('caret-down') .droplab-dropdown diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml index 03226de120d..7bd5c437942 100644 --- a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml +++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml @@ -1,5 +1,5 @@ %inline-conflict-lines{ "inline-template" => "true", ":file" => "file" } - %table.diff-wrap-lines.code.js-syntax-highlight + %table.diff-wrap-lines.code.code-commit.js-syntax-highlight %tr.line_holder.diff-inline{ "v-for" => "line in file.inlineLines" } %td.diff-line-num.new_line{ ":class" => "lineCssClass(line)", "v-if" => "!line.isHeader" } %a {{line.new_line}} diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 05aeb5d972b..a201fafb949 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -31,29 +31,26 @@ .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } .merge-request-tabs-container - .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller - .fade-left= icon('angle-left') - .fade-right= icon('angle-right') - %ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs - %li.notes-tab.qa-notes-tab - = tab_link_for @merge_request, :show, force_link: @commit.present? do - = _("Discussion") - %span.badge.badge-pill= @merge_request.related_notes.user.count - - if @merge_request.source_project - %li.commits-tab - = tab_link_for @merge_request, :commits do - = _("Commits") - %span.badge.badge-pill= @commits_count - - if @pipelines.any? - %li.pipelines-tab - = tab_link_for @merge_request, :pipelines do - = _("Pipelines") - %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size - %li.diffs-tab.qa-diffs-tab - = tab_link_for @merge_request, :diffs do - = _("Changes") - %span.badge.badge-pill= @merge_request.diff_size - .d-inline-flex.flex-wrap + %ul.merge-request-tabs.nav-tabs.nav.nav-links + %li.notes-tab.qa-notes-tab + = tab_link_for @merge_request, :show, force_link: @commit.present? do + = _("Discussion") + %span.badge.badge-pill= @merge_request.related_notes.user.count + - if @merge_request.source_project + %li.commits-tab + = tab_link_for @merge_request, :commits do + = _("Commits") + %span.badge.badge-pill= @commits_count + - if @pipelines.any? + %li.pipelines-tab + = tab_link_for @merge_request, :pipelines do + = _("Pipelines") + %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size + %li.diffs-tab.qa-diffs-tab + = tab_link_for @merge_request, :diffs do + = _("Changes") + %span.badge.badge-pill= @merge_request.diff_size + .d-flex.flex-wrap.align-items-center.justify-content-lg-end #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@merge_request), notes_filters: UserPreference.notes_filters.to_json } } #js-vue-discussion-counter diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index 8de84f82e9f..8a6e5fde99b 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -11,7 +11,7 @@ - unless is_current_user %li = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do - = _('Report abuse to GitLab') + = _('Report abuse to admin') - if note_editable %li = link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 458096f9dd6..2e78b0bff3e 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -9,7 +9,7 @@ .nav-text.row-main-content = s_('TagsPage|Tags give the ability to mark specific points in history as being important') - .nav-controls.row-fixed-content + .nav-controls = form_tag(filter_tags_path, method: :get) do = search_field_tag :search, params[:search], { placeholder: s_('TagsPage|Filter by tag name'), id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false } diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 0be62bc5612..59232372150 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -18,7 +18,7 @@ - else = s_("TagsPage|Can't find HEAD commit for this tag") - .nav-controls.controls-flex + .nav-controls - if can?(current_user, :push_code, @project) = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn btn-edit controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do = icon("pencil") diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index 9ec76d82d18..e83ca5eaab8 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -2,9 +2,12 @@ - group = group_link.group - can_admin_member = can?(current_user, :admin_project_member, @project) - dom_id = "group_member_#{group_link.id}" -%li.member.group_member{ id: dom_id } - %span.list-item-name - = group_icon(group, class: "avatar s40", alt: '') + +-# Note this is just for groups. For individual members please see shared/members/_member + +%li.member.group_member.py-2.px-3.d-flex.flex-column.flex-md-row{ id: dom_id } + %span.list-item-name.mb-2.m-md-0 + = group_icon(group, class: "avatar s40 flex-shrink-0 flex-grow-0", alt: '') .user-info = link_to group.full_name, group_path(group), class: 'member' .cgray @@ -13,10 +16,10 @@ · %span{ class: ('text-warning' if group_link.expires_soon?) } = _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(group_link.expires_at) } - .controls.member-controls - = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group row append-right-5' do + .controls.member-controls.align-items-center + = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group d-sm-flex' do = hidden_field_tag "group_link[group_access]", group_link.group_access - .member-form-control.dropdown.append-right-5 + .member-form-control.dropdown.mr-sm-2.d-sm-inline-block %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", disabled: !can_admin_member, data: { toggle: "dropdown", field_name: "group_link[group_access]" } } @@ -32,14 +35,14 @@ = link_to role, "javascript:void(0)", class: ("is-active" if group_link.group_access == role_id), data: { id: role_id, el_id: dom_id } - .prepend-left-5.clearable-input.member-form-control + .clearable-input.member-form-control.d-sm-inline-block = text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: _('Expiration date'), id: "member_expires_at_#{group.id}", disabled: !can_admin_member %i.clear-icon.js-clear-input - if can_admin_member = link_to project_group_link_path(@project, group_link), method: :delete, data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name } }, - class: 'btn btn-remove prepend-left-10' do + class: 'btn btn-remove m-0 ml-sm-2 align-self-center' do %span.d-block.d-sm-none = _("Delete") = icon('trash', class: 'd-none d-sm-block') diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index afcb2b71472..331283f7eec 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -6,10 +6,12 @@ - source = member.source - override = member.try(:override) -%li.member{ class: [dom_class(member), ("is-overridden" if override)], id: dom_id(member) } - %span.list-item-name +-# Note this is just for individual members. For groups please see shared/members/_group + +%li.member.py-2.px-3.d-flex.flex-column{ class: [dom_class(member), ("is-overridden" if override), ("flex-md-row" unless force_mobile_view)], id: dom_id(member) } + %span.list-item-name.mb-2.m-md-0 - if user - = image_tag avatar_icon_for_user(user, 40), class: "avatar s40", alt: '' + = image_tag avatar_icon_for_user(user, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: '' .user-info = link_to user.name, user_path(user), class: 'member js-user-link', data: { user_id: user.id } = user_status(user) @@ -43,7 +45,7 @@ = _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(member.expires_at) } - else - = image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40", alt: '' + = image_tag avatar_icon_for_email(member.invite_email, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: '' .user-info .member= member.invite_email .cgray @@ -54,20 +56,20 @@ = time_ago_with_tooltip(member.created_at) - if show_roles - current_resource = @project || @group - .controls.member-controls + .controls.member-controls.align-items-center = render_if_exists 'shared/members/ee/ldap_tag', can_override: member.can_override? - if show_controls && member.source == current_resource - if member.can_resend_invite? = link_to icon('paper-plane'), polymorphic_path([:resend_invite, member]), method: :post, - class: 'btn btn-default prepend-left-10 d-none d-sm-block', + class: 'btn btn-default align-self-center mr-sm-2', title: _('Resend invite') - if user != current_user && member.can_update? - = form_for member, remote: true, html: { class: 'js-edit-member-form form-group row append-right-5' } do |f| + = form_for member, remote: true, html: { class: "js-edit-member-form form-group #{'d-sm-flex' unless force_mobile_view}" } do |f| = f.hidden_field :access_level - .member-form-control.dropdown.append-right-5 + .member-form-control.dropdown{ class: [("mr-sm-2 d-sm-inline-block" unless force_mobile_view)] } %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", disabled: member.can_override? && !override, data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } } @@ -87,7 +89,7 @@ group: @group, member: member, can_override: member.can_override? - .prepend-left-5.clearable-input.member-form-control + .clearable-input.member-form-control{ class: [("d-sm-inline-block" unless force_mobile_view)] } = f.text_field :expires_at, disabled: member.can_override? && !override, class: 'form-control js-access-expiration-date js-member-update-control', @@ -96,12 +98,12 @@ data: { el_id: dom_id(member) } %i.clear-icon.js-clear-input - else - %span.member-access-text= member.human_access + %span.member-access-text.user-access-role= member.human_access - if member.can_approve? = link_to polymorphic_path([:approve_access_request, member]), method: :post, - class: 'btn btn-success prepend-left-10', + class: "btn btn-success align-self-center m-0 mb-2 #{'mb-sm-0 ml-sm-2' unless force_mobile_view}", title: _('Grant access') do %span{ class: ('d-block d-sm-none' unless force_mobile_view) } = _('Grant access') @@ -113,12 +115,12 @@ = link_to icon('sign-out', text: _('Leave')), polymorphic_path([:leave, member.source, :members]), method: :delete, data: { confirm: leave_confirmation_message(member.source) }, - class: 'btn btn-remove prepend-left-10' + class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}" - else = link_to member, method: :delete, data: { confirm: remove_member_message(member) }, - class: 'btn btn-remove prepend-left-10', + class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}", title: remove_member_title(member) do %span{ class: ('d-block d-sm-none' unless force_mobile_view) } = _("Delete") @@ -126,6 +128,6 @@ = icon('trash', class: 'd-none d-sm-block') = render_if_exists 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :edit, can_override: member.can_override? - else - %span.member-access-text= member.human_access + %span.member-access-text.user-access-role= member.human_access = render_if_exists 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :confirm, can_override: member.can_override? diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index 6fec435cc87..5c9dd72418e 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -31,7 +31,7 @@ .note-header .note-header-info %a{ href: user_path(note.author) } - %span.note-header-author-name + %span.note-header-author-name.bold = sanitize(note.author.name) = user_status(note.author) %span.note-headline-light diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 6dc61088e65..a71bfd624e4 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -9,7 +9,7 @@ = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") .user-profile - .cover-block.user-cover-block.top-area + .cover-block.user-cover-block .cover-controls - if @user == current_user = link_to profile_path, class: 'btn btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do |