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 updateBoardListMutation from '../queries/board_list_update.mutation.graphql';
|
||||||
import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
|
import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
|
||||||
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
|
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
|
||||||
|
import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
|
||||||
|
|
||||||
const notImplemented = () => {
|
const notImplemented = () => {
|
||||||
/* eslint-disable-next-line @gitlab/require-i18n-strings */
|
/* 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: () => {
|
fetchBacklog: () => {
|
||||||
notImplemented();
|
notImplemented();
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,6 +21,9 @@
|
||||||
= f.check_box :send_user_confirmation_email, class: 'form-check-input'
|
= f.check_box :send_user_confirmation_email, class: 'form-check-input'
|
||||||
= f.label :send_user_confirmation_email, class: 'form-check-label' do
|
= f.label :send_user_confirmation_email, class: 'form-check-label' do
|
||||||
Send confirmation email on sign-up
|
Send confirmation email on sign-up
|
||||||
|
|
||||||
|
= render_if_exists 'admin/application_settings/new_user_signups_cap', form: f
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
= f.label :minimum_password_length, _('Minimum password length (number of characters)'), class: 'label-bold'
|
= 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
|
= 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')
|
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||||
%p
|
%p
|
||||||
= _('Configure paths to be protected by Rack Attack.')
|
= _('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
|
.settings-content
|
||||||
= render 'protected_paths'
|
= 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
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29622
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235765
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235765
|
||||||
group: group::access
|
group: group::access
|
||||||
type: licensed
|
type: development
|
||||||
default_enabled: true
|
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
|
# being unique to licensed names. These feature flags should be reworked to
|
||||||
# be "development" with explicit check
|
# be "development" with explicit check
|
||||||
IGNORED_FEATURE_FLAGS = %i[
|
IGNORED_FEATURE_FLAGS = %i[
|
||||||
resource_access_token
|
|
||||||
ci_secrets_management
|
ci_secrets_management
|
||||||
feature_flags_related_issues
|
feature_flags_related_issues
|
||||||
group_coverage_reports
|
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,
|
secret_detection_token_revocation_url text,
|
||||||
encrypted_secret_detection_token_revocation_token text,
|
encrypted_secret_detection_token_revocation_token text,
|
||||||
encrypted_secret_detection_token_revocation_token_iv 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 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_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)),
|
||||||
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
|
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
|
||||||
|
|
|
@ -347,6 +347,7 @@ proxied
|
||||||
proxies
|
proxies
|
||||||
proxyable
|
proxyable
|
||||||
proxying
|
proxying
|
||||||
|
pseudocode
|
||||||
pseudonymized
|
pseudonymized
|
||||||
pseudonymizer
|
pseudonymizer
|
||||||
Puma
|
Puma
|
||||||
|
|
|
@ -112,7 +112,8 @@ you list:
|
||||||
|
|
||||||
## Queue selector (experimental)
|
## 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:**
|
CAUTION: **Caution:**
|
||||||
As this is marked as **experimental**, it is subject to change at any
|
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.
|
- Attributes that can be selected.
|
||||||
- Operators used to construct a query.
|
- Operators used to construct a query.
|
||||||
|
|
||||||
|
When `experimental_queue_selector` is set, all `queue_groups` must be in
|
||||||
|
the queue selector syntax.
|
||||||
|
|
||||||
### Available attributes
|
### Available attributes
|
||||||
|
|
||||||
- [Introduced](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/261) in GitLab 13.1, `tags`.
|
- [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.
|
- 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.
|
deletions or insertions.
|
||||||
|
|
||||||
- It's the best way to do infinite scrolling.
|
- It's the best way to do infinite scrolling.
|
||||||
|
|
||||||
- It's more difficult to program and maintain. Easy for `updated_at` and
|
- 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
|
## Implementation
|
||||||
|
|
||||||
|
@ -80,7 +80,124 @@ However, there are some cases where we have to use the offset
|
||||||
pagination connection, `OffsetActiveRecordRelationConnection`, such as when
|
pagination connection, `OffsetActiveRecordRelationConnection`, such as when
|
||||||
sorting by label priority in issues, due to the complexity of the sort.
|
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 -->
|
<!-- ### Offset pagination -->
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ module Gitlab
|
||||||
delegate :project, to: :repository
|
delegate :project, to: :repository
|
||||||
|
|
||||||
def supported_extensions
|
def supported_extensions
|
||||||
%w[.md].freeze
|
%w[.md .md.erb].freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
def commit_id
|
def commit_id
|
||||||
|
@ -50,8 +50,6 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def extension_supported?
|
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) }
|
supported_extensions.any? { |ext| path.end_with?(ext) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1914,6 +1914,9 @@ msgstr ""
|
||||||
msgid "AdminArea|New user"
|
msgid "AdminArea|New user"
|
||||||
msgstr ""
|
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"
|
msgid "AdminArea|Owner"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -1938,6 +1941,9 @@ msgstr ""
|
||||||
msgid "AdminArea|Total users"
|
msgid "AdminArea|Total users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AdminArea|User cap"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "AdminArea|Users statistics"
|
msgid "AdminArea|Users statistics"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -2867,6 +2873,9 @@ msgstr ""
|
||||||
msgid "An error occurred when toggling the notification subscription"
|
msgid "An error occurred when toggling the notification subscription"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "An error occurred when updating the issue due date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "An error occurred when updating the issue weight"
|
msgid "An error occurred when updating the issue weight"
|
||||||
msgstr ""
|
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."
|
msgid "These existing issues have a similar title. It might be better to comment there instead of creating another similar issue."
|
||||||
msgstr ""
|
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."
|
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 ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -52,10 +52,10 @@ gitlab:
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 855m
|
cpu: 855m
|
||||||
memory: 1071M
|
memory: 1285M
|
||||||
limits:
|
limits:
|
||||||
cpu: 1282m
|
cpu: 1282m
|
||||||
memory: 1606M
|
memory: 1927M
|
||||||
hpa:
|
hpa:
|
||||||
targetAverageValue: 650m
|
targetAverageValue: 650m
|
||||||
task-runner:
|
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', () => {
|
describe('fetchBacklog', () => {
|
||||||
expectNotImplemented(actions.fetchBacklog);
|
expectNotImplemented(actions.fetchBacklog);
|
||||||
});
|
});
|
||||||
|
|
|
@ -58,25 +58,9 @@ RSpec.describe Gitlab::StaticSiteEditor::Config::GeneratedConfig do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when feature flag is enabled' do
|
let(:path) { 'README.md.erb' }
|
||||||
let(:path) { 'FEATURE_ON.md.erb' }
|
|
||||||
|
|
||||||
before do
|
it { is_expected.to include(is_supported_content: true) }
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when file path is nested' do
|
context 'when file path is nested' do
|
||||||
|
|
Loading…
Reference in New Issue