Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-11 09:08:44 +00:00
parent 79b32f05d4
commit 6b5f961bef
46 changed files with 2034 additions and 798 deletions

View file

@ -52,7 +52,7 @@ export default {
},
methods: {
updateToDoCount(add) {
const oldCount = parseInt(document.querySelector('.todos-count').innerText, 10);
const oldCount = parseInt(document.querySelector('.js-todos-count').innerText, 10);
const count = add ? oldCount + 1 : oldCount - 1;
const headerTodoEvent = new CustomEvent('todo:toggle', {
detail: {

View file

@ -5,7 +5,7 @@ import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mu
import TodoButton from '~/vue_shared/components/todo_button.vue';
import allVersionsMixin from '../mixins/all_versions';
import { updateStoreAfterDeleteDesignTodo } from '../utils/cache_update';
import { findIssueId } from '../utils/design_management_utils';
import { findIssueId, findDesignId } from '../utils/design_management_utils';
import { CREATE_DESIGN_TODO_ERROR, DELETE_DESIGN_TODO_ERROR } from '../utils/error_messages';
export default {
@ -45,6 +45,7 @@ export default {
return {
projectPath: this.projectPath,
issueId: findIssueId(this.design.issue.id),
designId: findDesignId(this.design.id),
issueIid: this.issueIid,
filenames: [this.$route.params.id],
atVersion: this.designsVersion,
@ -59,6 +60,22 @@ export default {
},
},
methods: {
updateGlobalTodoCount(additionalTodoCount) {
const currentCount = parseInt(document.querySelector('.js-todos-count').innerText, 10);
const todoToggleEvent = new CustomEvent('todo:toggle', {
detail: {
count: Math.max(currentCount + additionalTodoCount, 0),
},
});
document.dispatchEvent(todoToggleEvent);
},
incrementGlobalTodoCount() {
this.updateGlobalTodoCount(1);
},
decrementGlobalTodoCount() {
this.updateGlobalTodoCount(-1);
},
createTodo() {
this.todoLoading = true;
return this.$apollo
@ -75,6 +92,9 @@ export default {
}
},
})
.then(() => {
this.incrementGlobalTodoCount();
})
.catch(err => {
this.$emit('error', Error(CREATE_DESIGN_TODO_ERROR));
throw err;
@ -115,6 +135,9 @@ export default {
}
},
})
.then(() => {
this.decrementGlobalTodoCount();
})
.catch(err => {
this.$emit('error', Error(DELETE_DESIGN_TODO_ERROR));
throw err;

View file

@ -30,11 +30,15 @@ const resolvers = {
cache.writeQuery({ query: activeDiscussionQuery, data });
},
createDesignTodo: (_, { projectPath, issueId, issueIid, filenames, atVersion }, { cache }) => {
createDesignTodo: (
_,
{ projectPath, issueId, designId, issueIid, filenames, atVersion },
{ cache },
) => {
return axios
.post(`/${projectPath}/todos`, {
issue_id: issueId,
issuable_id: issueIid,
issuable_id: designId,
issuable_type: 'design',
})
.then(({ data }) => {

View file

@ -1,6 +1,7 @@
mutation createDesignTodo(
$projectPath: String!
$issueId: String!
$designId: String!
$issueIid: String!
$filenames: [String]!
$atVersion: String
@ -8,6 +9,7 @@ mutation createDesignTodo(
createDesignTodo(
projectPath: $projectPath
issueId: $issueId
designId: $designId
issueIid: $issueIid
filenames: $filenames
atVersion: $atVersion

View file

@ -32,6 +32,8 @@ export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1];
export const findIssueId = id => (id.match('Issue/(.+$)') || [])[1];
export const findDesignId = id => (id.match('Design/(.+$)') || [])[1];
export const extractDesigns = data => data.project.issue.designCollection.designs.nodes;
export const extractDesign = data => (extractDesigns(data) || [])[0];

View file

@ -1,13 +1,11 @@
<script>
/* eslint-disable vue/no-v-html */
import { mapGetters } from 'vuex';
import { escape } from 'lodash';
import { GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { GlButton, GlSprintf } from '@gitlab/ui';
export default {
components: {
GlButton,
GlSprintf,
},
props: {
changesEmptyStateIllustration: {
@ -17,20 +15,6 @@ export default {
},
computed: {
...mapGetters(['getNoteableData']),
emptyStateText() {
return sprintf(
__(
'No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}',
),
{
ref_start: '<span class="ref-name">',
ref_end: '</span>',
source_branch: escape(this.getNoteableData.source_branch),
target_branch: escape(this.getNoteableData.target_branch),
},
false,
);
},
},
};
</script>
@ -42,7 +26,14 @@ export default {
</div>
<div class="col-12">
<div class="text-content text-center">
<span v-html="emptyStateText"></span>
<gl-sprintf :message="__('No changes between %{sourceBranch} and %{targetBranch}')">
<template #sourceBranch>
<span class="ref-name">{{ getNoteableData.source_branch }}</span>
</template>
<template #targetBranch>
<span class="ref-name">{{ getNoteableData.target_branch }}</span>
</template>
</gl-sprintf>
<div class="text-center">
<gl-button :href="getNoteableData.new_blob_path" variant="success" category="primary">{{
__('Create commit')

View file

@ -14,7 +14,7 @@ import Tracking from '~/tracking';
export default function initTodoToggle() {
$(document).on('todo:toggle', (e, count) => {
const updatedCount = count || e?.detail?.count || 0;
const $todoPendingCount = $('.todos-count');
const $todoPendingCount = $('.js-todos-count');
$todoPendingCount.text(highCountTrim(updatedCount));
$todoPendingCount.toggleClass('hidden', updatedCount === 0);

View file

@ -116,13 +116,15 @@ export default {
</template>
<template #right-secondary>
<gl-sprintf :message="__('Created %{timestamp}')">
<template #timestamp>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
{{ timeFormatted(packageEntity.created_at) }}
</span>
</template>
</gl-sprintf>
<span>
<gl-sprintf :message="__('Created %{timestamp}')">
<template #timestamp>
<span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
{{ timeFormatted(packageEntity.created_at) }}
</span>
</template>
</gl-sprintf>
</span>
</template>
<template v-if="!disableDelete" #right-action>

View file

@ -36,7 +36,7 @@ export default {
</script>
<template>
<div class="gl-display-flex gl-align-items-center gl-mb-2">
<div class="gl-display-flex gl-align-items-center">
<template v-if="hasPipeline">
<gl-icon name="git-merge" class="gl-mr-2" />
<span data-testid="pipeline-ref" class="gl-mr-2">{{ packageEntity.pipeline.ref }}</span>

View file

@ -39,7 +39,7 @@ export default {
created() {
this.observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
this.dispose(mutation.removedNodes);
mutation.removedNodes.forEach(this.dispose);
});
});
},
@ -61,22 +61,36 @@ export default {
childList: true,
});
},
dispose(elements) {
if (!elements) {
dispose(target) {
if (!target) {
this.tooltips = [];
return;
}
elements.forEach(element => {
const index = this.tooltips.findIndex(tooltip => tooltip.target === element);
} else {
const index = this.tooltips.indexOf(this.findTooltipByTarget(target));
if (index > -1) {
this.tooltips.splice(index, 1);
}
});
}
},
fixTitle(target) {
const tooltip = this.findTooltipByTarget(target);
if (tooltip) {
tooltip.title = target.getAttribute('title');
}
},
triggerEvent(target, event) {
const tooltip = this.findTooltipByTarget(target);
if (tooltip) {
this.$refs[tooltip.id][0].$emit(event);
}
},
tooltipExists(element) {
return this.tooltips.some(tooltip => tooltip.target === element);
return Boolean(this.findTooltipByTarget(element));
},
findTooltipByTarget(element) {
return this.tooltips.find(tooltip => tooltip.target === element);
},
},
};
@ -86,6 +100,7 @@ export default {
<gl-tooltip
v-for="(tooltip, index) in tooltips"
:id="tooltip.id"
:ref="tooltip.id"
:key="index"
:target="tooltip.target"
:triggers="tooltip.triggers"

View file

@ -1,4 +1,5 @@
import Vue from 'vue';
import { toArray } from 'lodash';
import Tooltips from './components/tooltips.vue';
let app;
@ -31,13 +32,13 @@ const tooltipsApp = () => {
}).$mount(container);
}
return app;
return app.$refs.tooltips;
};
const isTooltip = (node, selector) => node.matches && node.matches(selector);
const addTooltips = (elements, config) => {
tooltipsApp().$refs.tooltips.addTooltips(Array.from(elements), config);
tooltipsApp().addTooltips(toArray(elements), config);
};
const handleTooltipEvent = (rootTarget, e, selector, config = {}) => {
@ -63,9 +64,14 @@ export const initTooltips = (selector, config = {}) => {
return tooltipsApp();
};
export const dispose = elements => {
return tooltipsApp().$refs.tooltips.dispose(elements);
};
const elementsIterator = handler => elements => toArray(elements).forEach(handler);
export const dispose = elementsIterator(element => tooltipsApp().dispose(element));
export const fixTitle = elementsIterator(element => tooltipsApp().fixTitle(element));
export const enable = elementsIterator(element => tooltipsApp().triggerEvent(element, 'enable'));
export const disable = elementsIterator(element => tooltipsApp().triggerEvent(element, 'disable'));
export const hide = elementsIterator(element => tooltipsApp().triggerEvent(element, 'close'));
export const show = elementsIterator(element => tooltipsApp().triggerEvent(element, 'open'));
export const destroy = () => {
tooltipsApp().$destroy();

View file

@ -83,7 +83,7 @@ export default {
</div>
<div
v-if="$slots['left-secondary']"
class="gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
>
<slot name="left-secondary"></slot>
</div>
@ -93,11 +93,14 @@ export default {
>
<div
v-if="$slots['right-primary']"
class="gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
>
<slot name="right-primary"></slot>
</div>
<div v-if="$slots['right-secondary']" class="gl-mt-1 gl-min-h-6">
<div
v-if="$slots['right-secondary']"
class="gl-display-flex gl-align-items-center gl-mt-1 gl-min-h-6"
>
<slot name="right-secondary"></slot>
</div>
</div>

View file

@ -48,6 +48,14 @@ module Types
field :user, Types::UserType, null: true,
description: 'Pipeline user',
resolve: -> (pipeline, _args, _context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, pipeline.user_id).find }
field :retryable, GraphQL::BOOLEAN_TYPE,
description: 'Specifies if a pipeline can be retried',
method: :retryable?,
null: false
field :cancelable, GraphQL::BOOLEAN_TYPE,
description: 'Specifies if a pipeline can be canceled',
method: :cancelable?,
null: false
end
end
end

View file

@ -44,6 +44,10 @@
%span.light ID:
%strong
= @user.id
%li
%span.light= _('Namespace ID:')
%strong
= @user.namespace_id
%li.two-factor-status
%span.light Two-factor Authentication:

View file

@ -66,7 +66,7 @@
track_property: 'navigation',
container: 'body' } do
= sprite_icon('todo-done')
%span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count == 0) }
%span.badge.badge-pill.todos-count.js-todos-count{ class: ('hidden' if todos_pending_count == 0) }
= todos_count_format(todos_pending_count)
%li.nav-item.header-help.dropdown.d-none.d-md-block{ **tracking_attrs('main_navigation', 'click_question_mark_link', 'navigation') }
= link_to help_path, class: 'header-help-dropdown-toggle', data: { toggle: "dropdown" } do

View file

@ -2,8 +2,10 @@
- default_ref = params[:ref] || @project.default_branch
- if @error
.alert.alert-danger
%button.close{ type: "button", "data-dismiss" => "alert" } &times;
.gl-alert.gl-alert-danger
= sprite_icon('error', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
%button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
= sprite_icon('close', css_class: 'gl-icon')
= @error
%h3.page-title

View file

@ -131,6 +131,14 @@
:weight: 1
:idempotent:
:tags: []
- :name: cronjob:ci_platform_metrics_update_cron
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
:idempotent:
:tags: []
- :name: cronjob:container_expiration_policy
:feature_category: :container_registry
:has_external_dependencies:

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class CiPlatformMetricsUpdateCronWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
# This worker does not perform work scoped to a context
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
feature_category :continuous_integration
urgency :low
worker_resource_boundary :cpu
def perform
CiPlatformMetric.insert_auto_devops_platform_targets!
end
end

View file

@ -0,0 +1,5 @@
---
title: Replace v-html with v-safe-html in no_changes.vue
merge_request: 41471
author: Kev @KevSlashNull
type: other

View file

@ -0,0 +1,5 @@
---
title: Add namespace ID to user pages in the admin area
merge_request: 41877
author:
type: added

View file

@ -1,5 +0,0 @@
---
title: Skip warning for file changed during backup instead of failing the backup operation
merge_request: 40572
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: 'GraphQL: Add retryable and cancelable to PipelineType'
merge_request: 40780
author:
type: added

View file

@ -456,6 +456,10 @@ production: &base
schedule_migrate_external_diffs_worker:
cron: "15 * * * *"
# Update CI Platform Metrics daily
ci_platform_metrics_update_cron_worker:
cron: "47 9 * * *"
# GitLab EE only jobs. These jobs are automatically enabled for an EE
# installation, and ignored for a CE installation.
ee_cron_jobs:

View file

@ -511,6 +511,9 @@ Settings.cron_jobs['update_container_registry_info_worker']['job_class'] = 'Upda
Settings.cron_jobs['postgres_dynamic_partitions_creator'] ||= Settingslogic.new({})
Settings.cron_jobs['postgres_dynamic_partitions_creator']['cron'] ||= '21 */6 * * *'
Settings.cron_jobs['postgres_dynamic_partitions_creator']['job_class'] ||= 'PartitionCreationWorker'
Settings.cron_jobs['ci_platform_metrics_update_cron_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['ci_platform_metrics_update_cron_worker']['cron'] ||= '47 9 * * *'
Settings.cron_jobs['ci_platform_metrics_update_cron_worker']['job_class'] = 'CiPlatformMetricsUpdateCronWorker'
Gitlab.ee do
Settings.cron_jobs['adjourned_group_deletion_worker'] ||= Settingslogic.new({})

View file

@ -8555,7 +8555,12 @@ enum IssueType {
"""
Represents an iteration object.
"""
type Iteration {
type Iteration implements TimeboxBurnupTimeSeriesInterface {
"""
Daily scope and completed totals for burnup charts
"""
burnupTimeSeries: [BurnupChartDailyTotals!]
"""
Timestamp of iteration creation
"""
@ -10398,7 +10403,7 @@ type MetricsDashboardAnnotationEdge {
"""
Represents a milestone.
"""
type Milestone {
type Milestone implements TimeboxBurnupTimeSeriesInterface {
"""
Daily scope and completed totals for burnup charts
"""
@ -11333,6 +11338,11 @@ type Pipeline {
"""
beforeSha: String
"""
Specifies if a pipeline can be canceled
"""
cancelable: Boolean!
"""
Timestamp of the pipeline's commit
"""
@ -11380,6 +11390,11 @@ type Pipeline {
"""
iid: String!
"""
Specifies if a pipeline can be retried
"""
retryable: Boolean!
"""
Vulnerability and scanned resource counts for each security scanner of the pipeline
"""
@ -16504,6 +16519,13 @@ Time represented in ISO 8601
"""
scalar Time
interface TimeboxBurnupTimeSeriesInterface {
"""
Daily scope and completed totals for burnup charts
"""
burnupTimeSeries: [BurnupChartDailyTotals!]
}
type Timelog {
"""
Timestamp of when the time tracked was spent at. Deprecated in 12.10: Use `spentAt`

View file

@ -23567,6 +23567,28 @@
"name": "Iteration",
"description": "Represents an iteration object.",
"fields": [
{
"name": "burnupTimeSeries",
"description": "Daily scope and completed totals for burnup charts",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "BurnupChartDailyTotals",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "Timestamp of iteration creation",
@ -23798,7 +23820,11 @@
],
"inputFields": null,
"interfaces": [
{
"kind": "INTERFACE",
"name": "TimeboxBurnupTimeSeriesInterface",
"ofType": null
}
],
"enumValues": null,
"possibleTypes": null
@ -29142,7 +29168,11 @@
],
"inputFields": null,
"interfaces": [
{
"kind": "INTERFACE",
"name": "TimeboxBurnupTimeSeriesInterface",
"ofType": null
}
],
"enumValues": null,
"possibleTypes": null
@ -33724,6 +33754,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "cancelable",
"description": "Specifies if a pipeline can be canceled",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "committedAt",
"description": "Timestamp of the pipeline's commit",
@ -33866,6 +33914,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "retryable",
"description": "Specifies if a pipeline can be retried",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "securityReportSummary",
"description": "Vulnerability and scanned resource counts for each security scanner of the pipeline",
@ -48382,6 +48448,50 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INTERFACE",
"name": "TimeboxBurnupTimeSeriesInterface",
"description": null,
"fields": [
{
"name": "burnupTimeSeries",
"description": "Daily scope and completed totals for burnup charts",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "BurnupChartDailyTotals",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
"kind": "OBJECT",
"name": "Iteration",
"ofType": null
},
{
"kind": "OBJECT",
"name": "Milestone",
"ofType": null
}
]
},
{
"kind": "OBJECT",
"name": "Timelog",

File diff suppressed because it is too large Load diff

View file

@ -105,6 +105,8 @@ To display the Deploy Boards for a specific [environment](../../ci/environments/
re-deploy your application. If you are using Auto DevOps, this will
be done automatically and no action is necessary.
If you are using GCP to manage clusters, you can see the deployment details in GCP itself by going to **Workloads > deployment name > Details**:
![Deploy Boards Kubernetes Label](img/deploy_boards_kubernetes_label.png)
Once all of the above are set up and the pipeline has run at least once,

View file

@ -31,10 +31,10 @@ module Backup
raise Backup::Error, 'Backup failed'
end
run_pipeline!([%W(#{tar} --warning=no-file-changed --exclude=lost+found -C #{@backup_files_dir} -cf - .), gzip_cmd], out: [backup_tarball, 'w', 0600])
run_pipeline!([%W(#{tar} --exclude=lost+found -C #{@backup_files_dir} -cf - .), gzip_cmd], out: [backup_tarball, 'w', 0600])
FileUtils.rm_rf(@backup_files_dir)
else
run_pipeline!([%W(#{tar} --warning=no-file-changed --exclude=lost+found -C #{app_files_dir} -cf - .), gzip_cmd], out: [backup_tarball, 'w', 0600])
run_pipeline!([%W(#{tar} --exclude=lost+found -C #{app_files_dir} -cf - .), gzip_cmd], out: [backup_tarball, 'w', 0600])
end
end

View file

@ -21,30 +21,47 @@ module Gitlab
MD
end
def sorted_fields(fields)
fields.sort_by { |field| field[:name] }
def render_name_and_description(object)
content = "### #{object[:name]}\n"
if object[:description].present?
content += "\n#{object[:description]}\n"
end
content
end
def sorted_by_name(objects)
objects.sort_by { |o| o[:name] }
end
def render_field(field)
'| %s | %s | %s |' % [
render_field_name(field),
render_name(field),
render_field_type(field[:type][:info]),
render_field_description(field)
render_description(field)
]
end
def render_field_name(field)
rendered_name = "`#{field[:name]}`"
rendered_name += ' **{warning-solid}**' if field[:is_deprecated]
def render_enum_value(value)
'| %s | %s |' % [
render_name(value),
render_description(value)
]
end
def render_name(object)
rendered_name = "`#{object[:name]}`"
rendered_name += ' **{warning-solid}**' if object[:is_deprecated]
rendered_name
end
# Returns the field description. If the field has been deprecated,
# Returns the object description. If the object has been deprecated,
# the deprecation reason will be returned in place of the description.
def render_field_description(field)
return field[:description] unless field[:is_deprecated]
def render_description(object)
return object[:description] unless object[:is_deprecated]
"**Deprecated:** #{field[:deprecation_reason]}"
"**Deprecated:** #{object[:deprecation_reason]}"
end
# Some fields types are arrays of other types and are displayed
@ -70,6 +87,13 @@ module Gitlab
!object_type[:name]["__"]
end
end
# We ignore the built-in enum types.
def enums
graphql_enum_types.select do |enum_type|
!enum_type[:name].in?(%w(__DirectiveLocation __TypeKind))
end
end
end
end
end

View file

@ -15,15 +15,45 @@
CAUTION: **Caution:**
Fields that are deprecated are marked with **{warning-solid}**.
\
:plain
## Object types
Object types represent the resources that GitLab's GraphQL API can return.
They contain _fields_. Each field has its own type, which will either be one of the
basic GraphQL [scalar types](https://graphql.org/learn/schema/#scalar-types)
(e.g.: `String` or `Boolean`) or other object types.
For more information, see
[Object Types and Fields](https://graphql.org/learn/schema/#object-types-and-fields)
on `graphql.org`.
\
- objects.each do |type|
- unless type[:fields].empty?
= "## #{type[:name]}"
- if type[:description]&.present?
\
= type[:description]
\
~ "| Name | Type | Description |"
~ "| --- | ---- | ---------- |"
- sorted_fields(type[:fields]).each do |field|
= render_name_and_description(type)
~ "| Field | Type | Description |"
~ "| ----- | ---- | ----------- |"
- sorted_by_name(type[:fields]).each do |field|
= render_field(field)
\
:plain
## Enumeration types
Also called _Enums_, enumeration types are a special kind of scalar that
is restricted to a particular set of allowed values.
For more information, see
[Enumeration Types](https://graphql.org/learn/schema/#enumeration-types)
on `graphql.org`.
\
- enums.each do |enum|
- unless enum[:values].empty?
= render_name_and_description(enum)
~ "| Value | Description |"
~ "| ----- | ----------- |"
- sorted_by_name(enum[:values]).each do |value|
= render_enum_value(value)
\

View file

@ -16382,6 +16382,9 @@ msgstr ""
msgid "Namespace"
msgstr ""
msgid "Namespace ID:"
msgstr ""
msgid "Namespace is empty"
msgstr ""
@ -16870,7 +16873,7 @@ msgstr ""
msgid "No changes"
msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgid "No changes between %{sourceBranch} and %{targetBranch}"
msgstr ""
msgid "No child epics match applied filters"

View file

@ -21,6 +21,8 @@ module RuboCop
method_name = node.children[1]
return unless TRANSLATION_METHODS.include?(method_name)
translation_memoized = false
node.each_ancestor do |ancestor|
receiver, _ = *ancestor
break if lambda_node?(receiver) # translations defined in lambda nodes should be allowed
@ -30,6 +32,14 @@ module RuboCop
break
end
translation_memoized = true if memoization?(ancestor)
if translation_memoized && class_method_definition?(ancestor)
add_offense(node, location: :expression)
break
end
end
end
@ -38,6 +48,14 @@ module RuboCop
def constant_assignment?(node)
node.type == :casgn
end
def memoization?(node)
node.type == :or_asgn
end
def class_method_definition?(node)
node.type == :defs
end
end
end
end

View file

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Profiles::EmailsController do
let(:user) { create(:user) }
let_it_be(:user) { create(:user) }
before do
sign_in(user)
@ -16,36 +16,43 @@ RSpec.describe Profiles::EmailsController do
end
describe '#create' do
context 'when email address is valid' do
let(:email_params) { { email: "add_email@example.com" } }
let(:email) { 'add_email@example.com' }
let(:params) { { email: { email: email } } }
it 'sends an email confirmation' do
expect { post(:create, params: { email: email_params }) }.to change { ActionMailer::Base.deliveries.size }
end
subject { post(:create, params: params) }
it 'sends an email confirmation' do
expect { subject }.to change { ActionMailer::Base.deliveries.size }
end
context 'when email address is invalid' do
let(:email_params) { { email: "test.@example.com" } }
let(:email) { 'invalid.@example.com' }
it 'does not send an email confirmation' do
expect { post(:create, params: { email: email_params }) }.not_to change { ActionMailer::Base.deliveries.size }
expect { subject }.not_to change { ActionMailer::Base.deliveries.size }
end
end
end
describe '#resend_confirmation_instructions' do
let(:email_params) { { email: "add_email@example.com" } }
let_it_be(:email) { create(:email, user: user) }
let(:params) { { id: email.id } }
subject { put(:resend_confirmation_instructions, params: params) }
it 'resends an email confirmation' do
email = user.emails.create(email: 'add_email@example.com')
expect { subject }.to change { ActionMailer::Base.deliveries.size }
expect { put(:resend_confirmation_instructions, params: { id: email }) }.to change { ActionMailer::Base.deliveries.size }
expect(ActionMailer::Base.deliveries.last.to).to eq [email_params[:email]]
expect(ActionMailer::Base.deliveries.last.subject).to match "Confirmation instructions"
expect(ActionMailer::Base.deliveries.last.to).to eq [email.email]
expect(ActionMailer::Base.deliveries.last.subject).to match 'Confirmation instructions'
end
it 'unable to resend an email confirmation' do
expect { put(:resend_confirmation_instructions, params: { id: 1 }) }.not_to change { ActionMailer::Base.deliveries.size }
context 'email does not exist' do
let(:params) { { id: non_existing_record_id } }
it 'does not send an email confirmation' do
expect { subject }.not_to change { ActionMailer::Base.deliveries.size }
end
end
end
end

View file

@ -279,7 +279,8 @@ RSpec.describe "Admin::Users" do
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
expect(page).to have_content(user.id)
expect(page).to have_content("ID: #{user.id}")
expect(page).to have_content("Namespace ID: #{user.namespace_id}")
expect(page).to have_button('Deactivate user')
expect(page).to have_button('Block user')
expect(page).to have_button('Delete user')

View file

@ -3,44 +3,53 @@
require 'spec_helper'
RSpec.describe 'Tooltips on .timeago dates', :js do
let(:user) { create(:user) }
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:created_date) { Date.yesterday.to_time }
let(:expected_format) { created_date.in_time_zone.strftime('%b %-d, %Y %l:%M%P') }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:created_date) { 1.day.ago.beginning_of_minute - 1.hour }
before_all do
project.add_maintainer(user)
end
context 'on the activity tab' do
before do
project.add_maintainer(user)
Event.create( project: project, author_id: user.id, action: :joined,
updated_at: created_date, created_at: created_date)
sign_in user
visit user_activity_path(user)
wait_for_requests
page.find('.js-timeago').hover
end
it 'has the datetime formated correctly' do
expect(page).to have_selector('.local-timeago', text: expected_format)
expect(page).to have_selector('.js-timeago', text: '1 day ago')
page.find('.js-timeago').hover
expect(datetime_in_tooltip).to eq(created_date)
end
end
context 'on the snippets tab' do
before do
project.add_maintainer(user)
create(:snippet, author: user, updated_at: created_date, created_at: created_date)
sign_in user
visit user_snippets_path(user)
wait_for_requests
page.find('.js-timeago.snippet-created-ago').hover
end
it 'has the datetime formated correctly' do
expect(page).to have_selector('.local-timeago', text: expected_format)
expect(page).to have_selector('.js-timeago.snippet-created-ago', text: '1 day ago')
page.find('.js-timeago.snippet-created-ago').hover
expect(datetime_in_tooltip).to eq(created_date)
end
end
def datetime_in_tooltip
datetime_text = page.find('.local-timeago').text
DateTime.parse(datetime_text)
end
end

View file

@ -52,6 +52,7 @@ describe('Design management design todo button', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
jest.clearAllMocks();
});
it('renders TodoButton component', () => {
@ -68,7 +69,14 @@ describe('Design management design todo button', () => {
});
describe('when clicked', () => {
let dispatchEventSpy;
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
jest.spyOn(document, 'querySelector').mockReturnValue({
innerText: 2,
});
createComponent({ design: mockDesignWithPendingTodos }, { mountFn: mount });
wrapper.trigger('click');
return wrapper.vm.$nextTick();
@ -86,6 +94,14 @@ describe('Design management design todo button', () => {
expect(mutate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledWith(todoMarkDoneMutationVariables);
});
it('calls dispatchDocumentEvent to update global To-Do counter correctly', () => {
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
expect(dispatchedEvent.detail).toEqual({ count: 1 });
expect(dispatchedEvent.type).toBe('todo:toggle');
});
});
});
@ -99,7 +115,14 @@ describe('Design management design todo button', () => {
});
describe('when clicked', () => {
let dispatchEventSpy;
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
jest.spyOn(document, 'querySelector').mockReturnValue({
innerText: 2,
});
createComponent({}, { mountFn: mount });
wrapper.trigger('click');
return wrapper.vm.$nextTick();
@ -112,6 +135,7 @@ describe('Design management design todo button', () => {
variables: {
atVersion: null,
filenames: ['my-design.jpg'],
designId: '1',
issueId: '1',
issueIid: '10',
projectPath: 'project-path',
@ -121,6 +145,14 @@ describe('Design management design todo button', () => {
expect(mutate).toHaveBeenCalledTimes(1);
expect(mutate).toHaveBeenCalledWith(createDesignTodoMutationVariables);
});
it('calls dispatchDocumentEvent to update global To-Do counter correctly', () => {
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0];
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
expect(dispatchedEvent.detail).toEqual({ count: 3 });
expect(dispatchedEvent.type).toBe('todo:toggle');
});
});
});
});

View file

@ -4,7 +4,7 @@ import initTodoToggle, { initNavUserDropdownTracking } from '~/header';
describe('Header', () => {
describe('Todos notification', () => {
const todosPendingCount = '.todos-count';
const todosPendingCount = '.js-todos-count';
const fixtureTemplate = 'issues/open-issue.html';
function isTodosCountHidden() {

View file

@ -40,7 +40,7 @@ exports[`packages_list_row renders 1`] = `
</div>
<div
class="gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
>
<div
class="gl-display-flex"
@ -94,7 +94,7 @@ exports[`packages_list_row renders 1`] = `
class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
>
<div
class="gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
>
<publish-method-stub
packageentity="[object Object]"
@ -102,11 +102,13 @@ exports[`packages_list_row renders 1`] = `
</div>
<div
class="gl-mt-1 gl-min-h-6"
class="gl-display-flex gl-align-items-center gl-mt-1 gl-min-h-6"
>
<gl-sprintf-stub
message="Created %{timestamp}"
/>
<span>
<gl-sprintf-stub
message="Created %{timestamp}"
/>
</span>
</div>
</div>
</div>

View file

@ -2,7 +2,7 @@
exports[`publish_method renders 1`] = `
<div
class="gl-display-flex gl-align-items-center gl-mb-2"
class="gl-display-flex gl-align-items-center"
>
<gl-icon-stub
class="gl-mr-2"

View file

@ -120,7 +120,7 @@ describe('tooltips/components/tooltips.vue', () => {
wrapper.vm.addTooltips([target, createTooltipTarget()]);
await wrapper.vm.$nextTick();
wrapper.vm.dispose([target]);
wrapper.vm.dispose(target);
await wrapper.vm.$nextTick();
expect(allTooltips()).toHaveLength(1);
@ -148,6 +148,48 @@ describe('tooltips/components/tooltips.vue', () => {
});
});
describe('triggerEvent', () => {
it('triggers a bootstrap-vue tooltip global event for the tooltip specified', async () => {
const target = createTooltipTarget();
const event = 'hide';
buildWrapper();
wrapper.vm.addTooltips([target]);
await wrapper.vm.$nextTick();
wrapper.vm.triggerEvent(target, event);
expect(wrapper.find(GlTooltip).emitted(event)).toHaveLength(1);
});
});
describe('fixTitle', () => {
it('updates tooltip content with the latest value the target title property', async () => {
const target = createTooltipTarget();
const currentTitle = 'title';
const newTitle = 'new title';
target.setAttribute('title', currentTitle);
buildWrapper();
wrapper.vm.addTooltips([target]);
await wrapper.vm.$nextTick();
expect(wrapper.find(GlTooltip).text()).toBe(currentTitle);
target.setAttribute('title', newTitle);
wrapper.vm.fixTitle(target);
await wrapper.vm.$nextTick();
expect(wrapper.find(GlTooltip).text()).toBe(newTitle);
});
});
it('disconnects mutation observer on beforeDestroy', () => {
buildWrapper();
wrapper.vm.addTooltips([createTooltipTarget()]);

View file

@ -1,4 +1,4 @@
import { initTooltips, dispose, destroy } from '~/tooltips';
import { initTooltips, dispose, destroy, hide, show, enable, disable, fixTitle } from '~/tooltips';
describe('tooltips/index.js', () => {
let tooltipsApp;
@ -80,4 +80,41 @@ describe('tooltips/index.js', () => {
expect(document.querySelector('.gl-tooltip')).toBe(null);
});
});
it.each`
methodName | method | event
${'enable'} | ${enable} | ${'enable'}
${'disable'} | ${disable} | ${'disable'}
${'hide'} | ${hide} | ${'close'}
${'show'} | ${show} | ${'open'}
`(
'$methodName calls triggerEvent in tooltip app with $event event',
async ({ method, event }) => {
const target = createTooltipTarget();
buildTooltipsApp();
await tooltipsApp.$nextTick();
jest.spyOn(tooltipsApp, 'triggerEvent');
method([target]);
expect(tooltipsApp.triggerEvent).toHaveBeenCalledWith(target, event);
},
);
it('fixTitle calls fixTitle in tooltip app with the target specified', async () => {
const target = createTooltipTarget();
buildTooltipsApp();
await tooltipsApp.$nextTick();
jest.spyOn(tooltipsApp, 'fixTitle');
fixTitle([target]);
expect(tooltipsApp.fixTitle).toHaveBeenCalledWith(target);
});
});

View file

@ -198,6 +198,7 @@ RSpec.describe IssuablesHelper do
initialDescriptionHtml: '<p dir="auto">issue text</p>',
initialDescriptionText: 'issue text',
initialTaskStatus: '0 of 0 tasks completed',
issueType: 'issue',
iid: issue.iid.to_s
}
expect(helper.issuable_initial_data(issue)).to match(hash_including(expected_data))

View file

@ -36,10 +36,10 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
specify do
expectation = <<~DOC
## ArrayTest
### ArrayTest
| Name | Type | Description |
| --- | ---- | ---------- |
| Field | Type | Description |
| ----- | ---- | ----------- |
| `foo` | String! => Array | A description |
DOC
@ -59,10 +59,10 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
specify do
expectation = <<~DOC
## OrderingTest
### OrderingTest
| Name | Type | Description |
| --- | ---- | ---------- |
| Field | Type | Description |
| ----- | ---- | ----------- |
| `bar` | String! | A description of bar field |
| `foo` | String! | A description of foo field |
DOC
@ -82,15 +82,45 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do
specify do
expectation = <<~DOC
## DeprecatedTest
### DeprecatedTest
| Name | Type | Description |
| --- | ---- | ---------- |
| Field | Type | Description |
| ----- | ---- | ----------- |
| `foo` **{warning-solid}** | String! | **Deprecated:** This is deprecated. Deprecated in 1.10 |
DOC
is_expected.to include(expectation)
end
end
context 'A type with an emum field' do
let(:type) do
enum_type = Class.new(Types::BaseEnum) do
graphql_name 'MyEnum'
value 'BAZ', description: 'A description of BAZ'
value 'BAR', description: 'A description of BAR', deprecation_reason: 'This is deprecated'
end
Class.new(Types::BaseObject) do
graphql_name 'EnumTest'
field :foo, enum_type, null: false, description: 'A description of foo field'
end
end
specify do
expectation = <<~DOC
### MyEnum
| Value | Description |
| ----- | ----------- |
| `BAR` **{warning-solid}** | **Deprecated:** This is deprecated |
| `BAZ` | A description of BAZ |
DOC
is_expected.to include(expectation)
end
end
end
end

View file

@ -38,6 +38,17 @@ RSpec.describe RuboCop::Cop::StaticTranslationDefinition, type: :rubocop do
['A = _("a")', '_("a")', 1],
['B = s_("b")', 's_("b")', 1],
['C = n_("c")', 'n_("c")', 1],
[
<<~CODE,
class MyClass
def self.translations
@cache ||= { hello: _("hello") }
end
end
CODE
'_("hello")',
3
],
[
<<~CODE,
module MyModule
@ -78,6 +89,20 @@ RSpec.describe RuboCop::Cop::StaticTranslationDefinition, type: :rubocop do
'CONSTANT_1 = __("a")',
'CONSTANT_2 = s__("a")',
'CONSTANT_3 = n__("a")',
<<~CODE,
class MyClass
def self.method
@cache ||= { hello: -> { _("hello") } }
end
end
CODE
<<~CODE,
class MyClass
def method
@cache ||= { hello: _("hello") }
end
end
CODE
<<~CODE,
def method
s_('a')

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe CiPlatformMetricsUpdateCronWorker, type: :worker do
describe '#perform' do
subject { described_class.new.perform }
it 'inserts new platform metrics' do
expect(CiPlatformMetric).to receive(:insert_auto_devops_platform_targets!).and_call_original
subject
end
end
end