Commit 84af5d6b authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent d96c72b24ef8
import flash from '~/flash';
import $ from 'jquery';
import { sprintf, __ } from '../../locale';
// Renders diagrams and flowcharts from text using Mermaid in any element with the
......@@ -18,7 +19,7 @@ import { sprintf, __ } from '../../locale';
// This is an arbitrary number; Can be iterated upon when suitable.
const MAX_CHAR_LIMIT = 5000;
export default function renderMermaid($els) {
function renderMermaids($els) {
if (!$els.length) return;
// A diagram may have been truncated in search results which will cause errors, so abort the render.
......@@ -95,3 +96,19 @@ export default function renderMermaid($els) {
flash(`Can't load mermaid module: ${err}`);
});
}
export default function renderMermaid($els) {
if (!$els.length) return;
const visibleMermaids = $els.filter(function filter() {
return $(this).closest('details').length === 0;
});
renderMermaids(visibleMermaids);
$els.closest('details').one('toggle', function toggle() {
if (this.open) {
renderMermaids($(this).find('.js-render-mermaid'));
}
});
}
import $ from 'jquery';
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
import Vue from 'vue';
import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
import ShortcutsToggle from './shortcuts_toggle.vue';
import axios from '../../lib/utils/axios_utils';
import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
import findAndFollowLink from '../../lib/utils/navigation_utility';
......@@ -15,6 +18,15 @@ Mousetrap.stopCallback = (e, element, combo) => {
return defaultStopCallback(e, element, combo);
};
function initToggleButton() {
return new Vue({
el: document.querySelector('.js-toggle-shortcuts'),
render(createElement) {
return createElement(ShortcutsToggle);
},
});
}
export default class Shortcuts {
constructor() {
this.onToggleHelp = this.onToggleHelp.bind(this);
......@@ -48,6 +60,14 @@ export default class Shortcuts {
$(this).remove();
e.preventDefault();
});
$('.js-shortcuts-modal-trigger')
.off('click')
.on('click', this.onToggleHelp);
if (shouldDisableShortcuts()) {
disableShortcuts();
}
}
onToggleHelp(e) {
......@@ -104,7 +124,8 @@ export default class Shortcuts {
}
return $('.js-more-help-button').remove();
});
})
.then(initToggleButton);
}
focusFilter(e) {
......
import Mousetrap from 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause';
const shorcutsDisabledKey = 'shortcutsDisabled';
export const shouldDisableShortcuts = () => {
try {
return localStorage.getItem(shorcutsDisabledKey) === 'true';
} catch (e) {
return false;
}
};
export function enableShortcuts() {
localStorage.setItem(shorcutsDisabledKey, false);
Mousetrap.unpause();
}
export function disableShortcuts() {
localStorage.setItem(shorcutsDisabledKey, true);
Mousetrap.pause();
}
<script>
import { GlToggle, GlSprintf } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
import { disableShortcuts, enableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
export default {
components: {
GlSprintf,
GlToggle,
},
data() {
return {
localStorageUsable: AccessorUtilities.isLocalStorageAccessSafe(),
shortcutsEnabled: !shouldDisableShortcuts(),
};
},
methods: {
onChange(value) {
this.shortcutsEnabled = value;
if (value) {
enableShortcuts();
} else {
disableShortcuts();
}
},
},
};
</script>
<template>
<div v-if="localStorageUsable" class="d-inline-flex align-items-center js-toggle-shortcuts">
<gl-toggle
v-model="shortcutsEnabled"
aria-describedby="shortcutsToggle"
class="prepend-left-10 mb-0"
label-position="right"
@change="onChange"
>
<template #labelOn>
<gl-sprintf
:message="__('%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Enabled')"
>
<template #screenreaderOnly="{ content }">
<span class="sr-only">{{ content }}</span>
</template>
</gl-sprintf>
</template>
<template #labelOff>
<gl-sprintf
:message="__('%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Disabled')"
>
<template #screenreaderOnly="{ content }">
<span class="sr-only">{{ content }}</span>
</template>
</gl-sprintf>
</template>
</gl-toggle>
<div id="shortcutsToggle" class="sr-only">{{ __('Enable or disable keyboard shortcuts') }}</div>
</div>
</template>
import Vue from 'vue';
import pdfLab from '../../pdf/index.vue';
import { GlLoadingIcon } from '@gitlab/ui';
export default () => {
const el = document.getElementById('js-pdf-viewer');
......@@ -8,6 +9,7 @@ export default () => {
el,
components: {
pdfLab,
GlLoadingIcon,
},
data() {
return {
......@@ -32,11 +34,7 @@ export default () => {
<div
class="text-center loading"
v-if="loading && !error">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="PDF loading">
</i>
<gl-loading-icon class="mt-5" size="lg"/>
</div>
<pdf-lab
v-if="!loadError"
......
......@@ -12,7 +12,7 @@ export default class FilteredSearchDropdown {
this.filter = filter;
this.dropdown = dropdown;
this.loadingTemplate = `<div class="filter-dropdown-loading">
<i class="fa fa-spinner fa-spin"></i>
<span class="spinner"></span>
</div>`;
this.bindEvents();
}
......
......@@ -18,6 +18,7 @@ export const HISTORY_ONLY_FILTER_VALUE = 2;
export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0;
export const DISCUSSION_TAB_LABEL = 'show';
export const NOTE_UNDERSCORE = 'note_';
export const TIME_DIFFERENCE_VALUE = 10;
export const NOTEABLE_TYPE_MAPPING = {
Issue: ISSUE_NOTEABLE_TYPE,
......
......@@ -3,10 +3,12 @@
export default {
computed: {
canSeeDescriptionVersion() {},
canDeleteDescriptionVersion() {},
shouldShowDescriptionVersion() {},
descriptionVersionToggleIcon() {},
},
methods: {
toggleDescriptionVersion() {},
deleteDescriptionVersion() {},
},
};
......@@ -491,23 +491,66 @@ export const convertToDiscussion = ({ commit }, noteId) =>
export const removeConvertedDiscussion = ({ commit }, noteId) =>
commit(types.REMOVE_CONVERTED_DISCUSSION, noteId);
export const fetchDescriptionVersion = (_, { endpoint, startingVersion }) => {
export const setCurrentDiscussionId = ({ commit }, discussionId) =>
commit(types.SET_CURRENT_DISCUSSION_ID, discussionId);
export const fetchDescriptionVersion = ({ dispatch }, { endpoint, startingVersion }) => {
let requestUrl = endpoint;
if (startingVersion) {
requestUrl = mergeUrlParams({ start_version_id: startingVersion }, requestUrl);
}
dispatch('requestDescriptionVersion');
return axios
.get(requestUrl)
.then(res => res.data)
.catch(() => {
.then(res => {
dispatch('receiveDescriptionVersion', res.data);
})
.catch(error => {
dispatch('receiveDescriptionVersionError', error);
Flash(__('Something went wrong while fetching description changes. Please try again.'));
});
};
export const setCurrentDiscussionId = ({ commit }, discussionId) =>
commit(types.SET_CURRENT_DISCUSSION_ID, discussionId);
export const requestDescriptionVersion = ({ commit }) => {
commit(types.REQUEST_DESCRIPTION_VERSION);
};
export const receiveDescriptionVersion = ({ commit }, descriptionVersion) => {
commit(types.RECEIVE_DESCRIPTION_VERSION, descriptionVersion);
};
export const receiveDescriptionVersionError = ({ commit }, error) => {
commit(types.RECEIVE_DESCRIPTION_VERSION_ERROR, error);
};
export const softDeleteDescriptionVersion = ({ dispatch }, { endpoint, startingVersion }) => {
let requestUrl = endpoint;
if (startingVersion) {
requestUrl = mergeUrlParams({ start_version_id: startingVersion }, requestUrl);
}
dispatch('requestDeleteDescriptionVersion');
return axios
.delete(requestUrl)
.then(() => {
dispatch('receiveDeleteDescriptionVersion');
})
.catch(error => {
dispatch('receiveDeleteDescriptionVersionError', error);
Flash(__('Something went wrong while deleting description changes. Please try again.'));
});
};
export const requestDeleteDescriptionVersion = ({ commit }) => {
commit(types.REQUEST_DELETE_DESCRIPTION_VERSION);
};
export const receiveDeleteDescriptionVersion = ({ commit }) => {
commit(types.RECEIVE_DELETE_DESCRIPTION_VERSION, __('Deleted'));
};
export const receiveDeleteDescriptionVersionError = ({ commit }, error) => {
commit(types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR, error);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import { DESCRIPTION_TYPE } from '../constants';
import { DESCRIPTION_TYPE, TIME_DIFFERENCE_VALUE } from '../constants';
/**
* Checks the time difference between two notes from their 'created_at' dates
......@@ -45,7 +45,11 @@ export const collapseSystemNotes = notes => {
const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note);
// are they less than 10 minutes apart from the same user?
if (timeDifferenceMinutes > 10 || note.author.id !== lastDescriptionSystemNote.author.id) {
if (
timeDifferenceMinutes > TIME_DIFFERENCE_VALUE ||
note.author.id !== lastDescriptionSystemNote.author.id ||
lastDescriptionSystemNote.description_version_deleted
) {
// update the previous system note
lastDescriptionSystemNote = note;
lastDescriptionSystemNoteIndex = acc.length;
......
......@@ -14,6 +14,7 @@ export default () => ({
isToggleStateButtonLoading: false,
isNotesFetched: false,
isLoading: true,
isLoadingDescriptionVersion: false,
// holds endpoints and permissions provided through haml
notesData: {
......@@ -27,6 +28,7 @@ export default () => ({
commentsDisabled: false,
resolvableDiscussionsCount: 0,
unresolvedDiscussionsCount: 0,
descriptionVersion: null,
},
actions,
getters,
......
......@@ -31,3 +31,11 @@ export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID';
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING';
// Description version
export const REQUEST_DESCRIPTION_VERSION = 'REQUEST_DESCRIPTION_VERSION';
export const RECEIVE_DESCRIPTION_VERSION = 'RECEIVE_DESCRIPTION_VERSION';
export const RECEIVE_DESCRIPTION_VERSION_ERROR = 'RECEIVE_DESCRIPTION_VERSION_ERROR';
export const REQUEST_DELETE_DESCRIPTION_VERSION = 'REQUEST_DELETE_DESCRIPTION_VERSION';
export const RECEIVE_DELETE_DESCRIPTION_VERSION = 'RECEIVE_DELETE_DESCRIPTION_VERSION';
export const RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR = 'RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR';
......@@ -284,4 +284,25 @@ export default {
[types.SET_CURRENT_DISCUSSION_ID](state, discussionId) {
state.currentDiscussionId = discussionId;
},
[types.REQUEST_DESCRIPTION_VERSION](state) {
state.isLoadingDescriptionVersion = true;
},
[types.RECEIVE_DESCRIPTION_VERSION](state, descriptionVersion) {
state.isLoadingDescriptionVersion = false;
state.descriptionVersion = descriptionVersion;
},
[types.RECEIVE_DESCRIPTION_VERSION_ERROR](state) {
state.isLoadingDescriptionVersion = false;
},
[types.REQUEST_DELETE_DESCRIPTION_VERSION](state) {
state.isLoadingDescriptionVersion = true;
},
[types.RECEIVE_DELETE_DESCRIPTION_VERSION](state, descriptionVersion) {
state.isLoadingDescriptionVersion = false;
state.descriptionVersion = descriptionVersion;
},
[types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR](state) {
state.isLoadingDescriptionVersion = false;
},
};
......@@ -132,7 +132,7 @@ export default () => {
});
axios
.get(dataset.testReportEndpoint)
.get(dataset.testReportsCountEndpoint)
.then(({ data }) => {
document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count;
})
......
......@@ -17,11 +17,12 @@
* />
*/
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { GlSkeletonLoading } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import { GlButton, GlSkeletonLoading, GlTooltipDirective } from '@gitlab/ui';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
import noteHeader from '~/notes/components/note_header.vue';
import Icon from '~/vue_shared/components/icon.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
import initMRPopovers from '~/mr_popover/';
......@@ -34,9 +35,13 @@ export default {
Icon,
noteHeader,
TimelineEntryItem,
GlButton,
GlSkeletonLoading,
},
mixins: [descriptionVersionHistoryMixin],
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [descriptionVersionHistoryMixin, glFeatureFlagsMixin()],
props: {
note: {
type: Object,
......@@ -50,6 +55,7 @@ export default {
},
computed: {
...mapGetters(['targetNoteHash']),
...mapState(['descriptionVersion', 'isLoadingDescriptionVersion']),
noteAnchorId() {
return `note_${this.note.id}`;
},
......@@ -80,7 +86,7 @@ export default {
initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request'));
},
methods: {
...mapActions(['fetchDescriptionVersion']),
...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']),
},
};
</script>
......@@ -122,6 +128,16 @@ export default {
<gl-skeleton-loading />
</pre>
<pre v-else class="wrapper mt-2" v-html="descriptionVersion"></pre>
<gl-button
v-if="canDeleteDescriptionVersion"
ref="deleteDescriptionVersionButton"
v-gl-tooltip
:title="__('Remove description history')"
class="btn-transparent delete-description-history"
@click="deleteDescriptionVersion"
>
<icon name="remove" />
</gl-button>
</div>
</div>
</div>
......
......@@ -311,13 +311,18 @@ $note-form-margin-left: 72px;
overflow: hidden;
.description-version {
position: relative;
.btn.delete-description-history {
position: absolute;
top: 18px;
right: 0;
}
pre {
max-height: $dropdown-max-height-lg;
white-space: pre-wrap;
&.loading-state {
height: 94px;
}
padding-right: 30px;
}
}
......
......@@ -18,6 +18,7 @@ def self.failure_reasons
unmet_prerequisites: 10,
scheduler_failure: 11,
data_integrity_failure: 12,
forward_deployment_failure: 13,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
......
......@@ -41,6 +41,9 @@ class Deployment < ApplicationRecord
scope :visible, -> { where(status: %i[running success failed canceled]) }
scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success }
scope :active, -> { where(status: %i[created running]) }
scope :older_than, -> (deployment) { where('id < ?', deployment.id) }
scope :with_deployable, -> { includes(:deployable).where('deployable_id IS NOT NULL') }
state_machine :status, initial: :created do
event :run do
......@@ -74,6 +77,14 @@ class Deployment < ApplicationRecord
Deployments::FinishedWorker.perform_async(id)
end
end
after_transition any => :running do |deployment|
next unless deployment.project.forward_deployment_enabled?
deployment.run_after_commit do
Deployments::ForwardDeploymentWorker.perform_async(id)
end
end
end
enum status: {
......
......@@ -12,6 +12,7 @@ class Environment < ApplicationRecord
has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :successful_deployments, -> { success }, class_name: 'Deployment'
has_many :active_deployments, -> { active }, class_name: 'Deployment'
has_many :prometheus_alerts, inverse_of: :environment
has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
......
......@@ -75,6 +75,7 @@ class Member < ApplicationRecord
scope :reporters, -> { active.where(access_level: REPORTER) }
scope :developers, -> { active.where(access_level: DEVELOPER) }
scope :maintainers, -> { active.where(access_level: MAINTAINER) }
scope :non_guests, -> { where('members.access_level > ?', GUEST) }
scope :masters, -> { maintainers } # @deprecated
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment