Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-08-04 00:09:26 +00:00
parent 01fa7c10d9
commit 65b1882ddd
33 changed files with 854 additions and 431 deletions

View File

@ -133,6 +133,9 @@ gem 'apollo_upload_server', '~> 2.1.0'
gem 'graphql-docs', '~> 2.1.0', group: [:development, :test]
gem 'graphlient', '~> 0.5.0' # Used by BulkImport feature (group::import)
# Generate Fake data
gem 'ffaker', '~> 2.10'
gem 'hashie', '~> 5.0.0'
# Pagination
@ -415,9 +418,6 @@ group :development, :test do
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.11.0'
# Generate Fake data
gem 'ffaker', '~> 2.10'
gem 'spring', '~> 4.1.0'
gem 'spring-commands-rspec', '~> 1.0.4'

View File

@ -28,6 +28,11 @@ export default {
type: Boolean,
required: true,
},
inModal: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -119,16 +124,28 @@ export default {
exampleUrl,
});
},
cancelButtonType() {
return this.isEditing ? 'button' : 'reset';
},
saveText() {
return this.isEditing ? s__('Badges|Save changes') : s__('Badges|Add badge');
},
},
mounted() {
// declared here to make it cancel-able
this.debouncedPreview = debounce(function search() {
this.renderBadge();
}, badgePreviewDelayInMilliseconds);
},
methods: {
...mapActions(['addBadge', 'renderBadge', 'saveBadge', 'stopEditing', 'updateBadgeInForm']),
debouncedPreview: debounce(function preview() {
this.renderBadge();
}, badgePreviewDelayInMilliseconds),
onCancel() {
this.stopEditing();
updatePreview() {
this.debouncedPreview();
},
onSubmit() {
this.debouncedPreview.cancel();
this.renderBadge();
const form = this.$el;
if (!form.checkValidity()) {
this.wasValidated = true;
@ -161,6 +178,7 @@ export default {
variant: VARIANT_INFO,
});
this.wasValidated = false;
this.$emit('close-add-form');
})
.catch((error) => {
createAlert({
@ -171,6 +189,17 @@ export default {
throw error;
});
},
closeForm() {
this.$refs.form.reset();
this.$emit('close-add-form');
},
handleCancel() {
if (this.isEditing) {
this.stopEditing();
} else {
this.closeForm();
}
},
},
safeHtmlConfig: { ALLOW_TAGS: ['a', 'code'] },
};
@ -178,6 +207,7 @@ export default {
<template>
<form
ref="form"
:class="{ 'was-validated': wasValidated }"
class="gl-mt-3 gl-mb-3 needs-validation"
novalidate
@ -197,7 +227,7 @@ export default {
type="URL"
class="form-control gl-form-input"
required
@input="debouncedPreview"
@input="updatePreview"
/>
<div class="invalid-feedback">{{ s__('Badges|Enter a valid URL') }}</div>
<span class="form-text text-muted">{{ badgeLinkUrlExample }}</span>
@ -213,7 +243,7 @@ export default {
type="URL"
class="form-control gl-form-input"
required
@input="debouncedPreview"
@input="updatePreview"
/>
<div class="invalid-feedback">{{ s__('Badges|Enter a valid URL') }}</div>
<span class="form-text text-muted">{{ badgeImageUrlExample }}</span>
@ -235,29 +265,23 @@ export default {
</p>
</div>
<div v-if="isEditing" class="row-content-block">
<gl-button class="btn-cancel gl-mr-4" data-testid="cancelEditing" @click="onCancel">
{{ __('Cancel') }}
</gl-button>
<gl-button
:loading="isSaving"
type="submit"
variant="confirm"
category="primary"
data-testid="saveEditing"
>
{{ s__('Badges|Save changes') }}
</gl-button>
</div>
<div v-else class="form-group">
<div v-if="!inModal" class="form-group" data-testid="action-buttons">
<gl-button
:loading="isSaving"
type="submit"
variant="confirm"
category="primary"
data-qa-selector="add_badge_button"
class="gl-mr-3"
>
{{ s__('Badges|Add badge') }}
{{ saveText }}
</gl-button>
<gl-button
:type="cancelButtonType"
data-qa-selector="cancel_badge_button"
@click="handleCancel"
>
{{ __('Cancel') }}
</gl-button>
</div>
</form>

View File

@ -1,15 +1,42 @@
<script>
import { GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { mapState } from 'vuex';
import { GROUP_BADGE } from '../constants';
import BadgeListRow from './badge_list_row.vue';
import {
GlBadge,
GlLoadingIcon,
GlTable,
GlPagination,
GlButton,
GlModalDirective,
} from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { __, s__ } from '~/locale';
import { GROUP_BADGE, PROJECT_BADGE, INITIAL_PAGE, PAGE_SIZE } from '../constants';
import Badge from './badge.vue';
export default {
PAGE_SIZE,
INITIAL_PAGE,
name: 'BadgeList',
components: {
BadgeListRow,
GlLoadingIcon,
Badge,
GlBadge,
GlLoadingIcon,
GlTable,
GlPagination,
GlButton,
},
directives: {
GlModal: GlModalDirective,
},
i18n: {
emptyGroupMessage: s__('Badges|This group has no badges, start by creating a new one above.'),
emptyProjectMessage: s__(
'Badges|This project has no badges, start by creating a new one above.',
),
},
data() {
return {
currentPage: INITIAL_PAGE,
};
},
computed: {
...mapState(['badges', 'isLoading', 'kind']),
@ -19,28 +46,123 @@ export default {
isGroupBadge() {
return this.kind === GROUP_BADGE;
},
showPagination() {
return this.badges.length > PAGE_SIZE;
},
emptyMessage() {
return this.isGroupBadge
? this.$options.i18n.emptyGroupMessage
: this.$options.i18n.emptyProjectMessage;
},
fields() {
return [
{
key: 'name',
label: __('Name'),
tdClass: 'gl-vertical-align-middle!',
},
{
key: 'badge',
label: __('Badge'),
tdClass: 'gl-vertical-align-middle!',
},
{
key: 'url',
label: __('URL'),
tdClass: 'gl-vertical-align-middle!',
},
{
key: 'actions',
label: __('Actions'),
thClass: 'gl-text-right',
tdClass: 'gl-text-right',
},
];
},
},
methods: {
...mapActions(['editBadge', 'updateBadgeInModal']),
badgeKindText(item) {
if (item.kind === PROJECT_BADGE) {
return s__('Badges|Project Badge');
}
return s__('Badges|Group Badge');
},
canEditBadge(item) {
return item.kind === this.kind;
},
},
};
</script>
<template>
<div class="card">
<div class="card-header">
{{ s__('Badges|Your badges') }}
<gl-badge v-show="!isLoading" size="sm">{{ badges.length }}</gl-badge>
</div>
<gl-loading-icon v-show="isLoading" size="lg" class="card-body" />
<div v-if="hasNoBadges" class="card-body">
<span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
<span v-else>{{ s__('Badges|This project has no badges') }}</span>
</div>
<div v-else class="card-body" data-qa-selector="badge_list_content">
<badge-list-row
v-for="badge in badges"
:key="badge.id"
:badge="badge"
data-qa-selector="badge_list_row"
:data-qa-badge-name="badge.name"
<div>
<gl-loading-icon v-show="isLoading" size="md" />
<div data-qa-selector="badge_list_content">
<gl-table
:empty-text="emptyMessage"
:fields="fields"
:items="badges"
:per-page="$options.PAGE_SIZE"
:current-page="currentPage"
stacked="md"
show-empty
data-qa-selector="badge_list"
>
<template #cell(name)="{ item }">
<label class="label-bold str-truncated mb-0">{{ item.name }}</label>
<gl-badge size="sm">{{ badgeKindText(item) }}</gl-badge>
</template>
<template #cell(badge)="{ item }">
<badge :image-url="item.renderedImageUrl" :link-url="item.renderedLinkUrl" />
</template>
<template #cell(url)="{ item }">
{{ item.linkUrl }}
</template>
<template #cell(actions)="{ item }">
<div v-if="canEditBadge(item)" class="table-action-buttons" data-testid="badge-actions">
<gl-button
v-gl-modal.edit-badge-modal
:disabled="item.isDeleting"
class="gl-mr-3"
variant="default"
icon="pencil"
size="medium"
:aria-label="__('Edit')"
data-testid="edit-badge-button"
@click="editBadge(item)"
/>
<gl-button
v-gl-modal.delete-badge-modal
:disabled="item.isDeleting"
category="secondary"
variant="danger"
icon="remove"
size="medium"
:aria-label="__('Delete')"
data-testid="delete-badge"
@click="updateBadgeInModal(item)"
/>
<gl-loading-icon v-show="item.isDeleting" size="sm" :inline="true" />
</div>
</template>
</gl-table>
<gl-pagination
v-if="showPagination"
v-model="currentPage"
:per-page="$options.PAGE_SIZE"
:total-items="badges.length"
:prev-text="__('Prev')"
:next-text="__('Next')"
:label-next-page="__('Go to next page')"
:label-prev-page="__('Go to previous page')"
align="center"
class="gl-mt-5"
/>
</div>
</div>

View File

@ -1,81 +0,0 @@
<script>
import { GlLoadingIcon, GlButton, GlModalDirective, GlBadge } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import { PROJECT_BADGE } from '../constants';
import Badge from './badge.vue';
export default {
name: 'BadgeListRow',
components: {
Badge,
GlLoadingIcon,
GlButton,
GlBadge,
},
directives: {
GlModal: GlModalDirective,
},
props: {
badge: {
type: Object,
required: true,
},
},
computed: {
...mapState(['kind']),
badgeKindText() {
if (this.badge.kind === PROJECT_BADGE) {
return s__('Badges|Project Badge');
}
return s__('Badges|Group Badge');
},
canEditBadge() {
return this.badge.kind === this.kind;
},
},
methods: {
...mapActions(['editBadge', 'updateBadgeInModal']),
},
};
</script>
<template>
<div class="gl-responsive-table-row-layout gl-responsive-table-row">
<badge
:image-url="badge.renderedImageUrl"
:link-url="badge.renderedLinkUrl"
class="table-section section-30"
/>
<div class="table-section section-30">
<label class="label-bold str-truncated mb-0">{{ badge.name }}</label>
<gl-badge size="sm">{{ badgeKindText }}</gl-badge>
</div>
<span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span>
<div class="table-section section-10 table-button-footer">
<div v-if="canEditBadge" class="table-action-buttons">
<gl-button
:disabled="badge.isDeleting"
class="gl-mr-3"
variant="default"
icon="pencil"
size="medium"
:aria-label="__('Edit')"
@click="editBadge(badge)"
/>
<gl-button
v-gl-modal.delete-badge-modal
:disabled="badge.isDeleting"
variant="danger"
icon="remove"
size="medium"
:aria-label="__('Delete')"
data-testid="delete-badge"
@click="updateBadgeInModal(badge)"
/>
<gl-loading-icon v-show="badge.isDeleting" size="sm" :inline="true" />
</div>
</div>
</div>
</template>

View File

@ -1,5 +1,5 @@
<script>
import { GlSprintf, GlModal } from '@gitlab/ui';
import { GlButton, GlCard, GlModal, GlIcon, GlSprintf } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { createAlert, VARIANT_INFO } from '~/alert';
import { __, s__ } from '~/locale';
@ -13,17 +13,34 @@ export default {
Badge,
BadgeForm,
BadgeList,
GlButton,
GlCard,
GlModal,
GlIcon,
GlSprintf,
},
i18n: {
title: s__('Badges|Your badges'),
addButton: s__('Badges|Add badge'),
addFormTitle: s__('Badges|Add new badge'),
deleteModalText: s__(
'Badges|You are going to delete this badge. Deleted badges %{strongStart}cannot%{strongEnd} be restored.',
),
},
data() {
return {
addFormVisible: false,
};
},
computed: {
...mapState(['badgeInModal', 'isEditing']),
primaryProps() {
...mapState(['badges', 'badgeInModal', 'isEditing']),
saveProps() {
return {
text: __('Save changes'),
attributes: { category: 'primary', variant: 'confirm' },
};
},
deleteProps() {
return {
text: __('Delete badge'),
attributes: { category: 'primary', variant: 'danger' },
@ -37,7 +54,16 @@ export default {
},
methods: {
...mapActions(['deleteBadge']),
onSubmitModal() {
showAddForm() {
this.addFormVisible = !this.addFormVisible;
},
closeAddForm() {
this.addFormVisible = false;
},
onSubmitEditModal() {
this.$refs.editForm.onSubmit();
},
onSubmitDeleteModal() {
this.deleteBadge(this.badgeInModal)
.then(() => {
createAlert({
@ -58,12 +84,58 @@ export default {
<template>
<div class="badge-settings">
<gl-card
class="gl-new-card"
header-class="gl-new-card-header"
body-class="gl-new-card-body gl-overflow-hidden gl-px-0"
>
<template #header>
<div class="gl-new-card-title-wrapper">
<h3 class="gl-new-card-title">{{ $options.i18n.title }}</h3>
<span class="gl-new-card-count">
<gl-icon name="labels" class="gl-mr-2" />
{{ badges.length }}
</span>
</div>
<div class="gl-new-card-actions">
<gl-button
v-if="!addFormVisible"
size="small"
data-testid="show-badge-add-form"
@click="showAddForm"
>{{ $options.i18n.addButton }}</gl-button
>
</div>
</template>
<div v-if="addFormVisible" class="gl-new-card-add-form gl-m-5">
<h4 class="gl-mt-0">{{ $options.i18n.addFormTitle }}</h4>
<badge-form
:is-editing="false"
data-testid="add-new-badge"
@close-add-form="closeAddForm"
/>
</div>
<badge-list />
</gl-card>
<gl-modal
modal-id="edit-badge-modal"
:title="s__('Badges|Edit badge')"
:action-primary="saveProps"
:action-cancel="cancelProps"
@primary="onSubmitEditModal"
>
<badge-form ref="editForm" :is-editing="true" :in-modal="true" data-testid="edit-badge" />
</gl-modal>
<gl-modal
modal-id="delete-badge-modal"
:title="s__('Badges|Delete badge?')"
:action-primary="primaryProps"
:action-primary="deleteProps"
:action-cancel="cancelProps"
@primary="onSubmitModal"
@primary="onSubmitDeleteModal"
>
<div class="well">
<badge
@ -79,10 +151,5 @@ export default {
</gl-sprintf>
</p>
</gl-modal>
<badge-form v-show="isEditing" :is-editing="true" data-testid="edit-badge" />
<badge-form v-show="!isEditing" :is-editing="false" data-testid="add-new-badge" />
<badge-list v-show="!isEditing" />
</div>
</template>

View File

@ -8,3 +8,5 @@ export const PLACEHOLDERS = [
'default_branch',
'commit_sha',
];
export const INITIAL_PAGE = 1;
export const PAGE_SIZE = 10;

View File

@ -0,0 +1,48 @@
<script>
import {
EVENT_CLOSED_I18N,
TARGET_TYPE_MERGE_REQUEST,
EVENT_CLOSED_ICONS,
} from 'ee_else_ce/contribution_events/constants';
import ContributionEventBase from './contribution_event_base.vue';
export default {
name: 'ContributionEventClosed',
components: { ContributionEventBase },
props: {
event: {
type: Object,
required: true,
},
},
computed: {
target() {
return this.event.target;
},
targetType() {
return this.target.type;
},
issueType() {
return this.target.issue_type;
},
message() {
return EVENT_CLOSED_I18N[this.issueType || this.targetType] || EVENT_CLOSED_I18N.fallback;
},
iconName() {
return EVENT_CLOSED_ICONS[this.issueType || this.targetType] || EVENT_CLOSED_ICONS.fallback;
},
iconClass() {
return this.targetType === TARGET_TYPE_MERGE_REQUEST ? 'gl-text-red-500' : 'gl-text-blue-500';
},
},
};
</script>
<template>
<contribution-event-base
:event="event"
:message="message"
:icon-name="iconName"
:icon-class="iconClass"
/>
</template>

View File

@ -1,14 +1,9 @@
<script>
import {
EVENT_CREATED_I18N,
TARGET_TYPE_WORK_ITEM,
TARGET_TYPE_DESIGN,
} from 'ee_else_ce/contribution_events/constants';
import { EVENT_CREATED_I18N, TARGET_TYPE_DESIGN } from 'ee_else_ce/contribution_events/constants';
import ContributionEventBase from './contribution_event_base.vue';
export default {
name: 'ContributionEventCreated',
i18n: EVENT_CREATED_I18N,
components: { ContributionEventBase },
props: {
event: {
@ -23,16 +18,15 @@ export default {
resourceParent() {
return this.event.resource_parent;
},
issueType() {
return this.target.issue_type;
},
message() {
if (!this.target) {
return this.$options.i18n[this.resourceParent.type] || this.$options.i18n.fallback;
return EVENT_CREATED_I18N[this.resourceParent.type] || EVENT_CREATED_I18N.fallback;
}
if (this.target.type === TARGET_TYPE_WORK_ITEM) {
return this.$options.i18n[this.target.issue_type] || this.$options.i18n.fallback;
}
return this.$options.i18n[this.target.type] || this.$options.i18n.fallback;
return EVENT_CREATED_I18N[this.issueType || this.target.type] || EVENT_CREATED_I18N.fallback;
},
iconName() {
switch (this.target?.type) {

View File

@ -9,6 +9,7 @@ import {
EVENT_TYPE_PRIVATE,
EVENT_TYPE_MERGED,
EVENT_TYPE_CREATED,
EVENT_TYPE_CLOSED,
} from '../constants';
import ContributionEventApproved from './contribution_event/contribution_event_approved.vue';
import ContributionEventExpired from './contribution_event/contribution_event_expired.vue';
@ -18,6 +19,7 @@ import ContributionEventPushed from './contribution_event/contribution_event_pus
import ContributionEventPrivate from './contribution_event/contribution_event_private.vue';
import ContributionEventMerged from './contribution_event/contribution_event_merged.vue';
import ContributionEventCreated from './contribution_event/contribution_event_created.vue';
import ContributionEventClosed from './contribution_event/contribution_event_closed.vue';
export default {
props: {
@ -136,6 +138,9 @@ export default {
case EVENT_TYPE_CREATED:
return ContributionEventCreated;
case EVENT_TYPE_CLOSED:
return ContributionEventClosed;
default:
return EmptyComponent;
}

View File

@ -30,6 +30,7 @@ export const TARGET_TYPE_DESIGN = 'DesignManagement::Design';
export const TARGET_TYPE_WORK_ITEM = 'WorkItem';
// From app/models/work_items/type.rb#L28
export const WORK_ITEM_ISSUE_TYPE_ISSUE = 'issue';
export const WORK_ITEM_ISSUE_TYPE_TASK = 'task';
export const WORK_ITEM_ISSUE_TYPE_INCIDENT = 'incident';
@ -38,9 +39,6 @@ export const EVENT_CREATED_I18N = {
[TARGET_TYPE_MILESTONE]: s__(
'ContributionEvent|Opened milestone %{targetLink} in %{resourceParentLink}.',
),
[TARGET_TYPE_ISSUE]: s__(
'ContributionEvent|Opened issue %{targetLink} in %{resourceParentLink}.',
),
[TARGET_TYPE_MERGE_REQUEST]: s__(
'ContributionEvent|Opened merge request %{targetLink} in %{resourceParentLink}.',
),
@ -50,6 +48,9 @@ export const EVENT_CREATED_I18N = {
[TARGET_TYPE_DESIGN]: s__(
'ContributionEvent|Added design %{targetLink} in %{resourceParentLink}.',
),
[WORK_ITEM_ISSUE_TYPE_ISSUE]: s__(
'ContributionEvent|Opened issue %{targetLink} in %{resourceParentLink}.',
),
[WORK_ITEM_ISSUE_TYPE_TASK]: s__(
'ContributionEvent|Opened task %{targetLink} in %{resourceParentLink}.',
),
@ -58,3 +59,28 @@ export const EVENT_CREATED_I18N = {
),
fallback: s__('ContributionEvent|Created resource.'),
};
export const EVENT_CLOSED_I18N = {
[TARGET_TYPE_MILESTONE]: s__(
'ContributionEvent|Closed milestone %{targetLink} in %{resourceParentLink}.',
),
[TARGET_TYPE_MERGE_REQUEST]: s__(
'ContributionEvent|Closed merge request %{targetLink} in %{resourceParentLink}.',
),
[WORK_ITEM_ISSUE_TYPE_ISSUE]: s__(
'ContributionEvent|Closed issue %{targetLink} in %{resourceParentLink}.',
),
[WORK_ITEM_ISSUE_TYPE_TASK]: s__(
'ContributionEvent|Closed task %{targetLink} in %{resourceParentLink}.',
),
[WORK_ITEM_ISSUE_TYPE_INCIDENT]: s__(
'ContributionEvent|Closed incident %{targetLink} in %{resourceParentLink}.',
),
fallback: s__('ContributionEvent|Closed resource.'),
};
export const EVENT_CLOSED_ICONS = {
[WORK_ITEM_ISSUE_TYPE_ISSUE]: 'issue-closed',
[TARGET_TYPE_MERGE_REQUEST]: 'merge-request-close',
fallback: 'status_closed',
};

View File

@ -1,5 +1,5 @@
<script>
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { GlButton, GlModal, GlModalDirective, GlCard, GlIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
import { expandSection } from '~/settings_panels';
@ -14,6 +14,8 @@ export default {
BranchRule,
GlButton,
GlModal,
GlCard,
GlIcon,
},
directives: {
GlModal: GlModalDirective,
@ -55,29 +57,47 @@ export default {
</script>
<template>
<div class="settings-content gl-mb-0">
<branch-rule
v-for="(rule, index) in branchRules"
:key="`${rule.name}-${index}`"
:name="rule.name"
:is-default="rule.isDefault"
:branch-protection="rule.branchProtection"
:status-checks-total="rule.externalStatusChecks ? rule.externalStatusChecks.nodes.length : 0"
:approval-rules-total="rule.approvalRules ? rule.approvalRules.nodes.length : 0"
:matching-branches-count="rule.matchingBranchesCount"
/>
<div v-if="!branchRules.length" data-testid="empty">{{ $options.i18n.emptyState }}</div>
<gl-button
v-gl-modal="$options.modalId"
class="gl-mt-5"
data-qa-selector="add_branch_rule_button"
category="secondary"
variant="confirm"
>{{ $options.i18n.addBranchRule }}</gl-button
>
<gl-card
class="gl-new-card gl-overflow-hidden"
header-class="gl-new-card-header"
body-class="gl-new-card-body gl-px-0"
>
<template #header>
<div class="gl-new-card-title-wrapper" data-testid="title">
<h3 class="gl-new-card-title">
{{ __('Branch Rules') }}
</h3>
<div class="gl-new-card-count">
<gl-icon name="branch" class="gl-mr-2" />
{{ branchRules.length }}
</div>
</div>
<gl-button
v-gl-modal="$options.modalId"
size="small"
class="gl-ml-3"
data-qa-selector="add_branch_rule_button"
>{{ $options.i18n.addBranchRule }}</gl-button
>
</template>
<ul class="content-list">
<branch-rule
v-for="(rule, index) in branchRules"
:key="`${rule.name}-${index}`"
:name="rule.name"
:is-default="rule.isDefault"
:branch-protection="rule.branchProtection"
:status-checks-total="
rule.externalStatusChecks ? rule.externalStatusChecks.nodes.length : 0
"
:approval-rules-total="rule.approvalRules ? rule.approvalRules.nodes.length : 0"
:matching-branches-count="rule.matchingBranchesCount"
class="gl-px-5! gl-py-4!"
/>
<div v-if="!branchRules.length" class="gl-new-card-empty gl-px-5 gl-py-4" data-testid="empty">
{{ $options.i18n.emptyState }}
</div>
</ul>
<gl-modal
:ref="$options.modalId"
:modal-id="$options.modalId"
@ -88,5 +108,5 @@ export default {
<p>{{ $options.i18n.branchRuleModalDescription }}</p>
<p>{{ $options.i18n.branchRuleModalContent }}</p>
</gl-modal>
</div>
</gl-card>
</template>

View File

@ -6,7 +6,7 @@ import { getAccessLevels } from '../../../utils';
export const i18n = {
defaultLabel: s__('BranchRules|default'),
protectedLabel: s__('BranchRules|protected'),
detailsButtonLabel: s__('BranchRules|Details'),
detailsButtonLabel: s__('BranchRules|View details'),
allowForcePush: s__('BranchRules|Allowed to force push'),
codeOwnerApprovalRequired: s__('BranchRules|Requires CODEOWNERS approval'),
statusChecks: s__('BranchRules|%{total} status %{subject}'),
@ -153,28 +153,36 @@ export default {
</script>
<template>
<div
class="gl-border-b gl-pt-5 gl-pb-5 gl-display-flex gl-justify-content-space-between"
data-qa-selector="branch_content"
:data-qa-branch-name="name"
>
<div>
<strong class="gl-font-monospace">{{ name }}</strong>
<gl-badge v-if="isDefault" variant="info" size="sm" class="gl-ml-2">{{
$options.i18n.defaultLabel
}}</gl-badge>
<gl-badge v-if="isProtected" variant="success" size="sm" class="gl-ml-2">{{
$options.i18n.protectedLabel
}}</gl-badge>
<ul v-if="hasApprovalDetails" class="gl-pl-6 gl-mt-2 gl-mb-0 gl-text-gray-500">
<li v-for="(detail, index) in approvalDetails" :key="index">{{ detail }}</li>
</ul>
</div>
<gl-button class="gl-align-self-start" data-qa-selector="details_button" :href="detailsPath">
{{ $options.i18n.detailsButtonLabel }}</gl-button
<li>
<div
class="gl-display-flex gl-justify-content-space-between"
data-qa-selector="branch_content"
:data-qa-branch-name="name"
>
</div>
<div>
<strong class="gl-font-monospace">{{ name }}</strong>
<gl-badge v-if="isDefault" variant="info" size="sm" class="gl-ml-2">{{
$options.i18n.defaultLabel
}}</gl-badge>
<gl-badge v-if="isProtected" variant="success" size="sm" class="gl-ml-2">{{
$options.i18n.protectedLabel
}}</gl-badge>
<ul v-if="hasApprovalDetails" class="gl-pl-6 gl-mt-2 gl-mb-0 gl-text-gray-500">
<li v-for="(detail, index) in approvalDetails" :key="index">{{ detail }}</li>
</ul>
</div>
<gl-button
class="gl-align-self-start"
category="tertiary"
size="small"
data-qa-selector="details_button"
:href="detailsPath"
>
{{ $options.i18n.detailsButtonLabel }}</gl-button
>
</div>
</li>
</template>

View File

@ -26,7 +26,9 @@ module Enums
end
def self.purl_types
PURL_TYPES
# return 0 by default if the purl_type is not found, to prevent
# consumers from producing invalid SQL caused by null entries
@_purl_types ||= PURL_TYPES.dup.tap { |h| h.default = 0 }
end
end
end

View File

@ -51,7 +51,7 @@ module Profile
expose(:id) { |event| event.target.id }
expose(:target_type, as: :type)
expose(:target_title, as: :title)
expose(:issue_type, if: ->(event) { event.work_item? }) do |event|
expose(:issue_type, if: ->(event) { event.work_item? || event.issue? }) do |event|
event.target.issue_type
end

View File

@ -12,5 +12,5 @@
= _('Define rules for who can push, merge, and the required approvals for each branch.')
= link_to(_('Leave feedback.'), 'https://gitlab.com/gitlab-org/gitlab/-/issues/388149', target: '_blank', rel: 'noopener noreferrer')
.settings-content.gl-pr-0
.settings-content
#js-branch-rules{ data: { project_path: @project.full_path, branch_rules_path: project_settings_repository_branch_rules_path(@project), show_code_owners: show_code_owners.to_s, show_status_checks: show_status_checks.to_s, show_approvers: show_approvers.to_s } }

View File

@ -14,15 +14,24 @@ module Sbom
'composer' => 'composer',
'conan' => 'conan',
'go' => 'golang',
'gobinary' => 'golang', # this package manager is generated by trivy
'nuget' => 'nuget',
'pip' => 'pypi',
'pipenv' => 'pypi',
'setuptools' => 'pypi'
'setuptools' => 'pypi',
'python-pkg' => 'pypi' # this package manager is generated by trivy
}.with_indifferent_access.freeze
def self.purl_type_for_pkg_manager(package_manager)
matches = package_manager.match(TRIVY_PACKAGE_MANAGER_REGEX)
package_manager = matches['trivy-package-manager-type'] if matches
PACKAGE_MANAGER_TO_PURL_TYPE_MAP[package_manager]
end
TRIVY_PACKAGE_MANAGER_REGEX = /\((?<trivy-package-manager-type>.*?)\)/
private_constant :TRIVY_PACKAGE_MANAGER_REGEX
end
end
end

View File

@ -7108,12 +7108,18 @@ msgstr ""
msgid "BackgroundMigrations|Started at"
msgstr ""
msgid "Badge"
msgstr ""
msgid "Badges"
msgstr ""
msgid "Badges|Add badge"
msgstr ""
msgid "Badges|Add new badge"
msgstr ""
msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
msgstr ""
@ -7132,6 +7138,9 @@ msgstr ""
msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
msgid "Badges|Edit badge"
msgstr ""
msgid "Badges|Enter a valid URL"
msgstr ""
@ -7174,10 +7183,10 @@ msgstr ""
msgid "Badges|The badge was deleted."
msgstr ""
msgid "Badges|This group has no badges"
msgid "Badges|This group has no badges, start by creating a new one above."
msgstr ""
msgid "Badges|This project has no badges"
msgid "Badges|This project has no badges, start by creating a new one above."
msgstr ""
msgid "Badges|You are going to delete this badge. Deleted badges %{strongStart}cannot%{strongEnd} be restored."
@ -8190,6 +8199,9 @@ msgstr ""
msgid "Branch %{branch_name} was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr ""
msgid "Branch Rules"
msgstr ""
msgid "Branch already exists"
msgstr ""
@ -8295,9 +8307,6 @@ msgstr ""
msgid "BranchRules|Create wildcard: %{searchTerm}"
msgstr ""
msgid "BranchRules|Details"
msgstr ""
msgid "BranchRules|Does not allow force push"
msgstr ""
@ -8367,6 +8376,9 @@ msgstr ""
msgid "BranchRules|Users"
msgstr ""
msgid "BranchRules|View details"
msgstr ""
msgid "BranchRules|default"
msgstr ""
@ -13013,6 +13025,39 @@ msgstr ""
msgid "ContributionEvent|Approved merge request %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Closed Epic %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Closed incident %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Closed issue %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Closed key result %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Closed merge request %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Closed milestone %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Closed objective %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Closed requirement %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Closed resource."
msgstr ""
msgid "ContributionEvent|Closed task %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Closed test case %{targetLink} in %{resourceParentLink}."
msgstr ""
msgid "ContributionEvent|Created project %{resourceParentLink}."
msgstr ""

View File

@ -58,7 +58,7 @@
"@gitlab/cluster-client": "^1.2.0",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/svgs": "3.58.0",
"@gitlab/svgs": "3.59.0",
"@gitlab/ui": "64.20.1",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230802205337",

View File

@ -13,7 +13,7 @@ module QA
view 'app/assets/javascripts/badges/components/badge_list.vue' do
element :badge_list_content
element :badge_list_row
element :badge_list
end
view 'app/assets/javascripts/badges/components/badge.vue' do
@ -38,7 +38,7 @@ module QA
def has_badge?(badge_name)
within_element(:badge_list_content) do
has_element?(:badge_list_row, badge_name: badge_name)
has_element?(:badge_list, badge_name: badge_name)
end
end

View File

@ -24,7 +24,7 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
page.within '.badge-settings' do
wait_for_requests
rows = all('.card-body > div')
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
expect(rows[0]).to have_content badge_1.link_url
expect(rows[1]).to have_content badge_2.link_url
@ -33,6 +33,7 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
context 'adding a badge', :js do
it 'user can preview a badge' do
click_button 'Add badge'
page.within '.badge-settings form' do
fill_in 'badge-link-url', with: badge_link_url
fill_in 'badge-image-url', with: badge_image_url
@ -44,6 +45,7 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
end
it do
click_button 'Add badge'
page.within '.badge-settings' do
fill_in 'badge-link-url', with: badge_link_url
fill_in 'badge-image-url', with: badge_image_url
@ -51,7 +53,7 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
click_button 'Add badge'
wait_for_requests
within '.card-body' do
within '.gl-card-body' do
expect(find('a')[:href]).to eq badge_link_url
expect(find('a img')[:src]).to eq badge_image_url
end
@ -63,32 +65,35 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
it 'form is shown when clicking edit button in list' do
page.within '.badge-settings' do
wait_for_requests
rows = all('.card-body > div')
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
end
within 'form' do
expect(find('#badge-link-url').value).to eq badge_2.link_url
expect(find('#badge-image-url').value).to eq badge_2.image_url
end
page.within '.gl-modal' do
expect(find('#badge-link-url').value).to eq badge_2.link_url
expect(find('#badge-image-url').value).to eq badge_2.image_url
end
end
it 'updates a badge when submitting the edit form' do
page.within '.badge-settings' do
wait_for_requests
rows = all('.card-body > div')
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
within 'form' do
fill_in 'badge-link-url', with: badge_link_url
fill_in 'badge-image-url', with: badge_image_url
end
click_button 'Save changes'
wait_for_requests
end
page.within '.gl-modal' do
fill_in 'badge-link-url', with: badge_link_url
fill_in 'badge-image-url', with: badge_image_url
rows = all('.card-body > div')
click_button 'Save changes'
wait_for_requests
end
page.within '.badge-settings' do
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
expect(rows[1]).to have_content badge_link_url
end
@ -102,7 +107,7 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
it 'shows a modal when deleting a badge' do
wait_for_requests
rows = all('.card-body > div')
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
click_delete_button(rows[1])
@ -112,14 +117,14 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
it 'deletes a badge when confirming the modal' do
wait_for_requests
rows = all('.card-body > div')
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
click_delete_button(rows[1])
find('.modal .btn-danger').click
wait_for_requests
rows = all('.card-body > div')
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 1
expect(rows[0]).to have_content badge_1.link_url
end

View File

@ -24,7 +24,7 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
page.within '.badge-settings' do
wait_for_requests
rows = all('.card-body > div')
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
expect(rows[0]).to have_content group_badge.link_url
expect(rows[1]).to have_content project_badge.link_url
@ -33,6 +33,7 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
context 'adding a badge', :js do
it 'user can preview a badge' do
click_button 'Add badge'
page.within '.badge-settings form' do
fill_in 'badge-link-url', with: badge_link_url
fill_in 'badge-image-url', with: badge_image_url
@ -44,6 +45,7 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
end
it do
click_button 'Add badge'
page.within '.badge-settings' do
fill_in 'badge-link-url', with: badge_link_url
fill_in 'badge-image-url', with: badge_image_url
@ -51,7 +53,7 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
click_button 'Add badge'
wait_for_requests
within '.card-body' do
within '.gl-card-body' do
expect(find('a')[:href]).to eq badge_link_url
expect(find('a img')[:src]).to eq badge_image_url
end
@ -63,32 +65,35 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
it 'form is shown when clicking edit button in list' do
page.within '.badge-settings' do
wait_for_requests
rows = all('.card-body > div')
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
end
within 'form' do
expect(find('#badge-link-url').value).to eq project_badge.link_url
expect(find('#badge-image-url').value).to eq project_badge.image_url
end
page.within '.gl-modal' do
expect(find('#badge-link-url').value).to eq project_badge.link_url
expect(find('#badge-image-url').value).to eq project_badge.image_url
end
end
it 'updates a badge when submitting the edit form' do
page.within '.badge-settings' do
wait_for_requests
rows = all('.card-body > div')
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
within 'form' do
fill_in 'badge-link-url', with: badge_link_url
fill_in 'badge-image-url', with: badge_image_url
end
click_button 'Save changes'
wait_for_requests
end
page.within '.gl-modal' do
fill_in 'badge-link-url', with: badge_link_url
fill_in 'badge-image-url', with: badge_image_url
rows = all('.card-body > div')
click_button 'Save changes'
wait_for_requests
end
page.within '.badge-settings' do
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
expect(rows[1]).to have_content badge_link_url
end
@ -102,7 +107,7 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
it 'shows a modal when deleting a badge' do
wait_for_requests
rows = all('.card-body > div')
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
click_delete_button(rows[1])
@ -112,14 +117,14 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
it 'deletes a badge when confirming the modal' do
wait_for_requests
rows = all('.card-body > div')
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
click_delete_button(rows[1])
find('.modal .btn-danger').click
wait_for_requests
rows = all('.card-body > div')
rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 1
expect(rows[0]).to have_content group_badge.link_url
end

View File

@ -49,7 +49,7 @@ describe('BadgeForm component', () => {
it('stops editing when cancel button is clicked', async () => {
createComponent({ isEditing: true });
const cancelButton = wrapper.find('.row-content-block button');
const cancelButton = wrapper.findAll('[data-testid="action-buttons"] button').at(1);
await cancelButton.trigger('click');
@ -143,13 +143,13 @@ describe('BadgeForm component', () => {
describe('if isEditing is false', () => {
const props = { isEditing: false };
it('renders one button', () => {
it('renders two buttons', () => {
createComponent(props);
expect(wrapper.find('.row-content-block').exists()).toBe(false);
const buttons = wrapper.findAll('.form-group:last-of-type button');
const buttons = wrapper.findAll('[data-testid="action-buttons"] button');
expect(buttons).toHaveLength(1);
expect(buttons).toHaveLength(2);
const buttonAddWrapper = buttons.at(0);
expect(buttonAddWrapper.isVisible()).toBe(true);
@ -164,15 +164,15 @@ describe('BadgeForm component', () => {
it('renders two buttons', () => {
createComponent(props);
const buttons = wrapper.findAll('.row-content-block button');
const buttons = wrapper.findAll('[data-testid="action-buttons"] button');
expect(buttons).toHaveLength(2);
const saveButton = buttons.at(1);
const saveButton = buttons.at(0);
expect(saveButton.isVisible()).toBe(true);
expect(saveButton.text()).toBe('Save changes');
const cancelButton = buttons.at(0);
const cancelButton = buttons.at(1);
expect(cancelButton.isVisible()).toBe(true);
expect(cancelButton.text()).toBe('Cancel');
});

View File

@ -1,119 +0,0 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import BadgeListRow from '~/badges/components/badge_list_row.vue';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
import createState from '~/badges/store/state';
import mutations from '~/badges/store/mutations';
import actions from '~/badges/store/actions';
import { createDummyBadge } from '../dummy_badge';
Vue.use(Vuex);
describe('BadgeListRow component', () => {
let badge;
let wrapper;
let mockedActions;
const createComponent = (kind) => {
setHTMLFixture(`<div id="delete-badge-modal" class="modal"></div>`);
mockedActions = Object.fromEntries(Object.keys(actions).map((name) => [name, jest.fn()]));
const store = new Vuex.Store({
state: {
...createState(),
kind: PROJECT_BADGE,
},
mutations,
actions: mockedActions,
});
badge = createDummyBadge();
badge.kind = kind;
wrapper = mount(BadgeListRow, {
attachTo: document.body,
store,
propsData: { badge },
});
};
afterEach(() => {
resetHTMLFixture();
});
describe('for a project badge', () => {
beforeEach(() => {
createComponent(PROJECT_BADGE);
});
it('renders the badge', () => {
const badgeImage = wrapper.find('.project-badge');
expect(badgeImage.exists()).toBe(true);
expect(badgeImage.attributes('src')).toBe(badge.renderedImageUrl);
});
it('renders the badge name', () => {
expect(wrapper.text()).toMatch(badge.name);
});
it('renders the badge link', () => {
expect(wrapper.text()).toMatch(badge.linkUrl);
});
it('renders the badge kind', () => {
expect(wrapper.text()).toMatch('Project Badge');
});
it('shows edit and delete buttons', () => {
const buttons = wrapper.findAll('.table-button-footer button');
expect(buttons).toHaveLength(2);
const editButton = buttons.at(0);
expect(editButton.isVisible()).toBe(true);
expect(editButton.element).toHaveSpriteIcon('pencil');
const deleteButton = buttons.at(1);
expect(deleteButton.isVisible()).toBe(true);
expect(deleteButton.element).toHaveSpriteIcon('remove');
});
it('calls editBadge when clicking then edit button', async () => {
const editButton = wrapper.find('.table-button-footer button:first-of-type');
await editButton.trigger('click');
expect(mockedActions.editBadge).toHaveBeenCalled();
});
it('calls updateBadgeInModal and shows modal when clicking then delete button', async () => {
const deleteButton = wrapper.find('.table-button-footer button:last-of-type');
await deleteButton.trigger('click');
expect(mockedActions.updateBadgeInModal).toHaveBeenCalled();
});
});
describe('for a group badge', () => {
beforeEach(() => {
createComponent(GROUP_BADGE);
});
it('renders the badge kind', () => {
expect(wrapper.text()).toMatch('Group Badge');
});
it('hides edit and delete buttons', () => {
const buttons = wrapper.findAll('.table-button-footer button');
expect(buttons).toHaveLength(0);
});
});
});

View File

@ -1,14 +1,12 @@
import { GlTable, GlButton } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
import BadgeList from '~/badges/components/badge_list.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
import createState from '~/badges/store/state';
import mutations from '~/badges/store/mutations';
import actions from '~/badges/store/actions';
import BadgeList from '~/badges/components/badge_list.vue';
import { createDummyBadge } from '../dummy_badge';
Vue.use(Vuex);
@ -21,9 +19,16 @@ const badges = Array.from({ length: numberOfDummyBadges }).map((_, idx) => ({
describe('BadgeList component', () => {
let wrapper;
let mockedActions;
const findTable = () => wrapper.findComponent(GlTable);
const findTableRow = (pos) => findTable().find('tbody').findAll('tr').at(pos);
const findButtons = () => wrapper.findByTestId('badge-actions').findAllComponents(GlButton);
const findEditButton = () => wrapper.findByTestId('edit-badge-button');
const findDeleteButton = () => wrapper.findByTestId('delete-badge');
const createComponent = (customState) => {
const mockedActions = Object.fromEntries(Object.keys(actions).map((name) => [name, jest.fn()]));
mockedActions = Object.fromEntries(Object.keys(actions).map((name) => [name, jest.fn()]));
const store = new Vuex.Store({
state: {
@ -35,28 +40,23 @@ describe('BadgeList component', () => {
actions: mockedActions,
});
wrapper = mount(BadgeList, { store });
wrapper = mountExtended(BadgeList, {
store,
stubs: {
GlTable,
GlButton,
},
});
};
describe('for project badges', () => {
it('renders a header with the badge count', () => {
createComponent({
kind: PROJECT_BADGE,
badges,
});
const header = wrapper.find('.card-header');
expect(header.text()).toMatchInterpolatedText('Your badges 3');
});
it('renders a row for each badge', () => {
createComponent({
kind: PROJECT_BADGE,
badges,
});
const rows = wrapper.findAll('.gl-responsive-table-row');
const rows = findTable().find('tbody').findAll('tr');
expect(rows).toHaveLength(numberOfDummyBadges);
});
@ -89,4 +89,60 @@ describe('BadgeList component', () => {
expect(wrapper.text()).toMatch('This group has no badges');
});
});
describe('BadgeList item', () => {
beforeEach(() => {
createComponent({
kind: PROJECT_BADGE,
badges,
});
});
it('renders the badge', () => {
const badgeImage = wrapper.find('.project-badge');
expect(badgeImage.exists()).toBe(true);
expect(badgeImage.attributes('src')).toBe(badges[0].renderedImageUrl);
});
it('renders the badge name', () => {
const badgeCell = findTableRow(0).findAll('td').at(0);
expect(badgeCell.text()).toMatch(badges[0].name);
});
it('renders the badge link', () => {
expect(wrapper.text()).toMatch(badges[0].linkUrl);
});
it('renders the badge kind', () => {
expect(wrapper.text()).toMatch('Project Badge');
});
it('shows edit and delete buttons', () => {
expect(findButtons()).toHaveLength(2);
const editButton = findEditButton();
expect(editButton.isVisible()).toBe(true);
expect(editButton.element).toHaveSpriteIcon('pencil');
const deleteButton = findDeleteButton();
expect(deleteButton.isVisible()).toBe(true);
expect(deleteButton.element).toHaveSpriteIcon('remove');
});
it('calls editBadge when clicking then edit button', () => {
findEditButton().trigger('click');
expect(mockedActions.editBadge).toHaveBeenCalled();
});
it('calls updateBadgeInModal and shows modal when clicking then delete button', () => {
findDeleteButton().trigger('click');
expect(mockedActions.updateBadgeInModal).toHaveBeenCalled();
});
});
});

View File

@ -1,10 +1,10 @@
import { GlModal } from '@gitlab/ui';
import { GlCard, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vue from 'vue';
import Vuex from 'vuex';
import BadgeList from '~/badges/components/badge_list.vue';
import BadgeListRow from '~/badges/components/badge_list_row.vue';
import BadgeSettings from '~/badges/components/badge_settings.vue';
import BadgeList from '~/badges/components/badge_list.vue';
import BadgeForm from '~/badges/components/badge_form.vue';
import store from '~/badges/store';
import { createDummyBadge } from '../dummy_badge';
@ -22,8 +22,10 @@ describe('BadgeSettings component', () => {
wrapper = shallowMount(BadgeSettings, {
store,
stubs: {
GlCard,
GlTable,
'badge-list': BadgeList,
'badge-list-row': BadgeListRow,
'badge-form': BadgeForm,
},
});
};
@ -32,35 +34,35 @@ describe('BadgeSettings component', () => {
createComponent();
});
it('displays modal if button for deleting a badge is clicked', async () => {
const button = wrapper.find('[data-testid="delete-badge"]');
it('renders a header with the badge count', () => {
createComponent();
button.vm.$emit('click');
await nextTick();
const cardTitle = wrapper.find('.gl-new-card-title');
const cardCount = wrapper.find('.gl-new-card-count');
const modal = wrapper.findComponent(GlModal);
expect(modal.isVisible()).toBe(true);
expect(cardTitle.text()).toContain('Your badges');
expect(cardCount.text()).toContain('1');
});
it('displays a form to add a badge', () => {
expect(wrapper.find('[data-testid="add-new-badge"]').isVisible()).toBe(true);
it('displays a table', () => {
expect(wrapper.findComponent(GlTable).isVisible()).toBe(true);
});
it('displays badge list', () => {
it('renders badge add form', () => {
expect(wrapper.findComponent(BadgeForm).exists()).toBe(true);
});
it('renders badge list', () => {
expect(wrapper.findComponent(BadgeList).isVisible()).toBe(true);
});
describe('when editing', () => {
beforeEach(() => {
createComponent(true);
createComponent({ isEditing: true });
});
it('displays a form to edit a badge', () => {
expect(wrapper.find('[data-testid="edit-badge"]').isVisible()).toBe(true);
});
it('displays no badge list', () => {
expect(wrapper.findComponent(BadgeList).isVisible()).toBe(false);
});
});
});

View File

@ -0,0 +1,63 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContributionEventClosed from '~/contribution_events/components/contribution_event/contribution_event_closed.vue';
import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
import { TARGET_TYPE_WORK_ITEM } from '~/contribution_events/constants';
import {
eventMilestoneClosed,
eventIssueClosed,
eventMergeRequestClosed,
eventTaskClosed,
eventIncidentClosed,
} from '../../utils';
describe('ContributionEventClosed', () => {
let wrapper;
const createComponent = ({ propsData }) => {
wrapper = shallowMountExtended(ContributionEventClosed, {
propsData,
});
};
describe.each`
event | expectedMessage | iconName | iconClass
${eventMilestoneClosed()} | ${'Closed milestone %{targetLink} in %{resourceParentLink}.'} | ${'status_closed'} | ${'gl-text-blue-500'}
${eventIssueClosed()} | ${'Closed issue %{targetLink} in %{resourceParentLink}.'} | ${'issue-closed'} | ${'gl-text-blue-500'}
${eventMergeRequestClosed()} | ${'Closed merge request %{targetLink} in %{resourceParentLink}.'} | ${'merge-request-close'} | ${'gl-text-red-500'}
${{ target: { type: 'unsupported type' } }} | ${'Closed resource.'} | ${'status_closed'} | ${'gl-text-blue-500'}
`(
'when event target type is $event.target.type',
({ event, expectedMessage, iconName, iconClass }) => {
it('renders `ContributionEventBase` with correct props', () => {
createComponent({ propsData: { event } });
expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
event,
message: expectedMessage,
iconName,
iconClass,
});
});
},
);
describe(`when event target type is ${TARGET_TYPE_WORK_ITEM}`, () => {
describe.each`
event | expectedMessage
${eventTaskClosed()} | ${'Closed task %{targetLink} in %{resourceParentLink}.'}
${eventIncidentClosed()} | ${'Closed incident %{targetLink} in %{resourceParentLink}.'}
${{ target: { type: TARGET_TYPE_WORK_ITEM, issue_type: 'unsupported type' } }} | ${'Closed resource.'}
`('when issue type is $event.target.issue_type', ({ event, expectedMessage }) => {
it('renders `ContributionEventBase` with correct props', () => {
createComponent({ propsData: { event } });
expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
event,
message: expectedMessage,
iconName: 'status_closed',
iconClass: 'gl-text-blue-500',
});
});
});
});
});

View File

@ -8,6 +8,7 @@ import ContributionEventPushed from '~/contribution_events/components/contributi
import ContributionEventPrivate from '~/contribution_events/components/contribution_event/contribution_event_private.vue';
import ContributionEventMerged from '~/contribution_events/components/contribution_event/contribution_event_merged.vue';
import ContributionEventCreated from '~/contribution_events/components/contribution_event/contribution_event_created.vue';
import ContributionEventClosed from '~/contribution_events/components/contribution_event/contribution_event_closed.vue';
import {
eventApproved,
eventExpired,
@ -17,6 +18,7 @@ import {
eventPrivate,
eventMerged,
eventCreated,
eventClosed,
} from '../utils';
describe('ContributionEvents', () => {
@ -34,6 +36,7 @@ describe('ContributionEvents', () => {
eventPrivate(),
eventMerged(),
eventCreated(),
eventClosed(),
],
},
});
@ -49,6 +52,7 @@ describe('ContributionEvents', () => {
${ContributionEventPrivate} | ${eventPrivate()}
${ContributionEventMerged} | ${eventMerged()}
${ContributionEventCreated} | ${eventCreated()}
${ContributionEventClosed} | ${eventClosed()}
`(
'renders `$expectedComponent.name` component and passes expected event',
({ expectedComponent, expectedEvent }) => {

View File

@ -7,6 +7,7 @@ import {
EVENT_TYPE_PUSHED,
EVENT_TYPE_PRIVATE,
EVENT_TYPE_MERGED,
EVENT_TYPE_CLOSED,
PUSH_EVENT_REF_TYPE_BRANCH,
PUSH_EVENT_REF_TYPE_TAG,
EVENT_TYPE_CREATED,
@ -21,6 +22,15 @@ import {
} from '~/contribution_events/constants';
const findEventByAction = (action) => () => events.find((event) => event.action === action);
const findEventByActionAndTargetType = (action, targetType) => () =>
events.find((event) => event.action === action && event.target?.type === targetType);
const findEventByActionAndIssueType = (action, issueType) => () =>
events.find(
(event) =>
event.action === action &&
event.target?.type === TARGET_TYPE_WORK_ITEM &&
event.target.issue_type === issueType,
);
export const eventApproved = findEventByAction(EVENT_TYPE_APPROVED);
@ -62,15 +72,10 @@ export const eventPrivate = () => ({ ...events[0], action: EVENT_TYPE_PRIVATE })
export const eventCreated = findEventByAction(EVENT_TYPE_CREATED);
export const findCreatedEvent = (targetType) => () =>
events.find((event) => event.action === EVENT_TYPE_CREATED && event.target?.type === targetType);
export const findWorkItemCreatedEvent = (issueType) => () =>
events.find(
(event) =>
event.action === EVENT_TYPE_CREATED &&
event.target?.type === TARGET_TYPE_WORK_ITEM &&
event.target.issue_type === issueType,
);
export const findCreatedEvent = (targetType) =>
findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
export const findWorkItemCreatedEvent = (issueType) =>
findEventByActionAndIssueType(EVENT_TYPE_CREATED, issueType);
export const eventProjectCreated = findCreatedEvent(undefined);
export const eventMilestoneCreated = findCreatedEvent(TARGET_TYPE_MILESTONE);
@ -80,3 +85,18 @@ export const eventWikiPageCreated = findCreatedEvent(TARGET_TYPE_WIKI);
export const eventDesignCreated = findCreatedEvent(TARGET_TYPE_DESIGN);
export const eventTaskCreated = findWorkItemCreatedEvent(WORK_ITEM_ISSUE_TYPE_TASK);
export const eventIncidentCreated = findWorkItemCreatedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);
export const eventClosed = findEventByAction(EVENT_TYPE_CLOSED);
export const findClosedEvent = (targetType) =>
findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
export const findWorkItemClosedEvent = (issueType) =>
findEventByActionAndIssueType(EVENT_TYPE_CLOSED, issueType);
export const eventMilestoneClosed = findClosedEvent(TARGET_TYPE_MILESTONE);
export const eventIssueClosed = findClosedEvent(TARGET_TYPE_ISSUE);
export const eventMergeRequestClosed = findClosedEvent(TARGET_TYPE_MERGE_REQUEST);
export const eventWikiPageClosed = findClosedEvent(TARGET_TYPE_WIKI);
export const eventDesignClosed = findClosedEvent(TARGET_TYPE_DESIGN);
export const eventTaskClosed = findWorkItemClosedEvent(WORK_ITEM_ISSUE_TYPE_TASK);
export const eventIncidentClosed = findWorkItemClosedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Sbom::PurlType::Converter, feature_category: :dependency_management do
describe '.purl_type_for_pkg_manager' do
using RSpec::Parameterized::TableSyntax
subject(:actual_purl_type) { described_class.purl_type_for_pkg_manager(package_manager) }
where(:given_package_manager, :expected_purl_type) do
'bundler' | 'gem'
'yarn' | 'npm'
'npm' | 'npm'
'pnpm' | 'npm'
'maven' | 'maven'
'sbt' | 'maven'
'gradle' | 'maven'
'composer' | 'composer'
'conan' | 'conan'
'go' | 'golang'
'nuget' | 'nuget'
'pip' | 'pypi'
'pipenv' | 'pypi'
'setuptools' | 'pypi'
'Python (python-pkg)' | 'pypi'
'analyzer (gobinary)' | 'golang'
'unknown-pkg-manager' | nil
'Python (unknown)' | nil
end
with_them do
let(:package_manager) { given_package_manager }
it 'returns the expected purl_type' do
expect(actual_purl_type).to eql(expected_purl_type)
end
end
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Enums::Sbom, feature_category: :dependency_management do
describe '.purl_types' do
using RSpec::Parameterized::TableSyntax
subject(:actual_purl_type) { described_class.purl_types[package_manager] }
where(:given_package_manager, :expected_purl_type) do
:composer | 1
'composer' | 1
:conan | 2
'conan' | 2
:gem | 3
:golang | 4
:maven | 5
:npm | 6
:nuget | 7
:pypi | 8
:apk | 9
:rpm | 10
:deb | 11
:cbl_mariner | 12
'unknown-pkg-manager' | 0
'Python (unknown)' | 0
end
with_them do
let(:package_manager) { given_package_manager }
it 'returns the expected purl_type' do
expect(actual_purl_type).to eql(expected_purl_type)
end
end
end
end

View File

@ -140,6 +140,17 @@ RSpec.describe Profile::EventEntity, feature_category: :user_profile do
expect(subject[:target][:issue_type]).to eq('incident')
end
end
context 'when target is an issue' do
let(:issue) { build_stubbed(:issue, author: target_user, project: project) }
let(:event) do
build(:event, :created, author: target_user, project: project, target: issue)
end
it 'exposes `issue_type`' do
expect(subject[:target][:issue_type]).to eq('issue')
end
end
end
context 'with resource parent' do

View File

@ -34,11 +34,18 @@ RSpec.shared_context 'with user contribution events' do
# closed
let_it_be(:closed_issue_event) { create(:event, :closed, author: user, project: project, target: issue) }
let_it_be(:closed_milestone_event) { create(:event, :closed, author: user, project: project, target: milestone) }
let_it_be(:closed_incident_event) { create(:event, :closed, author: user, project: project, target: incident) }
let_it_be(:closed_merge_request_event) do
create(:event, :closed, author: user, project: project, target: merge_request)
end
let_it_be(:closed_task_event) do
create(:event, :closed, :for_work_item, author: user, project: project, target: task)
end
let_it_be(:closed_incident_event) do
create(:event, :closed, :for_work_item, author: user, project: project, target: incident)
end
# commented
let_it_be(:commented_event) do
create(:event, :commented, author: user, project: project, target: note_on_issue)

View File

@ -1127,10 +1127,10 @@
stylelint-declaration-strict-value "1.8.0"
stylelint-scss "4.2.0"
"@gitlab/svgs@3.58.0":
version "3.58.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.58.0.tgz#cae8483c81e260af6d1d55a25235099683ed76b7"
integrity sha512-4aCsp0sVn+XBYJAiO/7IdwVxfINBJ0bRvvuvM1R91KYjs2XFw/rtg1HPQ+9MxZHcD5x/cIdDL6dWwr3XzfFWjw==
"@gitlab/svgs@3.59.0":
version "3.59.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.59.0.tgz#21090154aa7987e059264e13182c4c60e6d0d4b3"
integrity sha512-5+FZ0Clwtf2X6oHEEVCwbhqhmnxT8Ds1CGFxHzzWsvQ5Hkdt658BVAicsbvQSU+TuEIhnKOK3BfooyleMUwLlQ==
"@gitlab/ui@64.20.1":
version "64.20.1"