summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorAchilleas Pipinellis <axil@gitlab.com>2019-08-21 11:24:55 +0200
committerAchilleas Pipinellis <axil@gitlab.com>2019-08-21 11:24:55 +0200
commita39228db8027c966e8117d40766b0cef4fbd50f6 (patch)
tree143045af7214ae6746e7b7c9df647c71ec2bd675 /app
parent05f50c9b52fde54513fe55fef97499b35719eae2 (diff)
parentaed489bf901745ced6618e680913d0d213998923 (diff)
downloadgitlab-ce-docs-zm002-gitlab-case.tar.gz
Merge branch 'master' into docs-zm002-gitlab-casedocs-zm002-gitlab-case
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue41
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue88
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue12
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue2
-rw-r--r--app/assets/javascripts/ide/components/file_row_extra.vue1
-rw-r--r--app/assets/javascripts/ide/stores/utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js8
-rw-r--r--app/assets/javascripts/members.js2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue41
-rw-r--r--app/assets/javascripts/monitoring/components/embed.vue28
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue15
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js3
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js4
-rw-r--r--app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue4
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue15
-rw-r--r--app/assets/javascripts/pages/projects/wikis/wikis.js42
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue7
-rw-r--r--app/assets/javascripts/vue_shared/directives/autofocusonshow.js39
-rw-r--r--app/assets/stylesheets/errors.scss2
-rw-r--r--app/assets/stylesheets/framework/badges.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/flash.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss2
-rw-r--r--app/assets/stylesheets/highlight/common.scss13
-rw-r--r--app/assets/stylesheets/highlight/themes/dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/themes/monokai.scss4
-rw-r--r--app/assets/stylesheets/highlight/themes/none.scss5
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-light.scss4
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss18
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss50
-rw-r--r--app/assets/stylesheets/pages/diff.scss3
-rw-r--r--app/assets/stylesheets/pages/issuable.scss1
-rw-r--r--app/assets/stylesheets/pages/reports.scss5
-rw-r--r--app/controllers/application_controller.rb1
-rw-r--r--app/controllers/concerns/confirm_email_warning.rb25
-rw-r--r--app/controllers/concerns/invisible_captcha.rb4
-rw-r--r--app/controllers/confirmations_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb6
-rw-r--r--app/controllers/projects/starrers_controller.rb22
-rw-r--r--app/controllers/projects/wikis_controller.rb31
-rw-r--r--app/controllers/registrations_controller.rb12
-rw-r--r--app/helpers/groups_helper.rb5
-rw-r--r--app/helpers/notifications_helper.rb14
-rw-r--r--app/helpers/projects_helper.rb10
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/clusters/clusters_hierarchy.rb4
-rw-r--r--app/models/members/group_member.rb2
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/repository.rb20
-rw-r--r--app/models/user.rb7
-rw-r--r--app/models/users_star_project.rb1
-rw-r--r--app/presenters/blobs/unfold_presenter.rb43
-rw-r--r--app/serializers/deployment_entity.rb1
-rw-r--r--app/serializers/deployment_serializer.rb2
-rw-r--r--app/serializers/issuable_sidebar_basic_entity.rb4
-rw-r--r--app/services/ci/process_pipeline_service.rb4
-rw-r--r--app/services/git/base_hooks_service.rb31
-rw-r--r--app/services/git/branch_hooks_service.rb25
-rw-r--r--app/services/system_note_service.rb4
-rw-r--r--app/views/admin/applications/_delete_form.html.haml2
-rw-r--r--app/views/doorkeeper/authorized_applications/_delete_form.html.haml2
-rw-r--r--app/views/errors/access_denied.html.haml4
-rw-r--r--app/views/groups/_home_panel.html.haml3
-rw-r--r--app/views/groups/settings/_permissions.html.haml11
-rw-r--r--app/views/layouts/errors.html.haml6
-rw-r--r--app/views/layouts/header/_default.html.haml7
-rw-r--r--app/views/profiles/notifications/_group_settings.html.haml9
-rw-r--r--app/views/profiles/notifications/_project_settings.html.haml9
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml4
-rw-r--r--app/views/projects/_home_panel.html.haml4
-rw-r--r--app/views/projects/commit/_ajax_signature.html.haml2
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml29
-rw-r--r--app/views/projects/pages_domains/_certificate.html.haml18
-rw-r--r--app/views/projects/pages_domains/show.html.haml7
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml18
-rw-r--r--app/views/projects/wikis/_main_links.html.haml6
-rw-r--r--app/views/projects/wikis/_new.html.haml18
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml11
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml6
-rw-r--r--app/views/shared/members/_group.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/shared/notifications/_button.html.haml15
-rw-r--r--app/views/shared/notifications/_new_button.html.haml13
-rw-r--r--app/workers/post_receive.rb30
-rw-r--r--app/workers/process_commit_worker.rb17
-rw-r--r--app/workers/project_cache_worker.rb6
97 files changed, 726 insertions, 337 deletions
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue
new file mode 100644
index 00000000000..d946594a069
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue
@@ -0,0 +1,41 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import { GlButton } from '@gitlab/ui';
+
+export default {
+ name: 'StageCardListItem',
+ components: {
+ Icon,
+ GlButton,
+ },
+ props: {
+ isActive: {
+ type: Boolean,
+ required: true,
+ },
+ canEdit: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <div :class="{ active: isActive }" class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded">
+ <slot></slot>
+ <div v-if="canEdit" class="dropdown">
+ <gl-button
+ :title="__('More actions')"
+ class="more-actions-toggle btn btn-transparent p-0"
+ data-toggle="dropdown"
+ >
+ <icon css-classes="icon" name="ellipsis_v" />
+ </gl-button>
+ <ul class="more-actions-dropdown dropdown-menu dropdown-open-left">
+ <slot name="dropdown-options"></slot>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue
new file mode 100644
index 00000000000..004d335f572
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue
@@ -0,0 +1,88 @@
+<script>
+import StageCardListItem from './stage_card_list_item.vue';
+
+export default {
+ name: 'StageNavItem',
+ components: {
+ StageCardListItem,
+ },
+ props: {
+ isDefaultStage: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ isActive: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ isUserAllowed: {
+ type: Boolean,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ canEdit: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ hasValue() {
+ return this.value && this.value.length > 0;
+ },
+ editable() {
+ return this.isUserAllowed && this.canEdit;
+ },
+ },
+};
+</script>
+
+<template>
+ <li @click="$emit('select')">
+ <stage-card-list-item :is-active="isActive" :can-edit="editable">
+ <div class="stage-nav-item-cell stage-name p-0" :class="{ 'font-weight-bold': isActive }">
+ {{ title }}
+ </div>
+ <div class="stage-nav-item-cell stage-median mr-4">
+ <template v-if="isUserAllowed">
+ <span v-if="hasValue">{{ value }}</span>
+ <span v-else class="stage-empty">{{ __('Not enough data') }}</span>
+ </template>
+ <template v-else>
+ <span class="not-available">{{ __('Not available') }}</span>
+ </template>
+ </div>
+ <template v-slot:dropdown-options>
+ <template v-if="isDefaultStage">
+ <li>
+ <button type="button" class="btn-default btn-transparent">
+ {{ __('Hide stage') }}
+ </button>
+ </li>
+ </template>
+ <template v-else>
+ <li>
+ <button type="button" class="btn-default btn-transparent">
+ {{ __('Edit stage') }}
+ </button>
+ </li>
+ <li>
+ <button type="button" class="btn-danger danger">
+ {{ __('Remove stage') }}
+ </button>
+ </li>
+ </template>
+ </template>
+ </stage-card-list-item>
+ </li>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 671405602cc..b3ae47af750 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -12,6 +12,7 @@ import stageComponent from './components/stage_component.vue';
import stageReviewComponent from './components/stage_review_component.vue';
import stageStagingComponent from './components/stage_staging_component.vue';
import stageTestComponent from './components/stage_test_component.vue';
+import stageNavItem from './components/stage_nav_item.vue';
import CycleAnalyticsService from './cycle_analytics_service';
import CycleAnalyticsStore from './cycle_analytics_store';
@@ -41,6 +42,7 @@ export default () => {
import('ee_component/analytics/shared/components/projects_dropdown_filter.vue'),
DateRangeDropdown: () =>
import('ee_component/analytics/shared/components/date_range_dropdown.vue'),
+ 'stage-nav-item': stageNavItem,
},
mixins: [filterMixins],
data() {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 81da0754752..19b85710710 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -305,7 +305,7 @@ export default {
<div
v-show="showTreeList"
:style="{ width: `${treeWidth}px` }"
- class="diff-tree-list js-diff-tree-list"
+ class="diff-tree-list js-diff-tree-list mr-3"
>
<panel-resizer
:size.sync="treeWidth"
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 925385fa98a..839ab542377 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -212,19 +212,18 @@ export default {
</script>
<template>
- <td :colspan="colspan">
+ <td :colspan="colspan" class="text-center">
<div class="content js-line-expansion-content">
<a
v-if="canExpandUp"
v-tooltip
- class="cursor-pointer js-unfold unfold-icon"
+ class="cursor-pointer js-unfold unfold-icon d-inline-block pt-2 pb-2"
data-placement="top"
data-container="body"
:title="__('Expand up')"
@click="handleExpandLines(EXPAND_UP)"
>
- <!-- TODO: remove style & replace with correct icon, waiting for MR https://gitlab.com/gitlab-org/gitlab-design/issues/499 -->
- <icon :size="12" name="expand-left" aria-hidden="true" style="transform: rotate(270deg);" />
+ <icon :size="12" name="expand-up" aria-hidden="true" />
</a>
<a class="mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()">
<span>{{ s__('Diffs|Show all lines') }}</span>
@@ -232,14 +231,13 @@ export default {
<a
v-if="canExpandDown"
v-tooltip
- class="cursor-pointer js-unfold-down has-tooltip unfold-icon"
+ class="cursor-pointer js-unfold-down has-tooltip unfold-icon d-inline-block pt-2 pb-2"
data-placement="top"
data-container="body"
:title="__('Expand down')"
@click="handleExpandLines(EXPAND_DOWN)"
>
- <!-- TODO: remove style & replace with correct icon, waiting for MR https://gitlab.com/gitlab-org/gitlab-design/issues/499 -->
- <icon :size="12" name="expand-left" aria-hidden="true" style="transform: rotate(90deg);" />
+ <icon :size="12" name="expand-down" aria-hidden="true" />
</a>
</div>
</td>
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 32fbeaaa905..69ec6ab8600 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -130,7 +130,7 @@ export default {
return `\`${this.diffFile.file_path}\``;
},
isFileRenamed() {
- return this.diffFile.viewer.name === diffViewerModes.renamed;
+ return this.diffFile.renamed_file;
},
isModeChanged() {
return this.diffFile.viewer.name === diffViewerModes.mode_changed;
diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue
index 80a6ab9598a..7254c50a568 100644
--- a/app/assets/javascripts/ide/components/file_row_extra.vue
+++ b/app/assets/javascripts/ide/components/file_row_extra.vue
@@ -87,7 +87,6 @@ export default {
:file="file"
:show-tooltip="true"
:show-staged-icon="true"
- :force-modified-icon="true"
/>
<new-dropdown
:type="file.type"
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 04e86afb268..52200ce7847 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -129,7 +129,7 @@ export const commitActionForFile = file => {
export const getCommitFiles = stagedFiles =>
stagedFiles.reduce((acc, file) => {
- if (file.moved) return acc;
+ if (file.moved || file.type === 'tree') return acc;
return acc.concat({
...file,
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 1336b6a5461..7ead9d46fbb 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,5 +1,11 @@
import { join as joinPaths } from 'path';
+// Returns a decoded url parameter value
+// - Treats '+' as '%20'
+function decodeUrlParameter(val) {
+ return decodeURIComponent(val.replace(/\+/g, '%20'));
+}
+
// Returns an array containing the value(s) of the
// of the key passed as an argument
export function getParameterValues(sParam, url = window.location) {
@@ -30,7 +36,7 @@ export function mergeUrlParams(params, url) {
.forEach(part => {
if (part.length) {
const kv = part.split('=');
- merged[decodeURIComponent(kv[0])] = decodeURIComponent(kv.slice(1).join('='));
+ merged[decodeUrlParameter(kv[0])] = decodeUrlParameter(kv.slice(1).join('='));
}
});
}
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index af2697444f2..d719fd8748d 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -17,6 +17,8 @@ export default class Members {
}
dropdownClicked(options) {
+ options.e.preventDefault();
+
this.formSubmit(null, options.$el);
}
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index 838447e6c75..90c764587a3 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -45,6 +45,11 @@ export default {
required: false,
default: () => false,
},
+ singleEmbed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
thresholds: {
type: Array,
required: false,
@@ -240,7 +245,10 @@ export default {
</script>
<template>
- <div class="prometheus-graph col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']">
+ <div
+ class="prometheus-graph col-12"
+ :class="[showBorder ? 'p-2' : 'p-0', { 'col-lg-6': !singleEmbed }]"
+ >
<div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 587392adbc3..ebd610af7b6 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -10,13 +10,13 @@ import {
} from '@gitlab/ui';
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
-import { getParameterValues } from '~/lib/utils/url_utility';
+import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
+import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import MonitorAreaChart from './charts/area.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
-import PanelType from './panel_type.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import { sidebarAnimationDuration, timeWindows } from '../constants';
@@ -141,6 +141,16 @@ export default {
required: false,
default: false,
},
+ alertsEndpoint: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ prometheusAlertsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -168,8 +178,11 @@ export default {
'multipleDashboardsEnabled',
'additionalPanelTypesEnabled',
]),
+ firstDashboard() {
+ return this.allDashboards[0] || {};
+ },
selectedDashboardText() {
- return this.currentDashboard || (this.allDashboards[0] && this.allDashboards[0].display_name);
+ return this.currentDashboard || this.firstDashboard.display_name;
},
addingMetricsAvailable() {
return IS_EE && this.canAddMetrics && !this.showEmptyState;
@@ -258,7 +271,15 @@ export default {
getGraphAlertValues(queries) {
return Object.values(this.getGraphAlerts(queries));
},
+ showToast() {
+ this.$toast.show(__('Link copied to clipboard'));
+ },
// TODO: END
+ generateLink(group, title, yLabel) {
+ const dashboard = this.currentDashboard || this.firstDashboard.path;
+ const params = _.pick({ dashboard, group, title, y_label: yLabel }, value => value != null);
+ return mergeUrlParams(params, window.location.href);
+ },
hideAddMetricModal() {
this.$refs.addMetricModal.hide();
},
@@ -435,8 +456,11 @@ export default {
<panel-type
v-for="(graphData, graphIndex) in groupData.metrics"
:key="`panel-type-${graphIndex}`"
+ :clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
:graph-data="graphData"
:dashboard-width="elWidth"
+ :alerts-endpoint="alertsEndpoint"
+ :prometheus-alerts-available="prometheusAlertsAvailable"
:index="`${index}-${graphIndex}`"
/>
</template>
@@ -475,6 +499,15 @@ export default {
{{ __('Download CSV') }}
</gl-dropdown-item>
<gl-dropdown-item
+ class="js-chart-link"
+ :data-clipboard-text="
+ generateLink(groupData.group, graphData.title, graphData.y_label)
+ "
+ @click="showToast"
+ >
+ {{ __('Generate link to chart') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
v-if="alertWidgetAvailable"
v-gl-modal="`alert-modal-${index}-${graphIndex}`"
>
diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue
index 9e85b0633fe..e3256147618 100644
--- a/app/assets/javascripts/monitoring/components/embed.vue
+++ b/app/assets/javascripts/monitoring/components/embed.vue
@@ -36,12 +36,15 @@ export default {
},
computed: {
...mapState('monitoringDashboard', ['groups', 'metricsWithData']),
- groupData() {
- const groupsWithData = this.groups.filter(group => this.chartsWithData(group.metrics).length);
- if (groupsWithData.length) {
- return groupsWithData[0];
- }
- return null;
+ charts() {
+ const groupWithMetrics = this.groups.find(group =>
+ group.metrics.find(chart => this.chartHasData(chart)),
+ ) || { metrics: [] };
+
+ return groupWithMetrics.metrics.filter(chart => this.chartHasData(chart));
+ },
+ isSingleChart() {
+ return this.charts.length === 1;
},
},
mounted() {
@@ -66,10 +69,8 @@ export default {
'setFeatureFlags',
'setShowErrorBanner',
]),
- chartsWithData(charts) {
- return charts.filter(chart =>
- chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
- );
+ chartHasData(chart) {
+ return chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id));
},
onSidebarMutation() {
setTimeout(() => {
@@ -89,16 +90,17 @@ export default {
};
</script>
<template>
- <div class="metrics-embed">
- <div v-if="groupData" class="row w-100 m-n2 pb-4">
+ <div class="metrics-embed" :class="{ 'd-inline-flex col-lg-6 p-0': isSingleChart }">
+ <div v-if="charts.length" class="row w-100 m-n2 pb-4">
<monitor-area-chart
- v-for="graphData in chartsWithData(groupData.metrics)"
+ v-for="graphData in charts"
:key="graphData.title"
:graph-data="graphData"
:container-width="elWidth"
group-id="monitor-area-chart"
:project-path="null"
:show-border="true"
+ :single-embed="isSingleChart"
/>
</div>
</div>
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index 3fbac71f3d7..96f62bc85ee 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -1,6 +1,7 @@
<script>
import { mapState } from 'vuex';
import _ from 'underscore';
+import { __ } from '~/locale';
import {
GlDropdown,
GlDropdownItem,
@@ -28,6 +29,10 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
+ clipboardText: {
+ type: String,
+ required: true,
+ },
graphData: {
type: Object,
required: true,
@@ -76,6 +81,9 @@ export default {
isPanelType(type) {
return this.graphData.type && this.graphData.type === type;
},
+ showToast() {
+ this.$toast.show(__('Link copied to clipboard'));
+ },
},
};
</script>
@@ -116,6 +124,13 @@ export default {
<gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv">
{{ __('Download CSV') }}
</gl-dropdown-item>
+ <gl-dropdown-item
+ class="js-chart-link"
+ :data-clipboard-text="clipboardText"
+ @click="showToast"
+ >
+ {{ __('Generate link to chart') }}
+ </gl-dropdown-item>
<gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}`">
{{ __('Alerts') }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index c0fee1ebb99..51cef20455c 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -1,9 +1,12 @@
import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue';
import store from './stores';
+Vue.use(GlToast);
+
export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index 8caac68e0d4..622db360d1f 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -59,6 +59,10 @@ export default () => {
render(createElement) {
const isDiffView = this.activeTab === 'diffs';
+ // NOTE: Even though `discussionKeyboardNavigator` is added to the `notes-app`,
+ // it adds a global key listener so it works on the diffs tab as well.
+ // If we create a single Vue app for all of the MR tabs, we should move this
+ // up the tree, to the root.
return createElement(discussionKeyboardNavigator, { props: { isDiffView } }, [
createElement('notes-app', {
props: {
diff --git a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
index 5fc2b6ba04c..7fbfe8eebb2 100644
--- a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
+++ b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
@@ -25,6 +25,10 @@ export default {
Mousetrap.bind('n', () => this.jumpToNextDiscussion());
Mousetrap.bind('p', () => this.jumpToPreviousDiscussion());
},
+ beforeDestroy() {
+ Mousetrap.unbind('n');
+ Mousetrap.unbind('p');
+ },
methods: {
...mapActions(['expandDiscussion']),
jumpToNextDiscussion() {
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 627d37bac68..a223a8f5b08 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -28,6 +28,11 @@ export default {
type: Object,
required: true,
},
+ canDisableEmails: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
canChangeVisibilityLevel: {
type: Boolean,
required: false,
@@ -104,6 +109,7 @@ export default {
lfsEnabled: true,
requestAccessEnabled: true,
highlightChangesClass: false,
+ emailsDisabled: false,
};
return { ...defaults, ...this.currentSettings };
@@ -341,5 +347,14 @@ export default {
/>
</project-setting-row>
</div>
+ <project-setting-row v-if="canDisableEmails" class="mb-3">
+ <label class="js-emails-disabled">
+ <input :value="emailsDisabled" type="hidden" name="project[emails_disabled]" />
+ <input v-model="emailsDisabled" type="checkbox" /> {{ __('Disable email notifications') }}
+ </label>
+ <span class="form-text text-muted">{{
+ __('This setting will override user notification preferences for all project members.')
+ }}</span>
+ </project-setting-row>
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js
index 9b58d42b47d..d41199f6374 100644
--- a/app/assets/javascripts/pages/projects/wikis/wikis.js
+++ b/app/assets/javascripts/pages/projects/wikis/wikis.js
@@ -1,6 +1,5 @@
import bp from '../../../breakpoints';
-import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils';
-import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility';
+import { s__, sprintf } from '~/locale';
export default class Wikis {
constructor() {
@@ -12,32 +11,37 @@ export default class Wikis {
sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e));
}
- this.newWikiForm = document.querySelector('form.new-wiki-page');
- if (this.newWikiForm) {
- this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
+ this.isNewWikiPage = Boolean(document.querySelector('.js-new-wiki-page'));
+ this.editTitleInput = document.querySelector('form.wiki-form #wiki_title');
+ this.commitMessageInput = document.querySelector('form.wiki-form #wiki_message');
+ this.commitMessageI18n = this.isNewWikiPage
+ ? s__('WikiPageCreate|Create %{pageTitle}')
+ : s__('WikiPageEdit|Update %{pageTitle}');
+
+ if (this.editTitleInput) {
+ // Initialize the commit message on load
+ if (this.editTitleInput.value) this.setWikiCommitMessage(this.editTitleInput.value);
+
+ // Set the commit message as the page title is changed
+ this.editTitleInput.addEventListener('keyup', e => this.handleWikiTitleChange(e));
}
window.addEventListener('resize', () => this.renderSidebar());
this.renderSidebar();
}
- handleNewWikiSubmit(e) {
- if (!this.newWikiForm) return;
-
- const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
-
- const slug = slugInput.value;
+ handleWikiTitleChange(e) {
+ this.setWikiCommitMessage(e.target.value);
+ }
- if (slug.length > 0) {
- const wikisPath = slugInput.getAttribute('data-wikis-path');
+ setWikiCommitMessage(rawTitle) {
+ let title = rawTitle;
- // If the wiki is empty, we need to merge the current URL params to keep the "create" view.
- const params = parseQueryStringIntoObject(window.location.search.substr(1));
- const url = mergeUrlParams(params, `${wikisPath}/${slug}`);
- redirectTo(url);
+ // Replace hyphens with spaces
+ if (title) title = title.replace(/-+/g, ' ');
- e.preventDefault();
- }
+ const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: title });
+ this.commitMessageInput.value = newCommitMessage;
}
handleToggleSidebar(e) {
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index e01080b04d6..4efc1d2408a 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -104,7 +104,7 @@ export default {
v-gl-tooltip
:title="
__(
- 'The code of a detached pipeline is tested against the source branch instead of merged results',
+ 'Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more on the documentation for Pipelines for Merged Results.',
)
"
class="js-pipeline-url-detached badge badge-info"
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index cdf2d1020ba..beb2ac09992 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -26,11 +26,6 @@ export default {
required: false,
default: false,
},
- forceModifiedIcon: {
- type: Boolean,
- required: false,
- default: false,
- },
size: {
type: Number,
required: false,
@@ -48,8 +43,6 @@ export default {
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : '';
- if (this.forceModifiedIcon) return `file-modified${suffix}`;
-
return `${getCommitIconMap(this.file).icon}${suffix}`;
},
changedIconClass() {
@@ -88,7 +81,7 @@ export default {
v-gl-tooltip.right
:title="tooltipTitle"
:class="{ 'ml-auto': isCentered }"
- class="file-changed-icon"
+ class="file-changed-icon d-inline-block"
>
<icon v-if="showIcon" :name="changedIcon" :size="size" :css-classes="changedIconClass" />
</span>
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index fe5289ff371..f49e69c473b 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -146,6 +146,7 @@ export default {
<span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated">
<file-icon
v-if="!showChangedIcon || file.type === 'tree'"
+ class="file-row-icon"
:file-name="file.name"
:loading="file.loading"
:folder="isTree"
@@ -223,13 +224,8 @@ export default {
white-space: nowrap;
}
-.file-row-name svg {
+.file-row-name .file-row-icon {
margin-right: 2px;
vertical-align: middle;
}
-
-.file-row-name .loading-container {
- display: inline-block;
- margin-right: 4px;
-}
</style>
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index df19906309c..f0aae20477b 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -30,9 +30,16 @@ export default {
},
mounted() {
+ if (window.recaptchaDialogCallback) {
+ throw new Error('recaptchaDialogCallback is already defined!');
+ }
window.recaptchaDialogCallback = this.submit.bind(this);
},
+ beforeDestroy() {
+ window.recaptchaDialogCallback = null;
+ },
+
methods: {
appendRecaptchaScript() {
this.removeRecaptchaScript();
diff --git a/app/assets/javascripts/vue_shared/directives/autofocusonshow.js b/app/assets/javascripts/vue_shared/directives/autofocusonshow.js
new file mode 100644
index 00000000000..4659ec20ceb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/directives/autofocusonshow.js
@@ -0,0 +1,39 @@
+/**
+ * Input/Textarea Autofocus Directive for Vue
+ */
+export default {
+ /**
+ * Set focus when element is rendered, but
+ * is not visible, using IntersectionObserver
+ *
+ * @param {Element} el Target element
+ */
+ inserted(el) {
+ if ('IntersectionObserver' in window) {
+ // Element visibility is dynamic, so we attach observer
+ el.visibilityObserver = new IntersectionObserver(entries => {
+ entries.forEach(entry => {
+ // Combining `intersectionRatio > 0` and
+ // element's `offsetParent` presence will
+ // deteremine if element is truely visible
+ if (entry.intersectionRatio > 0 && entry.target.offsetParent) {
+ entry.target.focus();
+ }
+ });
+ });
+
+ // Bind the observer.
+ el.visibilityObserver.observe(el, { root: document.documentElement });
+ }
+ },
+ /**
+ * Detach observer on unbind hook.
+ *
+ * @param {Element} el Target element
+ */
+ unbind(el) {
+ if (el.visibilityObserver) {
+ el.visibilityObserver.disconnect();
+ }
+ },
+};
diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss
index d287215096e..89029a58d1e 100644
--- a/app/assets/stylesheets/errors.scss
+++ b/app/assets/stylesheets/errors.scss
@@ -96,7 +96,7 @@ a {
}
.error-nav {
- padding: 0;
+ padding: $gl-padding 0 0;
text-align: center;
li {
diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss
index c6060161dec..c036267a7c8 100644
--- a/app/assets/stylesheets/framework/badges.scss
+++ b/app/assets/stylesheets/framework/badges.scss
@@ -1,6 +1,6 @@
.badge.badge-pill {
font-weight: $gl-font-weight-normal;
background-color: $badge-bg;
- color: $gl-text-color-secondary;
+ color: $gray-800;
vertical-align: baseline;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index f384a49e0ae..e9218dcec67 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -438,6 +438,7 @@ img.emoji {
.w-3rem { width: 3rem; }
.h-12em { height: 12em; }
+.h-32-px { height: 32px;}
.mw-460 { max-width: 460px; }
.mw-6em { max-width: 6em; }
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index e3dd127366d..96f6d02a68f 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -43,6 +43,7 @@
@extend .alert;
background-color: $orange-100;
color: $orange-900;
+ cursor: default;
margin: 0;
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 1bc597bd4ae..ca737c53318 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -131,7 +131,6 @@
> li:not(.d-none) a {
@include media-breakpoint-down(xs) {
margin-left: 0;
- min-width: 100%;
}
}
}
@@ -233,7 +232,6 @@
.impersonation-btn,
.impersonation-btn:hover {
background-color: $white-light;
- margin-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss
index ac3214a07d9..bdeac7e97c0 100644
--- a/app/assets/stylesheets/highlight/common.scss
+++ b/app/assets/stylesheets/highlight/common.scss
@@ -16,3 +16,16 @@
color: $dark-diff-match-bg;
background: $dark-diff-match-color;
}
+
+@mixin diff-expansion($background, $border, $link) {
+ background-color: $background;
+
+ td {
+ border-top: 1px solid $border;
+ border-bottom: 1px solid $border;
+ }
+
+ a {
+ color: $link;
+ }
+}
diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss
index 16893dd047e..cbce0ba3f1e 100644
--- a/app/assets/stylesheets/highlight/themes/dark.scss
+++ b/app/assets/stylesheets/highlight/themes/dark.scss
@@ -111,6 +111,10 @@ $dark-il: #de935f;
color: $dark-line-color;
}
+ .line_expansion {
+ @include diff-expansion($dark-main-bg, $dark-border, $dark-na);
+ }
+
// Diff line
.line_holder {
&.match .line_content,
diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss
index 37fe61b925c..1b61ffa37e3 100644
--- a/app/assets/stylesheets/highlight/themes/monokai.scss
+++ b/app/assets/stylesheets/highlight/themes/monokai.scss
@@ -111,6 +111,10 @@ $monokai-gi: #a6e22e;
color: $monokai-text-color;
}
+ .line_expansion {
+ @include diff-expansion($monokai-bg, $monokai-border, $monokai-k);
+ }
+
// Diff line
.line_holder {
&.match .line_content,
diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss
index b4217aac37a..a7ede266fb5 100644
--- a/app/assets/stylesheets/highlight/themes/none.scss
+++ b/app/assets/stylesheets/highlight/themes/none.scss
@@ -34,8 +34,11 @@
color: $gl-text-color;
}
-// Diff line
+ .line_expansion {
+ @include diff-expansion($gray-light, $white-normal, $gl-text-color);
+ }
+ // Diff line
$none-over-bg: #ded7fc;
$none-expanded-border: #e0e0e0;
$none-expanded-bg: #e0e0e0;
diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
index a4e9eda22c9..6569f3abc8b 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
@@ -115,6 +115,10 @@ $solarized-dark-il: #2aa198;
color: $solarized-dark-pre-color;
}
+ .line_expansion {
+ @include diff-expansion($solarized-dark-line-bg, $solarized-dark-border, $solarized-dark-kd);
+ }
+
// Diff line
.line_holder {
&.match .line_content,
diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss
index b604d1ccb6c..4e74a9ea50a 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-light.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss
@@ -122,6 +122,10 @@ $solarized-light-il: #2aa198;
color: $solarized-light-pre-color;
}
+ .line_expansion {
+ @include diff-expansion($solarized-light-line-bg, $solarized-light-border, $solarized-light-kd);
+ }
+
// Diff line
.line_holder {
&.match .line_content,
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
index b3974df8639..973f94c63aa 100644
--- a/app/assets/stylesheets/highlight/white_base.scss
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -101,24 +101,8 @@ pre.code,
color: $white-code-color;
}
-// Expansion line
.line_expansion {
- background-color: $gray-light;
-
- td {
- border-top: 1px solid $border-color;
- border-bottom: 1px solid $border-color;
- text-align: center;
- }
-
- a {
- color: $blue-600;
- }
-
- .unfold-icon {
- display: inline-block;
- padding: 8px 0;
- }
+ @include diff-expansion($gray-light, $border-color, $blue-600);
}
// Diff line
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 2b932d164a5..d80155a416d 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -51,27 +51,19 @@
}
.stage-header {
- width: 26%;
- padding-left: $gl-padding;
+ width: 18.5%;
}
.median-header {
- width: 14%;
+ width: 21.5%;
}
.event-header {
width: 45%;
- padding-left: $gl-padding;
}
.total-time-header {
width: 15%;
- text-align: right;
- padding-right: $gl-padding;
- }
-
- .stage-name {
- font-weight: $gl-font-weight-bold;
}
}
@@ -153,23 +145,13 @@
}
.stage-nav-item {
- display: flex;
line-height: 65px;
- border-top: 1px solid transparent;
- border-bottom: 1px solid transparent;
- border-right: 1px solid $border-color;
- background-color: $gray-light;
+ border: 1px solid $border-color;
&.active {
- background-color: transparent;
- border-right-color: transparent;
- border-top-color: $border-color;
- border-bottom-color: $border-color;
- box-shadow: inset 2px 0 0 0 $blue-500;
-
- .stage-name {
- font-weight: $gl-font-weight-bold;
- }
+ background: $blue-50;
+ border-color: $blue-300;
+ box-shadow: inset 4px 0 0 0 $blue-500;
}
&:hover:not(.active) {
@@ -178,24 +160,12 @@
cursor: pointer;
}
- &:first-child {
- border-top: 0;
- }
-
- &:last-child {
- border-bottom: 0;
- }
-
- .stage-nav-item-cell {
- &.stage-median {
- margin-left: auto;
- margin-right: $gl-padding;
- min-width: calc(35% - #{$gl-padding});
- }
+ .stage-nav-item-cell.stage-name {
+ width: 44.5%;
}
- .stage-name {
- padding-left: 16px;
+ .stage-nav-item-cell.stage-median {
+ min-width: 43%;
}
.stage-empty,
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index ffb27e54f34..77a2fd6b876 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1032,7 +1032,6 @@ table.code {
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
max-height: calc(100vh - #{$top-pos});
- padding-right: $gl-padding;
z-index: 202;
.with-performance-bar & {
@@ -1043,7 +1042,7 @@ table.code {
.drag-handle {
bottom: 16px;
- transform: translateX(-6px);
+ transform: translateX(10px);
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 66b4f3bad2b..fa52ce6402d 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -522,6 +522,7 @@
}
.multiple-users {
+ position: relative;
height: 24px;
margin-bottom: 17px;
margin-top: 4px;
diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss
index 85e9f303dde..0fbf7033aa5 100644
--- a/app/assets/stylesheets/pages/reports.scss
+++ b/app/assets/stylesheets/pages/reports.scss
@@ -48,11 +48,6 @@
padding: $gl-padding-top $gl-padding;
border-top: 1px solid $border-color;
}
-
- .report-block-list-icon .loading-container {
- position: relative;
- left: -2px;
- }
}
.report-block-container {
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 5e65084a110..af6644b8fcc 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
include SessionlessAuthentication
+ include ConfirmEmailWarning
before_action :authenticate_user!
before_action :enforce_terms!, if: :should_enforce_terms?
diff --git a/app/controllers/concerns/confirm_email_warning.rb b/app/controllers/concerns/confirm_email_warning.rb
new file mode 100644
index 00000000000..5a4b5897a4f
--- /dev/null
+++ b/app/controllers/concerns/confirm_email_warning.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module ConfirmEmailWarning
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :set_confirm_warning, if: -> { Feature.enabled?(:soft_email_confirmation) }
+ end
+
+ protected
+
+ def set_confirm_warning
+ return unless current_user
+ return if current_user.confirmed?
+ return if peek_request? || json_request? || !request.get?
+
+ email = current_user.unconfirmed_email || current_user.email
+
+ flash.now[:warning] = _("Please check your email (%{email}) to verify that you own this address. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}.").html_safe % {
+ email: email,
+ resend_link: view_context.link_to(_('Resend it'), user_confirmation_path(user: { email: email }), method: :post),
+ update_link: view_context.link_to(_('Update it'), profile_path)
+ }
+ end
+end
diff --git a/app/controllers/concerns/invisible_captcha.rb b/app/controllers/concerns/invisible_captcha.rb
index c9f66e5c194..45c0a5c58ef 100644
--- a/app/controllers/concerns/invisible_captcha.rb
+++ b/app/controllers/concerns/invisible_captcha.rb
@@ -41,9 +41,9 @@ module InvisibleCaptcha
request_information = {
message: message,
env: :invisible_captcha_signup_bot_detected,
- ip: request.ip,
+ remote_ip: request.ip,
request_method: request.request_method,
- fullpath: request.fullpath
+ path: request.fullpath
}
Gitlab::AuthLogger.error(request_information)
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 2ae500a2fdf..b192189ba3c 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -11,7 +11,7 @@ class ConfirmationsController < Devise::ConfirmationsController
protected
def after_resending_confirmation_instructions_path_for(resource)
- users_almost_there_path
+ Feature.enabled?(:soft_email_confirmation) ? stored_location_for(resource) || dashboard_projects_path : users_almost_there_path
end
def after_confirmation_path_for(resource_name, resource)
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index b04ffe80db4..4125f44d00a 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -92,7 +92,7 @@ class Projects::BlobController < Projects::ApplicationController
def diff
apply_diff_view_cookie!
- @form = Blobs::UnfoldPresenter.new(blob, params.to_unsafe_h)
+ @form = Blobs::UnfoldPresenter.new(blob, diff_params)
# keep only json rendering when
# https://gitlab.com/gitlab-org/gitlab-ce/issues/44988 is done
@@ -239,4 +239,8 @@ class Projects::BlobController < Projects::ApplicationController
def tree_path
@path.rpartition('/').first
end
+
+ def diff_params
+ params.permit(:full, :since, :to, :bottom, :unfold, :offset, :indent)
+ end
end
diff --git a/app/controllers/projects/starrers_controller.rb b/app/controllers/projects/starrers_controller.rb
index c8facea1d70..4efe956e973 100644
--- a/app/controllers/projects/starrers_controller.rb
+++ b/app/controllers/projects/starrers_controller.rb
@@ -5,25 +5,11 @@ class Projects::StarrersController < Projects::ApplicationController
def index
@starrers = UsersStarProjectsFinder.new(@project, params, current_user: @current_user).execute
-
- # Normally the number of public starrers is equal to the number of visible
- # starrers. We need to fix the counts in two cases: when the current user
- # is an admin (and can see everything) and when the current user has a
- # private profile and has starred the project (and can see itself).
- @public_count =
- if @current_user&.admin?
- @starrers.with_public_profile.count
- elsif @current_user&.private_profile && has_starred_project?(@starrers)
- @starrers.size - 1
- else
- @starrers.size
- end
-
- @total_count = @project.starrers.size
- @private_count = @total_count - @public_count
-
@sort = params[:sort].presence || sort_value_name
- @starrers = @starrers.sort_by_attribute(@sort).page(params[:page])
+ @starrers = @starrers.preload_users.sort_by_attribute(@sort).page(params[:page])
+ @public_count = @project.starrers.with_public_profile.size
+ @total_count = @project.starrers.size
+ @private_count = @total_count - @public_count
end
private
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index d1914c35bd3..b187fdb2723 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -16,6 +16,10 @@ class Projects::WikisController < Projects::ApplicationController
redirect_to(project_wiki_path(@project, @page))
end
+ def new
+ redirect_to project_wiki_path(@project, SecureRandom.uuid, random_title: true)
+ end
+
def pages
@wiki_pages = Kaminari.paginate_array(
@project_wiki.list_pages(sort: params[:sort], direction: params[:direction])
@@ -24,17 +28,25 @@ class Projects::WikisController < Projects::ApplicationController
@wiki_entries = WikiPage.group_by_directory(@wiki_pages)
end
+ # `#show` handles a number of scenarios:
+ #
+ # - If `id` matches a WikiPage, then show the wiki page.
+ # - If `id` is a file in the wiki repository, then send the file.
+ # - If we know the user wants to create a new page with the given `id`,
+ # then display a create form.
+ # - Otherwise show the empty wiki page and invite the user to create a page.
def show
- view_param = @project_wiki.empty? ? params[:view] : 'create'
-
if @page
set_encoding_error unless valid_encoding?
render 'show'
elsif file_blob
send_blob(@project_wiki.repository, file_blob)
- elsif can?(current_user, :create_wiki, @project) && view_param == 'create'
- @page = build_page(title: params[:id])
+ elsif show_create_form?
+ # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new
+ title = params[:id] unless params[:random_title].present?
+
+ @page = build_page(title: title)
render 'edit'
else
@@ -110,6 +122,15 @@ class Projects::WikisController < Projects::ApplicationController
private
+ def show_create_form?
+ can?(current_user, :create_wiki, @project) &&
+ @page.nil? &&
+ # Always show the create form when the wiki has had at least one page created.
+ # Otherwise, we only show the form when the user has navigated from
+ # the 'empty wiki' page
+ (@project_wiki.exists? || params[:view] == 'create')
+ end
+
def load_project_wiki
@project_wiki = load_wiki
@@ -135,7 +156,7 @@ class Projects::WikisController < Projects::ApplicationController
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
- def build_page(args)
+ def build_page(args = {})
WikiPage.new(@project_wiki).tap do |page|
page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index db10515c0b4..e773ec09924 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -69,12 +69,12 @@ class RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(user)
Gitlab::AppLogger.info(user_created_message(confirmed: user.confirmed?))
- user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path
+ confirmed_or_unconfirmed_access_allowed(user) ? stored_location_or_dashboard(user) : users_almost_there_path
end
def after_inactive_sign_up_path_for(resource)
Gitlab::AppLogger.info(user_created_message)
- users_almost_there_path
+ Feature.enabled?(:soft_email_confirmation) ? dashboard_projects_path : users_almost_there_path
end
private
@@ -135,4 +135,12 @@ class RegistrationsController < Devise::RegistrationsController
def terms_accepted?
Gitlab::Utils.to_boolean(params[:terms_opt_in])
end
+
+ def confirmed_or_unconfirmed_access_allowed(user)
+ user.confirmed? || Feature.enabled?(:soft_email_confirmation)
+ end
+
+ def stored_location_or_dashboard(user)
+ stored_location_for(user) || dashboard_projects_path
+ end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 160f9ac4793..bd26bd01313 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -31,6 +31,11 @@ module GroupsHelper
can?(current_user, :change_share_with_group_lock, group)
end
+ def can_disable_group_emails?(group)
+ Feature.enabled?(:emails_disabled, group, default_enabled: true) &&
+ can?(current_user, :set_emails_disabled, group) && !group.parent&.emails_disabled?
+ end
+
def group_issues_count(state:)
IssuesFinder
.new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 11b9cf22142..5678304ffcf 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -5,7 +5,7 @@ module NotificationsHelper
def notification_icon_class(level)
case level.to_sym
- when :disabled
+ when :disabled, :owner_disabled
'microphone-slash'
when :participating
'volume-up'
@@ -18,6 +18,16 @@ module NotificationsHelper
end
end
+ def notification_icon_level(notification_setting, emails_disabled = false)
+ if emails_disabled
+ 'owner_disabled'
+ elsif notification_setting.global?
+ current_user.global_notification_setting.level
+ else
+ notification_setting.level
+ end
+ end
+
def notification_icon(level, text = nil)
icon("#{notification_icon_class(level)} fw", text: text)
end
@@ -53,6 +63,8 @@ module NotificationsHelper
_('Use your global notification setting')
when :custom
_('You will only receive notifications for the events you choose')
+ when :owner_disabled
+ _('Notifications have been disabled by the project or group owner')
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 71c9c121e48..33bf2d57fae 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -155,6 +155,12 @@ module ProjectsHelper
end
end
+ def can_disable_emails?(project, current_user)
+ return false if project.group&.emails_disabled?
+
+ can?(current_user, :set_emails_disabled, project) && Feature.enabled?(:emails_disabled, project, default_enabled: true)
+ end
+
def last_push_event
current_user&.recent_push(@project)
end
@@ -541,13 +547,15 @@ module ProjectsHelper
snippetsAccessLevel: feature.snippets_access_level,
pagesAccessLevel: feature.pages_access_level,
containerRegistryEnabled: !!project.container_registry_enabled,
- lfsEnabled: !!project.lfs_enabled
+ lfsEnabled: !!project.lfs_enabled,
+ emailsDisabled: project.emails_disabled?
}
end
def project_permissions_panel_data(project)
{
currentSettings: project_permissions_settings(project),
+ canDisableEmails: can_disable_emails?(project, current_user),
canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
allowedVisibilityOptions: project_allowed_visibility_levels(project),
visibilityHelpPath: help_page_path('public_access/public_access'),
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index f705e67121f..3c0efca31db 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -716,7 +716,7 @@ module Ci
depended_jobs = depends_on_builds
# find all jobs that are needed
- if Feature.enabled?(:ci_dag_support, project) && needs.exists?
+ if Feature.enabled?(:ci_dag_support, project, default_enabled: true) && needs.exists?
depended_jobs = depended_jobs.where(name: needs.select(:name))
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 3b28eb246db..0a943a33bbb 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -328,6 +328,10 @@ module Ci
config_sources.values_at(:repository_source, :auto_devops_source, :unknown_source)
end
+ def self.bridgeable_statuses
+ ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created preparing pending]
+ end
+
def stages_count
statuses.select(:stage).distinct.count
end
diff --git a/app/models/clusters/clusters_hierarchy.rb b/app/models/clusters/clusters_hierarchy.rb
index dab034b7234..5556fc8d3f0 100644
--- a/app/models/clusters/clusters_hierarchy.rb
+++ b/app/models/clusters/clusters_hierarchy.rb
@@ -46,7 +46,7 @@ module Clusters
def group_clusters_base_query
group_parent_id_alias = alias_as_column(groups[:parent_id], 'group_parent_id')
- join_sources = ::Group.left_joins(:clusters).join_sources
+ join_sources = ::Group.left_joins(:clusters).arel.join_sources
model
.unscoped
@@ -59,7 +59,7 @@ module Clusters
def project_clusters_base_query
projects = ::Project.arel_table
project_parent_id_alias = alias_as_column(projects[:namespace_id], 'group_parent_id')
- join_sources = ::Project.left_joins(:clusters).join_sources
+ join_sources = ::Project.left_joins(:clusters).arel.join_sources
model
.unscoped
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index f6b19317c50..3d6f397e599 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -15,8 +15,8 @@ class GroupMember < Member
default_scope { where(source_type: SOURCE_TYPE) }
scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) }
-
scope :count_users_by_group_id, -> { joins(:user).group(:source_id).count }
+ scope :of_ldap_type, -> { where(ldap: true) }
after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite?
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index c91add6439f..4a19e05bf76 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -85,6 +85,10 @@ class ProjectWiki
list_pages(limit: 1).empty?
end
+ def exists?
+ !empty?
+ end
+
# Lists wiki pages of the repository.
#
# limit - max number of pages returned by the method.
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 9d45a12fa6e..b957b9b0bdd 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -239,13 +239,13 @@ class Repository
def branch_exists?(branch_name)
return false unless raw_repository
- branch_names.include?(branch_name)
+ branch_names_include?(branch_name)
end
def tag_exists?(tag_name)
return false unless raw_repository
- tag_names.include?(tag_name)
+ tag_names_include?(tag_name)
end
def ref_exists?(ref)
@@ -389,11 +389,15 @@ class Repository
expire_statistics_caches
end
- # Runs code after a repository has been created.
- def after_create
+ def expire_status_cache
expire_exists_cache
expire_root_ref_cache
expire_emptiness_caches
+ end
+
+ # Runs code after a repository has been created.
+ def after_create
+ expire_status_cache
repository_event(:create_repository)
end
@@ -561,10 +565,10 @@ class Repository
end
delegate :branch_names, to: :raw_repository
- cache_method :branch_names, fallback: []
+ cache_method_as_redis_set :branch_names, fallback: []
delegate :tag_names, to: :raw_repository
- cache_method :tag_names, fallback: []
+ cache_method_as_redis_set :tag_names, fallback: []
delegate :branch_count, :tag_count, :has_visible_content?, to: :raw_repository
cache_method :branch_count, fallback: 0
@@ -1126,6 +1130,10 @@ class Repository
@cache ||= Gitlab::RepositoryCache.new(self)
end
+ def redis_set_cache
+ @redis_set_cache ||= Gitlab::RepositorySetCache.new(self)
+ end
+
def request_store_cache
@request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 374e00987c5..6131a8dc710 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1507,6 +1507,13 @@ class User < ApplicationRecord
super
end
+ # override from Devise::Confirmable
+ def confirmation_period_valid?
+ return false if Feature.disabled?(:soft_email_confirmation)
+
+ super
+ end
+
private
def default_private_profile_to_false
diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb
index 3c7a805cc5c..c633e2d8b3d 100644
--- a/app/models/users_star_project.rb
+++ b/app/models/users_star_project.rb
@@ -17,6 +17,7 @@ class UsersStarProject < ApplicationRecord
scope :by_project, -> (project) { where(project_id: project.id) }
scope :with_visible_profile, -> (user) { joins(:user).merge(User.with_visible_profile(user)) }
scope :with_public_profile, -> { joins(:user).merge(User.with_public_profile) }
+ scope :preload_users, -> { preload(:user) }
class << self
def sort_by_attribute(method)
diff --git a/app/presenters/blobs/unfold_presenter.rb b/app/presenters/blobs/unfold_presenter.rb
index 4c6dd6895cf..a256dd05a4d 100644
--- a/app/presenters/blobs/unfold_presenter.rb
+++ b/app/presenters/blobs/unfold_presenter.rb
@@ -1,30 +1,34 @@
# frozen_string_literal: true
-require 'gt_one_coercion'
-
module Blobs
class UnfoldPresenter < BlobPresenter
- include Virtus.model
+ include ActiveModel::Attributes
+ include ActiveModel::AttributeAssignment
include Gitlab::Utils::StrongMemoize
- attribute :full, Boolean, default: false
- attribute :since, GtOneCoercion
- attribute :to, Integer
- attribute :bottom, Boolean
- attribute :unfold, Boolean, default: true
- attribute :offset, Integer
- attribute :indent, Integer, default: 0
+ attribute :full, :boolean, default: false
+ attribute :since, :integer, default: 1
+ attribute :to, :integer, default: 1
+ attribute :bottom, :boolean, default: false
+ attribute :unfold, :boolean, default: true
+ attribute :offset, :integer, default: 0
+ attribute :indent, :integer, default: 0
+
+ alias_method :full?, :full
+ alias_method :bottom?, :bottom
+ alias_method :unfold?, :unfold
def initialize(blob, params)
+ super(blob)
+ self.attributes = params
+
# Load all blob data first as we need to ensure they're all loaded first
# so we can accurately show the rest of the diff when unfolding.
load_all_blob_data
- @subject = blob
@all_lines = blob.data.lines
- super(params)
- self.attributes = prepare_attributes
+ handle_full_or_end!
end
# Returns an array of Gitlab::Diff::Line with match line added
@@ -56,21 +60,18 @@ module Blobs
private
- def prepare_attributes
- return attributes unless full? || to == -1
+ def handle_full_or_end!
+ return unless full? || to == -1
- full_opts = {
- since: 1,
+ self.since = 1 if full?
+
+ self.attributes = {
to: all_lines_size,
bottom: false,
unfold: false,
offset: 0,
indent: 0
}
-
- return full_opts if full?
-
- full_opts.merge(attributes.slice(:since))
end
def all_lines_size
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index 943c707218d..6e91317eb20 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -18,6 +18,7 @@ class DeploymentEntity < Grape::Entity
end
expose :created_at
+ expose :finished_at
expose :tag
expose :last?
expose :user, using: UserEntity
diff --git a/app/serializers/deployment_serializer.rb b/app/serializers/deployment_serializer.rb
index 04db6b88489..3fd3e1b9cc8 100644
--- a/app/serializers/deployment_serializer.rb
+++ b/app/serializers/deployment_serializer.rb
@@ -4,7 +4,7 @@ class DeploymentSerializer < BaseSerializer
entity DeploymentEntity
def represent_concise(resource, opts = {})
- opts[:only] = [:iid, :id, :sha, :created_at, :tag, :last?, :id, ref: [:name]]
+ opts[:only] = [:iid, :id, :sha, :created_at, :finished_at, :tag, :last?, :id, ref: [:name]]
represent(resource, opts)
end
end
diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb
index 61de3c93337..c02fd024345 100644
--- a/app/serializers/issuable_sidebar_basic_entity.rb
+++ b/app/serializers/issuable_sidebar_basic_entity.rb
@@ -98,6 +98,10 @@ class IssuableSidebarBasicEntity < Grape::Entity
autocomplete_projects_path(project_id: issuable.project.id)
end
+ expose :project_emails_disabled do |issuable|
+ issuable.project.emails_disabled?
+ end
+
private
def current_user
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index f4bd457ebc6..3b145a65d79 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -40,7 +40,7 @@ module Ci
def process_builds_with_needs(trigger_build_ids)
return false unless trigger_build_ids.present?
- return false unless Feature.enabled?(:ci_dag_support, project)
+ return false unless Feature.enabled?(:ci_dag_support, project, default_enabled: true)
# we find processables that are dependent:
# 1. because of current dependency,
@@ -96,7 +96,7 @@ module Ci
end
def created_processables_without_needs
- if Feature.enabled?(:ci_dag_support, project)
+ if Feature.enabled?(:ci_dag_support, project, default_enabled: true)
pipeline.processables.created.without_needs
else
pipeline.processables.created
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index 1db18fcf401..47c308c8280 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -8,8 +8,6 @@ module Git
PROCESS_COMMIT_LIMIT = 100
def execute
- project.repository.after_create if project.empty_repo?
-
create_events
create_pipelines
execute_project_hooks
@@ -58,7 +56,7 @@ module Git
return unless params.fetch(:create_pipelines, true)
Ci::CreatePipelineService
- .new(project, current_user, base_params)
+ .new(project, current_user, pipeline_params)
.execute(:push, pipeline_options)
end
@@ -70,31 +68,36 @@ module Git
end
def enqueue_invalidate_cache
- ProjectCacheWorker.perform_async(
- project.id,
- invalidated_file_types,
- [:commit_count, :repository_size]
- )
+ file_types = invalidated_file_types
+
+ return unless file_types.present?
+
+ ProjectCacheWorker.perform_async(project.id, file_types, [], false)
end
- def base_params
+ def pipeline_params
{
- oldrev: params[:oldrev],
- newrev: params[:newrev],
+ before: params[:oldrev],
+ after: params[:newrev],
ref: params[:ref],
- push_options: params[:push_options] || {}
+ push_options: params[:push_options] || {},
+ checkout_sha: Gitlab::DataBuilder::Push.checkout_sha(
+ project.repository, params[:newrev], params[:ref])
}
end
def push_data_params(commits:, with_changed_files: true)
- base_params.merge(
+ {
+ oldrev: params[:oldrev],
+ newrev: params[:newrev],
+ ref: params[:ref],
project: project,
user: current_user,
commits: commits,
message: event_message,
commits_count: commits_count,
with_changed_files: with_changed_files
- )
+ }
end
def event_push_data
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index 431a5aedf2e..d2b037a680c 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -83,8 +83,16 @@ module Git
# Schedules processing of commit messages
def enqueue_process_commit_messages
- limited_commits.each do |commit|
- next unless commit.matches_cross_reference_regex?
+ referencing_commits = limited_commits.select(&:matches_cross_reference_regex?)
+
+ upstream_commit_ids = upstream_commit_ids(referencing_commits)
+
+ referencing_commits.each do |commit|
+ # Avoid reprocessing commits that already exist upstream if the project
+ # is a fork. This will prevent duplicated/superfluous system notes on
+ # mentionables referenced by a commit that is pushed to the upstream,
+ # that is then also pushed to forks when these get synced by users.
+ next if upstream_commit_ids.include?(commit.id)
ProcessCommitWorker.perform_async(
project.id,
@@ -142,5 +150,18 @@ module Git
def branch_name
strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) }
end
+
+ def upstream_commit_ids(commits)
+ set = Set.new
+
+ upstream_project = project.fork_source
+ if upstream_project
+ upstream_project
+ .commits_by(oids: commits.map(&:id))
+ .each { |commit| set << commit.id }
+ end
+
+ set
+ end
end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index e30debbbe75..ee7223d6349 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -598,11 +598,11 @@ module SystemNoteService
end
def zoom_link_added(issue, project, author)
- create_note(NoteSummary.new(issue, project, author, _('a Zoom call was added to this issue'), action: 'pinned_embed'))
+ create_note(NoteSummary.new(issue, project, author, _('added a Zoom call to this issue'), action: 'pinned_embed'))
end
def zoom_link_removed(issue, project, author)
- create_note(NoteSummary.new(issue, project, author, _('a Zoom call was removed from this issue'), action: 'pinned_embed'))
+ create_note(NoteSummary.new(issue, project, author, _('removed a Zoom call from this issue'), action: 'pinned_embed'))
end
private
diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml
index 82781f6716d..86f09bf1cb0 100644
--- a/app/views/admin/applications/_delete_form.html.haml
+++ b/app/views/admin/applications/_delete_form.html.haml
@@ -1,4 +1,4 @@
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag admin_application_path(application) do
%input{ :name => "_method", :type => "hidden", :value => "delete" }/
- = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
+ = submit_tag 'Destroy', class: submit_btn_css, data: { confirm: _('Are you sure?') }
diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
index 69cc510e9c1..9bc5e2ee42f 100644
--- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
@@ -5,4 +5,4 @@
= form_tag path do
%input{ :name => "_method", :type => "hidden", :value => "delete" }/
- = submit_tag _('Revoke'), onclick: "return confirm('#{_('Are you sure?')}')", class: 'btn btn-remove btn-sm'
+ = submit_tag _('Revoke'), class: 'btn btn-remove btn-sm', data: { confirm: _('Are you sure?') }
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index 46931b5932d..1ed7b56db1d 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -10,7 +10,7 @@
= message
%p
= s_('403|Please contact your GitLab administrator to get permission.')
- .action-container.js-go-back{ style: 'display: none' }
- %a{ href: 'javascript:history.back()', class: 'btn btn-success' }
+ .action-container.js-go-back{ hidden: true }
+ %button{ type: 'button', class: 'btn btn-success' }
= s_('Go Back')
= render "errors/footer"
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 4daf3683eaf..e50d2b8e994 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -1,4 +1,5 @@
- can_create_subgroups = can?(current_user, :create_subgroup, @group)
+- emails_disabled = @group.emails_disabled?
.group-home-panel
.row.mb-3
@@ -21,7 +22,7 @@
.home-panel-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end
- if current_user
.group-buttons
- = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn'
+ = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn', emails_disabled: emails_disabled
- if can? current_user, :create_projects, @group
- new_project_label = _("New project")
- new_subgroup_label = _("New subgroup")
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index d3375e00bad..94a938021f9 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -11,13 +11,20 @@
.form-check
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input'
= f.label :share_with_group_lock, class: 'form-check-label' do
- %span
+ %span.d-block
- group_link = link_to @group.name, group_path(@group)
= s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link }
- %br
%span.descr.text-muted= share_with_group_lock_help_text(@group)
+ .form-group.append-bottom-default
+ .form-check
+ = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'form-check-input'
+ = f.label :emails_disabled, class: 'form-check-label' do
+ %span.d-block= s_('GroupSettings|Disable email notifications')
+ %span.text-muted= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.')
+
= render_if_exists 'groups/settings/ip_restriction', f: f, group: @group
+ = render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group
= render 'groups/settings/lfs', f: f
= render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/subgroup_creation_level', f: f, group: @group
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index 74484005b48..dc924a0e25d 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -14,6 +14,10 @@
var goBackElement = document.querySelector('.js-go-back');
if (goBackElement && history.length > 1) {
- goBackElement.style.display = 'block';
+ goBackElement.removeAttribute('hidden');
+
+ goBackElement.querySelector('button').addEventListener('click', function() {
+ history.back();
+ });
}
}());
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 89f99472270..14c7b2428b2 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -4,6 +4,7 @@
- search_path_url = search_path(group_id: group.id)
- else
- search_path_url = search_path
+- has_impersonation_link = header_link?(:admin_impersonation)
%header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } }
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
@@ -64,14 +65,14 @@
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/help_dropdown'
- if header_link?(:user_dropdown)
- %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' } }
+ %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' }, class: ('mr-0' if has_impersonation_link) }
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/current_user_dropdown'
- - if header_link?(:admin_impersonation)
- %li.nav-item.impersonation
+ - if has_impersonation_link
+ %li.nav-item.impersonation.ml-0
= link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret')
- if header_link?(:sign_in)
diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml
index cf17ee44145..1776d260e19 100644
--- a/app/views/profiles/notifications/_group_settings.html.haml
+++ b/app/views/profiles/notifications/_group_settings.html.haml
@@ -1,16 +1,15 @@
+- emails_disabled = group.emails_disabled?
+
.gl-responsive-table-row.notification-list-item
.table-section.section-40
%span.notification.fa.fa-holder.append-right-5
- - if setting.global?
- = notification_icon(current_user.global_notification_setting.level)
- - else
- = notification_icon(setting.level)
+ = notification_icon(notification_icon_level(setting, emails_disabled))
%span.str-truncated
= link_to group.name, group_path(group)
.table-section.section-30.text-right
- = render 'shared/notifications/button', notification_setting: setting
+ = render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled
.table-section.section-30
= form_for @user.notification_settings.find { |ns| ns.source == group }, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications' } do |f|
diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml
index 823fec3fab4..63a77b335b6 100644
--- a/app/views/profiles/notifications/_project_settings.html.haml
+++ b/app/views/profiles/notifications/_project_settings.html.haml
@@ -1,12 +1,11 @@
+- emails_disabled = project.emails_disabled?
+
%li.notification-list-item
%span.notification.fa.fa-holder.append-right-5
- - if setting.global?
- = notification_icon(current_user.global_notification_setting.level)
- - else
- = notification_icon(setting.level)
+ = notification_icon(notification_icon_level(setting, emails_disabled))
%span.str-truncated
= link_to_project(project)
.float-right
- = render 'shared/notifications/button', notification_setting: setting
+ = render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 65ef9690062..08a39fc4f58 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -31,7 +31,7 @@
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.feed-token-reset
= label_tag :feed_token, s_('AccessTokens|Feed token'), class: "label-bold"
- = text_field_tag :feed_token, current_user.feed_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ = text_field_tag :feed_token, current_user.feed_token, class: 'form-control js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset it'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.') }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link }
@@ -49,7 +49,7 @@
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.incoming-email-token-reset
= label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: "label-bold"
- = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()'
+ = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset it'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.') }
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link }
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 824fe3c791d..4783b10cf6d 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,6 +1,8 @@
- empty_repo = @project.empty_repo?
- show_auto_devops_callout = show_auto_devops_callout?(@project)
- max_project_topic_length = 15
+- emails_disabled = @project.emails_disabled?
+
.project-home-panel{ class: [("empty-project" if empty_repo), ("js-keep-hidden-on-navigation" if vue_file_list_enabled?)] }
.row.append-bottom-8
.home-panel-title-row.col-md-12.col-lg-6.d-flex
@@ -41,7 +43,7 @@
.project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end
- if current_user
.d-inline-flex
- = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs'
+ = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', emails_disabled: emails_disabled
.count-buttons.d-inline-flex
= render 'projects/buttons/star'
diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml
index ae9aef5a9b0..e1bf0940f59 100644
--- a/app/views/projects/commit/_ajax_signature.html.haml
+++ b/app/views/projects/commit/_ajax_signature.html.haml
@@ -1,2 +1,2 @@
- if commit.has_signature?
- %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: _('GPG signature (loading...)'), 'commit-sha' => commit.sha } }
+ %button{ tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: _('GPG signature (loading...)'), 'commit-sha' => commit.sha } }
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index 1331fa179fc..cbd998c60ef 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -24,5 +24,5 @@
= link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
-%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
+%button{ tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= label
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 59f0afd59e6..2b594c125f4 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -34,40 +34,29 @@
{{ n__('Last %d day', 'Last %d days', 90) }}
.stage-panel-container
.card.stage-panel
- .card-header
+ .card-header.border-bottom-0
%nav.col-headers
%ul
- %li.stage-header
- %span.stage-name
+ %li.stage-header.pl-5
+ %span.stage-name.font-weight-bold
{{ s__('ProjectLifecycle|Stage') }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The phase of the development lifecycle."), "aria-hidden" => "true" }
%li.median-header
- %span.stage-name
+ %span.stage-name.font-weight-bold
{{ __('Median') }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."), "aria-hidden" => "true" }
- %li.event-header
- %span.stage-name
+ %li.event-header.pl-3
+ %span.stage-name.font-weight-bold
{{ currentStage ? __(currentStage.legend) : __('Related Issues') }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The collection of events added to the data gathered for that stage."), "aria-hidden" => "true" }
- %li.total-time-header
- %span.stage-name
+ %li.total-time-header.pr-5.text-right
+ %span.stage-name.font-weight-bold
{{ __('Total Time') }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The time taken by each data entry gathered by that stage."), "aria-hidden" => "true" }
.stage-panel-body
%nav.stage-nav
%ul
- %li.stage-nav-item{ ':class' => '{ active: stage.active }', '@click' => 'selectStage(stage)', "v-for" => "stage in state.stages" }
- .stage-nav-item-cell.stage-name
- {{ stage.title }}
- .stage-nav-item-cell.stage-median
- %template{ "v-if" => "stage.isUserAllowed" }
- %span{ "v-if" => "stage.value" }
- {{ stage.value }}
- %span.stage-empty{ "v-else" => true }
- {{ __('Not enough data') }}
- %template{ "v-else" => true }
- %span.not-available
- {{ __('Not available') }}
+ %stage-nav-item{ "v-for" => "stage in state.stages", ":key" => '`ca-stage-title-${stage.title}`', '@select' => 'selectStage(stage)', ":title" => "stage.title", ":is-user-allowed" => "stage.isUserAllowed", ":value" => "stage.value", ":is-active" => "stage.active" }
.section.stage-events
%template{ "v-if" => "isLoadingStage" }
= icon("spinner spin")
diff --git a/app/views/projects/pages_domains/_certificate.html.haml b/app/views/projects/pages_domains/_certificate.html.haml
new file mode 100644
index 00000000000..42631fca5e8
--- /dev/null
+++ b/app/views/projects/pages_domains/_certificate.html.haml
@@ -0,0 +1,18 @@
+- if @domain.auto_ssl_enabled?
+ - if @domain.enabled?
+ - if @domain.certificate_text
+ %pre
+ = @domain.certificate_text
+ - else
+ .bs-callout.bs-callout-info
+ = _("GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later.")
+ - else
+ .bs-callout.bs-callout-warning
+ = _("A Let's Encrypt SSL certificate can not be obtained until your domain is verified.")
+- else
+ - if @domain.certificate_text
+ %pre
+ = @domain.certificate_text
+ - else
+ .light
+ = _("missing")
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index e9019175219..d0b54946f7e 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -60,9 +60,4 @@
%td
= _("Certificate")
%td
- - if @domain.certificate_text
- %pre
- = @domain.certificate_text
- - else
- .light
- = _("missing")
+ = render 'certificate'
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 2b56ada8b73..9614f33fe2f 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -43,7 +43,7 @@
} }
Auto DevOps
- if @pipeline.detached_merge_request_pipeline?
- %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: "The code of a detached pipeline is tested against the source branch instead of merged results" }
+ %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: _('Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more on the documentation for Pipelines for Merged Results.') }
detached
- if @pipeline.stuck?
%span.js-pipeline-url-stuck.badge.badge-warning
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 858731b2dda..a153f527ee0 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,8 +1,8 @@
-- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}")
-- commit_message = commit_message % { page_title: @page.title }
+- form_classes = 'wiki-form common-note-form prepend-top-default js-quick-submit'
+- form_classes += ' js-new-wiki-page' unless @page.persisted?
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post,
- html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' },
+ html: { class: form_classes },
data: { uploads_path: uploads_path } do |f|
= form_errors(@page)
@@ -12,12 +12,14 @@
.form-group.row
.col-sm-12= f.label :title, class: 'control-label-full-width'
.col-sm-12
- = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title
- - if @page.persisted?
- %span.d-inline-block.mw-100.prepend-top-5
- = icon('lightbulb-o')
+ = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title, required: true, autofocus: !@page.persisted?, placeholder: _('Wiki|Page title')
+ %span.d-inline-block.mw-100.prepend-top-5
+ = icon('lightbulb-o')
+ - if @page.persisted?
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank'
+ - else
+ = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.")
.form-group.row
.col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12
@@ -43,7 +45,7 @@
.form-group.row
.col-sm-12= f.label :commit_message, class: 'control-label-full-width'
- .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: commit_message
+ .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: nil
.form-actions
- if @page && @page.persisted?
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 643b51e01d1..2e1e176c42a 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,9 +1,9 @@
- if (@page && @page.persisted?)
- if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-success", "data-toggle" => "modal" do
+ = link_to project_wikis_new_path(@project), class: "add-new-wiki btn btn-success", role: "button" do
= s_("Wiki|New page")
- = link_to project_wiki_history_path(@project, @page), class: "btn" do
+ = link_to project_wiki_history_path(@project, @page), class: "btn", role: "button" do
= s_("Wiki|Page history")
- if can?(current_user, :create_wiki, @project) && @page.latest? && @valid_encoding
- = link_to project_wiki_edit_path(@project, @page), class: "btn js-wiki-edit" do
+ = link_to project_wiki_edit_path(@project, @page), class: "btn js-wiki-edit", role: "button" do
= _("Edit")
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
deleted file mode 100644
index 2c675c0de9c..00000000000
--- a/app/views/projects/wikis/_new.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-#modal-new-wiki.modal
- .modal-dialog
- .modal-content
- .modal-header
- %h3.page-title= s_("WikiNewPageTitle|New Wiki Page")
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
- %span{ "aria-hidden": true } &times;
- .modal-body
- %form.new-wiki-page
- .form-group
- = label_tag :new_wiki_path do
- %span= s_("WikiPage|Page slug")
- = text_field_tag :new_wiki_path, nil, placeholder: s_("WikiNewPagePlaceholder|how-to-setup"), class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project), autofocus: true
- %span.d-inline-block.mw-100.prepend-top-5
- = icon('lightbulb-o')
- = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.")
- .form-actions
- = button_tag s_("Wiki|Create page"), class: "build-new-wiki btn btn-success"
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 28353927135..a9d21470944 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -19,5 +19,3 @@
.block
= link_to project_wikis_pages_path(@project), class: 'btn btn-block' do
= s_("Wiki|More Pages")
-
-= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index e8b59a3b8c4..815b4a51261 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -13,14 +13,11 @@
%h2.wiki-page-title
- if @page.persisted?
= link_to @page.human_title, project_wiki_path(@project, @page)
- - else
- = @page.human_title
- %span.light
- &middot;
- - if @page.persisted?
+ %span.light
+ &middot;
= s_("Wiki|Edit Page")
- - else
- = s_("Wiki|Create Page")
+ - else
+ = s_("Wiki|Create New Page")
.nav-controls
- if @page.persisted?
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index b4f8377c008..825088a58e7 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -137,7 +137,11 @@
.js-sidebar-participants-entry-point
- if signed_in
- .js-sidebar-subscriptions-entry-point
+ - if issuable_sidebar[:project_emails_disabled]
+ .block.js-emails-disabled
+ = notification_description(:owner_disabled)
+ - else
+ .js-sidebar-subscriptions-entry-point
- project_ref = issuable_sidebar[:reference]
.block.project-reference
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index e83ca5eaab8..42a823e3a8d 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -32,7 +32,7 @@
%ul
- Gitlab::Access.options.each do |role, role_id|
%li
- = link_to role, "javascript:void(0)",
+ = link_to role, '#',
class: ("is-active" if group_link.group_access == role_id),
data: { id: role_id, el_id: dom_id }
.clearable-input.member-form-control.d-sm-inline-block
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 331283f7eec..6762f211a80 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -82,7 +82,7 @@
%ul
- member.valid_level_roles.each do |role, role_id|
%li
- = link_to role, "javascript:void(0)",
+ = link_to role, '#',
class: ("is-active" if member.access_level == role_id),
data: { id: role_id, el_id: dom_id(member) }
= render_if_exists 'shared/members/ee/revert_ldap_group_sync_option',
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index 749aa258af6..b4266937a4e 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,6 +1,15 @@
-- btn_class = local_assigns.fetch(:btn_class, nil)
+- btn_class = local_assigns.fetch(:btn_class, '')
+- emails_disabled = local_assigns.fetch(:emails_disabled, false)
- if notification_setting
+ - if emails_disabled
+ - button_title = notification_description(:owner_disabled)
+ - aria_label = button_title
+ - btn_class << " disabled"
+ - else
+ - button_title = _("Notification setting")
+ - aria_label = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }
+
.js-notification-dropdown.notification-dropdown.mr-md-2.home-panel-action-button.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
@@ -8,14 +17,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
.float-left
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
diff --git a/app/views/shared/notifications/_new_button.html.haml b/app/views/shared/notifications/_new_button.html.haml
index 052e6da5bae..3c8cc023848 100644
--- a/app/views/shared/notifications/_new_button.html.haml
+++ b/app/views/shared/notifications/_new_button.html.haml
@@ -1,6 +1,13 @@
-- btn_class = local_assigns.fetch(:btn_class, nil)
+- btn_class = local_assigns.fetch(:btn_class, '')
+- emails_disabled = local_assigns.fetch(:emails_disabled, false)
- if notification_setting
+ - if emails_disabled
+ - button_title = notification_description(:owner_disabled)
+ - btn_class << " disabled"
+ - else
+ - button_title = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }
+
.js-notification-dropdown.notification-dropdown.home-panel-action-button.prepend-top-default.append-right-8.dropdown.inline
= form_for notification_setting, remote: true, html: { class: "inline notification-form no-label" } do |f|
= hidden_setting_source_input(notification_setting)
@@ -9,14 +16,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= notification_setting_icon(notification_setting)
%span.js-notification-loading.fa.hidden
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" }
= sprite_icon("arrow-down", css_class: "icon mr-0")
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= notification_setting_icon(notification_setting)
%span.js-notification-loading.fa.hidden
= sprite_icon("arrow-down", css_class: "icon")
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 622bd6f1f48..61d34981458 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -42,10 +42,8 @@ class PostReceive
user = identify_user(post_received)
return false unless user
- # Expire the branches cache so we have updated data for this push
- post_received.project.repository.expire_branches_cache if post_received.includes_branches?
- # We only need to expire tags once per push
- post_received.project.repository.expire_caches_for_tags if post_received.includes_tags?
+ # We only need to expire certain caches once per push
+ expire_caches(post_received)
post_received.enum_for(:changes_refs).with_index do |(oldrev, newrev, ref), index|
service_klass =
@@ -74,6 +72,30 @@ class PostReceive
after_project_changes_hooks(post_received, user, refs.to_a, changes)
end
+ # Expire the project, branch, and tag cache once per push. Schedule an
+ # update for the repository size and commit count if necessary.
+ def expire_caches(post_received)
+ project = post_received.project
+
+ project.repository.expire_status_cache if project.empty_repo?
+ project.repository.expire_branches_cache if post_received.includes_branches?
+ project.repository.expire_caches_for_tags if post_received.includes_tags?
+
+ enqueue_repository_cache_update(post_received)
+ end
+
+ def enqueue_repository_cache_update(post_received)
+ stats_to_invalidate = [:repository_size]
+ stats_to_invalidate << :commit_count if post_received.includes_default_branch?
+
+ ProjectCacheWorker.perform_async(
+ post_received.project.id,
+ [],
+ stats_to_invalidate,
+ true
+ )
+ end
+
def after_project_changes_hooks(post_received, user, refs, changes)
hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, user, changes, refs)
SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks)
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 3efb5343a96..f6ebe4ab006 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -2,7 +2,8 @@
# Worker for processing individual commit messages pushed to a repository.
#
-# Jobs for this worker are scheduled for every commit that is being pushed. As a
+# Jobs for this worker are scheduled for every commit that contains mentionable
+# references in its message and does not exist in the upstream project. As a
# result of this the workload of this worker should be kept to a bare minimum.
# Consider using an extra worker if you need to add any extra (and potentially
# slow) processing of commits.
@@ -19,7 +20,6 @@ class ProcessCommitWorker
project = Project.find_by(id: project_id)
return unless project
- return if commit_exists_in_upstream?(project, commit_hash)
user = User.find_by(id: user_id)
@@ -77,17 +77,4 @@ class ProcessCommitWorker
Commit.from_hash(hash, project)
end
-
- private
-
- # Avoid reprocessing commits that already exist in the upstream
- # when project is forked. This will also prevent duplicated system notes.
- def commit_exists_in_upstream?(project, commit_hash)
- upstream_project = project.fork_source
-
- return false unless upstream_project
-
- commit_id = commit_hash.with_indifferent_access[:id]
- upstream_project.commit(commit_id).present?
- end
end
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index 4e8ea903139..5ac860c93e0 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -12,13 +12,15 @@ class ProjectCacheWorker
# CHANGELOG.
# statistics - An Array containing columns from ProjectStatistics to
# refresh, if empty all columns will be refreshed
+ # refresh_statistics - A boolean that determines whether project statistics should
+ # be updated.
# rubocop: disable CodeReuse/ActiveRecord
- def perform(project_id, files = [], statistics = [])
+ def perform(project_id, files = [], statistics = [], refresh_statistics = true)
project = Project.find_by(id: project_id)
return unless project
- update_statistics(project, statistics)
+ update_statistics(project, statistics) if refresh_statistics
return unless project.repository.exists?