Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1f1e53f43f
commit
1ec60cf53b
|
@ -46,7 +46,8 @@ static-analysis:
|
|||
stage: test
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
parallel: 2
|
||||
ENABLE_SPRING: "1"
|
||||
parallel: 4
|
||||
script:
|
||||
- scripts/static-analysis
|
||||
cache:
|
||||
|
|
|
@ -233,6 +233,7 @@ danger-review:
|
|||
- .review:rules:danger
|
||||
image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
|
||||
stage: test
|
||||
allow_failure: true
|
||||
needs: []
|
||||
script:
|
||||
- source scripts/utils.sh
|
||||
|
|
|
@ -9,7 +9,7 @@ import CommitForm from './commit_sidebar/form.vue';
|
|||
import IdeReview from './ide_review.vue';
|
||||
import SuccessMessage from './commit_sidebar/success_message.vue';
|
||||
import IdeProjectHeader from './ide_project_header.vue';
|
||||
import { leftSidebarViews, LEFT_SIDEBAR_INIT_WIDTH } from '../constants';
|
||||
import { leftSidebarViews, SIDEBAR_INIT_WIDTH } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -33,13 +33,13 @@ export default {
|
|||
);
|
||||
},
|
||||
},
|
||||
LEFT_SIDEBAR_INIT_WIDTH,
|
||||
SIDEBAR_INIT_WIDTH,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<resizable-panel
|
||||
:initial-width="$options.LEFT_SIDEBAR_INIT_WIDTH"
|
||||
:initial-width="$options.SIDEBAR_INIT_WIDTH"
|
||||
side="left"
|
||||
class="multi-file-commit-panel flex-column"
|
||||
>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { mapActions, mapState } from 'vuex';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import ResizablePanel from '../resizable_panel.vue';
|
||||
import IdeSidebarNav from '../ide_sidebar_nav.vue';
|
||||
|
||||
export default {
|
||||
|
@ -12,7 +11,6 @@ export default {
|
|||
},
|
||||
components: {
|
||||
Icon,
|
||||
ResizablePanel,
|
||||
IdeSidebarNav,
|
||||
},
|
||||
props: {
|
||||
|
@ -25,10 +23,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
|
@ -75,25 +69,20 @@ export default {
|
|||
:data-qa-selector="`ide_${side}_sidebar`"
|
||||
class="multi-file-commit-panel ide-sidebar"
|
||||
>
|
||||
<resizable-panel
|
||||
<div
|
||||
v-show="isOpen"
|
||||
:initial-width="width"
|
||||
:min-size="width"
|
||||
:class="`ide-${side}-sidebar-${currentView}`"
|
||||
:side="side"
|
||||
class="multi-file-commit-panel-inner"
|
||||
>
|
||||
<div class="h-100 d-flex flex-column align-items-stretch">
|
||||
<div
|
||||
v-for="tabView in aliveTabViews"
|
||||
v-show="tabView.name === currentView"
|
||||
:key="tabView.name"
|
||||
class="flex-fill gl-overflow-hidden js-tab-view"
|
||||
>
|
||||
<component :is="tabView.component" />
|
||||
</div>
|
||||
<div
|
||||
v-for="tabView in aliveTabViews"
|
||||
v-show="tabView.name === currentView"
|
||||
:key="tabView.name"
|
||||
class="flex-fill gl-overflow-hidden js-tab-view gl-h-full"
|
||||
>
|
||||
<component :is="tabView.component" />
|
||||
</div>
|
||||
</resizable-panel>
|
||||
</div>
|
||||
<ide-sidebar-nav
|
||||
:tabs="tabs"
|
||||
:side="side"
|
||||
|
|
|
@ -2,15 +2,20 @@
|
|||
import { mapGetters, mapState } from 'vuex';
|
||||
import { __ } from '~/locale';
|
||||
import CollapsibleSidebar from './collapsible_sidebar.vue';
|
||||
import { rightSidebarViews } from '../../constants';
|
||||
import ResizablePanel from '../resizable_panel.vue';
|
||||
import { rightSidebarViews, SIDEBAR_INIT_WIDTH, SIDEBAR_NAV_WIDTH } from '../../constants';
|
||||
import PipelinesList from '../pipelines/list.vue';
|
||||
import JobsDetail from '../jobs/detail.vue';
|
||||
import Clientside from '../preview/clientside.vue';
|
||||
|
||||
// Need to add the width of the nav buttons since the resizable container contains those as well
|
||||
const WIDTH = SIDEBAR_INIT_WIDTH + SIDEBAR_NAV_WIDTH;
|
||||
|
||||
export default {
|
||||
name: 'RightPane',
|
||||
components: {
|
||||
CollapsibleSidebar,
|
||||
ResizablePanel,
|
||||
},
|
||||
props: {
|
||||
extensionTabs: {
|
||||
|
@ -22,6 +27,7 @@ export default {
|
|||
computed: {
|
||||
...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled']),
|
||||
...mapGetters(['packageJson']),
|
||||
...mapState('rightPane', ['isOpen']),
|
||||
showLivePreview() {
|
||||
return this.packageJson && this.clientsidePreviewEnabled;
|
||||
},
|
||||
|
@ -46,9 +52,18 @@ export default {
|
|||
];
|
||||
},
|
||||
},
|
||||
WIDTH,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<collapsible-sidebar :extension-tabs="rightExtensionTabs" side="right" :width="350" />
|
||||
<resizable-panel
|
||||
class="gl-display-flex gl-overflow-hidden"
|
||||
side="right"
|
||||
:initial-width="$options.WIDTH"
|
||||
:min-size="$options.WIDTH"
|
||||
:resizable="isOpen"
|
||||
>
|
||||
<collapsible-sidebar class="gl-w-full" :extension-tabs="rightExtensionTabs" side="right" />
|
||||
</resizable-panel>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import { DEFAULT_SIDEBAR_MIN_WIDTH } from '../constants';
|
||||
import { SIDEBAR_MIN_WIDTH } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -15,12 +15,17 @@ export default {
|
|||
minSize: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: DEFAULT_SIDEBAR_MIN_WIDTH,
|
||||
default: SIDEBAR_MIN_WIDTH,
|
||||
},
|
||||
side: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
resizable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -29,7 +34,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
panelStyle() {
|
||||
if (!this.collapsed) {
|
||||
if (this.resizable) {
|
||||
return {
|
||||
width: `${this.width}px`,
|
||||
};
|
||||
|
@ -46,9 +51,10 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="panelStyle">
|
||||
<div class="gl-relative" :style="panelStyle">
|
||||
<slot></slot>
|
||||
<panel-resizer
|
||||
v-show="resizable"
|
||||
:size.sync="width"
|
||||
:start-size="initialWidth"
|
||||
:min-size="minSize"
|
||||
|
|
|
@ -4,8 +4,9 @@ export const MAX_WINDOW_HEIGHT_COMPACT = 750;
|
|||
export const MAX_TITLE_LENGTH = 50;
|
||||
export const MAX_BODY_LENGTH = 72;
|
||||
|
||||
export const LEFT_SIDEBAR_INIT_WIDTH = 340;
|
||||
export const DEFAULT_SIDEBAR_MIN_WIDTH = 340;
|
||||
export const SIDEBAR_INIT_WIDTH = 340;
|
||||
export const SIDEBAR_MIN_WIDTH = 340;
|
||||
export const SIDEBAR_NAV_WIDTH = 60;
|
||||
|
||||
// File view modes
|
||||
export const FILE_VIEW_MODE_EDITOR = 'editor';
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { GlDeprecatedButton, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDeprecatedButton,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlLink,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'externalDashboardHelpPagePath',
|
||||
'externalDashboardUrl',
|
||||
'operationsSettingsEndpoint',
|
||||
]),
|
||||
userDashboardUrl: {
|
||||
get() {
|
||||
return this.externalDashboardUrl;
|
||||
},
|
||||
set(url) {
|
||||
this.setExternalDashboardUrl(url);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setExternalDashboardUrl', 'updateExternalDashboardUrl']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="settings no-animate">
|
||||
<div class="settings-header">
|
||||
<h3 class="js-section-header h4">
|
||||
{{ s__('ExternalMetrics|External Dashboard') }}
|
||||
</h3>
|
||||
<gl-deprecated-button class="js-settings-toggle">{{ __('Expand') }}</gl-deprecated-button>
|
||||
<p class="js-section-sub-header">
|
||||
{{
|
||||
s__(
|
||||
'ExternalMetrics|Add a button to the metrics dashboard linking directly to your existing external dashboards.',
|
||||
)
|
||||
}}
|
||||
<gl-link :href="externalDashboardHelpPagePath">{{ __('Learn more') }}</gl-link>
|
||||
</p>
|
||||
</div>
|
||||
<div class="settings-content">
|
||||
<form>
|
||||
<gl-form-group
|
||||
:label="s__('ExternalMetrics|Full dashboard URL')"
|
||||
label-for="full-dashboard-url"
|
||||
:description="s__('ExternalMetrics|Enter the URL of the dashboard you want to link to')"
|
||||
>
|
||||
<!-- placeholder with a url is a false positive -->
|
||||
<!-- eslint-disable @gitlab/vue-require-i18n-attribute-strings -->
|
||||
<gl-form-input
|
||||
id="full-dashboard-url"
|
||||
v-model="userDashboardUrl"
|
||||
placeholder="https://my-org.gitlab.io/my-dashboards"
|
||||
@keydown.enter.native.prevent="updateExternalDashboardUrl"
|
||||
/>
|
||||
<!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings -->
|
||||
</gl-form-group>
|
||||
<gl-deprecated-button variant="success" @click="updateExternalDashboardUrl">
|
||||
{{ __('Save Changes') }}
|
||||
</gl-deprecated-button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
|
@ -0,0 +1,50 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlLink,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['externalDashboard']),
|
||||
userDashboardUrl: {
|
||||
get() {
|
||||
return this.externalDashboard.url;
|
||||
},
|
||||
set(url) {
|
||||
this.setExternalDashboardUrl(url);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setExternalDashboardUrl']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form-group
|
||||
:label="s__('MetricsSettings|External dashboard URL')"
|
||||
label-for="external-dashboard-url"
|
||||
>
|
||||
<template #description>
|
||||
{{
|
||||
s__(
|
||||
'MetricsSettings|Add a button to the metrics dashboard linking directly to your existing external dashboard.',
|
||||
)
|
||||
}}
|
||||
<gl-link :href="externalDashboard.helpPage">{{ __('Learn more') }}</gl-link>
|
||||
</template>
|
||||
<!-- placeholder with a url is a false positive -->
|
||||
<!-- eslint-disable @gitlab/vue-require-i18n-attribute-strings -->
|
||||
<gl-form-input
|
||||
id="external-dashboard-url"
|
||||
v-model="userDashboardUrl"
|
||||
placeholder="https://my-org.gitlab.io/my-dashboards"
|
||||
/>
|
||||
<!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings -->
|
||||
</gl-form-group>
|
||||
</template>
|
|
@ -0,0 +1,50 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { GlDeprecatedButton, GlLink } from '@gitlab/ui';
|
||||
import ExternalDashboard from './form_group/external_dashboard.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDeprecatedButton,
|
||||
GlLink,
|
||||
ExternalDashboard,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['helpPage']),
|
||||
userDashboardUrl: {
|
||||
get() {
|
||||
return this.externalDashboard.url;
|
||||
},
|
||||
set(url) {
|
||||
this.setExternalDashboardUrl(url);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['saveChanges']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="settings no-animate">
|
||||
<div class="settings-header">
|
||||
<h3 class="js-section-header h4">
|
||||
{{ s__('MetricsSettings|Metrics Dashboard') }}
|
||||
</h3>
|
||||
<gl-deprecated-button class="js-settings-toggle">{{ __('Expand') }}</gl-deprecated-button>
|
||||
<p class="js-section-sub-header">
|
||||
{{ s__('MetricsSettings|Manage Metrics Dashboard settings.') }}
|
||||
<gl-link :href="helpPage">{{ __('Learn more') }}</gl-link>
|
||||
</p>
|
||||
</div>
|
||||
<div class="settings-content">
|
||||
<form>
|
||||
<external-dashboard />
|
||||
<gl-deprecated-button variant="success" @click="saveChanges">
|
||||
{{ __('Save Changes') }}
|
||||
</gl-deprecated-button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import store from './store';
|
||||
import ExternalDashboardForm from './components/external_dashboard.vue';
|
||||
import MetricsSettingsForm from './components/metrics_settings.vue';
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('.js-operation-settings');
|
||||
|
@ -9,7 +9,7 @@ export default () => {
|
|||
el,
|
||||
store: store(el.dataset),
|
||||
render(createElement) {
|
||||
return createElement(ExternalDashboardForm);
|
||||
return createElement(MetricsSettingsForm);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -7,19 +7,19 @@ import * as mutationTypes from './mutation_types';
|
|||
export const setExternalDashboardUrl = ({ commit }, url) =>
|
||||
commit(mutationTypes.SET_EXTERNAL_DASHBOARD_URL, url);
|
||||
|
||||
export const updateExternalDashboardUrl = ({ state, dispatch }) =>
|
||||
export const saveChanges = ({ state, dispatch }) =>
|
||||
axios
|
||||
.patch(state.operationsSettingsEndpoint, {
|
||||
project: {
|
||||
metrics_setting_attributes: {
|
||||
external_dashboard_url: state.externalDashboardUrl,
|
||||
external_dashboard_url: state.externalDashboard.url,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => dispatch('receiveExternalDashboardUpdateSuccess'))
|
||||
.catch(error => dispatch('receiveExternalDashboardUpdateError', error));
|
||||
.then(() => dispatch('receiveSaveChangesSuccess'))
|
||||
.catch(error => dispatch('receiveSaveChangesError', error));
|
||||
|
||||
export const receiveExternalDashboardUpdateSuccess = () => {
|
||||
export const receiveSaveChangesSuccess = () => {
|
||||
/**
|
||||
* The operations_controller currently handles successful requests
|
||||
* by creating a flash banner messsage to notify the user.
|
||||
|
@ -27,8 +27,8 @@ export const receiveExternalDashboardUpdateSuccess = () => {
|
|||
refreshCurrentPage();
|
||||
};
|
||||
|
||||
export const receiveExternalDashboardUpdateError = (_, error) => {
|
||||
const { response } = error;
|
||||
export const receiveSaveChangesError = (_, error) => {
|
||||
const { response = {} } = error;
|
||||
const message = response.data && response.data.message ? response.data.message : '';
|
||||
|
||||
createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
|
||||
|
|
|
@ -2,6 +2,6 @@ import * as types from './mutation_types';
|
|||
|
||||
export default {
|
||||
[types.SET_EXTERNAL_DASHBOARD_URL](state, url) {
|
||||
state.externalDashboardUrl = url;
|
||||
state.externalDashboard.url = url;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
export default (initialState = {}) => ({
|
||||
externalDashboardUrl: initialState.externalDashboardUrl || '',
|
||||
operationsSettingsEndpoint: initialState.operationsSettingsEndpoint,
|
||||
externalDashboardHelpPagePath: initialState.externalDashboardHelpPagePath,
|
||||
helpPage: initialState.helpPage,
|
||||
externalDashboard: {
|
||||
url: initialState.externalDashboardUrl,
|
||||
helpPage: initialState.externalDashboardHelpPage,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -282,7 +282,6 @@ $ide-commit-header-height: 48px;
|
|||
.multi-file-commit-panel {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 340px;
|
||||
padding: 0;
|
||||
background-color: var(--ide-background, $gray-light);
|
||||
|
||||
|
@ -874,13 +873,11 @@ $ide-commit-header-height: 48px;
|
|||
}
|
||||
|
||||
.ide-sidebar {
|
||||
width: auto;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.ide-right-sidebar {
|
||||
.multi-file-commit-panel-inner {
|
||||
width: 350px;
|
||||
padding: $grid-size 0;
|
||||
background-color: var(--ide-highlight-background, $white);
|
||||
border-right: 1px solid var(--ide-border-color, $white-dark);
|
||||
|
|
|
@ -17,7 +17,9 @@ class Dashboard::TodosController < Dashboard::ApplicationController
|
|||
end
|
||||
|
||||
def destroy
|
||||
TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user)
|
||||
todo = current_user.todos.find(params[:id])
|
||||
|
||||
TodoService.new.resolve_todo(todo, current_user, resolved_by_action: :mark_done)
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
|
@ -31,7 +33,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
|
|||
end
|
||||
|
||||
def destroy_all
|
||||
updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user)
|
||||
updated_ids = TodoService.new.resolve_todos(@todos, current_user, resolved_by_action: :mark_all_done)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to dashboard_todos_path, status: :found, notice: _('Everything on your to-do list is marked as done.') }
|
||||
|
@ -41,13 +43,13 @@ class Dashboard::TodosController < Dashboard::ApplicationController
|
|||
end
|
||||
|
||||
def restore
|
||||
TodoService.new.mark_todos_as_pending_by_ids(params[:id], current_user)
|
||||
TodoService.new.restore_todo(current_user.todos.find(params[:id]), current_user)
|
||||
|
||||
render json: todos_counts
|
||||
end
|
||||
|
||||
def bulk_restore
|
||||
TodoService.new.mark_todos_as_pending_by_ids(params[:ids], current_user)
|
||||
TodoService.new.restore_todos(current_user.todos.for_ids(params[:ids]), current_user)
|
||||
|
||||
render json: todos_counts
|
||||
end
|
||||
|
|
|
@ -28,7 +28,9 @@ module Mutations
|
|||
def mark_all_todos_done
|
||||
return [] unless current_user
|
||||
|
||||
TodoService.new.mark_all_todos_as_done_by_user(current_user)
|
||||
todos = TodosFinder.new(current_user).execute
|
||||
|
||||
TodoService.new.resolve_todos(todos, current_user, resolved_by_action: :api_all_done)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,7 +30,7 @@ module Mutations
|
|||
private
|
||||
|
||||
def mark_done(todo)
|
||||
TodoService.new.mark_todo_as_done(todo, current_user)
|
||||
TodoService.new.resolve_todo(todo, current_user, resolved_by_action: :api_done)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,7 @@ module Mutations
|
|||
|
||||
def resolve(id:)
|
||||
todo = authorized_find!(id: id)
|
||||
restore(todo.id) if todo.done?
|
||||
restore(todo)
|
||||
|
||||
{
|
||||
todo: todo.reset,
|
||||
|
@ -28,8 +28,8 @@ module Mutations
|
|||
|
||||
private
|
||||
|
||||
def restore(id)
|
||||
TodoService.new.mark_todos_as_pending_by_ids([id], current_user)
|
||||
def restore(todo)
|
||||
TodoService.new.restore_todo(todo, current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -68,7 +68,7 @@ module Mutations
|
|||
end
|
||||
|
||||
def restore(todos)
|
||||
TodoService.new.mark_todos_as_pending(todos, current_user)
|
||||
TodoService.new.restore_todos(todos, current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,6 +66,8 @@ class Todo < ApplicationRecord
|
|||
scope :with_entity_associations, -> { preload(:target, :author, :note, group: :route, project: [:route, { namespace: :route }]) }
|
||||
scope :joins_issue_and_assignees, -> { left_joins(issue: :assignees) }
|
||||
|
||||
enum resolved_by_action: { system_done: 0, api_all_done: 1, api_done: 2, mark_all_done: 3, mark_done: 4 }, _prefix: :resolved_by
|
||||
|
||||
state_machine :state, initial: :pending do
|
||||
event :done do
|
||||
transition [:pending] => :done
|
||||
|
@ -100,17 +102,17 @@ class Todo < ApplicationRecord
|
|||
state.nil? ? exists?(target: target) : exists?(target: target, state: state)
|
||||
end
|
||||
|
||||
# Updates the state of a relation of todos to the new state.
|
||||
# Updates attributes of a relation of todos to the new state.
|
||||
#
|
||||
# new_state - The new state of the todos.
|
||||
# new_attributes - The new attributes of the todos.
|
||||
#
|
||||
# Returns an `Array` containing the IDs of the updated todos.
|
||||
def update_state(new_state)
|
||||
# Only update those that are not really on that state
|
||||
base = where.not(state: new_state).except(:order)
|
||||
def batch_update(**new_attributes)
|
||||
# Only update those that have different state
|
||||
base = where.not(state: new_attributes[:state]).except(:order)
|
||||
ids = base.pluck(:id)
|
||||
|
||||
base.update_all(state: new_state, updated_at: Time.current)
|
||||
base.update_all(new_attributes.merge(updated_at: Time.current))
|
||||
|
||||
ids
|
||||
end
|
||||
|
|
|
@ -1636,10 +1636,6 @@ class User < ApplicationRecord
|
|||
super.presence || build_user_detail
|
||||
end
|
||||
|
||||
def todos_limited_to(ids)
|
||||
todos.where(id: ids)
|
||||
end
|
||||
|
||||
def pending_todo_for(target)
|
||||
todos.find_by(target: target, state: :pending)
|
||||
end
|
||||
|
|
|
@ -350,7 +350,7 @@ class IssuableBaseService < BaseService
|
|||
todo_service.mark_todo(issuable, current_user)
|
||||
when 'done'
|
||||
todo = TodosFinder.new(current_user).find_by(target: issuable)
|
||||
todo_service.mark_todos_as_done_by_ids(todo, current_user) if todo
|
||||
todo_service.resolve_todo(todo, current_user) if todo
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
|
|
@ -32,7 +32,7 @@ module Issues
|
|||
old_assignees = old_associations.fetch(:assignees, [])
|
||||
|
||||
if has_changes?(issue, old_labels: old_labels, old_assignees: old_assignees)
|
||||
todo_service.mark_pending_todos_as_done(issue, current_user)
|
||||
todo_service.resolve_todos_for_target(issue, current_user)
|
||||
end
|
||||
|
||||
if issue.previous_changes.include?('title') ||
|
||||
|
@ -68,7 +68,7 @@ module Issues
|
|||
end
|
||||
|
||||
def handle_task_changes(issuable)
|
||||
todo_service.mark_pending_todos_as_done(issuable, current_user)
|
||||
todo_service.resolve_todos_for_target(issuable, current_user)
|
||||
todo_service.update_issue(issuable, current_user)
|
||||
end
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ module MergeRequests
|
|||
old_assignees = old_associations.fetch(:assignees, [])
|
||||
|
||||
if has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees)
|
||||
todo_service.mark_pending_todos_as_done(merge_request, current_user)
|
||||
todo_service.resolve_todos_for_target(merge_request, current_user)
|
||||
end
|
||||
|
||||
if merge_request.previous_changes.include?('title') ||
|
||||
|
@ -73,7 +73,7 @@ module MergeRequests
|
|||
end
|
||||
|
||||
def handle_task_changes(merge_request)
|
||||
todo_service.mark_pending_todos_as_done(merge_request, current_user)
|
||||
todo_service.resolve_todos_for_target(merge_request, current_user)
|
||||
todo_service.update_merge_request(merge_request, current_user)
|
||||
end
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ class TodoService
|
|||
# * mark all pending todos related to the target for the current user as done
|
||||
#
|
||||
def close_issue(issue, current_user)
|
||||
mark_pending_todos_as_done(issue, current_user)
|
||||
resolve_todos_for_target(issue, current_user)
|
||||
end
|
||||
|
||||
# When we destroy a todo target we should:
|
||||
|
@ -79,7 +79,7 @@ class TodoService
|
|||
# * mark all pending todos related to the target for the current user as done
|
||||
#
|
||||
def close_merge_request(merge_request, current_user)
|
||||
mark_pending_todos_as_done(merge_request, current_user)
|
||||
resolve_todos_for_target(merge_request, current_user)
|
||||
end
|
||||
|
||||
# When merge a merge request we should:
|
||||
|
@ -87,7 +87,7 @@ class TodoService
|
|||
# * mark all pending todos related to the target for the current user as done
|
||||
#
|
||||
def merge_merge_request(merge_request, current_user)
|
||||
mark_pending_todos_as_done(merge_request, current_user)
|
||||
resolve_todos_for_target(merge_request, current_user)
|
||||
end
|
||||
|
||||
# When a build fails on the HEAD of a merge request we should:
|
||||
|
@ -105,7 +105,7 @@ class TodoService
|
|||
# * mark all pending todos related to the merge request for that user as done
|
||||
#
|
||||
def merge_request_push(merge_request, current_user)
|
||||
mark_pending_todos_as_done(merge_request, current_user)
|
||||
resolve_todos_for_target(merge_request, current_user)
|
||||
end
|
||||
|
||||
# When a build is retried to a merge request we should:
|
||||
|
@ -114,7 +114,7 @@ class TodoService
|
|||
#
|
||||
def merge_request_build_retried(merge_request)
|
||||
merge_request.merge_participants.each do |user|
|
||||
mark_pending_todos_as_done(merge_request, user)
|
||||
resolve_todos_for_target(merge_request, user)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -151,50 +151,7 @@ class TodoService
|
|||
# * mark all pending todos related to the awardable for the current user as done
|
||||
#
|
||||
def new_award_emoji(awardable, current_user)
|
||||
mark_pending_todos_as_done(awardable, current_user)
|
||||
end
|
||||
|
||||
# When marking pending todos as done we should:
|
||||
#
|
||||
# * mark all pending todos related to the target for the current user as done
|
||||
#
|
||||
def mark_pending_todos_as_done(target, user)
|
||||
attributes = attributes_for_target(target)
|
||||
pending_todos(user, attributes).update_all(state: :done)
|
||||
user.update_todos_count_cache
|
||||
end
|
||||
|
||||
# When user marks some todos as done
|
||||
def mark_todos_as_done(todos, current_user)
|
||||
update_todos_state(todos, current_user, :done)
|
||||
end
|
||||
|
||||
def mark_todos_as_done_by_ids(ids, current_user)
|
||||
todos = todos_by_ids(ids, current_user)
|
||||
mark_todos_as_done(todos, current_user)
|
||||
end
|
||||
|
||||
def mark_all_todos_as_done_by_user(current_user)
|
||||
todos = TodosFinder.new(current_user).execute
|
||||
mark_todos_as_done(todos, current_user)
|
||||
end
|
||||
|
||||
def mark_todo_as_done(todo, current_user)
|
||||
return if todo.done?
|
||||
|
||||
todo.update(state: :done)
|
||||
|
||||
current_user.update_todos_count_cache
|
||||
end
|
||||
|
||||
# When user marks some todos as pending
|
||||
def mark_todos_as_pending(todos, current_user)
|
||||
update_todos_state(todos, current_user, :pending)
|
||||
end
|
||||
|
||||
def mark_todos_as_pending_by_ids(ids, current_user)
|
||||
todos = todos_by_ids(ids, current_user)
|
||||
mark_todos_as_pending(todos, current_user)
|
||||
resolve_todos_for_target(awardable, current_user)
|
||||
end
|
||||
|
||||
# When user marks an issue as todo
|
||||
|
@ -207,20 +164,47 @@ class TodoService
|
|||
TodosFinder.new(current_user).any_for_target?(issuable, :pending)
|
||||
end
|
||||
|
||||
private
|
||||
# Resolves all todos related to target
|
||||
def resolve_todos_for_target(target, current_user)
|
||||
attributes = attributes_for_target(target)
|
||||
|
||||
def todos_by_ids(ids, current_user)
|
||||
current_user.todos_limited_to(Array(ids))
|
||||
resolve_todos(pending_todos(current_user, attributes), current_user)
|
||||
end
|
||||
|
||||
def update_todos_state(todos, current_user, state)
|
||||
todos_ids = todos.update_state(state)
|
||||
def resolve_todos(todos, current_user, resolution: :done, resolved_by_action: :system_done)
|
||||
todos_ids = todos.batch_update(state: resolution, resolved_by_action: resolved_by_action)
|
||||
|
||||
current_user.update_todos_count_cache
|
||||
|
||||
todos_ids
|
||||
end
|
||||
|
||||
def resolve_todo(todo, current_user, resolution: :done, resolved_by_action: :system_done)
|
||||
return if todo.done?
|
||||
|
||||
todo.update(state: resolution, resolved_by_action: resolved_by_action)
|
||||
|
||||
current_user.update_todos_count_cache
|
||||
end
|
||||
|
||||
def restore_todos(todos, current_user)
|
||||
todos_ids = todos.batch_update(state: :pending)
|
||||
|
||||
current_user.update_todos_count_cache
|
||||
|
||||
todos_ids
|
||||
end
|
||||
|
||||
def restore_todo(todo, current_user)
|
||||
return if todo.pending?
|
||||
|
||||
todo.update(state: :pending)
|
||||
|
||||
current_user.update_todos_count_cache
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_todos(users, attributes)
|
||||
Array(users).map do |user|
|
||||
next if pending_todos(user, attributes).exists?
|
||||
|
@ -252,9 +236,9 @@ class TodoService
|
|||
return unless note.can_create_todo?
|
||||
|
||||
project = note.project
|
||||
target = note.noteable
|
||||
target = note.noteable
|
||||
|
||||
mark_pending_todos_as_done(target, author)
|
||||
resolve_todos_for_target(target, author)
|
||||
create_mention_todos(project, target, author, note, skip_users)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.js-operation-settings{ data: { operations_settings_endpoint: project_settings_operations_path(@project),
|
||||
external_dashboard: { url: metrics_external_dashboard_url,
|
||||
help_page_path: help_page_path('user/project/operations/linking_to_an_external_dashboard') } } }
|
|
@ -0,0 +1,5 @@
|
|||
.js-operation-settings{ data: { operations_settings_endpoint: project_settings_operations_path(@project),
|
||||
help_page: help_page_path('user/project/integrations/prometheus'),
|
||||
external_dashboard: { url: metrics_external_dashboard_url,
|
||||
help_page: help_page_path('user/project/operations/linking_to_an_external_dashboard'),
|
||||
} } }
|
|
@ -5,7 +5,7 @@
|
|||
= render 'projects/settings/operations/incidents'
|
||||
= render 'projects/settings/operations/error_tracking'
|
||||
= render 'projects/settings/operations/prometheus', service: prometheus_service if Feature.enabled?(:settings_operations_prometheus_service)
|
||||
= render 'projects/settings/operations/external_dashboard'
|
||||
= render 'projects/settings/operations/metrics_dashboard'
|
||||
= render 'projects/settings/operations/grafana_integration'
|
||||
= render_if_exists 'projects/settings/operations/tracing'
|
||||
= render_if_exists 'projects/settings/operations/status_page'
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update operations metrics settings title and description to make them general
|
||||
merge_request: 32494
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Store Todo resolution method
|
||||
merge_request: 32753
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add version history information on U2F support
|
||||
merge_request: 33229
|
||||
author: Takuya Noguchi
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Document github rate limit behavior
|
||||
merge_request: 33090
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTodoResolvedByAction < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_column :todos, :resolved_by_action, :integer, limit: 2
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_column :todos, :resolved_by_action
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6520,7 +6520,8 @@ CREATE TABLE public.todos (
|
|||
updated_at timestamp without time zone,
|
||||
note_id integer,
|
||||
commit_id character varying,
|
||||
group_id integer
|
||||
group_id integer,
|
||||
resolved_by_action smallint
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.todos_id_seq
|
||||
|
@ -13949,6 +13950,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200519115908
|
||||
20200519171058
|
||||
20200519194042
|
||||
20200520103514
|
||||
20200525114553
|
||||
20200525121014
|
||||
20200526120714
|
||||
|
|
|
@ -12,6 +12,11 @@ Bear in mind that the syntax is very specific. Remove any spaces within the argu
|
|||
before/after the brackets. Also, some shells (for example, `zsh`) can interpret the open/close brackets
|
||||
(`[]`) separately. You may need to either escape the brackets or use double quotes.
|
||||
|
||||
## Caveats
|
||||
|
||||
If the GitHub [rate limit](https://developer.github.com/v3/#rate-limiting) is reached while importing,
|
||||
the importing process will wait (`sleep()`) until it can continue importing.
|
||||
|
||||
## Importing multiple projects
|
||||
|
||||
To import a project from the list of your GitHub projects available:
|
||||
|
|
|
@ -65,8 +65,11 @@ in a safe place.
|
|||
|
||||
### Enable 2FA via U2F device
|
||||
|
||||
> Introduced in [GitLab 8.9](https://about.gitlab.com/blog/2016/06/22/gitlab-adds-support-for-u2f/).
|
||||
|
||||
GitLab officially only supports [YubiKey](https://www.yubico.com/products/)
|
||||
U2F devices, but users have successfully used [SoloKeys](https://solokeys.com/).
|
||||
U2F devices, but users have successfully used [SoloKeys](https://solokeys.com/)
|
||||
or [Google Titan Security Key](https://cloud.google.com/titan-security-key).
|
||||
|
||||
The U2F workflow is [supported by](https://caniuse.com/#search=U2F) the
|
||||
following desktop browsers:
|
||||
|
|
|
@ -89,16 +89,18 @@ module API
|
|||
requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
|
||||
end
|
||||
post ':id/mark_as_done' do
|
||||
TodoService.new.mark_todos_as_done_by_ids(params[:id], current_user)
|
||||
todo = current_user.todos.find(params[:id])
|
||||
|
||||
TodoService.new.resolve_todo(todo, current_user, resolved_by_action: :api_done)
|
||||
|
||||
present todo, with: Entities::Todo, current_user: current_user
|
||||
end
|
||||
|
||||
desc 'Mark all todos as done'
|
||||
post '/mark_as_done' do
|
||||
todos = find_todos
|
||||
TodoService.new.mark_todos_as_done(todos, current_user)
|
||||
|
||||
TodoService.new.resolve_todos(todos, current_user, resolved_by_action: :api_all_done)
|
||||
|
||||
no_content!
|
||||
end
|
||||
|
|
|
@ -9119,18 +9119,6 @@ msgstr ""
|
|||
msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
|
||||
msgstr ""
|
||||
|
||||
msgid "ExternalMetrics|Add a button to the metrics dashboard linking directly to your existing external dashboards."
|
||||
msgstr ""
|
||||
|
||||
msgid "ExternalMetrics|Enter the URL of the dashboard you want to link to"
|
||||
msgstr ""
|
||||
|
||||
msgid "ExternalMetrics|External Dashboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "ExternalMetrics|Full dashboard URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "ExternalWikiService|External Wiki"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13727,6 +13715,18 @@ msgstr ""
|
|||
msgid "Metrics::UsersStarredDashboards|You are not authorized to add star to this dashboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "MetricsSettings|Add a button to the metrics dashboard linking directly to your existing external dashboard."
|
||||
msgstr ""
|
||||
|
||||
msgid "MetricsSettings|External dashboard URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "MetricsSettings|Manage Metrics Dashboard settings."
|
||||
msgstr ""
|
||||
|
||||
msgid "MetricsSettings|Metrics Dashboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Metrics|Add metric"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -5,103 +5,118 @@ require_relative '../lib/gitlab'
|
|||
require_relative '../lib/gitlab/popen'
|
||||
require_relative '../lib/gitlab/popen/runner'
|
||||
|
||||
def emit_warnings(static_analysis)
|
||||
static_analysis.warned_results.each do |result|
|
||||
puts
|
||||
puts "**** #{result.cmd.join(' ')} had the following warning(s):"
|
||||
puts
|
||||
puts result.stderr
|
||||
puts
|
||||
end
|
||||
end
|
||||
class StaticAnalysis
|
||||
ALLOWED_WARNINGS = [
|
||||
# https://github.com/browserslist/browserslist/blob/d0ec62eb48c41c218478cd3ac28684df051cc865/node.js#L329
|
||||
# warns if caniuse-lite package is older than 6 months. Ignore this
|
||||
# warning message so that GitLab backports don't fail.
|
||||
"Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`"
|
||||
].freeze
|
||||
|
||||
def emit_errors(static_analysis)
|
||||
static_analysis.failed_results.each do |result|
|
||||
puts
|
||||
puts "**** #{result.cmd.join(' ')} failed with the following error(s):"
|
||||
puts
|
||||
puts result.stdout
|
||||
puts result.stderr
|
||||
puts
|
||||
end
|
||||
end
|
||||
# `gettext:updated_check` and `gitlab:sidekiq:sidekiq_queues_yml:check` will fail on FOSS installations
|
||||
# (e.g. gitlab-org/gitlab-foss) since they test against a single
|
||||
# file that is generated by an EE installation, which can
|
||||
# contain values that a FOSS installation won't find. To work
|
||||
# around this we will only enable this task on EE installations.
|
||||
TASKS_BY_DURATIONS_SECONDS_DESC = {
|
||||
%w[bin/rake lint:haml] => 338,
|
||||
(Gitlab.ee? ? %w[bin/rake gettext:updated_check] : nil) => 308,
|
||||
# Most of the time, RuboCop finishes in 30 seconds, but sometimes it can take around 1200 seconds so we set a
|
||||
# duration of 300 to lower the likelihood that it will run in the same job as another long task...
|
||||
%w[bundle exec rubocop --parallel] => 300,
|
||||
%w[yarn run eslint] => 197,
|
||||
%w[yarn run prettier-all] => 124,
|
||||
%w[bin/rake gettext:lint] => 96,
|
||||
%w[bundle exec license_finder] => 49,
|
||||
%w[bin/rake scss_lint] => 38,
|
||||
%w[bin/rake lint:static_verification] => 22,
|
||||
%w[bin/rake gitlab:sidekiq:all_queues_yml:check] => 13,
|
||||
(Gitlab.ee? ? %w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check] : nil) => 13,
|
||||
%w[bin/rake config_lint] => 11,
|
||||
%w[yarn run stylelint] => 9,
|
||||
%w[scripts/lint-conflicts.sh] => 0.59,
|
||||
%w[yarn run block-dependencies] => 0.35,
|
||||
%w[scripts/lint-rugged] => 0.23,
|
||||
%w[scripts/gemfile_lock_changed.sh] => 0.02,
|
||||
%w[scripts/frontend/check_no_partial_karma_jest.sh] => 0.01,
|
||||
%w[scripts/lint-changelog-filenames] => 0.01
|
||||
}.reject { |k| k.nil? }.sort_by { |a| -a[1] }.to_h.keys.freeze
|
||||
|
||||
ALLOWED_WARNINGS = [
|
||||
# https://github.com/browserslist/browserslist/blob/d0ec62eb48c41c218478cd3ac28684df051cc865/node.js#L329
|
||||
# warns if caniuse-lite package is older than 6 months. Ignore this
|
||||
# warning message so that GitLab backports don't fail.
|
||||
"Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`"
|
||||
].freeze
|
||||
def run_tasks!
|
||||
tasks = tasks_to_run((ENV['CI_NODE_INDEX'] || 1).to_i, (ENV['CI_NODE_TOTAL'] || 1).to_i)
|
||||
|
||||
def warning_count(static_analysis)
|
||||
static_analysis.warned_results
|
||||
.count { |result| !ALLOWED_WARNINGS.include?(result.stderr.strip) }
|
||||
end
|
||||
static_analysis = Gitlab::Popen::Runner.new
|
||||
|
||||
def jobs_to_run(node_index, node_total)
|
||||
all_tasks = [
|
||||
%w[bin/rake lint:all],
|
||||
%w[bundle exec license_finder],
|
||||
%w[yarn run eslint],
|
||||
%w[yarn run stylelint],
|
||||
%w[yarn run prettier-all],
|
||||
%w[yarn run block-dependencies],
|
||||
%w[bundle exec rubocop --parallel],
|
||||
%w[scripts/lint-conflicts.sh],
|
||||
%w[scripts/lint-rugged],
|
||||
%w[scripts/frontend/check_no_partial_karma_jest.sh],
|
||||
%w[scripts/lint-changelog-filenames],
|
||||
%w[scripts/gemfile_lock_changed.sh]
|
||||
]
|
||||
static_analysis.run(tasks) do |cmd, &run|
|
||||
puts
|
||||
puts "$ #{cmd.join(' ')}"
|
||||
|
||||
case node_total
|
||||
when 1
|
||||
all_tasks
|
||||
when 2
|
||||
rake_lint_all, *rest_jobs = all_tasks
|
||||
case node_index
|
||||
when 1
|
||||
[rake_lint_all]
|
||||
else
|
||||
rest_jobs
|
||||
result = run.call
|
||||
|
||||
puts "==> Finished in #{result.duration} seconds"
|
||||
puts
|
||||
end
|
||||
else
|
||||
raise "Parallelization > 2 (currently set to #{node_total}) isn't supported yet!"
|
||||
|
||||
puts
|
||||
puts '==================================================='
|
||||
puts
|
||||
puts
|
||||
|
||||
if static_analysis.all_success_and_clean?
|
||||
puts 'All static analyses passed successfully.'
|
||||
elsif static_analysis.all_success?
|
||||
puts 'All static analyses passed successfully, but we have warnings:'
|
||||
puts
|
||||
|
||||
emit_warnings(static_analysis)
|
||||
|
||||
exit 2 if warning_count(static_analysis).nonzero?
|
||||
else
|
||||
puts 'Some static analyses failed:'
|
||||
|
||||
emit_warnings(static_analysis)
|
||||
emit_errors(static_analysis)
|
||||
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def emit_warnings(static_analysis)
|
||||
static_analysis.warned_results.each do |result|
|
||||
puts
|
||||
puts "**** #{result.cmd.join(' ')} had the following warning(s):"
|
||||
puts
|
||||
puts result.stderr
|
||||
puts
|
||||
end
|
||||
end
|
||||
|
||||
def emit_errors(static_analysis)
|
||||
static_analysis.failed_results.each do |result|
|
||||
puts
|
||||
puts "**** #{result.cmd.join(' ')} failed with the following error(s):"
|
||||
puts
|
||||
puts result.stdout
|
||||
puts result.stderr
|
||||
puts
|
||||
end
|
||||
end
|
||||
|
||||
def warning_count(static_analysis)
|
||||
static_analysis.warned_results
|
||||
.count { |result| !ALLOWED_WARNINGS.include?(result.stderr.strip) }
|
||||
end
|
||||
|
||||
def tasks_to_run(node_index, node_total)
|
||||
tasks = []
|
||||
TASKS_BY_DURATIONS_SECONDS_DESC.each_with_index do |task, i|
|
||||
tasks << task if i % node_total == (node_index - 1)
|
||||
end
|
||||
|
||||
tasks
|
||||
end
|
||||
end
|
||||
|
||||
tasks = jobs_to_run((ENV['CI_NODE_INDEX'] || 1).to_i, (ENV['CI_NODE_TOTAL'] || 1).to_i)
|
||||
static_analysis = Gitlab::Popen::Runner.new
|
||||
|
||||
static_analysis.run(tasks) do |cmd, &run|
|
||||
puts
|
||||
puts "$ #{cmd.join(' ')}"
|
||||
|
||||
result = run.call
|
||||
|
||||
puts "==> Finished in #{result.duration} seconds"
|
||||
puts
|
||||
end
|
||||
|
||||
puts
|
||||
puts '==================================================='
|
||||
puts
|
||||
puts
|
||||
|
||||
if static_analysis.all_success_and_clean?
|
||||
puts 'All static analyses passed successfully.'
|
||||
elsif static_analysis.all_success?
|
||||
puts 'All static analyses passed successfully, but we have warnings:'
|
||||
puts
|
||||
|
||||
emit_warnings(static_analysis)
|
||||
|
||||
exit 2 if warning_count(static_analysis).nonzero?
|
||||
else
|
||||
puts 'Some static analyses failed:'
|
||||
|
||||
emit_warnings(static_analysis)
|
||||
emit_errors(static_analysis)
|
||||
|
||||
exit 1
|
||||
if $0 == __FILE__
|
||||
StaticAnalysis.new.run_tasks!
|
||||
end
|
||||
|
|
|
@ -114,7 +114,7 @@ describe 'Dashboard Todos' do
|
|||
context 'todo is stale on the page' do
|
||||
before do
|
||||
todos = TodosFinder.new(user, state: :pending).execute
|
||||
TodoService.new.mark_todos_as_done(todos, user)
|
||||
TodoService.new.resolve_todos(todos, user)
|
||||
end
|
||||
|
||||
it_behaves_like 'deleting the todo'
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import ResizablePanel from '~/ide/components/resizable_panel.vue';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import { SIDE_LEFT, SIDE_RIGHT } from '~/ide/constants';
|
||||
|
||||
const TEST_WIDTH = 500;
|
||||
const TEST_MIN_WIDTH = 400;
|
||||
|
||||
describe('~/ide/components/resizable_panel', () => {
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new Vuex.Store({});
|
||||
jest.spyOn(store, 'dispatch').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(ResizablePanel, {
|
||||
propsData: {
|
||||
initialWidth: TEST_WIDTH,
|
||||
minSize: TEST_MIN_WIDTH,
|
||||
side: SIDE_LEFT,
|
||||
...props,
|
||||
},
|
||||
store,
|
||||
localVue,
|
||||
});
|
||||
};
|
||||
const findResizer = () => wrapper.find(PanelResizer);
|
||||
const findInlineStyle = () => wrapper.element.style.cssText;
|
||||
const createInlineStyle = width => `width: ${width}px;`;
|
||||
|
||||
describe.each`
|
||||
props | showResizer | resizerSide | expectedStyle
|
||||
${{ resizable: true, side: SIDE_LEFT }} | ${true} | ${SIDE_RIGHT} | ${createInlineStyle(TEST_WIDTH)}
|
||||
${{ resizable: true, side: SIDE_RIGHT }} | ${true} | ${SIDE_LEFT} | ${createInlineStyle(TEST_WIDTH)}
|
||||
${{ resizable: false, side: SIDE_LEFT }} | ${false} | ${SIDE_RIGHT} | ${''}
|
||||
`('with props $props', ({ props, showResizer, resizerSide, expectedStyle }) => {
|
||||
beforeEach(() => {
|
||||
createComponent(props);
|
||||
});
|
||||
|
||||
it(`show resizer is ${showResizer}`, () => {
|
||||
const expectedDisplay = showResizer ? '' : 'none';
|
||||
const resizer = findResizer();
|
||||
|
||||
expect(resizer.exists()).toBe(true);
|
||||
expect(resizer.element.style.display).toBe(expectedDisplay);
|
||||
});
|
||||
|
||||
it(`resizer side is '${resizerSide}'`, () => {
|
||||
const resizer = findResizer();
|
||||
|
||||
expect(resizer.props('side')).toBe(resizerSide);
|
||||
});
|
||||
|
||||
it(`has style '${expectedStyle}'`, () => {
|
||||
expect(findInlineStyle()).toBe(expectedStyle);
|
||||
});
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('does not dispatch anything', () => {
|
||||
expect(store.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each`
|
||||
event | dispatchArgs
|
||||
${'resize-start'} | ${['setResizingStatus', true]}
|
||||
${'resize-end'} | ${['setResizingStatus', false]}
|
||||
`('when resizer emits $event, dispatch $dispatchArgs', ({ event, dispatchArgs }) => {
|
||||
const resizer = findResizer();
|
||||
|
||||
resizer.vm.$emit(event);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(...dispatchArgs);
|
||||
});
|
||||
|
||||
it('renders resizer', () => {
|
||||
const resizer = findResizer();
|
||||
|
||||
expect(resizer.props()).toMatchObject({
|
||||
maxSize: window.innerWidth / 2,
|
||||
minSize: TEST_MIN_WIDTH,
|
||||
startSize: TEST_WIDTH,
|
||||
});
|
||||
});
|
||||
|
||||
it('when resizer emits update:size, changes inline width', () => {
|
||||
const newSize = TEST_WIDTH - 100;
|
||||
const resizer = findResizer();
|
||||
|
||||
resizer.vm.$emit('update:size', newSize);
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findInlineStyle()).toBe(createInlineStyle(newSize));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,7 +1,8 @@
|
|||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import { GlDeprecatedButton, GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import ExternalDashboard from '~/operation_settings/components/external_dashboard.vue';
|
||||
import MetricsSettings from '~/operation_settings/components/metrics_settings.vue';
|
||||
import ExternalDashboard from '~/operation_settings/components/form_group/external_dashboard.vue';
|
||||
import store from '~/operation_settings/store';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { refreshCurrentPage } from '~/lib/utils/url_utility';
|
||||
|
@ -12,18 +13,25 @@ jest.mock('~/flash');
|
|||
|
||||
describe('operation settings external dashboard component', () => {
|
||||
let wrapper;
|
||||
|
||||
const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`;
|
||||
const helpPage = `${TEST_HOST}/help/metrics/page/path`;
|
||||
const externalDashboardUrl = `http://mock-external-domain.com/external/dashboard/url`;
|
||||
const externalDashboardHelpPagePath = `${TEST_HOST}/help/page/path`;
|
||||
const externalDashboardHelpPage = `${TEST_HOST}/help/external/page/path`;
|
||||
|
||||
const mountComponent = (shallow = true) => {
|
||||
const config = [
|
||||
ExternalDashboard,
|
||||
MetricsSettings,
|
||||
{
|
||||
store: store({
|
||||
operationsSettingsEndpoint,
|
||||
helpPage,
|
||||
externalDashboardUrl,
|
||||
externalDashboardHelpPagePath,
|
||||
externalDashboardHelpPage,
|
||||
}),
|
||||
stubs: {
|
||||
ExternalDashboard,
|
||||
},
|
||||
},
|
||||
];
|
||||
wrapper = shallow ? shallowMount(...config) : mount(...config);
|
||||
|
@ -44,7 +52,7 @@ describe('operation settings external dashboard component', () => {
|
|||
|
||||
it('renders header text', () => {
|
||||
mountComponent();
|
||||
expect(wrapper.find('.js-section-header').text()).toBe('External Dashboard');
|
||||
expect(wrapper.find('.js-section-header').text()).toBe('Metrics Dashboard');
|
||||
});
|
||||
|
||||
describe('expand/collapse button', () => {
|
||||
|
@ -64,16 +72,14 @@ describe('operation settings external dashboard component', () => {
|
|||
});
|
||||
|
||||
it('renders descriptive text', () => {
|
||||
expect(subHeader.text()).toContain(
|
||||
'Add a button to the metrics dashboard linking directly to your existing external dashboards.',
|
||||
);
|
||||
expect(subHeader.text()).toContain('Manage Metrics Dashboard settings.');
|
||||
});
|
||||
|
||||
it('renders help page link', () => {
|
||||
const link = subHeader.find(GlLink);
|
||||
|
||||
expect(link.text()).toBe('Learn more');
|
||||
expect(link.attributes().href).toBe(externalDashboardHelpPagePath);
|
||||
expect(link.attributes().href).toBe(helpPage);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -82,18 +88,17 @@ describe('operation settings external dashboard component', () => {
|
|||
let formGroup;
|
||||
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
formGroup = wrapper.find(GlFormGroup);
|
||||
mountComponent(false);
|
||||
formGroup = wrapper.find(ExternalDashboard).find(GlFormGroup);
|
||||
});
|
||||
|
||||
it('uses label text', () => {
|
||||
expect(formGroup.attributes().label).toBe('Full dashboard URL');
|
||||
expect(formGroup.find('label').text()).toBe('External dashboard URL');
|
||||
});
|
||||
|
||||
it('uses description text', () => {
|
||||
expect(formGroup.attributes().description).toBe(
|
||||
'Enter the URL of the dashboard you want to link to',
|
||||
);
|
||||
const description = formGroup.find('small');
|
||||
expect(description.find('a').attributes('href')).toBe(externalDashboardHelpPage);
|
||||
});
|
||||
});
|
||||
|
|
@ -13,7 +13,7 @@ describe('operation settings mutations', () => {
|
|||
const mockUrl = 'mockUrl';
|
||||
mutations.SET_EXTERNAL_DASHBOARD_URL(localState, mockUrl);
|
||||
|
||||
expect(localState.externalDashboardUrl).toBe(mockUrl);
|
||||
expect(localState.externalDashboard.url).toBe(mockUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ exports[`WebIDE runs 1`] = `
|
|||
(jest: contents hidden)
|
||||
</div>
|
||||
<div
|
||||
class="multi-file-commit-panel flex-column"
|
||||
class="gl-relative multi-file-commit-panel flex-column"
|
||||
style="width: 340px;"
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -9,23 +9,18 @@ describe GitlabSchema.types['Query'] do
|
|||
|
||||
it 'has the expected fields' do
|
||||
expected_fields = %i[
|
||||
current_user
|
||||
design_management
|
||||
geoNode
|
||||
project
|
||||
namespace
|
||||
group
|
||||
echo
|
||||
instanceSecurityDashboard
|
||||
metadata
|
||||
namespace
|
||||
project
|
||||
projects
|
||||
current_user
|
||||
snippets
|
||||
design_management
|
||||
user
|
||||
vulnerabilities
|
||||
vulnerabilitiesCountByDayAndSeverity
|
||||
]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
expect(described_class).to have_graphql_fields(*expected_fields).at_least
|
||||
end
|
||||
|
||||
describe 'namespace field' do
|
||||
|
|
|
@ -393,10 +393,10 @@ describe Todo do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.update_state' do
|
||||
describe '.batch_update' do
|
||||
it 'updates the state of todos' do
|
||||
todo = create(:todo, :pending)
|
||||
ids = described_class.update_state(:done)
|
||||
ids = described_class.batch_update(state: :done)
|
||||
|
||||
todo.reload
|
||||
|
||||
|
@ -407,7 +407,7 @@ describe Todo do
|
|||
it 'does not update todos that already have the given state' do
|
||||
create(:todo, :pending)
|
||||
|
||||
expect(described_class.update_state(:pending)).to be_empty
|
||||
expect(described_class.batch_update(state: :pending)).to be_empty
|
||||
end
|
||||
|
||||
it 'updates updated_at' do
|
||||
|
@ -416,7 +416,7 @@ describe Todo do
|
|||
Timecop.freeze(1.day.from_now) do
|
||||
expected_update_date = Time.current.utc
|
||||
|
||||
ids = described_class.update_state(:done)
|
||||
ids = described_class.batch_update(state: :done)
|
||||
|
||||
expect(Todo.where(id: ids).map(&:updated_at)).to all(be_like_time(expected_update_date))
|
||||
end
|
||||
|
|
|
@ -274,12 +274,12 @@ describe TodoService do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#mark_pending_todos_as_done' do
|
||||
describe '#resolve_todos_for_target' do
|
||||
it 'marks related pending todos to the target for the user as done' do
|
||||
first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
|
||||
second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
|
||||
|
||||
service.mark_pending_todos_as_done(issue, john_doe)
|
||||
service.resolve_todos_for_target(issue, john_doe)
|
||||
|
||||
expect(first_todo.reload).to be_done
|
||||
expect(second_todo.reload).to be_done
|
||||
|
@ -293,7 +293,7 @@ describe TodoService do
|
|||
expect(john_doe.todos_pending_count).to eq(1)
|
||||
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
|
||||
|
||||
service.mark_pending_todos_as_done(issue, john_doe)
|
||||
service.resolve_todos_for_target(issue, john_doe)
|
||||
|
||||
expect(john_doe.todos_done_count).to eq(1)
|
||||
expect(john_doe.todos_pending_count).to eq(0)
|
||||
|
@ -301,59 +301,6 @@ describe TodoService do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'updating todos state' do |meth, state, new_state|
|
||||
let!(:first_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) }
|
||||
let!(:second_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) }
|
||||
|
||||
it 'updates related todos for the user with the new_state' do
|
||||
service.send(meth, collection, john_doe)
|
||||
|
||||
expect(first_todo.reload.state?(new_state)).to be true
|
||||
expect(second_todo.reload.state?(new_state)).to be true
|
||||
end
|
||||
|
||||
it 'returns the updated ids' do
|
||||
expect(service.send(meth, collection, john_doe)).to match_array([first_todo.id, second_todo.id])
|
||||
end
|
||||
|
||||
describe 'cached counts' do
|
||||
it 'updates when todos change' do
|
||||
expect(john_doe.todos.where(state: new_state).count).to eq(0)
|
||||
expect(john_doe.todos.where(state: state).count).to eq(2)
|
||||
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
|
||||
|
||||
service.send(meth, collection, john_doe)
|
||||
|
||||
expect(john_doe.todos.where(state: new_state).count).to eq(2)
|
||||
expect(john_doe.todos.where(state: state).count).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mark_todos_as_done' do
|
||||
it_behaves_like 'updating todos state', :mark_todos_as_done, :pending, :done do
|
||||
let(:collection) { Todo.all }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mark_todos_as_done_by_ids' do
|
||||
it_behaves_like 'updating todos state', :mark_todos_as_done_by_ids, :pending, :done do
|
||||
let(:collection) { [first_todo, second_todo].map(&:id) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mark_todos_as_pending' do
|
||||
it_behaves_like 'updating todos state', :mark_todos_as_pending, :done, :pending do
|
||||
let(:collection) { Todo.all }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mark_todos_as_pending_by_ids' do
|
||||
it_behaves_like 'updating todos state', :mark_todos_as_pending_by_ids, :done, :pending do
|
||||
let(:collection) { [first_todo, second_todo].map(&:id) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#new_note' do
|
||||
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
|
||||
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
|
||||
|
@ -1000,121 +947,111 @@ describe TodoService do
|
|||
expect(john_doe.todos_pending_count).to eq(1)
|
||||
end
|
||||
|
||||
describe '#mark_todos_as_done' do
|
||||
let(:issue) { create(:issue, project: project, author: author, assignees: [john_doe]) }
|
||||
let(:another_issue) { create(:issue, project: project, author: author, assignees: [john_doe]) }
|
||||
shared_examples 'updating todos state' do |state, new_state, new_resolved_by = nil|
|
||||
let!(:first_todo) { create(:todo, state, user: john_doe) }
|
||||
let!(:second_todo) { create(:todo, state, user: john_doe) }
|
||||
let(:collection) { Todo.all }
|
||||
|
||||
it 'marks a relation of todos as done' do
|
||||
create(:todo, :mentioned, user: john_doe, target: issue, project: project)
|
||||
it 'updates related todos for the user with the new_state' do
|
||||
method_call
|
||||
|
||||
todos = TodosFinder.new(john_doe, {}).execute
|
||||
expect { described_class.new.mark_todos_as_done(todos, john_doe) }
|
||||
.to change { john_doe.todos.done.count }.from(0).to(1)
|
||||
expect(collection.all? { |todo| todo.reload.state?(new_state)}).to be_truthy
|
||||
end
|
||||
|
||||
it 'marks an array of todos as done' do
|
||||
todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
|
||||
if new_resolved_by
|
||||
it 'updates resolution mechanism' do
|
||||
method_call
|
||||
|
||||
todos = TodosFinder.new(john_doe, {}).execute
|
||||
expect { described_class.new.mark_todos_as_done(todos, john_doe) }
|
||||
.to change { todo.reload.state }.from('pending').to('done')
|
||||
end
|
||||
|
||||
it 'returns the ids of updated todos' do # Needed on API
|
||||
todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
|
||||
|
||||
todos = TodosFinder.new(john_doe, {}).execute
|
||||
expect(described_class.new.mark_todos_as_done(todos, john_doe)).to eq([todo.id])
|
||||
end
|
||||
|
||||
context 'when some of the todos are done already' do
|
||||
let!(:first_todo) { create(:todo, :mentioned, user: john_doe, target: issue, project: project) }
|
||||
let!(:second_todo) { create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) }
|
||||
|
||||
it 'returns the ids of those still pending' do
|
||||
described_class.new.mark_pending_todos_as_done(issue, john_doe)
|
||||
|
||||
expect(described_class.new.mark_todos_as_done(Todo.all, john_doe)).to eq([second_todo.id])
|
||||
expect(collection.all? { |todo| todo.reload.resolved_by_action == new_resolved_by }).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns an empty array if all are done' do
|
||||
described_class.new.mark_pending_todos_as_done(issue, john_doe)
|
||||
described_class.new.mark_pending_todos_as_done(another_issue, john_doe)
|
||||
it 'returns the updated ids' do
|
||||
expect(method_call).to match_array([first_todo.id, second_todo.id])
|
||||
end
|
||||
|
||||
expect(described_class.new.mark_todos_as_done(Todo.all, john_doe)).to eq([])
|
||||
describe 'cached counts' do
|
||||
it 'updates when todos change' do
|
||||
expect(john_doe.todos.where(state: new_state).count).to eq(0)
|
||||
expect(john_doe.todos.where(state: state).count).to eq(2)
|
||||
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
|
||||
|
||||
method_call
|
||||
|
||||
expect(john_doe.todos.where(state: new_state).count).to eq(2)
|
||||
expect(john_doe.todos.where(state: state).count).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mark_todo_as_done' do
|
||||
it 'marks a todo done' do
|
||||
todo1 = create(:todo, :pending, user: john_doe)
|
||||
|
||||
described_class.new.mark_todo_as_done(todo1, john_doe)
|
||||
|
||||
expect(todo1.reload.state).to eq('done')
|
||||
end
|
||||
|
||||
context 'when todo is already in state done' do
|
||||
let(:todo1) { create(:todo, :done, user: john_doe) }
|
||||
|
||||
it 'does not update the todo' do
|
||||
expect { described_class.new.mark_todo_as_done(todo1, john_doe) }.not_to change(todo1.reload, :state)
|
||||
end
|
||||
|
||||
it 'does not update cache count' do
|
||||
expect(john_doe).not_to receive(:update_todos_count_cache)
|
||||
|
||||
described_class.new.mark_todo_as_done(todo1, john_doe)
|
||||
describe '#resolve_todos' do
|
||||
it_behaves_like 'updating todos state', :pending, :done, 'mark_done' do
|
||||
subject(:method_call) do
|
||||
service.resolve_todos(collection, john_doe, resolution: :done, resolved_by_action: :mark_done)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mark_all_todos_as_done_by_user' do
|
||||
it 'marks all todos done' do
|
||||
todo1 = create(:todo, user: john_doe, state: :pending)
|
||||
todo2 = create(:todo, user: john_doe, state: :done)
|
||||
todo3 = create(:todo, user: john_doe, state: :pending)
|
||||
|
||||
ids = described_class.new.mark_all_todos_as_done_by_user(john_doe)
|
||||
|
||||
expect(ids).to contain_exactly(todo1.id, todo3.id)
|
||||
expect(todo1.reload.state).to eq('done')
|
||||
expect(todo2.reload.state).to eq('done')
|
||||
expect(todo3.reload.state).to eq('done')
|
||||
describe '#restore_todos' do
|
||||
it_behaves_like 'updating todos state', :done, :pending do
|
||||
subject(:method_call) do
|
||||
service.restore_todos(collection, john_doe)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#mark_todos_as_done_by_ids' do
|
||||
let(:issue) { create(:issue, project: project, author: author, assignees: [john_doe]) }
|
||||
let(:another_issue) { create(:issue, project: project, author: author, assignees: [john_doe]) }
|
||||
describe '#resolve_todo' do
|
||||
let!(:todo) { create(:todo, :assigned, user: john_doe) }
|
||||
|
||||
it 'marks an array of todo ids as done' do
|
||||
todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
|
||||
another_todo = create(:todo, :mentioned, user: john_doe, target: another_issue, project: project)
|
||||
|
||||
expect { described_class.new.mark_todos_as_done_by_ids([todo.id, another_todo.id], john_doe) }
|
||||
.to change { john_doe.todos.done.count }.from(0).to(2)
|
||||
it 'marks pending todo as done' do
|
||||
expect do
|
||||
service.resolve_todo(todo, john_doe)
|
||||
todo.reload
|
||||
end.to change { todo.done? }.to(true)
|
||||
end
|
||||
|
||||
it 'marks a single todo id as done' do
|
||||
todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
|
||||
|
||||
expect { described_class.new.mark_todos_as_done_by_ids(todo.id, john_doe) }
|
||||
.to change { todo.reload.state }.from('pending').to('done')
|
||||
it 'saves resolution mechanism' do
|
||||
expect do
|
||||
service.resolve_todo(todo, john_doe, resolved_by_action: :mark_done)
|
||||
todo.reload
|
||||
end.to change { todo.resolved_by_mark_done? }.to(true)
|
||||
end
|
||||
|
||||
it 'caches the number of todos of a user', :use_clean_rails_memory_store_caching do
|
||||
create(:todo, :mentioned, user: john_doe, target: issue, project: project)
|
||||
todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
|
||||
context 'cached counts' do
|
||||
it 'updates when todos change' do
|
||||
expect(john_doe.todos_done_count).to eq(0)
|
||||
expect(john_doe.todos_pending_count).to eq(1)
|
||||
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
|
||||
|
||||
described_class.new.mark_todos_as_done_by_ids(todo, john_doe)
|
||||
service.resolve_todo(todo, john_doe)
|
||||
|
||||
# Make sure no TodosFinder is inialized to perform counting
|
||||
expect(TodosFinder).not_to receive(:new)
|
||||
expect(john_doe.todos_done_count).to eq(1)
|
||||
expect(john_doe.todos_pending_count).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
expect(john_doe.todos_done_count).to eq(1)
|
||||
expect(john_doe.todos_pending_count).to eq(1)
|
||||
describe '#restore_todo' do
|
||||
let!(:todo) { create(:todo, :done, user: john_doe) }
|
||||
|
||||
it 'marks resolved todo as pending' do
|
||||
expect do
|
||||
service.restore_todo(todo, john_doe)
|
||||
todo.reload
|
||||
end.to change { todo.pending? }.to(true)
|
||||
end
|
||||
|
||||
context 'cached counts' do
|
||||
it 'updates when todos change' do
|
||||
expect(john_doe.todos_done_count).to eq(1)
|
||||
expect(john_doe.todos_pending_count).to eq(0)
|
||||
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
|
||||
|
||||
service.restore_todo(todo, john_doe)
|
||||
|
||||
expect(john_doe.todos_done_count).to eq(0)
|
||||
expect(john_doe.todos_pending_count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue