Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-28 21:08:37 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-28 21:08:37 +0300
commit1ec60cf53bc498b12c597ff0d91434a1bdb7cba9 (patch)
treef2c1b77f47cab1fb9fd11141337be67b12f2316b
parent1f1e53f43f87cada9b515571cc973e9eadcbc4e4 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml1
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue6
-rw-r--r--app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue29
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue19
-rw-r--r--app/assets/javascripts/ide/components/resizable_panel.vue14
-rw-r--r--app/assets/javascripts/ide/constants.js5
-rw-r--r--app/assets/javascripts/operation_settings/components/external_dashboard.vue72
-rw-r--r--app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue50
-rw-r--r--app/assets/javascripts/operation_settings/components/metrics_settings.vue50
-rw-r--r--app/assets/javascripts/operation_settings/index.js4
-rw-r--r--app/assets/javascripts/operation_settings/store/actions.js14
-rw-r--r--app/assets/javascripts/operation_settings/store/mutations.js2
-rw-r--r--app/assets/javascripts/operation_settings/store/state.js7
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss3
-rw-r--r--app/controllers/dashboard/todos_controller.rb10
-rw-r--r--app/graphql/mutations/todos/mark_all_done.rb4
-rw-r--r--app/graphql/mutations/todos/mark_done.rb2
-rw-r--r--app/graphql/mutations/todos/restore.rb6
-rw-r--r--app/graphql/mutations/todos/restore_many.rb2
-rw-r--r--app/models/todo.rb14
-rw-r--r--app/models/user.rb4
-rw-r--r--app/services/issuable_base_service.rb2
-rw-r--r--app/services/issues/update_service.rb4
-rw-r--r--app/services/merge_requests/update_service.rb4
-rw-r--r--app/services/todo_service.rb88
-rw-r--r--app/views/projects/settings/operations/_external_dashboard.html.haml3
-rw-r--r--app/views/projects/settings/operations/_metrics_dashboard.html.haml5
-rw-r--r--app/views/projects/settings/operations/show.html.haml2
-rw-r--r--changelogs/unreleased/214370-extend-metrics-settings.yml5
-rw-r--r--changelogs/unreleased/216045-capture-todo-resolution.yml5
-rw-r--r--changelogs/unreleased/docs-u2f-version-history.yml5
-rw-r--r--changelogs/unreleased/kborges-github-import-rake-add-rate-limit-doc.yml5
-rw-r--r--db/migrate/20200520103514_add_todo_resolved_by_action.rb19
-rw-r--r--db/structure.sql4
-rw-r--r--[-rwxr-xr-x]doc/.vale/gitlab/SentenceLength.yml0
-rw-r--r--doc/administration/raketasks/github_import.md5
-rw-r--r--doc/user/profile/account/two_factor_authentication.md5
-rw-r--r--lib/api/todos.rb6
-rw-r--r--locale/gitlab.pot24
-rwxr-xr-xscripts/static-analysis181
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb2
-rw-r--r--spec/frontend/ide/components/resizable_panel_spec.js114
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js (renamed from spec/frontend/operation_settings/components/external_dashboard_spec.js)35
-rw-r--r--spec/frontend/operation_settings/store/mutations_spec.js2
-rw-r--r--spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap2
-rw-r--r--spec/graphql/types/query_type_spec.rb15
-rw-r--r--spec/models/todo_spec.rb8
-rw-r--r--spec/services/todo_service_spec.rb211
49 files changed, 614 insertions, 468 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index e8087aebcef..b9c19b21ef3 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -46,7 +46,8 @@ static-analysis:
stage: test
variables:
SETUP_DB: "false"
- parallel: 2
+ ENABLE_SPRING: "1"
+ parallel: 4
script:
- scripts/static-analysis
cache:
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 46a281cd48f..d26685645f9 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -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
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 31a239092c2..1eb89b41495 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -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"
>
diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
index 2814844d157..4e8e1e3a470 100644
--- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
+++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
@@ -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"
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index 4a9de9e0c03..caa05daf8bd 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -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>
diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue
index d55150f48ad..b49d743d877 100644
--- a/app/assets/javascripts/ide/components/resizable_panel.vue
+++ b/app/assets/javascripts/ide/components/resizable_panel.vue
@@ -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"
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index bc2cb3fdadc..555754c7104 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -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';
diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
deleted file mode 100644
index e9c7d7c5d56..00000000000
--- a/app/assets/javascripts/operation_settings/components/external_dashboard.vue
+++ /dev/null
@@ -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>
diff --git a/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue
new file mode 100644
index 00000000000..8f8e7db783d
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue
@@ -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>
diff --git a/app/assets/javascripts/operation_settings/components/metrics_settings.vue b/app/assets/javascripts/operation_settings/components/metrics_settings.vue
new file mode 100644
index 00000000000..ebf486162fc
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/components/metrics_settings.vue
@@ -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>
diff --git a/app/assets/javascripts/operation_settings/index.js b/app/assets/javascripts/operation_settings/index.js
index f075291ce98..426a060949e 100644
--- a/app/assets/javascripts/operation_settings/index.js
+++ b/app/assets/javascripts/operation_settings/index.js
@@ -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);
},
});
};
diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js
index ec05b0c76cf..62d00ac50e3 100644
--- a/app/assets/javascripts/operation_settings/store/actions.js
+++ b/app/assets/javascripts/operation_settings/store/actions.js
@@ -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');
diff --git a/app/assets/javascripts/operation_settings/store/mutations.js b/app/assets/javascripts/operation_settings/store/mutations.js
index 64bb33bb89f..eb495839616 100644
--- a/app/assets/javascripts/operation_settings/store/mutations.js
+++ b/app/assets/javascripts/operation_settings/store/mutations.js
@@ -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;
},
};
diff --git a/app/assets/javascripts/operation_settings/store/state.js b/app/assets/javascripts/operation_settings/store/state.js
index 72167141c48..5376f6b32e6 100644
--- a/app/assets/javascripts/operation_settings/store/state.js
+++ b/app/assets/javascripts/operation_settings/store/state.js
@@ -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,
+ },
});
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index ddfdf8d0553..199778a8529 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -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);
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index ebee8e9094e..8a8064b24c2 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -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
diff --git a/app/graphql/mutations/todos/mark_all_done.rb b/app/graphql/mutations/todos/mark_all_done.rb
index 5694985717c..d30d1bcbcf0 100644
--- a/app/graphql/mutations/todos/mark_all_done.rb
+++ b/app/graphql/mutations/todos/mark_all_done.rb
@@ -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
diff --git a/app/graphql/mutations/todos/mark_done.rb b/app/graphql/mutations/todos/mark_done.rb
index d738e387c43..748e02d8782 100644
--- a/app/graphql/mutations/todos/mark_done.rb
+++ b/app/graphql/mutations/todos/mark_done.rb
@@ -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
diff --git a/app/graphql/mutations/todos/restore.rb b/app/graphql/mutations/todos/restore.rb
index c4597bd84a2..a0a1772db0a 100644
--- a/app/graphql/mutations/todos/restore.rb
+++ b/app/graphql/mutations/todos/restore.rb
@@ -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
diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb
index 8a6265207cd..e95651b232f 100644
--- a/app/graphql/mutations/todos/restore_many.rb
+++ b/app/graphql/mutations/todos/restore_many.rb
@@ -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
diff --git a/app/models/todo.rb b/app/models/todo.rb
index b5ca2ec7466..3af2d16deaa 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index 4311d2e37b1..e2455e667be 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 18062bd60da..3fa5b60369c 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -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
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index ee1a22634af..d59bc0cc970 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -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
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 2d33e87bf4b..561695baeab 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -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
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 55f888d5664..9a1934b322f 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -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,76 +151,60 @@ 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)
+ resolve_todos_for_target(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
+ # When user marks an issue as todo
+ def mark_todo(issuable, current_user)
+ attributes = attributes_for_todo(issuable.project, issuable, current_user, Todo::MARKED)
+ create_todos(current_user, attributes)
end
- # When user marks some todos as done
- def mark_todos_as_done(todos, current_user)
- update_todos_state(todos, current_user, :done)
+ def todo_exist?(issuable, current_user)
+ TodosFinder.new(current_user).any_for_target?(issuable, :pending)
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
+ # Resolves all todos related to target
+ def resolve_todos_for_target(target, current_user)
+ attributes = attributes_for_target(target)
- def mark_all_todos_as_done_by_user(current_user)
- todos = TodosFinder.new(current_user).execute
- mark_todos_as_done(todos, current_user)
+ resolve_todos(pending_todos(current_user, attributes), current_user)
end
- def mark_todo_as_done(todo, current_user)
- return if todo.done?
-
- todo.update(state: :done)
+ 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
- end
- # When user marks some todos as pending
- def mark_todos_as_pending(todos, current_user)
- update_todos_state(todos, current_user, :pending)
+ todos_ids
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)
- end
+ def resolve_todo(todo, current_user, resolution: :done, resolved_by_action: :system_done)
+ return if todo.done?
- # When user marks an issue as todo
- def mark_todo(issuable, current_user)
- attributes = attributes_for_todo(issuable.project, issuable, current_user, Todo::MARKED)
- create_todos(current_user, attributes)
- end
+ todo.update(state: resolution, resolved_by_action: resolved_by_action)
- def todo_exist?(issuable, current_user)
- TodosFinder.new(current_user).any_for_target?(issuable, :pending)
+ current_user.update_todos_count_cache
end
- private
+ def restore_todos(todos, current_user)
+ todos_ids = todos.batch_update(state: :pending)
- def todos_by_ids(ids, current_user)
- current_user.todos_limited_to(Array(ids))
+ current_user.update_todos_count_cache
+
+ todos_ids
end
- def update_todos_state(todos, current_user, state)
- todos_ids = todos.update_state(state)
+ def restore_todo(todo, current_user)
+ return if todo.pending?
- current_user.update_todos_count_cache
+ todo.update(state: :pending)
- todos_ids
+ 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
diff --git a/app/views/projects/settings/operations/_external_dashboard.html.haml b/app/views/projects/settings/operations/_external_dashboard.html.haml
deleted file mode 100644
index 08d50a336fd..00000000000
--- a/app/views/projects/settings/operations/_external_dashboard.html.haml
+++ /dev/null
@@ -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') } } }
diff --git a/app/views/projects/settings/operations/_metrics_dashboard.html.haml b/app/views/projects/settings/operations/_metrics_dashboard.html.haml
new file mode 100644
index 00000000000..eda8a52f188
--- /dev/null
+++ b/app/views/projects/settings/operations/_metrics_dashboard.html.haml
@@ -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'),
+ } } }
diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml
index ee47d70171b..9e4fbf81ca4 100644
--- a/app/views/projects/settings/operations/show.html.haml
+++ b/app/views/projects/settings/operations/show.html.haml
@@ -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'
diff --git a/changelogs/unreleased/214370-extend-metrics-settings.yml b/changelogs/unreleased/214370-extend-metrics-settings.yml
new file mode 100644
index 00000000000..56045e3cb5d
--- /dev/null
+++ b/changelogs/unreleased/214370-extend-metrics-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Update operations metrics settings title and description to make them general
+merge_request: 32494
+author:
+type: changed
diff --git a/changelogs/unreleased/216045-capture-todo-resolution.yml b/changelogs/unreleased/216045-capture-todo-resolution.yml
new file mode 100644
index 00000000000..cc43635a120
--- /dev/null
+++ b/changelogs/unreleased/216045-capture-todo-resolution.yml
@@ -0,0 +1,5 @@
+---
+title: Store Todo resolution method
+merge_request: 32753
+author:
+type: added
diff --git a/changelogs/unreleased/docs-u2f-version-history.yml b/changelogs/unreleased/docs-u2f-version-history.yml
new file mode 100644
index 00000000000..e847faa317e
--- /dev/null
+++ b/changelogs/unreleased/docs-u2f-version-history.yml
@@ -0,0 +1,5 @@
+---
+title: Add version history information on U2F support
+merge_request: 33229
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/kborges-github-import-rake-add-rate-limit-doc.yml b/changelogs/unreleased/kborges-github-import-rake-add-rate-limit-doc.yml
new file mode 100644
index 00000000000..5d797b0f7a1
--- /dev/null
+++ b/changelogs/unreleased/kborges-github-import-rake-add-rate-limit-doc.yml
@@ -0,0 +1,5 @@
+---
+title: Document github rate limit behavior
+merge_request: 33090
+author:
+type: other
diff --git a/db/migrate/20200520103514_add_todo_resolved_by_action.rb b/db/migrate/20200520103514_add_todo_resolved_by_action.rb
new file mode 100644
index 00000000000..0aa91091451
--- /dev/null
+++ b/db/migrate/20200520103514_add_todo_resolved_by_action.rb
@@ -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
diff --git a/db/structure.sql b/db/structure.sql
index 0f5065719da..a8192b2f78e 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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
diff --git a/doc/.vale/gitlab/SentenceLength.yml b/doc/.vale/gitlab/SentenceLength.yml
index 5894fd28f99..5894fd28f99 100755..100644
--- a/doc/.vale/gitlab/SentenceLength.yml
+++ b/doc/.vale/gitlab/SentenceLength.yml
diff --git a/doc/administration/raketasks/github_import.md b/doc/administration/raketasks/github_import.md
index 83a3d2c8884..a46a2b34687 100644
--- a/doc/administration/raketasks/github_import.md
+++ b/doc/administration/raketasks/github_import.md
@@ -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:
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index 6a0f19d61b0..18f9fc59d7a 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -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:
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 43ed9c96486..e36ddf21277 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 20a26662b4a..1ecbda9c7f5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/scripts/static-analysis b/scripts/static-analysis
index ede29b85b8d..9103a9c14af 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -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
+
+ # `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
+
+ def run_tasks!
+ tasks = tasks_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
-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
- puts result.stdout
- puts result.stderr
puts
- end
-end
-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
+ 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
-def warning_count(static_analysis)
- static_analysis.warned_results
- .count { |result| !ALLOWED_WARNINGS.include?(result.stderr.strip) }
-end
+ emit_warnings(static_analysis)
-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]
- ]
-
- case node_total
- when 1
- all_tasks
- when 2
- rake_lint_all, *rest_jobs = all_tasks
- case node_index
- when 1
- [rake_lint_all]
+ exit 2 if warning_count(static_analysis).nonzero?
else
- rest_jobs
- end
- else
- raise "Parallelization > 2 (currently set to #{node_total}) isn't supported yet!"
- 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
+ puts 'Some static analyses failed:'
-static_analysis.run(tasks) do |cmd, &run|
- puts
- puts "$ #{cmd.join(' ')}"
+ emit_warnings(static_analysis)
+ emit_errors(static_analysis)
- result = run.call
-
- puts "==> Finished in #{result.duration} seconds"
- puts
-end
+ exit 1
+ end
+ end
-puts
-puts '==================================================='
-puts
-puts
+ 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
-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
+ 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
- emit_warnings(static_analysis)
+ def warning_count(static_analysis)
+ static_analysis.warned_results
+ .count { |result| !ALLOWED_WARNINGS.include?(result.stderr.strip) }
+ end
- exit 2 if warning_count(static_analysis).nonzero?
-else
- puts 'Some static analyses failed:'
+ 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
- emit_warnings(static_analysis)
- emit_errors(static_analysis)
+ tasks
+ end
+end
- exit 1
+if $0 == __FILE__
+ StaticAnalysis.new.run_tasks!
end
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 63867d5796a..c4f6d9a279f 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -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'
diff --git a/spec/frontend/ide/components/resizable_panel_spec.js b/spec/frontend/ide/components/resizable_panel_spec.js
new file mode 100644
index 00000000000..7368de0cee7
--- /dev/null
+++ b/spec/frontend/ide/components/resizable_panel_spec.js
@@ -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));
+ });
+ });
+ });
+});
diff --git a/spec/frontend/operation_settings/components/external_dashboard_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index 19214d1d954..7b7cf841fb7 100644
--- a/spec/frontend/operation_settings/components/external_dashboard_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -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);
});
});
diff --git a/spec/frontend/operation_settings/store/mutations_spec.js b/spec/frontend/operation_settings/store/mutations_spec.js
index 1854142c89a..670ba95ab03 100644
--- a/spec/frontend/operation_settings/store/mutations_spec.js
+++ b/spec/frontend/operation_settings/store/mutations_spec.js
@@ -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);
});
});
});
diff --git a/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap b/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap
index a76f7960d03..fe714924c2b 100644
--- a/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap
+++ b/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap
@@ -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
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index a23103b4e58..1bb04f1bb68 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -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
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 08f0627191a..4ec93f5a408 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -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
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 4894cf12372..56725dd0bd8 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -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')
+ expect(collection.all? { |todo| todo.reload.resolved_by_action == new_resolved_by }).to be_truthy
+ end
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])
+ it 'returns the updated ids' do
+ expect(method_call).to match_array([first_todo.id, second_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) }
+ 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
- it 'returns the ids of those still pending' do
- described_class.new.mark_pending_todos_as_done(issue, john_doe)
+ method_call
- expect(described_class.new.mark_todos_as_done(Todo.all, john_doe)).to eq([second_todo.id])
- 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)
-
- expect(described_class.new.mark_todos_as_done(Todo.all, john_doe)).to eq([])
+ 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)
+ 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
- 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 '#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_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)
+ describe '#resolve_todo' do
+ let!(:todo) { create(:todo, :assigned, user: john_doe) }
- ids = described_class.new.mark_all_todos_as_done_by_user(john_doe)
+ 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
- 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')
+ 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
- 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]) }
+ 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
- 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)
+ service.resolve_todo(todo, john_doe)
- 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)
+ expect(john_doe.todos_done_count).to eq(1)
+ expect(john_doe.todos_pending_count).to eq(0)
+ end
end
+ end
- it 'marks a single todo id as done' do
- todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+ describe '#restore_todo' do
+ let!(:todo) { create(:todo, :done, user: john_doe) }
- expect { described_class.new.mark_todos_as_done_by_ids(todo.id, john_doe) }
- .to change { todo.reload.state }.from('pending').to('done')
+ 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
- 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(1)
+ expect(john_doe.todos_pending_count).to eq(0)
+ 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.restore_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(1)
+ expect(john_doe.todos_done_count).to eq(0)
+ expect(john_doe.todos_pending_count).to eq(1)
+ end
end
end