Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-04 12:09:46 +00:00
parent 5b4eca2afd
commit 72797f4a60
56 changed files with 2105 additions and 94 deletions

View file

@ -2,22 +2,6 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. 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) ## 12.10.2 (2020-04-30)
### Security (8 changes) ### Security (8 changes)

View file

@ -0,0 +1,3 @@
import { createConsumer } from '@rails/actioncable';
export default createConsumer();

View file

@ -1,5 +1,7 @@
import white from './white'; import white from './white';
import dark from './dark'; import dark from './dark';
import monokai from './monokai';
import solarizedDark from './solarized_dark';
export const themes = [ export const themes = [
{ {
@ -10,6 +12,14 @@ export const themes = [
name: 'dark', name: 'dark',
data: dark, data: dark,
}, },
{
name: 'solarized-dark',
data: solarizedDark,
},
{
name: 'monokai',
data: monokai,
},
]; ];
export const DEFAULT_THEME = 'white'; export const DEFAULT_THEME = 'white';

View 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',
},
};

File diff suppressed because it is too large Load diff

View file

@ -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
}
}
}
}
}

View file

@ -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 BACKSPACE_KEY_CODE = 8;
export const ENTER_KEY_CODE = 13; export const ENTER_KEY_CODE = 13;
export const ESC_KEY_CODE = 27; export const ESC_KEY_CODE = 27;

View 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

View file

@ -19,6 +19,7 @@ import {
import DashboardPanel from './dashboard_panel.vue'; import DashboardPanel from './dashboard_panel.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import createFlash from '~/flash'; 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 CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import { mergeUrlParams, redirectTo, updateHistory } from '~/lib/utils/url_utility'; import { mergeUrlParams, redirectTo, updateHistory } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url'; import invalidUrl from '~/lib/utils/invalid_url';
@ -248,6 +249,10 @@ export default {
logsPath: this.logsPath, logsPath: this.logsPath,
currentEnvironmentName: this.currentEnvironmentName, currentEnvironmentName: this.currentEnvironmentName,
}); });
window.addEventListener('keyup', this.onKeyup);
},
destroyed() {
window.removeEventListener('keyup', this.onKeyup);
}, },
mounted() { mounted() {
if (!this.hasMetrics) { if (!this.hasMetrics) {
@ -371,13 +376,19 @@ export default {
onGoBack() { onGoBack() {
this.clearExpandedPanel(); this.clearExpandedPanel();
}, },
onKeyup(event) {
const { key } = event;
if (key === ESC_KEY || key === ESC_KEY_IE11) {
this.clearExpandedPanel();
}
},
}, },
addMetric: { addMetric: {
title: s__('Metrics|Add metric'), title: s__('Metrics|Add metric'),
modalId: 'add-metric', modalId: 'add-metric',
}, },
i18n: { i18n: {
goBackLabel: s__('Metrics|Go back'), goBackLabel: s__('Metrics|Go back (Esc)'),
}, },
}; };
</script> </script>

View file

@ -29,9 +29,6 @@ export default {
resolveAllDiscussionsIssuePath() { resolveAllDiscussionsIssuePath() {
return this.getNoteableData.create_issue_to_resolve_discussions_path; return this.getNoteableData.create_issue_to_resolve_discussions_path;
}, },
resolvedDiscussionsCount() {
return this.resolvableDiscussionsCount - this.unresolvedDiscussionsCount;
},
toggeableDiscussions() { toggeableDiscussions() {
return this.discussions.filter(discussion => !discussion.individual_note); 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="full-width-mobile d-flex d-sm-flex">
<div class="line-resolve-all"> <div class="line-resolve-all">
<span <span
:class="{ 'is-active': allResolved }" :class="{ 'line-resolve-btn is-active': allResolved, 'line-resolve-text': !allResolved }"
class="line-resolve-btn is-disabled"
type="button"
> >
<icon :name="allResolved ? 'check-circle-filled' : 'check-circle'" /> <template v-if="allResolved">
</span> <icon name="check-circle-filled" />
<span class="line-resolve-text"> {{ __('All threads resolved') }}
{{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }} </template>
{{ n__('thread resolved', 'threads resolved', resolvableDiscussionsCount) }} <template v-else>
{{ n__('%d unresolved thread', '%d unresolved threads', unresolvedDiscussionsCount) }}
</template>
</span> </span>
</div> </div>
<div <div

View file

@ -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>

View file

@ -3,8 +3,10 @@ import Flash from '~/flash';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store'; import Store from '~/sidebar/stores/sidebar_store';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; 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 AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue'; import Assignees from './assignees.vue';
import AssigneesRealtime from './assignees_realtime.vue';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default { export default {
@ -12,7 +14,9 @@ export default {
components: { components: {
AssigneeTitle, AssigneeTitle,
Assignees, Assignees,
AssigneesRealtime,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
mediator: { mediator: {
type: Object, type: Object,
@ -32,6 +36,14 @@ export default {
required: false, required: false,
default: 'issue', default: 'issue',
}, },
issuableIid: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
@ -39,6 +51,12 @@ export default {
loading: false, 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() { created() {
this.removeAssignee = this.store.removeAssignee.bind(this.store); this.removeAssignee = this.store.removeAssignee.bind(this.store);
this.addAssignee = this.store.addAssignee.bind(this.store); this.addAssignee = this.store.addAssignee.bind(this.store);
@ -84,6 +102,12 @@ export default {
<template> <template>
<div> <div>
<assignees-realtime
v-if="shouldEnableRealtime"
:issuable-iid="issuableIid"
:project-path="projectPath"
:mediator="mediator"
/>
<assignee-title <assignee-title
:number-of-assignees="store.assignees.length" :number-of-assignees="store.assignees.length"
:loading="loading || store.isFetching.assignees" :loading="loading || store.isFetching.assignees"

View file

@ -1,5 +1,6 @@
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.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 sidebarParticipants from './components/participants/sidebar_participants.vue';
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue'; import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
Vue.use(Translate); Vue.use(Translate);
Vue.use(VueApollo);
function getSidebarOptions() {
return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
}
function mountAssigneesComponent(mediator) { function mountAssigneesComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-assignees'); const el = document.getElementById('js-vue-sidebar-assignees');
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
if (!el) return; if (!el) return;
const { iid, fullPath } = getSidebarOptions();
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
apolloProvider,
components: { components: {
SidebarAssignees, SidebarAssignees,
}, },
@ -26,6 +38,8 @@ function mountAssigneesComponent(mediator) {
createElement('sidebar-assignees', { createElement('sidebar-assignees', {
props: { props: {
mediator, mediator,
issuableIid: String(iid),
projectPath: fullPath,
field: el.dataset.field, field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'), signedIn: el.hasAttribute('data-signed-in'),
issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request', issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
@ -144,6 +158,4 @@ export function mountSidebar(mediator) {
mountTimeTrackingComponent(); mountTimeTrackingComponent();
} }
export function getSidebarOptions() { export { getSidebarOptions };
return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
}

View file

@ -89,6 +89,10 @@ export default class SidebarStore {
this.assignees = []; this.assignees = [];
} }
setAssigneesFromRealtime(data) {
this.assignees = data;
}
setAutocompleteProjects(projects) { setAutocompleteProjects(projects) {
this.autocompleteProjects = projects; this.autocompleteProjects = projects;
} }

View file

@ -27,6 +27,9 @@
$btn-disabled-border: rgba(223, 223, 223, 0.24); $btn-disabled-border: rgba(223, 223, 223, 0.24);
$btn-disabled-color: rgba(145, 145, 145, 0.48); $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-insert: rgba(155, 185, 85, 0.2);
$diff-remove: rgba(255, 0, 0, 0.2); $diff-remove: rgba(255, 0, 0, 0.2);
@ -54,7 +57,12 @@
textarea, textarea,
.md-area.is-focused, .md-area.is-focused,
.ide-entry-dropdown-toggle, .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; color: $text-color;
} }
@ -82,11 +90,17 @@
color: $text-color !important; color: $text-color !important;
} }
input[type='search']::placeholder,
input[type='text']::placeholder, input[type='text']::placeholder,
textarea::placeholder { textarea::placeholder,
.dropdown-input .fa {
color: $input-border; color: $input-border;
} }
.ide-nav-form .input-icon {
fill: $input-border;
}
.ide-staged-action-btn { .ide-staged-action-btn {
background-color: transparent; background-color: transparent;
} }
@ -112,7 +126,8 @@
background-color: inherit; background-color: inherit;
} }
.ide-sidebar-link:hover { .ide-sidebar-link:hover,
.multi-file-tabs li {
background-color: $background-hover; background-color: $background-hover;
} }
@ -204,21 +219,40 @@
background-color: $footer-background; background-color: $footer-background;
} }
input[type='text'] { input[type='text'],
input[type='search'],
.filtered-search-box {
border-color: $input-border; border-color: $input-border;
background: $input-background; background-color: $input-background;
} }
input[type='text'], input[type='text'],
input[type='search'],
.filtered-search-box,
textarea { textarea {
color: $input-color !important; 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 { .ide-entry-dropdown-toggle:hover {
background: $gray-800; background: $gray-800;
} }
.btn:hover { .btn:not(.btn-link):hover {
border-width: 2px; border-width: 2px;
padding: 5px 9px; 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 { .btn-primary {
background-color: $btn-primary-background; background-color: $btn-primary-background;
border-color: $btn-primary-border !important; border-color: $btn-primary-border !important;
@ -320,3 +396,7 @@
.navbar.theme-dark { .navbar.theme-dark {
border-bottom-color: transparent; border-bottom-color: transparent;
} }
.theme-dark ~ .popover {
box-shadow: none;
}

View file

@ -908,11 +908,10 @@ $note-form-margin-left: 72px;
border-right: 0; border-right: 0;
.line-resolve-btn { .line-resolve-btn {
margin-right: 5px;
color: $gray-700; color: $gray-700;
svg { svg {
vertical-align: middle; vertical-align: text-top;
} }
} }

View file

@ -50,6 +50,10 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:save_issuable_health_status, project.group, default_enabled: true) push_frontend_feature_flag(:save_issuable_health_status, project.group, default_enabled: true)
end 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] around_action :allow_gitaly_ref_name_caching, only: [:discussions]
respond_to :html respond_to :html

View file

@ -14,6 +14,8 @@ module Types
description: 'ID of the user' description: 'ID of the user'
field :name, GraphQL::STRING_TYPE, null: false, field :name, GraphQL::STRING_TYPE, null: false,
description: 'Human-readable name of the user' 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, field :username, GraphQL::STRING_TYPE, null: false,
description: 'Username of the user. Unique within this instance of GitLab' description: 'Username of the user. Unique within this instance of GitLab'
field :avatar_url, GraphQL::STRING_TYPE, null: true, field :avatar_url, GraphQL::STRING_TYPE, null: true,

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
module DesignManagement
class DesignAtVersionPolicy < ::BasePolicy
delegate { @subject.version }
delegate { @subject.design }
end
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
module DesignManagement
class DesignCollectionPolicy < DesignPolicy
# Delegates everything to the `issue` just like the `DesignPolicy`
end
end

View 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

View 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

View file

@ -15,6 +15,9 @@ class IssuePolicy < IssuablePolicy
desc "Issue is confidential" desc "Issue is confidential"
condition(:confidential, scope: :subject) { @subject.confidential? } condition(:confidential, scope: :subject) { @subject.confidential? }
desc "Issue has moved"
condition(:moved) { @subject.moved? }
rule { confidential & ~can_read_confidential }.policy do rule { confidential & ~can_read_confidential }.policy do
prevent(*create_read_update_admin_destroy(:issue)) prevent(*create_read_update_admin_destroy(:issue))
prevent :read_issue_iid prevent :read_issue_iid
@ -25,6 +28,15 @@ class IssuePolicy < IssuablePolicy
rule { locked }.policy do rule { locked }.policy do
prevent :reopen_issue prevent :reopen_issue
end 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

View file

@ -11,6 +11,7 @@ class ProjectPolicy < BasePolicy
milestone milestone
snippet snippet
wiki wiki
design
note note
pipeline pipeline
pipeline_schedule pipeline_schedule
@ -107,6 +108,11 @@ class ProjectPolicy < BasePolicy
) )
end 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 # 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 # 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 # (`: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 :create_metrics_dashboard_annotation
enable :delete_metrics_dashboard_annotation enable :delete_metrics_dashboard_annotation
enable :update_metrics_dashboard_annotation enable :update_metrics_dashboard_annotation
enable :create_design
enable :destroy_design
end end
rule { can?(:developer_access) & user_confirmed? }.policy do rule { can?(:developer_access) & user_confirmed? }.policy do
@ -511,6 +519,17 @@ class ProjectPolicy < BasePolicy
rule { admin }.enable :change_repository_storage 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 private
def team_member? def team_member?

View file

@ -68,6 +68,7 @@
= csrf_meta_tags = csrf_meta_tags
= csp_meta_tag = csp_meta_tag
= action_cable_meta_tag
- unless browser.safari? - unless browser.safari?
%meta{ name: 'referrer', content: 'origin-when-cross-origin' } %meta{ name: 'referrer', content: 'origin-when-cross-origin' }

View file

@ -5,6 +5,9 @@
%banner{ "v-if" => "!isOverviewDialogDismissed", %banner{ "v-if" => "!isOverviewDialogDismissed",
"documentation-link": help_page_path('user/analytics/value_stream_analytics.md'), "documentation-link": help_page_path('user/analytics/value_stream_analytics.md'),
"v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" } "v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" }
.mb-3
%h3
= _("Value Stream Analytics")
%gl-loading-icon{ "v-show" => "isLoading", "size" => "lg" } %gl-loading-icon{ "v-show" => "isLoading", "size" => "lg" }
.wrapper{ "v-show" => "!isLoading && !hasError" } .wrapper{ "v-show" => "!isLoading && !hasError" }
.card .card

View file

@ -1,7 +1,7 @@
- issuable_type = issuable_sidebar[:type] - issuable_type = issuable_sidebar[:type]
- signed_in = !!issuable_sidebar.dig(:current_user, :id) - 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 .title.hide-collapsed
= _('Assignee') = _('Assignee')
.spinner.spinner-sm.align-bottom .spinner.spinner-sm.align-bottom

View file

@ -0,0 +1,5 @@
---
title: Monokai and Solarized Dark syntax highlighting theme for Web IDE
merge_request: 30931
author:
type: added

View 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

View file

@ -0,0 +1,5 @@
---
title: Fix incorrect commits number in commits list
merge_request: 30412
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Handle possible RSA key exceptions when generating CI_JOB_JWT
merge_request: 30702
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: 'Code review analytics: Change margin between title and description'
merge_request: 30834
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: 'Issues Analytics: Add title to page'
merge_request: 30836
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: 'Insights Analytics: Add title to page'
merge_request: 30853
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: 'Value Stream Analytics: Add title and remove separator'
merge_request: 30841
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Change wording of merge request threads counter
merge_request: 30217
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Fixes branch name not getting escaped correctly on frontend
merge_request:
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Fix second 500 error with NULL restricted visibility levels
merge_request: 30414
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Disable schema dumping after migrations in production
merge_request: 30812
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Fix errors creating project with active Prometheus service template
merge_request: 30340
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Add LFS badge feature flag to RefsController#logs_tree
merge_request: 30442
author:
type: fixed

View file

@ -608,7 +608,7 @@ installations from source.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19186) in GitLab 12.6. > [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 Omnibus GitLab packages or in `/home/git/gitlab/log/mail_room_json.log` for
installations from source. installations from source.
@ -648,7 +648,7 @@ It's stored at:
- `/var/log/gitlab/gitlab-rails/database_load_balancing.log` for Omnibus GitLab packages. - `/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. - `/home/git/gitlab/log/database_load_balancing.log` for installations from source.
## `elasticsearch.log` ## `elasticsearch.log` **(STARTER ONLY)**
> Introduced in GitLab 12.6. > 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. > Introduced in 9.5.

View file

@ -10091,6 +10091,11 @@ type User {
visibility: VisibilityScopesEnum visibility: VisibilityScopesEnum
): SnippetConnection ): SnippetConnection
"""
State of the issue
"""
state: String!
""" """
Todos of the user Todos of the user
""" """

View file

@ -30221,6 +30221,24 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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", "name": "todos",
"description": "Todos of the user", "description": "Todos of the user",

View file

@ -1567,6 +1567,7 @@ Autogenerated return type of UpdateSnippet
| `avatarUrl` | String | URL of the user's avatar | | `avatarUrl` | String | URL of the user's avatar |
| `id` | ID! | ID of the user | | `id` | ID! | ID of the user |
| `name` | String! | Human-readable name 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 | | `userPermissions` | UserPermissions! | Permissions for the current user on the resource |
| `username` | String! | Username of the user. Unique within this instance of GitLab | | `username` | String! | Username of the user. Unique within this instance of GitLab |
| `webUrl` | String! | Web URL of the user | | `webUrl` | String! | Web URL of the user |

View file

@ -184,6 +184,9 @@ This can help to quickly understand the control flow.
// bad // bad
if (isThingNull) return ''; if (isThingNull) return '';
if (isThingNull)
return '';
// good // good
if (isThingNull) { if (isThingNull) {
return ''; return '';

View file

@ -214,6 +214,11 @@ msgid_plural "%d tags"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d unresolved thread"
msgid_plural "%d unresolved threads"
msgstr[0] ""
msgstr[1] ""
msgid "%d vulnerability dismissed" msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed" msgid_plural "%d vulnerabilities dismissed"
msgstr[0] "" msgstr[0] ""
@ -1827,6 +1832,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project" msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr "" msgstr ""
msgid "All threads resolved"
msgstr ""
msgid "All users" msgid "All users"
msgstr "" msgstr ""
@ -13174,7 +13182,7 @@ msgstr ""
msgid "Metrics|For grouping similar metrics" msgid "Metrics|For grouping similar metrics"
msgstr "" msgstr ""
msgid "Metrics|Go back" msgid "Metrics|Go back (Esc)"
msgstr "" msgstr ""
msgid "Metrics|Invalid time range, please verify." msgid "Metrics|Invalid time range, please verify."
@ -23434,6 +23442,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved" msgid "VulnerabilityStatusTypes|Resolved"
msgstr "" msgstr ""
msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
msgstr ""
msgid "Vulnerability|Class" msgid "Vulnerability|Class"
msgstr "" msgstr ""
@ -23467,7 +23478,10 @@ msgstr ""
msgid "Vulnerability|Project" msgid "Vulnerability|Project"
msgstr "" msgstr ""
msgid "Vulnerability|Report Type" msgid "Vulnerability|Scanner Provider"
msgstr ""
msgid "Vulnerability|Scanner Type"
msgstr "" msgstr ""
msgid "Vulnerability|Severity" msgid "Vulnerability|Severity"
@ -25699,11 +25713,6 @@ msgstr ""
msgid "this document" msgid "this document"
msgstr "" msgstr ""
msgid "thread resolved"
msgid_plural "threads resolved"
msgstr[0] ""
msgstr[1] ""
msgid "to help your contributors communicate effectively!" msgid "to help your contributors communicate effectively!"
msgstr "" msgstr ""

View file

@ -42,6 +42,7 @@
"@gitlab/svgs": "1.121.0", "@gitlab/svgs": "1.121.0",
"@gitlab/ui": "13.6.1", "@gitlab/ui": "13.6.1",
"@gitlab/visual-review-tools": "1.6.1", "@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.2-2",
"@sentry/browser": "^5.10.2", "@sentry/browser": "^5.10.2",
"@sourcegraph/code-host-integration": "0.0.37", "@sourcegraph/code-host-integration": "0.0.37",
"@toast-ui/editor": "^2.0.1", "@toast-ui/editor": "^2.0.1",

View file

@ -43,7 +43,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
context 'single thread' do context 'single thread' do
it 'shows text with how many threads' do it 'shows text with how many threads' do
page.within '.line-resolve-all-container' 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
end end
@ -60,7 +60,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' do 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') expect(page).to have_selector('.line-resolve-btn.is-active')
end end
end end
@ -77,7 +77,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' do 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') expect(page).to have_selector('.line-resolve-btn.is-active')
end end
end end
@ -89,7 +89,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' 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
end end
@ -162,7 +162,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' do 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 end
@ -174,7 +174,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' 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')
expect(page).not_to have_selector('.line-resolve-btn.is-active') expect(page).not_to have_selector('.line-resolve-btn.is-active')
end end
end end
@ -189,7 +189,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' 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
end end
end end
@ -203,7 +203,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' do 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') expect(page).to have_selector('.line-resolve-btn.is-active')
end end
end end
@ -218,7 +218,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' do 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') expect(page).to have_selector('.line-resolve-btn.is-active')
end end
end end
@ -275,7 +275,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
expect(page).to have_content('Last updated') expect(page).to have_content('Last updated')
page.within '.line-resolve-all-container' 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
end end
@ -292,7 +292,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' do 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 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 it 'shows text with how many threads' do
page.within '.line-resolve-all-container' 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
end end
@ -313,7 +313,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
click_button('Resolve thread', match: :first) click_button('Resolve thread', match: :first)
page.within '.line-resolve-all-container' do 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
end end
@ -323,7 +323,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' do 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') expect(page).to have_selector('.line-resolve-btn.is-active')
end end
end end
@ -336,7 +336,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' do 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') expect(page).to have_selector('.line-resolve-btn.is-active')
end end
end end
@ -392,7 +392,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
context 'changes tab' do context 'changes tab' do
it 'shows text with how many threads' do it 'shows text with how many threads' do
page.within '.line-resolve-all-container' 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
end end
@ -408,7 +408,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' do 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') expect(page).to have_selector('.line-resolve-btn.is-active')
end end
end end
@ -423,7 +423,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' do 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') expect(page).to have_selector('.line-resolve-btn.is-active')
end end
end end
@ -435,7 +435,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' 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
end end
@ -449,7 +449,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' do 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') expect(page).to have_selector('.line-resolve-btn.is-active')
end end
end end
@ -466,7 +466,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' 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
end end
end end
@ -489,7 +489,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' 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
end end
@ -519,7 +519,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' do 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') expect(page).to have_selector('.line-resolve-btn.is-active')
end end
end end
@ -538,7 +538,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
page.within '.line-resolve-all-container' 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
end end
end end
@ -550,17 +550,17 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end end
it 'shows resolved icon' do 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' click_button 'Toggle thread'
expect(page).to have_selector('.line-resolve-btn.is-active') expect(page).to have_selector('.line-resolve-btn.is-active')
end end
it 'does not allow user to click resolve button' do 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' 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 end
end end

View file

@ -1,5 +1,6 @@
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys';
import { GlModal, GlDropdownItem, GlDeprecatedButton } from '@gitlab/ui'; import { GlModal, GlDropdownItem, GlDeprecatedButton } from '@gitlab/ui';
import VueDraggable from 'vuedraggable'; import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
@ -248,6 +249,8 @@ describe('Dashboard', () => {
let group; let group;
let panel; let panel;
const mockKeyup = key => window.dispatchEvent(new KeyboardEvent('keyup', { key }));
const MockPanel = { const MockPanel = {
template: `<div><slot name="topLeft"/></div>`, template: `<div><slot name="topLeft"/></div>`,
}; };
@ -265,6 +268,9 @@ describe('Dashboard', () => {
group, group,
panel, panel,
}); });
jest.spyOn(store, 'dispatch');
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
@ -289,17 +295,30 @@ describe('Dashboard', () => {
}); });
it('restores full dashboard by clicking `back`', () => { it('restores full dashboard by clicking `back`', () => {
const backBtn = wrapper.find({ ref: 'goBackBtn' }); wrapper.find({ ref: 'goBackBtn' }).vm.$emit('click');
expect(backBtn.exists()).toBe(true);
jest.spyOn(store, 'dispatch');
backBtn.vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith( expect(store.dispatch).toHaveBeenCalledWith(
'monitoringDashboard/clearExpandedPanel', 'monitoringDashboard/clearExpandedPanel',
undefined, 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,
);
});
}); });
}); });

View file

@ -75,15 +75,14 @@ describe('DiscussionCounter component', () => {
}); });
it.each` it.each`
title | resolved | isActive | icon | groupLength title | resolved | isActive | groupLength
${'not allResolved'} | ${false} | ${false} | ${'check-circle'} | ${3} ${'not allResolved'} | ${false} | ${false} | ${3}
${'allResolved'} | ${true} | ${true} | ${'check-circle-filled'} | ${1} ${'allResolved'} | ${true} | ${true} | ${1}
`('renders correctly if $title', ({ resolved, isActive, icon, groupLength }) => { `('renders correctly if $title', ({ resolved, isActive, groupLength }) => {
updateStore({ resolvable: true, resolved }); updateStore({ resolvable: true, resolved });
wrapper = shallowMount(DiscussionCounter, { store, localVue }); wrapper = shallowMount(DiscussionCounter, { store, localVue });
expect(wrapper.find(`.is-active`).exists()).toBe(isActive); expect(wrapper.find(`.is-active`).exists()).toBe(isActive);
expect(wrapper.find({ name: icon }).exists()).toBe(true);
expect(wrapper.findAll('[role="group"').length).toBe(groupLength); expect(wrapper.findAll('[role="group"').length).toBe(groupLength);
}); });
}); });

View 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);
});
});
});
});

View file

@ -3,6 +3,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import axios from 'axios'; import axios from 'axios';
import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.vue'; import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.vue';
import Assigness from '~/sidebar/components/assignees/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 SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store'; import SidebarStore from '~/sidebar/stores/sidebar_store';
@ -12,12 +13,19 @@ describe('sidebar assignees', () => {
let wrapper; let wrapper;
let mediator; let mediator;
let axiosMock; let axiosMock;
const createComponent = (realTimeIssueSidebar = false, props) => {
const createComponent = () => {
wrapper = shallowMount(SidebarAssignees, { wrapper = shallowMount(SidebarAssignees, {
propsData: { propsData: {
issuableIid: '1',
mediator, mediator,
field: '', field: '',
projectPath: 'projectPath',
...props,
},
provide: {
glFeatures: {
realTimeIssueSidebar,
},
}, },
// Attaching to document is required because this component emits something from the parent element :/ // Attaching to document is required because this component emits something from the parent element :/
attachToDocument: true, attachToDocument: true,
@ -30,8 +38,6 @@ describe('sidebar assignees', () => {
jest.spyOn(mediator, 'saveAssignees'); jest.spyOn(mediator, 'saveAssignees');
jest.spyOn(mediator, 'assignYourself'); jest.spyOn(mediator, 'assignYourself');
createComponent();
}); });
afterEach(() => { afterEach(() => {
@ -45,6 +51,8 @@ describe('sidebar assignees', () => {
}); });
it('calls the mediator when saves the assignees', () => { it('calls the mediator when saves the assignees', () => {
createComponent();
expect(mediator.saveAssignees).not.toHaveBeenCalled(); expect(mediator.saveAssignees).not.toHaveBeenCalled();
wrapper.vm.saveAssignees(); wrapper.vm.saveAssignees();
@ -53,6 +61,8 @@ describe('sidebar assignees', () => {
}); });
it('calls the mediator when "assignSelf" method is called', () => { it('calls the mediator when "assignSelf" method is called', () => {
createComponent();
expect(mediator.assignYourself).not.toHaveBeenCalled(); expect(mediator.assignYourself).not.toHaveBeenCalled();
expect(mediator.store.assignees.length).toBe(0); expect(mediator.store.assignees.length).toBe(0);
@ -63,6 +73,8 @@ describe('sidebar assignees', () => {
}); });
it('hides assignees until fetched', () => { it('hides assignees until fetched', () => {
createComponent();
expect(wrapper.find(Assigness).exists()).toBe(false); expect(wrapper.find(Assigness).exists()).toBe(false);
wrapper.vm.store.isFetching.assignees = false; wrapper.vm.store.isFetching.assignees = false;
@ -71,4 +83,30 @@ describe('sidebar assignees', () => {
expect(wrapper.find(Assigness).exists()).toBe(true); 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);
});
});
}); });

View file

@ -9,7 +9,7 @@ describe GitlabSchema.types['User'] do
it 'has the expected fields' do it 'has the expected fields' do
expected_fields = %w[ 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) expect(described_class).to have_graphql_fields(*expected_fields)

View 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

View file

@ -985,6 +985,11 @@
consola "^2.10.1" consola "^2.10.1"
node-fetch "^2.6.0" 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": "@sentry/browser@^5.10.2":
version "5.10.2" version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.10.2.tgz#0bbb05505c58ea998c833cffec3f922fe4b4fa58" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.10.2.tgz#0bbb05505c58ea998c833cffec3f922fe4b4fa58"