Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5b4eca2afd
commit
72797f4a60
56 changed files with 2105 additions and 94 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -2,22 +2,6 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 12.10.3 (2020-05-04)
|
||||
|
||||
### Fixed (6 changes)
|
||||
|
||||
- Fix errors creating project with active Prometheus service template. !30340
|
||||
- Fix incorrect commits number in commits list. !30412
|
||||
- Fix second 500 error with NULL restricted visibility levels. !30414
|
||||
- Add LFS badge feature flag to RefsController#logs_tree. !30442
|
||||
- Disable schema dumping after migrations in production. !30812
|
||||
- Fixes branch name not getting escaped correctly on frontend.
|
||||
|
||||
### Changed (1 change)
|
||||
|
||||
- Handle possible RSA key exceptions when generating CI_JOB_JWT. !30702
|
||||
|
||||
|
||||
## 12.10.2 (2020-04-30)
|
||||
|
||||
### Security (8 changes)
|
||||
|
|
3
app/assets/javascripts/actioncable_consumer.js
Normal file
3
app/assets/javascripts/actioncable_consumer.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { createConsumer } from '@rails/actioncable';
|
||||
|
||||
export default createConsumer();
|
|
@ -1,5 +1,7 @@
|
|||
import white from './white';
|
||||
import dark from './dark';
|
||||
import monokai from './monokai';
|
||||
import solarizedDark from './solarized_dark';
|
||||
|
||||
export const themes = [
|
||||
{
|
||||
|
@ -10,6 +12,14 @@ export const themes = [
|
|||
name: 'dark',
|
||||
data: dark,
|
||||
},
|
||||
{
|
||||
name: 'solarized-dark',
|
||||
data: solarizedDark,
|
||||
},
|
||||
{
|
||||
name: 'monokai',
|
||||
data: monokai,
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_THEME = 'white';
|
||||
|
|
169
app/assets/javascripts/ide/lib/themes/monokai.js
Normal file
169
app/assets/javascripts/ide/lib/themes/monokai.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
|
||||
https://github.com/brijeshb42/monaco-themes/blob/master/themes/Tomorrow-Night.json
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Brijesh Bittu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
export default {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{
|
||||
foreground: '75715e',
|
||||
token: 'comment',
|
||||
},
|
||||
{
|
||||
foreground: 'e6db74',
|
||||
token: 'string',
|
||||
},
|
||||
{
|
||||
foreground: 'ae81ff',
|
||||
token: 'constant.numeric',
|
||||
},
|
||||
{
|
||||
foreground: 'ae81ff',
|
||||
token: 'constant.language',
|
||||
},
|
||||
{
|
||||
foreground: 'ae81ff',
|
||||
token: 'constant.character',
|
||||
},
|
||||
{
|
||||
foreground: 'ae81ff',
|
||||
token: 'constant.other',
|
||||
},
|
||||
{
|
||||
foreground: 'f92672',
|
||||
token: 'keyword',
|
||||
},
|
||||
{
|
||||
foreground: 'f92672',
|
||||
token: 'storage',
|
||||
},
|
||||
{
|
||||
foreground: '66d9ef',
|
||||
fontStyle: 'italic',
|
||||
token: 'storage.type',
|
||||
},
|
||||
{
|
||||
foreground: 'a6e22e',
|
||||
fontStyle: 'underline',
|
||||
token: 'entity.name.class',
|
||||
},
|
||||
{
|
||||
foreground: 'a6e22e',
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
fontStyle: 'italic underline',
|
||||
token: 'entity.other.inherited-class',
|
||||
},
|
||||
{
|
||||
foreground: 'a6e22e',
|
||||
token: 'entity.name.function',
|
||||
},
|
||||
{
|
||||
foreground: 'fd971f',
|
||||
fontStyle: 'italic',
|
||||
token: 'variable.parameter',
|
||||
},
|
||||
{
|
||||
foreground: 'f92672',
|
||||
token: 'entity.name.tag',
|
||||
},
|
||||
{
|
||||
foreground: 'a6e22e',
|
||||
token: 'entity.other.attribute-name',
|
||||
},
|
||||
{
|
||||
foreground: '66d9ef',
|
||||
token: 'support.function',
|
||||
},
|
||||
{
|
||||
foreground: '66d9ef',
|
||||
token: 'support.constant',
|
||||
},
|
||||
{
|
||||
foreground: '66d9ef',
|
||||
fontStyle: 'italic',
|
||||
token: 'support.type',
|
||||
},
|
||||
{
|
||||
foreground: '66d9ef',
|
||||
fontStyle: 'italic',
|
||||
token: 'support.class',
|
||||
},
|
||||
{
|
||||
foreground: 'f8f8f0',
|
||||
background: 'f92672',
|
||||
token: 'invalid',
|
||||
},
|
||||
{
|
||||
foreground: 'f8f8f0',
|
||||
background: 'ae81ff',
|
||||
token: 'invalid.deprecated',
|
||||
},
|
||||
{
|
||||
foreground: 'cfcfc2',
|
||||
token: 'meta.structure.dictionary.json string.quoted.double.json',
|
||||
},
|
||||
{
|
||||
foreground: '75715e',
|
||||
token: 'meta.diff',
|
||||
},
|
||||
{
|
||||
foreground: '75715e',
|
||||
token: 'meta.diff.header',
|
||||
},
|
||||
{
|
||||
foreground: 'f92672',
|
||||
token: 'markup.deleted',
|
||||
},
|
||||
{
|
||||
foreground: 'a6e22e',
|
||||
token: 'markup.inserted',
|
||||
},
|
||||
{
|
||||
foreground: 'e6db74',
|
||||
token: 'markup.changed',
|
||||
},
|
||||
{
|
||||
foreground: 'ae81ffa0',
|
||||
token: 'constant.numeric.line-number.find-in-files - match',
|
||||
},
|
||||
{
|
||||
foreground: 'e6db74',
|
||||
token: 'entity.name.filename.find-in-files',
|
||||
},
|
||||
],
|
||||
colors: {
|
||||
'editor.foreground': '#F8F8F2',
|
||||
'editor.background': '#272822',
|
||||
'editor.selectionBackground': '#49483E',
|
||||
'editor.lineHighlightBackground': '#3E3D32',
|
||||
'editorCursor.foreground': '#F8F8F0',
|
||||
'editorWhitespace.foreground': '#3B3A32',
|
||||
'editorIndentGuide.activeBackground': '#9D550FB0',
|
||||
'editor.selectionHighlightBorder': '#222218',
|
||||
},
|
||||
};
|
1110
app/assets/javascripts/ide/lib/themes/solarized_dark.js
Normal file
1110
app/assets/javascripts/ide/lib/themes/solarized_dark.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,15 @@
|
|||
#import "~/graphql_shared/fragments/author.fragment.graphql"
|
||||
|
||||
query getProjectIssue($iid: String!, $fullPath: ID!) {
|
||||
project(fullPath: $fullPath) {
|
||||
issue(iid: $iid) {
|
||||
assignees {
|
||||
nodes {
|
||||
...Author
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
// `e.keyCode` is deprecated, these values should be migrated
|
||||
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/216102
|
||||
|
||||
export const BACKSPACE_KEY_CODE = 8;
|
||||
export const ENTER_KEY_CODE = 13;
|
||||
export const ESC_KEY_CODE = 27;
|
||||
|
|
4
app/assets/javascripts/lib/utils/keys.js
Normal file
4
app/assets/javascripts/lib/utils/keys.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
|
||||
export const ESC_KEY = 'Escape';
|
||||
export const ESC_KEY_IE11 = 'Esc'; // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
|
|
@ -19,6 +19,7 @@ import {
|
|||
import DashboardPanel from './dashboard_panel.vue';
|
||||
import { s__ } from '~/locale';
|
||||
import createFlash from '~/flash';
|
||||
import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys';
|
||||
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
|
||||
import { mergeUrlParams, redirectTo, updateHistory } from '~/lib/utils/url_utility';
|
||||
import invalidUrl from '~/lib/utils/invalid_url';
|
||||
|
@ -248,6 +249,10 @@ export default {
|
|||
logsPath: this.logsPath,
|
||||
currentEnvironmentName: this.currentEnvironmentName,
|
||||
});
|
||||
window.addEventListener('keyup', this.onKeyup);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('keyup', this.onKeyup);
|
||||
},
|
||||
mounted() {
|
||||
if (!this.hasMetrics) {
|
||||
|
@ -371,13 +376,19 @@ export default {
|
|||
onGoBack() {
|
||||
this.clearExpandedPanel();
|
||||
},
|
||||
onKeyup(event) {
|
||||
const { key } = event;
|
||||
if (key === ESC_KEY || key === ESC_KEY_IE11) {
|
||||
this.clearExpandedPanel();
|
||||
}
|
||||
},
|
||||
},
|
||||
addMetric: {
|
||||
title: s__('Metrics|Add metric'),
|
||||
modalId: 'add-metric',
|
||||
},
|
||||
i18n: {
|
||||
goBackLabel: s__('Metrics|Go back'),
|
||||
goBackLabel: s__('Metrics|Go back (Esc)'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -29,9 +29,6 @@ export default {
|
|||
resolveAllDiscussionsIssuePath() {
|
||||
return this.getNoteableData.create_issue_to_resolve_discussions_path;
|
||||
},
|
||||
resolvedDiscussionsCount() {
|
||||
return this.resolvableDiscussionsCount - this.unresolvedDiscussionsCount;
|
||||
},
|
||||
toggeableDiscussions() {
|
||||
return this.discussions.filter(discussion => !discussion.individual_note);
|
||||
},
|
||||
|
@ -60,15 +57,15 @@ export default {
|
|||
<div class="full-width-mobile d-flex d-sm-flex">
|
||||
<div class="line-resolve-all">
|
||||
<span
|
||||
:class="{ 'is-active': allResolved }"
|
||||
class="line-resolve-btn is-disabled"
|
||||
type="button"
|
||||
:class="{ 'line-resolve-btn is-active': allResolved, 'line-resolve-text': !allResolved }"
|
||||
>
|
||||
<icon :name="allResolved ? 'check-circle-filled' : 'check-circle'" />
|
||||
</span>
|
||||
<span class="line-resolve-text">
|
||||
{{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }}
|
||||
{{ n__('thread resolved', 'threads resolved', resolvableDiscussionsCount) }}
|
||||
<template v-if="allResolved">
|
||||
<icon name="check-circle-filled" />
|
||||
{{ __('All threads resolved') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ n__('%d unresolved thread', '%d unresolved threads', unresolvedDiscussionsCount) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
<script>
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql';
|
||||
import actionCable from '~/actioncable_consumer';
|
||||
|
||||
export default {
|
||||
name: 'AssigneesRealtime',
|
||||
props: {
|
||||
mediator: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
issuableIid: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
project: {
|
||||
query,
|
||||
variables() {
|
||||
return {
|
||||
iid: this.issuableIid,
|
||||
fullPath: this.projectPath,
|
||||
};
|
||||
},
|
||||
result(data) {
|
||||
this.handleFetchResult(data);
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initActionCablePolling();
|
||||
},
|
||||
methods: {
|
||||
received(data) {
|
||||
if (data.event === 'updated') {
|
||||
this.$apollo.queries.project.refetch();
|
||||
}
|
||||
},
|
||||
initActionCablePolling() {
|
||||
actionCable.subscriptions.create(
|
||||
{
|
||||
channel: 'IssuesChannel',
|
||||
project_path: this.projectPath,
|
||||
iid: this.issuableIid,
|
||||
},
|
||||
{ received: this.received },
|
||||
);
|
||||
},
|
||||
handleFetchResult({ data }) {
|
||||
const { nodes } = data.project.issue.assignees;
|
||||
|
||||
const assignees = nodes.map(n => ({
|
||||
...n,
|
||||
avatar_url: n.avatarUrl,
|
||||
id: getIdFromGraphQLId(n.id),
|
||||
}));
|
||||
|
||||
this.mediator.store.setAssigneesFromRealtime(assignees);
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return this.$slots.default;
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -3,8 +3,10 @@ import Flash from '~/flash';
|
|||
import eventHub from '~/sidebar/event_hub';
|
||||
import Store from '~/sidebar/stores/sidebar_store';
|
||||
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import AssigneeTitle from './assignee_title.vue';
|
||||
import Assignees from './assignees.vue';
|
||||
import AssigneesRealtime from './assignees_realtime.vue';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
|
@ -12,7 +14,9 @@ export default {
|
|||
components: {
|
||||
AssigneeTitle,
|
||||
Assignees,
|
||||
AssigneesRealtime,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
mediator: {
|
||||
type: Object,
|
||||
|
@ -32,6 +36,14 @@ export default {
|
|||
required: false,
|
||||
default: 'issue',
|
||||
},
|
||||
issuableIid: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -39,6 +51,12 @@ export default {
|
|||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
shouldEnableRealtime() {
|
||||
// Note: Realtime is only available on issues right now, future support for MR wil be built later.
|
||||
return this.glFeatures.realTimeIssueSidebar && this.issuableType === 'issue';
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.removeAssignee = this.store.removeAssignee.bind(this.store);
|
||||
this.addAssignee = this.store.addAssignee.bind(this.store);
|
||||
|
@ -84,6 +102,12 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<assignees-realtime
|
||||
v-if="shouldEnableRealtime"
|
||||
:issuable-iid="issuableIid"
|
||||
:project-path="projectPath"
|
||||
:mediator="mediator"
|
||||
/>
|
||||
<assignee-title
|
||||
:number-of-assignees="store.assignees.length"
|
||||
:loading="loading || store.isFetching.assignees"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
|
||||
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
|
||||
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
|
||||
|
@ -8,17 +9,28 @@ import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
|
|||
import sidebarParticipants from './components/participants/sidebar_participants.vue';
|
||||
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
|
||||
import Translate from '../vue_shared/translate';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
|
||||
Vue.use(Translate);
|
||||
Vue.use(VueApollo);
|
||||
|
||||
function getSidebarOptions() {
|
||||
return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
|
||||
}
|
||||
|
||||
function mountAssigneesComponent(mediator) {
|
||||
const el = document.getElementById('js-vue-sidebar-assignees');
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
if (!el) return;
|
||||
|
||||
const { iid, fullPath } = getSidebarOptions();
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
apolloProvider,
|
||||
components: {
|
||||
SidebarAssignees,
|
||||
},
|
||||
|
@ -26,6 +38,8 @@ function mountAssigneesComponent(mediator) {
|
|||
createElement('sidebar-assignees', {
|
||||
props: {
|
||||
mediator,
|
||||
issuableIid: String(iid),
|
||||
projectPath: fullPath,
|
||||
field: el.dataset.field,
|
||||
signedIn: el.hasAttribute('data-signed-in'),
|
||||
issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
|
||||
|
@ -144,6 +158,4 @@ export function mountSidebar(mediator) {
|
|||
mountTimeTrackingComponent();
|
||||
}
|
||||
|
||||
export function getSidebarOptions() {
|
||||
return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
|
||||
}
|
||||
export { getSidebarOptions };
|
||||
|
|
|
@ -89,6 +89,10 @@ export default class SidebarStore {
|
|||
this.assignees = [];
|
||||
}
|
||||
|
||||
setAssigneesFromRealtime(data) {
|
||||
this.assignees = data;
|
||||
}
|
||||
|
||||
setAutocompleteProjects(projects) {
|
||||
this.autocompleteProjects = projects;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,9 @@
|
|||
$btn-disabled-border: rgba(223, 223, 223, 0.24);
|
||||
$btn-disabled-color: rgba(145, 145, 145, 0.48);
|
||||
|
||||
$dropdown-background: #404040;
|
||||
$dropdown-hover-background: #525252;
|
||||
|
||||
$diff-insert: rgba(155, 185, 85, 0.2);
|
||||
$diff-remove: rgba(255, 0, 0, 0.2);
|
||||
|
||||
|
@ -54,7 +57,12 @@
|
|||
textarea,
|
||||
.md-area.is-focused,
|
||||
.ide-entry-dropdown-toggle,
|
||||
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover {
|
||||
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover,
|
||||
.dropdown-menu li button,
|
||||
.ide-merge-request-project-path,
|
||||
.dropdown-menu-selectable li a.is-active,
|
||||
.dropdown-menu-inner-title,
|
||||
.dropdown-menu-inner-content {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
|
@ -82,11 +90,17 @@
|
|||
color: $text-color !important;
|
||||
}
|
||||
|
||||
input[type='search']::placeholder,
|
||||
input[type='text']::placeholder,
|
||||
textarea::placeholder {
|
||||
textarea::placeholder,
|
||||
.dropdown-input .fa {
|
||||
color: $input-border;
|
||||
}
|
||||
|
||||
.ide-nav-form .input-icon {
|
||||
fill: $input-border;
|
||||
}
|
||||
|
||||
.ide-staged-action-btn {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
@ -112,7 +126,8 @@
|
|||
background-color: inherit;
|
||||
}
|
||||
|
||||
.ide-sidebar-link:hover {
|
||||
.ide-sidebar-link:hover,
|
||||
.multi-file-tabs li {
|
||||
background-color: $background-hover;
|
||||
}
|
||||
|
||||
|
@ -204,21 +219,40 @@
|
|||
background-color: $footer-background;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
input[type='text'],
|
||||
input[type='search'],
|
||||
.filtered-search-box {
|
||||
border-color: $input-border;
|
||||
background: $input-background;
|
||||
background-color: $input-background;
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
input[type='search'],
|
||||
.filtered-search-box,
|
||||
textarea {
|
||||
color: $input-color !important;
|
||||
}
|
||||
|
||||
.filtered-search-box input[type='search'] {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.filtered-search-token .value-container,
|
||||
.filtered-search-term .value-container {
|
||||
background-color: $dropdown-hover-background;
|
||||
|
||||
color: $text-color;
|
||||
|
||||
&:hover {
|
||||
background-color: $input-border;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-entry-dropdown-toggle:hover {
|
||||
background: $gray-800;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
.btn:not(.btn-link):hover {
|
||||
border-width: 2px;
|
||||
padding: 5px 9px;
|
||||
}
|
||||
|
@ -257,6 +291,48 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
color: $text-color;
|
||||
border-color: $background;
|
||||
background-color: $dropdown-background;
|
||||
|
||||
.divider,
|
||||
.nav-links:not(.quick-links) {
|
||||
background-color: $dropdown-hover-background;
|
||||
border-color: $dropdown-hover-background;
|
||||
}
|
||||
|
||||
.nav-links li a.active {
|
||||
border-color: $highlight-accent;
|
||||
}
|
||||
|
||||
.ide-nav-form .nav-links li a:not(.active) {
|
||||
background-color: $dropdown-background;
|
||||
}
|
||||
|
||||
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a {
|
||||
color: $text-color;
|
||||
|
||||
&.active {
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
li > a:not(.disable-hover):hover,
|
||||
li > a:not(.disable-hover):focus,
|
||||
li button:not(.disable-hover):hover,
|
||||
li button:not(.disable-hover):focus,
|
||||
li button.is-focused {
|
||||
background-color: $dropdown-hover-background;
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-title,
|
||||
.dropdown-input {
|
||||
border-color: $dropdown-hover-background !important;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: $btn-primary-background;
|
||||
border-color: $btn-primary-border !important;
|
||||
|
@ -320,3 +396,7 @@
|
|||
.navbar.theme-dark {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.theme-dark ~ .popover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
|
|
@ -908,11 +908,10 @@ $note-form-margin-left: 72px;
|
|||
border-right: 0;
|
||||
|
||||
.line-resolve-btn {
|
||||
margin-right: 5px;
|
||||
color: $gray-700;
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,10 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:save_issuable_health_status, project.group, default_enabled: true)
|
||||
end
|
||||
|
||||
before_action only: :show do
|
||||
push_frontend_feature_flag(:real_time_issue_sidebar, @project)
|
||||
end
|
||||
|
||||
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
|
||||
|
||||
respond_to :html
|
||||
|
|
|
@ -14,6 +14,8 @@ module Types
|
|||
description: 'ID of the user'
|
||||
field :name, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Human-readable name of the user'
|
||||
field :state, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'State of the issue'
|
||||
field :username, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Username of the user. Unique within this instance of GitLab'
|
||||
field :avatar_url, GraphQL::STRING_TYPE, null: true,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DesignManagement
|
||||
class DesignAtVersionPolicy < ::BasePolicy
|
||||
delegate { @subject.version }
|
||||
delegate { @subject.design }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DesignManagement
|
||||
class DesignCollectionPolicy < DesignPolicy
|
||||
# Delegates everything to the `issue` just like the `DesignPolicy`
|
||||
end
|
||||
end
|
8
app/policies/design_management/design_policy.rb
Normal file
8
app/policies/design_management/design_policy.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DesignManagement
|
||||
class DesignPolicy < ::BasePolicy
|
||||
# The IssuePolicy will delegate to the ProjectPolicy
|
||||
delegate { @subject.issue }
|
||||
end
|
||||
end
|
8
app/policies/design_management/version_policy.rb
Normal file
8
app/policies/design_management/version_policy.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DesignManagement
|
||||
class VersionPolicy < ::BasePolicy
|
||||
# The IssuePolicy will delegate to the ProjectPolicy
|
||||
delegate { @subject.issue }
|
||||
end
|
||||
end
|
|
@ -15,6 +15,9 @@ class IssuePolicy < IssuablePolicy
|
|||
desc "Issue is confidential"
|
||||
condition(:confidential, scope: :subject) { @subject.confidential? }
|
||||
|
||||
desc "Issue has moved"
|
||||
condition(:moved) { @subject.moved? }
|
||||
|
||||
rule { confidential & ~can_read_confidential }.policy do
|
||||
prevent(*create_read_update_admin_destroy(:issue))
|
||||
prevent :read_issue_iid
|
||||
|
@ -25,6 +28,15 @@ class IssuePolicy < IssuablePolicy
|
|||
rule { locked }.policy do
|
||||
prevent :reopen_issue
|
||||
end
|
||||
end
|
||||
|
||||
IssuePolicy.prepend_if_ee('::EE::IssuePolicy')
|
||||
rule { ~can?(:read_issue) }.policy do
|
||||
prevent :read_design
|
||||
prevent :create_design
|
||||
prevent :destroy_design
|
||||
end
|
||||
|
||||
rule { locked | moved }.policy do
|
||||
prevent :create_design
|
||||
prevent :destroy_design
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,7 @@ class ProjectPolicy < BasePolicy
|
|||
milestone
|
||||
snippet
|
||||
wiki
|
||||
design
|
||||
note
|
||||
pipeline
|
||||
pipeline_schedule
|
||||
|
@ -107,6 +108,11 @@ class ProjectPolicy < BasePolicy
|
|||
)
|
||||
end
|
||||
|
||||
with_scope :subject
|
||||
condition(:design_management_disabled) do
|
||||
!@subject.design_management_enabled?
|
||||
end
|
||||
|
||||
# We aren't checking `:read_issue` or `:read_merge_request` in this case
|
||||
# because it could be possible for a user to see an issuable-iid
|
||||
# (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
|
||||
|
@ -299,6 +305,8 @@ class ProjectPolicy < BasePolicy
|
|||
enable :create_metrics_dashboard_annotation
|
||||
enable :delete_metrics_dashboard_annotation
|
||||
enable :update_metrics_dashboard_annotation
|
||||
enable :create_design
|
||||
enable :destroy_design
|
||||
end
|
||||
|
||||
rule { can?(:developer_access) & user_confirmed? }.policy do
|
||||
|
@ -511,6 +519,17 @@ class ProjectPolicy < BasePolicy
|
|||
|
||||
rule { admin }.enable :change_repository_storage
|
||||
|
||||
rule { can?(:read_issue) }.policy do
|
||||
enable :read_design
|
||||
end
|
||||
|
||||
# Design abilities could also be prevented in the issue policy.
|
||||
rule { design_management_disabled }.policy do
|
||||
prevent :read_design
|
||||
prevent :create_design
|
||||
prevent :destroy_design
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def team_member?
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
|
||||
= csrf_meta_tags
|
||||
= csp_meta_tag
|
||||
= action_cable_meta_tag
|
||||
|
||||
- unless browser.safari?
|
||||
%meta{ name: 'referrer', content: 'origin-when-cross-origin' }
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
%banner{ "v-if" => "!isOverviewDialogDismissed",
|
||||
"documentation-link": help_page_path('user/analytics/value_stream_analytics.md'),
|
||||
"v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" }
|
||||
.mb-3
|
||||
%h3
|
||||
= _("Value Stream Analytics")
|
||||
%gl-loading-icon{ "v-show" => "isLoading", "size" => "lg" }
|
||||
.wrapper{ "v-show" => "!isLoading && !hasError" }
|
||||
.card
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- issuable_type = issuable_sidebar[:type]
|
||||
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
|
||||
|
||||
#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}", signed_in: signed_in } }
|
||||
#js-vue-sidebar-assignees{ data: { field: issuable_type, signed_in: signed_in } }
|
||||
.title.hide-collapsed
|
||||
= _('Assignee')
|
||||
.spinner.spinner-sm.align-bottom
|
||||
|
|
5
changelogs/unreleased/201927-solarized-dark.yml
Normal file
5
changelogs/unreleased/201927-solarized-dark.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Monokai and Solarized Dark syntax highlighting theme for Web IDE
|
||||
merge_request: 30931
|
||||
author:
|
||||
type: added
|
6
changelogs/unreleased/214882-esc-key-handler.yml
Normal file
6
changelogs/unreleased/214882-esc-key-handler.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: When viewing a single panel, return to a full dashboard by pressing the Escape
|
||||
key
|
||||
merge_request: 30126
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix incorrect commits number in commits list
|
||||
merge_request: 30412
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Handle possible RSA key exceptions when generating CI_JOB_JWT
|
||||
merge_request: 30702
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/mw-cr-title-margin.yml
Normal file
5
changelogs/unreleased/mw-cr-title-margin.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Code review analytics: Change margin between title and description'
|
||||
merge_request: 30834
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/mw-ia-add-title.yml
Normal file
5
changelogs/unreleased/mw-ia-add-title.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Issues Analytics: Add title to page'
|
||||
merge_request: 30836
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/mw-insights-add-title.yml
Normal file
5
changelogs/unreleased/mw-insights-add-title.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Insights Analytics: Add title to page'
|
||||
merge_request: 30853
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/mw-vsa-title-cleanup.yml
Normal file
5
changelogs/unreleased/mw-vsa-title-cleanup.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Value Stream Analytics: Add title and remove separator'
|
||||
merge_request: 30841
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Change wording of merge request threads counter
|
||||
merge_request: 30217
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/ph-215917-escapeRef.yml
Normal file
5
changelogs/unreleased/ph-215917-escapeRef.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fixes branch name not getting escaped correctly on frontend
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix second 500 error with NULL restricted visibility levels
|
||||
merge_request: 30414
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/sh-disable-schema-dump-prod.yml
Normal file
5
changelogs/unreleased/sh-disable-schema-dump-prod.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Disable schema dumping after migrations in production
|
||||
merge_request: 30812
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix errors creating project with active Prometheus service template
|
||||
merge_request: 30340
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/sh-fix-lfs-badge-feature-flag.yml
Normal file
5
changelogs/unreleased/sh-fix-lfs-badge-feature-flag.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add LFS badge feature flag to RefsController#logs_tree
|
||||
merge_request: 30442
|
||||
author:
|
||||
type: fixed
|
|
@ -608,7 +608,7 @@ installations from source.
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19186) in GitLab 12.6.
|
||||
|
||||
This file lives in `/var/log/gitlab/mail_room/mail_room_json.log` for
|
||||
This file lives in `/var/log/gitlab/mailroom/mail_room_json.log` for
|
||||
Omnibus GitLab packages or in `/home/git/gitlab/log/mail_room_json.log` for
|
||||
installations from source.
|
||||
|
||||
|
@ -648,7 +648,7 @@ It's stored at:
|
|||
- `/var/log/gitlab/gitlab-rails/database_load_balancing.log` for Omnibus GitLab packages.
|
||||
- `/home/git/gitlab/log/database_load_balancing.log` for installations from source.
|
||||
|
||||
## `elasticsearch.log`
|
||||
## `elasticsearch.log` **(STARTER ONLY)**
|
||||
|
||||
> Introduced in GitLab 12.6.
|
||||
|
||||
|
@ -718,7 +718,7 @@ Each line contains a JSON line that can be ingested by Elasticsearch. For exampl
|
|||
}
|
||||
```
|
||||
|
||||
## `geo.log`
|
||||
## `geo.log` **(PREMIUM ONLY)**
|
||||
|
||||
> Introduced in 9.5.
|
||||
|
||||
|
|
|
@ -10091,6 +10091,11 @@ type User {
|
|||
visibility: VisibilityScopesEnum
|
||||
): SnippetConnection
|
||||
|
||||
"""
|
||||
State of the issue
|
||||
"""
|
||||
state: String!
|
||||
|
||||
"""
|
||||
Todos of the user
|
||||
"""
|
||||
|
|
|
@ -30221,6 +30221,24 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "state",
|
||||
"description": "State of the issue",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "todos",
|
||||
"description": "Todos of the user",
|
||||
|
|
|
@ -1567,6 +1567,7 @@ Autogenerated return type of UpdateSnippet
|
|||
| `avatarUrl` | String | URL of the user's avatar |
|
||||
| `id` | ID! | ID of the user |
|
||||
| `name` | String! | Human-readable name of the user |
|
||||
| `state` | String! | State of the issue |
|
||||
| `userPermissions` | UserPermissions! | Permissions for the current user on the resource |
|
||||
| `username` | String! | Username of the user. Unique within this instance of GitLab |
|
||||
| `webUrl` | String! | Web URL of the user |
|
||||
|
|
|
@ -184,6 +184,9 @@ This can help to quickly understand the control flow.
|
|||
// bad
|
||||
if (isThingNull) return '';
|
||||
|
||||
if (isThingNull)
|
||||
return '';
|
||||
|
||||
// good
|
||||
if (isThingNull) {
|
||||
return '';
|
||||
|
|
|
@ -214,6 +214,11 @@ msgid_plural "%d tags"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d unresolved thread"
|
||||
msgid_plural "%d unresolved threads"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d vulnerability dismissed"
|
||||
msgid_plural "%d vulnerabilities dismissed"
|
||||
msgstr[0] ""
|
||||
|
@ -1827,6 +1832,9 @@ msgstr ""
|
|||
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
|
||||
msgstr ""
|
||||
|
||||
msgid "All threads resolved"
|
||||
msgstr ""
|
||||
|
||||
msgid "All users"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13174,7 +13182,7 @@ msgstr ""
|
|||
msgid "Metrics|For grouping similar metrics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Go back"
|
||||
msgid "Metrics|Go back (Esc)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Invalid time range, please verify."
|
||||
|
@ -23434,6 +23442,9 @@ msgstr ""
|
|||
msgid "VulnerabilityStatusTypes|Resolved"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Class"
|
||||
msgstr ""
|
||||
|
||||
|
@ -23467,7 +23478,10 @@ msgstr ""
|
|||
msgid "Vulnerability|Project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Report Type"
|
||||
msgid "Vulnerability|Scanner Provider"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Scanner Type"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulnerability|Severity"
|
||||
|
@ -25699,11 +25713,6 @@ msgstr ""
|
|||
msgid "this document"
|
||||
msgstr ""
|
||||
|
||||
msgid "thread resolved"
|
||||
msgid_plural "threads resolved"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "to help your contributors communicate effectively!"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
"@gitlab/svgs": "1.121.0",
|
||||
"@gitlab/ui": "13.6.1",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "^6.0.2-2",
|
||||
"@sentry/browser": "^5.10.2",
|
||||
"@sourcegraph/code-host-integration": "0.0.37",
|
||||
"@toast-ui/editor": "^2.0.1",
|
||||
|
|
|
@ -43,7 +43,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
context 'single thread' do
|
||||
it 'shows text with how many threads' do
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('0/1 thread resolved')
|
||||
expect(page).to have_content('1 unresolved thread')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -60,7 +60,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('1/1 thread resolved')
|
||||
expect(page).to have_content('All threads resolved')
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
end
|
||||
|
@ -77,7 +77,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('1/1 thread resolved')
|
||||
expect(page).to have_content('All threads resolved')
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
end
|
||||
|
@ -89,7 +89,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('0/1 thread resolved')
|
||||
expect(page).to have_content('1 unresolved thread')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -162,7 +162,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('1/1 thread resolved')
|
||||
expect(page).to have_content('All threads resolved')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -174,7 +174,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('0/1 thread resolved')
|
||||
expect(page).to have_content('1 unresolved thread')
|
||||
expect(page).not_to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
end
|
||||
|
@ -189,7 +189,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('0/1 thread resolved')
|
||||
expect(page).to have_content('1 unresolved thread')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -203,7 +203,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('1/1 thread resolved')
|
||||
expect(page).to have_content('All threads resolved')
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
end
|
||||
|
@ -218,7 +218,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('1/1 thread resolved')
|
||||
expect(page).to have_content('All threads resolved')
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
end
|
||||
|
@ -275,7 +275,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
expect(page).to have_content('Last updated')
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('0/1 thread resolved')
|
||||
expect(page).to have_content('1 unresolved thread')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -292,7 +292,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('1/1 thread resolved')
|
||||
expect(page).to have_content('All threads resolved')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -305,7 +305,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
|
||||
it 'shows text with how many threads' do
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('0/2 threads resolved')
|
||||
expect(page).to have_content('2 unresolved threads')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -313,7 +313,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
click_button('Resolve thread', match: :first)
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('1/2 threads resolved')
|
||||
expect(page).to have_content('1 unresolved thread')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -323,7 +323,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('2/2 threads resolved')
|
||||
expect(page).to have_content('All threads resolved')
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
end
|
||||
|
@ -336,7 +336,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('2/2 threads resolved')
|
||||
expect(page).to have_content('All threads resolved')
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
end
|
||||
|
@ -392,7 +392,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
context 'changes tab' do
|
||||
it 'shows text with how many threads' do
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('0/1 thread resolved')
|
||||
expect(page).to have_content('1 unresolved thread')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -408,7 +408,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('1/1 thread resolved')
|
||||
expect(page).to have_content('All threads resolved')
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
end
|
||||
|
@ -423,7 +423,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('1/1 thread resolved')
|
||||
expect(page).to have_content('All threads resolved')
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
end
|
||||
|
@ -435,7 +435,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('0/1 thread resolved')
|
||||
expect(page).to have_content('1 unresolved thread')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -449,7 +449,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('1/1 thread resolved')
|
||||
expect(page).to have_content('All threads resolved')
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
end
|
||||
|
@ -466,7 +466,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('0/1 thread resolved')
|
||||
expect(page).to have_content('1 unresolved thread')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -489,7 +489,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('0/1 thread resolved')
|
||||
expect(page).to have_content('1 unresolved thread')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -519,7 +519,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('1/1 thread resolved')
|
||||
expect(page).to have_content('All threads resolved')
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
end
|
||||
|
@ -538,7 +538,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
page.within '.line-resolve-all-container' do
|
||||
expect(page).to have_content('0/1 thread resolved')
|
||||
expect(page).to have_content('1 unresolved thread')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -550,17 +550,17 @@ describe 'Merge request > User resolves diff notes and threads', :js do
|
|||
end
|
||||
|
||||
it 'shows resolved icon' do
|
||||
expect(page).to have_content '1/1 thread resolved'
|
||||
expect(page).to have_content 'All threads resolved'
|
||||
|
||||
click_button 'Toggle thread'
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
|
||||
it 'does not allow user to click resolve button' do
|
||||
expect(page).to have_selector('.line-resolve-btn.is-disabled')
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
click_button 'Toggle thread'
|
||||
|
||||
expect(page).to have_selector('.line-resolve-btn.is-disabled')
|
||||
expect(page).to have_selector('.line-resolve-btn.is-active')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import Tracking from '~/tracking';
|
||||
import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys';
|
||||
import { GlModal, GlDropdownItem, GlDeprecatedButton } from '@gitlab/ui';
|
||||
import VueDraggable from 'vuedraggable';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
@ -248,6 +249,8 @@ describe('Dashboard', () => {
|
|||
let group;
|
||||
let panel;
|
||||
|
||||
const mockKeyup = key => window.dispatchEvent(new KeyboardEvent('keyup', { key }));
|
||||
|
||||
const MockPanel = {
|
||||
template: `<div><slot name="topLeft"/></div>`,
|
||||
};
|
||||
|
@ -265,6 +268,9 @@ describe('Dashboard', () => {
|
|||
group,
|
||||
panel,
|
||||
});
|
||||
|
||||
jest.spyOn(store, 'dispatch');
|
||||
|
||||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
|
@ -289,17 +295,30 @@ describe('Dashboard', () => {
|
|||
});
|
||||
|
||||
it('restores full dashboard by clicking `back`', () => {
|
||||
const backBtn = wrapper.find({ ref: 'goBackBtn' });
|
||||
expect(backBtn.exists()).toBe(true);
|
||||
|
||||
jest.spyOn(store, 'dispatch');
|
||||
backBtn.vm.$emit('click');
|
||||
wrapper.find({ ref: 'goBackBtn' }).vm.$emit('click');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
'monitoringDashboard/clearExpandedPanel',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('restores dashboard from full screen by typing the Escape key', () => {
|
||||
mockKeyup(ESC_KEY);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
`monitoringDashboard/clearExpandedPanel`,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('restores dashboard from full screen by typing the Escape key on IE11', () => {
|
||||
mockKeyup(ESC_KEY_IE11);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
`monitoringDashboard/clearExpandedPanel`,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -75,15 +75,14 @@ describe('DiscussionCounter component', () => {
|
|||
});
|
||||
|
||||
it.each`
|
||||
title | resolved | isActive | icon | groupLength
|
||||
${'not allResolved'} | ${false} | ${false} | ${'check-circle'} | ${3}
|
||||
${'allResolved'} | ${true} | ${true} | ${'check-circle-filled'} | ${1}
|
||||
`('renders correctly if $title', ({ resolved, isActive, icon, groupLength }) => {
|
||||
title | resolved | isActive | groupLength
|
||||
${'not allResolved'} | ${false} | ${false} | ${3}
|
||||
${'allResolved'} | ${true} | ${true} | ${1}
|
||||
`('renders correctly if $title', ({ resolved, isActive, groupLength }) => {
|
||||
updateStore({ resolvable: true, resolved });
|
||||
wrapper = shallowMount(DiscussionCounter, { store, localVue });
|
||||
|
||||
expect(wrapper.find(`.is-active`).exists()).toBe(isActive);
|
||||
expect(wrapper.find({ name: icon }).exists()).toBe(true);
|
||||
expect(wrapper.findAll('[role="group"').length).toBe(groupLength);
|
||||
});
|
||||
});
|
||||
|
|
100
spec/frontend/sidebar/assignees_realtime_spec.js
Normal file
100
spec/frontend/sidebar/assignees_realtime_spec.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import ActionCable from '@rails/actioncable';
|
||||
import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
|
||||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||
import Mock from './mock_data';
|
||||
import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql';
|
||||
|
||||
jest.mock('@rails/actioncable', () => {
|
||||
const mockConsumer = { subscriptions: { create: jest.fn() } };
|
||||
return {
|
||||
createConsumer: jest.fn().mockReturnValue(mockConsumer),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Assignees Realtime', () => {
|
||||
let wrapper;
|
||||
let mediator;
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMount(AssigneesRealtime, {
|
||||
propsData: {
|
||||
issuableIid: '1',
|
||||
mediator,
|
||||
projectPath: 'path/to/project',
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
query,
|
||||
queries: {
|
||||
project: {
|
||||
refetch: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mediator = new SidebarMediator(Mock.mediator);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
SidebarMediator.singleton = null;
|
||||
});
|
||||
|
||||
describe('when handleFetchResult is called from smart query', () => {
|
||||
it('sets assignees to the store', () => {
|
||||
const data = {
|
||||
project: {
|
||||
issue: {
|
||||
assignees: {
|
||||
nodes: [{ id: 'gid://gitlab/Environments/123', avatarUrl: 'url' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = [{ id: 123, avatar_url: 'url', avatarUrl: 'url' }];
|
||||
createComponent();
|
||||
|
||||
wrapper.vm.handleFetchResult({ data });
|
||||
|
||||
expect(mediator.store.assignees).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when mounted', () => {
|
||||
it('calls create subscription', () => {
|
||||
const cable = ActionCable.createConsumer();
|
||||
|
||||
createComponent();
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(cable.subscriptions.create).toHaveBeenCalledTimes(1);
|
||||
expect(cable.subscriptions.create).toHaveBeenCalledWith(
|
||||
{
|
||||
channel: 'IssuesChannel',
|
||||
iid: wrapper.props('issuableIid'),
|
||||
project_path: wrapper.props('projectPath'),
|
||||
},
|
||||
{ received: wrapper.vm.received },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when subscription is recieved', () => {
|
||||
it('refetches the GraphQL project query', () => {
|
||||
createComponent();
|
||||
|
||||
wrapper.vm.received({ event: 'updated' });
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.$apollo.queries.project.refetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -3,6 +3,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
|
|||
import axios from 'axios';
|
||||
import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.vue';
|
||||
import Assigness from '~/sidebar/components/assignees/assignees.vue';
|
||||
import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
|
||||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||
import SidebarService from '~/sidebar/services/sidebar_service';
|
||||
import SidebarStore from '~/sidebar/stores/sidebar_store';
|
||||
|
@ -12,12 +13,19 @@ describe('sidebar assignees', () => {
|
|||
let wrapper;
|
||||
let mediator;
|
||||
let axiosMock;
|
||||
|
||||
const createComponent = () => {
|
||||
const createComponent = (realTimeIssueSidebar = false, props) => {
|
||||
wrapper = shallowMount(SidebarAssignees, {
|
||||
propsData: {
|
||||
issuableIid: '1',
|
||||
mediator,
|
||||
field: '',
|
||||
projectPath: 'projectPath',
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
realTimeIssueSidebar,
|
||||
},
|
||||
},
|
||||
// Attaching to document is required because this component emits something from the parent element :/
|
||||
attachToDocument: true,
|
||||
|
@ -30,8 +38,6 @@ describe('sidebar assignees', () => {
|
|||
|
||||
jest.spyOn(mediator, 'saveAssignees');
|
||||
jest.spyOn(mediator, 'assignYourself');
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -45,6 +51,8 @@ describe('sidebar assignees', () => {
|
|||
});
|
||||
|
||||
it('calls the mediator when saves the assignees', () => {
|
||||
createComponent();
|
||||
|
||||
expect(mediator.saveAssignees).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.vm.saveAssignees();
|
||||
|
@ -53,6 +61,8 @@ describe('sidebar assignees', () => {
|
|||
});
|
||||
|
||||
it('calls the mediator when "assignSelf" method is called', () => {
|
||||
createComponent();
|
||||
|
||||
expect(mediator.assignYourself).not.toHaveBeenCalled();
|
||||
expect(mediator.store.assignees.length).toBe(0);
|
||||
|
||||
|
@ -63,6 +73,8 @@ describe('sidebar assignees', () => {
|
|||
});
|
||||
|
||||
it('hides assignees until fetched', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.find(Assigness).exists()).toBe(false);
|
||||
|
||||
wrapper.vm.store.isFetching.assignees = false;
|
||||
|
@ -71,4 +83,30 @@ describe('sidebar assignees', () => {
|
|||
expect(wrapper.find(Assigness).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when realTimeIssueSidebar is turned on', () => {
|
||||
describe('when issuableType is issue', () => {
|
||||
it('finds AssigneesRealtime componeont', () => {
|
||||
createComponent(true);
|
||||
|
||||
expect(wrapper.find(AssigneesRealtime).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when issuableType is MR', () => {
|
||||
it('does not find AssigneesRealtime componeont', () => {
|
||||
createComponent(true, { issuableType: 'MR' });
|
||||
|
||||
expect(wrapper.find(AssigneesRealtime).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when realTimeIssueSidebar is turned off', () => {
|
||||
it('does not find AssigneesRealtime', () => {
|
||||
createComponent(false, { issuableType: 'issue' });
|
||||
|
||||
expect(wrapper.find(AssigneesRealtime).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ describe GitlabSchema.types['User'] do
|
|||
|
||||
it 'has the expected fields' do
|
||||
expected_fields = %w[
|
||||
id user_permissions snippets name username avatarUrl webUrl todos
|
||||
id user_permissions snippets name username avatarUrl webUrl todos state
|
||||
]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
|
|
174
spec/policies/design_management/design_policy_spec.rb
Normal file
174
spec/policies/design_management/design_policy_spec.rb
Normal file
|
@ -0,0 +1,174 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe DesignManagement::DesignPolicy do
|
||||
include DesignManagementTestHelpers
|
||||
|
||||
include_context 'ProjectPolicy context'
|
||||
|
||||
let(:guest_design_abilities) { %i[read_design] }
|
||||
let(:developer_design_abilities) do
|
||||
%i[create_design destroy_design]
|
||||
end
|
||||
let(:design_abilities) { guest_design_abilities + developer_design_abilities }
|
||||
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
let(:design) { create(:design, issue: issue) }
|
||||
|
||||
subject(:design_policy) { described_class.new(current_user, design) }
|
||||
|
||||
shared_examples_for "design abilities not available" do
|
||||
context "for owners" do
|
||||
let(:current_user) { owner }
|
||||
|
||||
it { is_expected.to be_disallowed(*design_abilities) }
|
||||
end
|
||||
|
||||
context "for admins" do
|
||||
let(:current_user) { admin }
|
||||
|
||||
it { is_expected.to be_disallowed(*design_abilities) }
|
||||
end
|
||||
|
||||
context "for maintainers" do
|
||||
let(:current_user) { maintainer }
|
||||
|
||||
it { is_expected.to be_disallowed(*design_abilities) }
|
||||
end
|
||||
|
||||
context "for developers" do
|
||||
let(:current_user) { developer }
|
||||
|
||||
it { is_expected.to be_disallowed(*design_abilities) }
|
||||
end
|
||||
|
||||
context "for reporters" do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
it { is_expected.to be_disallowed(*design_abilities) }
|
||||
end
|
||||
|
||||
context "for guests" do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it { is_expected.to be_disallowed(*design_abilities) }
|
||||
end
|
||||
|
||||
context "for anonymous users" do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it { is_expected.to be_disallowed(*design_abilities) }
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples_for "design abilities available for members" do
|
||||
context "for owners" do
|
||||
let(:current_user) { owner }
|
||||
|
||||
it { is_expected.to be_allowed(*design_abilities) }
|
||||
end
|
||||
|
||||
context "for admins" do
|
||||
let(:current_user) { admin }
|
||||
|
||||
it { is_expected.to be_allowed(*design_abilities) }
|
||||
end
|
||||
|
||||
context "for maintainers" do
|
||||
let(:current_user) { maintainer }
|
||||
|
||||
it { is_expected.to be_allowed(*design_abilities) }
|
||||
end
|
||||
|
||||
context "for developers" do
|
||||
let(:current_user) { developer }
|
||||
|
||||
it { is_expected.to be_allowed(*design_abilities) }
|
||||
end
|
||||
|
||||
context "for reporters" do
|
||||
let(:current_user) { reporter }
|
||||
|
||||
it { is_expected.to be_allowed(*guest_design_abilities) }
|
||||
it { is_expected.to be_disallowed(*developer_design_abilities) }
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples_for "read-only design abilities" do
|
||||
it { is_expected.to be_allowed(:read_design) }
|
||||
it { is_expected.to be_disallowed(:create_design, :destroy_design) }
|
||||
end
|
||||
|
||||
context "when DesignManagement is not enabled" do
|
||||
before do
|
||||
enable_design_management(false)
|
||||
end
|
||||
|
||||
it_behaves_like "design abilities not available"
|
||||
end
|
||||
|
||||
context "when the feature is available" do
|
||||
before do
|
||||
enable_design_management
|
||||
end
|
||||
|
||||
it_behaves_like "design abilities available for members"
|
||||
|
||||
context "for guests in private projects" do
|
||||
let(:project) { create(:project, :private) }
|
||||
let(:current_user) { guest }
|
||||
|
||||
it { is_expected.to be_allowed(*guest_design_abilities) }
|
||||
it { is_expected.to be_disallowed(*developer_design_abilities) }
|
||||
end
|
||||
|
||||
context "for anonymous users in public projects" do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it { is_expected.to be_allowed(*guest_design_abilities) }
|
||||
it { is_expected.to be_disallowed(*developer_design_abilities) }
|
||||
end
|
||||
|
||||
context "when the issue is confidential" do
|
||||
let(:issue) { create(:issue, :confidential, project: project) }
|
||||
|
||||
it_behaves_like "design abilities available for members"
|
||||
|
||||
context "for guests" do
|
||||
let(:current_user) { guest }
|
||||
|
||||
it { is_expected.to be_disallowed(*design_abilities) }
|
||||
end
|
||||
|
||||
context "for anonymous users" do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it { is_expected.to be_disallowed(*design_abilities) }
|
||||
end
|
||||
end
|
||||
|
||||
context "when the issue is locked" do
|
||||
let(:current_user) { owner }
|
||||
let(:issue) { create(:issue, :locked, project: project) }
|
||||
|
||||
it_behaves_like "read-only design abilities"
|
||||
end
|
||||
|
||||
context "when the issue has moved" do
|
||||
let(:current_user) { owner }
|
||||
let(:issue) { create(:issue, project: project, moved_to: create(:issue)) }
|
||||
|
||||
it_behaves_like "read-only design abilities"
|
||||
end
|
||||
|
||||
context "when the project is archived" do
|
||||
let(:current_user) { owner }
|
||||
|
||||
before do
|
||||
project.update!(archived: true)
|
||||
end
|
||||
|
||||
it_behaves_like "read-only design abilities"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -985,6 +985,11 @@
|
|||
consola "^2.10.1"
|
||||
node-fetch "^2.6.0"
|
||||
|
||||
"@rails/actioncable@^6.0.2-2":
|
||||
version "6.0.2-2"
|
||||
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.0.2-2.tgz#237907f8111707950381387c273b19ac25958408"
|
||||
integrity sha512-0sKStf8hnberH1TKup10PJ92JT2dVqf3gf+OT4lJ7DiYSBEuDcvICHxWsyML2oWTpjUhC4kLvUJ3pXL2JJrJuQ==
|
||||
|
||||
"@sentry/browser@^5.10.2":
|
||||
version "5.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.10.2.tgz#0bbb05505c58ea998c833cffec3f922fe4b4fa58"
|
||||
|
|
Loading…
Reference in a new issue