Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5d47d3f8ed
commit
42883e12f1
|
@ -1 +1 @@
|
|||
092d4e489d7de1dcc38d57a4c667e85df8b8377f
|
||||
6aabab39e06f8a4f786a99449a49d5f0cb332310
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { GlButton, GlDatepicker } from '@gitlab/ui';
|
||||
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
|
||||
import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BoardEditableItem,
|
||||
GlButton,
|
||||
GlDatepicker,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ issue: 'getActiveIssue' }),
|
||||
hasDueDate() {
|
||||
return this.issue.dueDate != null;
|
||||
},
|
||||
parsedDueDate() {
|
||||
if (!this.hasDueDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsePikadayDate(this.issue.dueDate);
|
||||
},
|
||||
formattedDueDate() {
|
||||
if (!this.hasDueDate) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return dateInWords(this.parsedDueDate, true);
|
||||
},
|
||||
projectPath() {
|
||||
const referencePath = this.issue.referencePath || '';
|
||||
return referencePath.slice(0, referencePath.indexOf('#'));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setActiveIssueDueDate']),
|
||||
async openDatePicker() {
|
||||
await this.$nextTick();
|
||||
this.$refs.datePicker.calendar.show();
|
||||
},
|
||||
async setDueDate(date) {
|
||||
this.loading = true;
|
||||
this.$refs.sidebarItem.collapse();
|
||||
|
||||
try {
|
||||
const dueDate = date ? formatDate(date, 'yyyy-mm-dd') : null;
|
||||
await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPath });
|
||||
} catch (e) {
|
||||
createFlash({ message: this.$options.i18n.updateDueDateError });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
dueDate: __('Due date'),
|
||||
removeDueDate: __('remove due date'),
|
||||
updateDueDateError: __('An error occurred when updating the issue due date'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<board-editable-item
|
||||
ref="sidebarItem"
|
||||
class="board-sidebar-due-date"
|
||||
:title="$options.i18n.dueDate"
|
||||
:loading="loading"
|
||||
@open="openDatePicker"
|
||||
>
|
||||
<template v-if="hasDueDate" #collapsed>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<strong class="gl-text-gray-900">{{ formattedDueDate }}</strong>
|
||||
<span class="gl-mx-2">-</span>
|
||||
<gl-button
|
||||
variant="link"
|
||||
class="gl-text-gray-400!"
|
||||
data-testid="reset-button"
|
||||
:disabled="loading"
|
||||
@click="setDueDate(null)"
|
||||
>
|
||||
{{ $options.i18n.removeDueDate }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
<template>
|
||||
<gl-datepicker
|
||||
ref="datePicker"
|
||||
:value="parsedDueDate"
|
||||
show-clear-button
|
||||
@input="setDueDate"
|
||||
@clear="setDueDate(null)"
|
||||
/>
|
||||
</template>
|
||||
</board-editable-item>
|
||||
</template>
|
||||
<style>
|
||||
/*
|
||||
* This can be removed after closing:
|
||||
* https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1048
|
||||
*/
|
||||
.board-sidebar-due-date .gl-datepicker,
|
||||
.board-sidebar-due-date .gl-datepicker-input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,8 @@
|
|||
mutation issueSetDueDate($input: UpdateIssueInput!) {
|
||||
updateIssue(input: $input) {
|
||||
issue {
|
||||
dueDate
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import createBoardListMutation from '../queries/board_list_create.mutation.graph
|
|||
import updateBoardListMutation from '../queries/board_list_update.mutation.graphql';
|
||||
import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
|
||||
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
|
||||
import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
|
||||
|
||||
const notImplemented = () => {
|
||||
/* eslint-disable-next-line @gitlab/require-i18n-strings */
|
||||
|
@ -327,6 +328,30 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
setActiveIssueDueDate: async ({ commit, getters }, input) => {
|
||||
const activeIssue = getters.getActiveIssue;
|
||||
const { data } = await gqlClient.mutate({
|
||||
mutation: issueSetDueDate,
|
||||
variables: {
|
||||
input: {
|
||||
iid: String(activeIssue.iid),
|
||||
projectPath: input.projectPath,
|
||||
dueDate: input.dueDate,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (data.updateIssue?.errors?.length > 0) {
|
||||
throw new Error(data.updateIssue.errors);
|
||||
}
|
||||
|
||||
commit(types.UPDATE_ISSUE_BY_ID, {
|
||||
issueId: activeIssue.id,
|
||||
prop: 'dueDate',
|
||||
value: data.updateIssue.issue.dueDate,
|
||||
});
|
||||
},
|
||||
|
||||
fetchBacklog: () => {
|
||||
notImplemented();
|
||||
},
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
= f.check_box :send_user_confirmation_email, class: 'form-check-input'
|
||||
= f.label :send_user_confirmation_email, class: 'form-check-label' do
|
||||
Send confirmation email on sign-up
|
||||
|
||||
= render_if_exists 'admin/application_settings/new_user_signups_cap', form: f
|
||||
|
||||
.form-group
|
||||
= f.label :minimum_password_length, _('Minimum password length (number of characters)'), class: 'label-bold'
|
||||
= f.number_field :minimum_password_length, class: 'form-control', rows: 4, min: ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH, max: Devise.password_length.max
|
||||
|
|
|
@ -43,6 +43,10 @@
|
|||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
%p
|
||||
= _('Configure paths to be protected by Rack Attack.')
|
||||
.help-block
|
||||
= _('These paths are protected for POST requests.')
|
||||
= link_to _('More information'), help_page_path('security/rack_attack', anchor: 'protected-paths-throttle'), target: '_blank'
|
||||
|
||||
.settings-content
|
||||
= render 'protected_paths'
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add support for .md.erb files in Static Site Editor
|
||||
merge_request: 42353
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add migration to add a new configuration option for setting the new user signups count
|
||||
merge_request: 45643
|
||||
author:
|
||||
type: other
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: resource_access_token
|
||||
name: resource_access_token_feature
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29622
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235765
|
||||
group: group::access
|
||||
type: licensed
|
||||
type: development
|
||||
default_enabled: true
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: sse_erb_support
|
||||
introduced_by_url:
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235460
|
||||
group: group::static site editor
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -14,7 +14,6 @@ if Gitlab.ee? && Gitlab.dev_or_test_env?
|
|||
# being unique to licensed names. These feature flags should be reworked to
|
||||
# be "development" with explicit check
|
||||
IGNORED_FEATURE_FLAGS = %i[
|
||||
resource_access_token
|
||||
ci_secrets_management
|
||||
feature_flags_related_issues
|
||||
group_coverage_reports
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddNewUserSignupsCapToApplicationSettings < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :application_settings, :new_user_signups_cap, :integer
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
fe57e8e74ebbe0e9567c1487e6e4f8b499afa6404c73424157c43ae79c005f08
|
|
@ -9305,6 +9305,7 @@ CREATE TABLE application_settings (
|
|||
secret_detection_token_revocation_url text,
|
||||
encrypted_secret_detection_token_revocation_token text,
|
||||
encrypted_secret_detection_token_revocation_token_iv text,
|
||||
new_user_signups_cap integer,
|
||||
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
|
||||
CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
|
||||
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
|
||||
|
|
|
@ -347,6 +347,7 @@ proxied
|
|||
proxies
|
||||
proxyable
|
||||
proxying
|
||||
pseudocode
|
||||
pseudonymized
|
||||
pseudonymizer
|
||||
Puma
|
||||
|
|
|
@ -112,7 +112,8 @@ you list:
|
|||
|
||||
## Queue selector (experimental)
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/45) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.8.
|
||||
> - [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/45) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.8.
|
||||
> - [Sidekiq cluster including queue selector moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Core](https://about.gitlab.com/pricing/#self-managed) in GitLab 12.10.
|
||||
|
||||
CAUTION: **Caution:**
|
||||
As this is marked as **experimental**, it is subject to change at any
|
||||
|
@ -130,6 +131,9 @@ in a more general way using the following components:
|
|||
- Attributes that can be selected.
|
||||
- Operators used to construct a query.
|
||||
|
||||
When `experimental_queue_selector` is set, all `queue_groups` must be in
|
||||
the queue selector syntax.
|
||||
|
||||
### Available attributes
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/261) in GitLab 13.1, `tags`.
|
||||
|
|
|
@ -59,13 +59,13 @@ Some of the benefits and tradeoffs of keyset pagination are
|
|||
|
||||
- Performance is much better.
|
||||
|
||||
- Data stability is greater since you're not going to miss records due to
|
||||
- More data stability for end-users since records are not missing from lists due to
|
||||
deletions or insertions.
|
||||
|
||||
- It's the best way to do infinite scrolling.
|
||||
|
||||
- It's more difficult to program and maintain. Easy for `updated_at` and
|
||||
`sort_order`, complicated (or impossible) for complex sorting scenarios.
|
||||
`sort_order`, complicated (or impossible) for [complex sorting scenarios](#limitations-of-query-complexity).
|
||||
|
||||
## Implementation
|
||||
|
||||
|
@ -80,7 +80,124 @@ However, there are some cases where we have to use the offset
|
|||
pagination connection, `OffsetActiveRecordRelationConnection`, such as when
|
||||
sorting by label priority in issues, due to the complexity of the sort.
|
||||
|
||||
<!-- ### Keyset pagination -->
|
||||
### Keyset pagination
|
||||
|
||||
The keyset pagination implementation is a subclass of `GraphQL::Pagination::ActiveRecordRelationConnection`,
|
||||
which is a part of the `graphql` gem. This is installed as the default for all `ActiveRecord::Relation`.
|
||||
However, instead of using a cursor based on an offset (which is the default), GitLab uses a more specialized cursor.
|
||||
|
||||
The cursor is created by encoding a JSON object which contains the relevant ordering fields. For example:
|
||||
|
||||
```ruby
|
||||
ordering = {"id"=>"72410125", "created_at"=>"2020-10-08 18:05:21.953398000 UTC"}
|
||||
json = ordering.to_json
|
||||
cursor = Base64Bp.urlsafe_encode64(json, padding: false)
|
||||
|
||||
"eyJpZCI6IjcyNDEwMTI1IiwiY3JlYXRlZF9hdCI6IjIwMjAtMTAtMDggMTg6MDU6MjEuOTUzMzk4MDAwIFVUQyJ9"
|
||||
|
||||
json = Base64Bp.urlsafe_decode64(cursor)
|
||||
Gitlab::Json.parse(json)
|
||||
|
||||
{"id"=>"72410125", "created_at"=>"2020-10-08 18:05:21.953398000 UTC"}
|
||||
```
|
||||
|
||||
The benefits of storing the order attribute values in the cursor:
|
||||
|
||||
- If only the ID of the object were stored, the object and its attributes could be queried.
|
||||
That would require an additional query, and if the object is no longer there, then the needed
|
||||
attributes are not available.
|
||||
- If an attribute is `NULL`, then one SQL query can be used. If it's not `NULL`, then a
|
||||
different SQL query can be used.
|
||||
|
||||
Based on whether the main attribute field being sorted on is `NULL` in the cursor, the proper query
|
||||
condition is built. The last ordering field is considered to be unique (a primary key), meaning the
|
||||
column never contains `NULL` values.
|
||||
|
||||
#### Limitations of query complexity
|
||||
|
||||
We only support two ordering fields, and one of those fields needs to be the primary key.
|
||||
|
||||
Here are two examples of pseudocode for the query:
|
||||
|
||||
- **Two-condition query.** `X` represents the values from the cursor. `C` represents
|
||||
the columns in the database, sorted in ascending order, using an `:after` cursor, and with `NULL`
|
||||
values sorted last.
|
||||
|
||||
```plaintext
|
||||
X1 IS NOT NULL
|
||||
AND
|
||||
(C1 > X1)
|
||||
OR
|
||||
(C1 IS NULL)
|
||||
OR
|
||||
(C1 = X1
|
||||
AND
|
||||
C2 > X2)
|
||||
|
||||
X1 IS NULL
|
||||
AND
|
||||
(C1 IS NULL
|
||||
AND
|
||||
C2 > X2)
|
||||
```
|
||||
|
||||
Below is an example based on the relation `Issue.order(relative_position: :asc).order(id: :asc)`
|
||||
with an after cursor of `relative_position: 1500, id: 500`:
|
||||
|
||||
```plaintext
|
||||
when cursor[relative_position] is not NULL
|
||||
|
||||
("issues"."relative_position" > 1500)
|
||||
OR (
|
||||
"issues"."relative_position" = 1500
|
||||
AND
|
||||
"issues"."id" > 500
|
||||
)
|
||||
OR ("issues"."relative_position" IS NULL)
|
||||
|
||||
when cursor[relative_position] is NULL
|
||||
|
||||
"issues"."relative_position" IS NULL
|
||||
AND
|
||||
"issues"."id" > 500
|
||||
```
|
||||
|
||||
- **Three-condition query.** The example below is not complete, but shows the
|
||||
complexity of adding one more condition. `X` represents the values from the cursor. `C` represents
|
||||
the columns in the database, sorted in ascending order, using an `:after` cursor, and with `NULL`
|
||||
values sorted last.
|
||||
|
||||
```plaintext
|
||||
X1 IS NOT NULL
|
||||
AND
|
||||
(C1 > X1)
|
||||
OR
|
||||
(C1 IS NULL)
|
||||
OR
|
||||
(C1 = X1 AND C2 > X2)
|
||||
OR
|
||||
(C1 = X1
|
||||
AND
|
||||
X2 IS NOT NULL
|
||||
AND
|
||||
((C2 > X2)
|
||||
OR
|
||||
(C2 IS NULL)
|
||||
OR
|
||||
(C2 = X2 AND C3 > X3)
|
||||
OR
|
||||
X2 IS NULL.....
|
||||
```
|
||||
|
||||
By using
|
||||
[`Gitlab::Graphql::Pagination::Keyset::QueryBuilder`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/graphql/pagination/keyset/query_builder.rb),
|
||||
we're able to build the necessary SQL conditions and apply them to the Active Record relation.
|
||||
|
||||
Complex queries can be difficult or impossible to use. For example,
|
||||
in [`issuable.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/concerns/issuable.rb),
|
||||
the `order_due_date_and_labels_priority` method creates a very complex query.
|
||||
|
||||
These types of queries are not supported. In these instances, you can use offset pagination.
|
||||
|
||||
<!-- ### Offset pagination -->
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ module Gitlab
|
|||
delegate :project, to: :repository
|
||||
|
||||
def supported_extensions
|
||||
%w[.md].freeze
|
||||
%w[.md .md.erb].freeze
|
||||
end
|
||||
|
||||
def commit_id
|
||||
|
@ -50,8 +50,6 @@ module Gitlab
|
|||
end
|
||||
|
||||
def extension_supported?
|
||||
return true if path.end_with?('.md.erb') && Feature.enabled?(:sse_erb_support, project)
|
||||
|
||||
supported_extensions.any? { |ext| path.end_with?(ext) }
|
||||
end
|
||||
|
||||
|
|
|
@ -1914,6 +1914,9 @@ msgstr ""
|
|||
msgid "AdminArea|New user"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminArea|Once the instance reaches the user cap, any user who is added or requests access will have to be approved by an admin. Leave the field empty for unlimited."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminArea|Owner"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1938,6 +1941,9 @@ msgstr ""
|
|||
msgid "AdminArea|Total users"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminArea|User cap"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminArea|Users statistics"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2867,6 +2873,9 @@ msgstr ""
|
|||
msgid "An error occurred when toggling the notification subscription"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred when updating the issue due date"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred when updating the issue weight"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26978,6 +26987,9 @@ msgstr ""
|
|||
msgid "These existing issues have a similar title. It might be better to comment there instead of creating another similar issue."
|
||||
msgstr ""
|
||||
|
||||
msgid "These paths are protected for POST requests."
|
||||
msgstr ""
|
||||
|
||||
msgid "These variables are configured in the parent group settings, and will be active in the current project in addition to the project variables."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -52,10 +52,10 @@ gitlab:
|
|||
resources:
|
||||
requests:
|
||||
cpu: 855m
|
||||
memory: 1071M
|
||||
memory: 1285M
|
||||
limits:
|
||||
cpu: 1282m
|
||||
memory: 1606M
|
||||
memory: 1927M
|
||||
hpa:
|
||||
targetAverageValue: 650m
|
||||
task-runner:
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlDatepicker } from '@gitlab/ui';
|
||||
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
|
||||
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
|
||||
import { createStore } from '~/boards/stores';
|
||||
import createFlash from '~/flash';
|
||||
|
||||
const TEST_DUE_DATE = '2020-02-20';
|
||||
const TEST_FORMATTED_DUE_DATE = 'Feb 20, 2020';
|
||||
const TEST_PARSED_DATE = new Date(2020, 1, 20);
|
||||
const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, dueDate: null, referencePath: 'h/b#2' };
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
store = null;
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
const createWrapper = ({ dueDate = null } = {}) => {
|
||||
store = createStore();
|
||||
store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } };
|
||||
store.state.activeId = TEST_ISSUE.id;
|
||||
|
||||
wrapper = shallowMount(BoardSidebarDueDate, {
|
||||
store,
|
||||
provide: {
|
||||
canUpdate: true,
|
||||
},
|
||||
stubs: {
|
||||
'board-editable-item': BoardEditableItem,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findDatePicker = () => wrapper.find(GlDatepicker);
|
||||
const findResetButton = () => wrapper.find('[data-testid="reset-button"]');
|
||||
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
|
||||
|
||||
it('renders "None" when no due date is set', () => {
|
||||
createWrapper();
|
||||
|
||||
expect(findCollapsed().text()).toBe('None');
|
||||
expect(findResetButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders formatted due date with reset button when set', () => {
|
||||
createWrapper({ dueDate: TEST_DUE_DATE });
|
||||
|
||||
expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
|
||||
expect(findResetButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when due date is submitted', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper();
|
||||
|
||||
jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
|
||||
store.state.issues[TEST_ISSUE.id].dueDate = TEST_DUE_DATE;
|
||||
});
|
||||
findDatePicker().vm.$emit('input', TEST_PARSED_DATE);
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('collapses sidebar and renders formatted due date with reset button', () => {
|
||||
expect(findCollapsed().isVisible()).toBe(true);
|
||||
expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
|
||||
expect(findResetButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('commits change to the server', () => {
|
||||
expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalledWith({
|
||||
dueDate: TEST_DUE_DATE,
|
||||
projectPath: 'h/b',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when due date is cleared', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper();
|
||||
|
||||
jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
|
||||
store.state.issues[TEST_ISSUE.id].dueDate = null;
|
||||
});
|
||||
findDatePicker().vm.$emit('clear');
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('collapses sidebar and renders "None"', () => {
|
||||
expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled();
|
||||
expect(findCollapsed().isVisible()).toBe(true);
|
||||
expect(findCollapsed().text()).toBe('None');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when due date is resetted', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper({ dueDate: TEST_DUE_DATE });
|
||||
|
||||
jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
|
||||
store.state.issues[TEST_ISSUE.id].dueDate = null;
|
||||
});
|
||||
findResetButton().vm.$emit('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('collapses sidebar and renders "None"', () => {
|
||||
expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled();
|
||||
expect(findCollapsed().isVisible()).toBe(true);
|
||||
expect(findCollapsed().text()).toBe('None');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the mutation fails', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper({ dueDate: TEST_DUE_DATE });
|
||||
|
||||
jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
|
||||
throw new Error(['failed mutation']);
|
||||
});
|
||||
findDatePicker().vm.$emit('input', 'Invalid date');
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('collapses sidebar and renders former issue due date', () => {
|
||||
expect(findCollapsed().isVisible()).toBe(true);
|
||||
expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
|
||||
expect(createFlash).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -640,6 +640,57 @@ describe('setActiveIssueLabels', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setActiveIssueDueDate', () => {
|
||||
const state = { issues: { [mockIssue.id]: mockIssue } };
|
||||
const getters = { getActiveIssue: mockIssue };
|
||||
const testDueDate = '2020-02-20';
|
||||
const input = {
|
||||
dueDate: testDueDate,
|
||||
projectPath: 'h/b',
|
||||
};
|
||||
|
||||
it('should commit due date after setting the issue', done => {
|
||||
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
||||
data: {
|
||||
updateIssue: {
|
||||
issue: {
|
||||
dueDate: testDueDate,
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const payload = {
|
||||
issueId: getters.getActiveIssue.id,
|
||||
prop: 'dueDate',
|
||||
value: testDueDate,
|
||||
};
|
||||
|
||||
testAction(
|
||||
actions.setActiveIssueDueDate,
|
||||
input,
|
||||
{ ...state, ...getters },
|
||||
[
|
||||
{
|
||||
type: types.UPDATE_ISSUE_BY_ID,
|
||||
payload,
|
||||
},
|
||||
],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error if fails', async () => {
|
||||
jest
|
||||
.spyOn(gqlClient, 'mutate')
|
||||
.mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
|
||||
|
||||
await expect(actions.setActiveIssueDueDate({ getters }, input)).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchBacklog', () => {
|
||||
expectNotImplemented(actions.fetchBacklog);
|
||||
});
|
||||
|
|
|
@ -58,25 +58,9 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do
|
|||
)
|
||||
end
|
||||
|
||||
context 'when feature flag is enabled' do
|
||||
let(:path) { 'FEATURE_ON.md.erb' }
|
||||
let(:path) { 'README.md.erb' }
|
||||
|
||||
before do
|
||||
stub_feature_flags(sse_erb_support: project)
|
||||
end
|
||||
|
||||
it { is_expected.to include(is_supported_content: true) }
|
||||
end
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
let(:path) { 'FEATURE_OFF.md.erb' }
|
||||
|
||||
before do
|
||||
stub_feature_flags(sse_erb_support: false)
|
||||
end
|
||||
|
||||
it { is_expected.to include(is_supported_content: false) }
|
||||
end
|
||||
it { is_expected.to include(is_supported_content: true) }
|
||||
end
|
||||
|
||||
context 'when file path is nested' do
|
||||
|
|
Loading…
Reference in New Issue