Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9f9d994f13
commit
5cd8380e46
|
@ -5,6 +5,7 @@ import { buildApiUrl } from './api_utils';
|
|||
const GROUP_PATH = '/api/:version/groups/:id';
|
||||
const GROUPS_PATH = '/api/:version/groups.json';
|
||||
const DESCENDANT_GROUPS_PATH = '/api/:version/groups/:id/descendant_groups';
|
||||
const GROUP_TRANSFER_LOCATIONS_PATH = 'api/:version/groups/:id/transfer_locations';
|
||||
|
||||
const axiosGet = (url, query, options, callback) => {
|
||||
return axios
|
||||
|
@ -37,3 +38,10 @@ export function updateGroup(groupId, data = {}) {
|
|||
|
||||
return axios.put(url, data);
|
||||
}
|
||||
|
||||
export const getGroupTransferLocations = (groupId, params = {}) => {
|
||||
const url = buildApiUrl(GROUP_TRANSFER_LOCATIONS_PATH).replace(':id', groupId);
|
||||
const defaultParams = { per_page: DEFAULT_PER_PAGE };
|
||||
|
||||
return axios.get(url, { params: { ...defaultParams, ...params } });
|
||||
};
|
||||
|
|
|
@ -1,29 +1,24 @@
|
|||
<script>
|
||||
import { GlFormGroup } from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
|
||||
import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
|
||||
import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
|
||||
import { getGroupTransferLocations } from '~/api/groups_api';
|
||||
|
||||
export const i18n = {
|
||||
confirmationMessage: __(
|
||||
'You are going to transfer %{group_name} to another namespace. Are you ABSOLUTELY sure?',
|
||||
),
|
||||
emptyNamespaceTitle: __('No parent group'),
|
||||
dropdownTitle: s__('GroupSettings|Select parent group'),
|
||||
dropdownLabel: s__('GroupSettings|Select parent group'),
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'TransferGroupForm',
|
||||
components: {
|
||||
ConfirmDanger,
|
||||
GlFormGroup,
|
||||
NamespaceSelect,
|
||||
TransferLocations,
|
||||
},
|
||||
props: {
|
||||
groupNamespaces: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
isPaidGroup: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
@ -39,36 +34,41 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
selectedId: null,
|
||||
selectedTransferLocation: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
disableSubmitButton() {
|
||||
return this.isPaidGroup || !this.selectedId;
|
||||
return this.isPaidGroup || !this.selectedTransferLocation;
|
||||
},
|
||||
selectedTransferLocationId() {
|
||||
return this.selectedTransferLocation?.id;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleSelected({ id }) {
|
||||
this.selectedId = id;
|
||||
},
|
||||
getGroupTransferLocations,
|
||||
},
|
||||
i18n,
|
||||
additionalDropdownItems: [
|
||||
{
|
||||
id: -1,
|
||||
humanName: i18n.emptyNamespaceTitle,
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<gl-form-group v-if="!isPaidGroup">
|
||||
<namespace-select
|
||||
:default-text="$options.i18n.dropdownTitle"
|
||||
:group-namespaces="groupNamespaces"
|
||||
:empty-namespace-title="$options.i18n.emptyNamespaceTitle"
|
||||
:include-headers="false"
|
||||
include-empty-namespace
|
||||
data-testid="transfer-group-namespace-select"
|
||||
@select="handleSelected"
|
||||
/>
|
||||
<input type="hidden" name="new_parent_group_id" :value="selectedId" />
|
||||
</gl-form-group>
|
||||
<input type="hidden" name="new_parent_group_id" :value="selectedTransferLocationId" />
|
||||
<transfer-locations
|
||||
v-if="!isPaidGroup"
|
||||
v-model="selectedTransferLocation"
|
||||
:show-user-transfer-locations="false"
|
||||
data-testid="transfer-group-namespace"
|
||||
:group-transfer-locations-api-method="getGroupTransferLocations"
|
||||
:additional-dropdown-items="$options.additionalDropdownItems"
|
||||
:label="$options.i18n.dropdownLabel"
|
||||
/>
|
||||
<confirm-danger
|
||||
:disabled="disableSubmitButton"
|
||||
:phrase="confirmationPhrase"
|
||||
|
|
|
@ -1,42 +1,38 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { sprintf } from '~/locale';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import TransferGroupForm, { i18n } from './components/transfer_group_form.vue';
|
||||
|
||||
const prepareGroups = (rawGroups) => {
|
||||
if (!rawGroups) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return JSON.parse(rawGroups).map(({ id, text: humanName }) => ({
|
||||
id,
|
||||
humanName,
|
||||
}));
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('.js-transfer-group-form');
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const {
|
||||
targetFormId = null,
|
||||
buttonText: confirmButtonText = '',
|
||||
groupName = '',
|
||||
parentGroups,
|
||||
groupId: resourceId,
|
||||
isPaidGroup,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
apolloProvider: new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
}),
|
||||
provide: {
|
||||
confirmDangerMessage: sprintf(i18n.confirmationMessage, { group_name: groupName }),
|
||||
resourceId,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(TransferGroupForm, {
|
||||
props: {
|
||||
groupNamespaces: prepareGroups(parentGroups),
|
||||
isPaidGroup: parseBoolean(isPaidGroup),
|
||||
confirmButtonText,
|
||||
confirmationPhrase: groupName,
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlDropdownDivider,
|
||||
GlSearchBoxByType,
|
||||
GlIntersectionObserver,
|
||||
GlLoadingIcon,
|
||||
|
@ -34,6 +35,7 @@ export default {
|
|||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlDropdownDivider,
|
||||
GlSearchBoxByType,
|
||||
GlIntersectionObserver,
|
||||
GlLoadingIcon,
|
||||
|
@ -49,6 +51,23 @@ export default {
|
|||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
showUserTransferLocations: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
additionalDropdownItems: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: i18n.SELECT_A_NAMESPACE,
|
||||
},
|
||||
},
|
||||
initialTransferLocationsLoaded: false,
|
||||
data() {
|
||||
|
@ -56,6 +75,7 @@ export default {
|
|||
searchTerm: '',
|
||||
userTransferLocations: [],
|
||||
groupTransferLocations: [],
|
||||
filteredAdditionalDropdownItems: this.additionalDropdownItems,
|
||||
isLoading: false,
|
||||
isSearchLoading: false,
|
||||
hasError: false,
|
||||
|
@ -71,11 +91,14 @@ export default {
|
|||
return this.groupTransferLocations.length;
|
||||
},
|
||||
selectedText() {
|
||||
return this.value?.humanName || i18n.SELECT_A_NAMESPACE;
|
||||
return this.value?.humanName || this.label;
|
||||
},
|
||||
hasNextPageOfGroups() {
|
||||
return this.page < this.totalPages;
|
||||
},
|
||||
showAdditionalDropdownItems() {
|
||||
return !this.isLoading && this.filteredAdditionalDropdownItems.length;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchTerm() {
|
||||
|
@ -128,6 +151,10 @@ export default {
|
|||
}
|
||||
},
|
||||
async getUserTransferLocations() {
|
||||
if (!this.showUserTransferLocations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
|
@ -167,6 +194,10 @@ export default {
|
|||
|
||||
this.groupTransferLocations = await this.getGroupTransferLocations();
|
||||
|
||||
this.filteredAdditionalDropdownItems = this.additionalDropdownItems.filter((dropdownItem) =>
|
||||
dropdownItem.humanName.toLowerCase().includes(this.searchTerm.toLowerCase()),
|
||||
);
|
||||
|
||||
this.isSearchLoading = false;
|
||||
}, DEBOUNCE_DELAY),
|
||||
handleError() {
|
||||
|
@ -188,8 +219,15 @@ export default {
|
|||
@dismiss="handleAlertDismiss"
|
||||
>{{ $options.i18n.ERROR_MESSAGE }}</gl-alert
|
||||
>
|
||||
<gl-form-group :label="$options.i18n.SELECT_A_NAMESPACE">
|
||||
<gl-dropdown :text="selectedText" data-qa-selector="namespaces_list" block @show="handleShow">
|
||||
<gl-form-group :label="label">
|
||||
<gl-dropdown
|
||||
:text="selectedText"
|
||||
data-qa-selector="namespaces_list"
|
||||
data-testid="transfer-locations-dropdown"
|
||||
block
|
||||
toggle-class="gl-mb-0"
|
||||
@show="handleShow"
|
||||
>
|
||||
<template #header>
|
||||
<gl-search-box-by-type
|
||||
v-model.trim="searchTerm"
|
||||
|
@ -197,6 +235,15 @@ export default {
|
|||
data-qa-selector="namespaces_list_search"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="showAdditionalDropdownItems">
|
||||
<gl-dropdown-item
|
||||
v-for="item in filteredAdditionalDropdownItems"
|
||||
:key="item.id"
|
||||
@click="handleSelect(item)"
|
||||
>{{ item.humanName }}</gl-dropdown-item
|
||||
>
|
||||
<gl-dropdown-divider />
|
||||
</template>
|
||||
<div
|
||||
v-if="hasUserTransferLocations"
|
||||
data-qa-selector="namespaces_list_users"
|
||||
|
@ -216,7 +263,9 @@ export default {
|
|||
data-qa-selector="namespaces_list_groups"
|
||||
data-testid="group-transfer-locations"
|
||||
>
|
||||
<gl-dropdown-section-header>{{ $options.i18n.GROUPS }}</gl-dropdown-section-header>
|
||||
<gl-dropdown-section-header v-if="showUserTransferLocations">{{
|
||||
$options.i18n.GROUPS
|
||||
}}</gl-dropdown-section-header>
|
||||
<gl-dropdown-item
|
||||
v-for="item in groupTransferLocations"
|
||||
:key="item.id"
|
||||
|
|
|
@ -1,212 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlSearchBoxByType,
|
||||
GlIntersectionObserver,
|
||||
GlLoadingIcon,
|
||||
} from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export const EMPTY_NAMESPACE_ID = -1;
|
||||
export const i18n = {
|
||||
DEFAULT_TEXT: __('Select a new namespace'),
|
||||
DEFAULT_EMPTY_NAMESPACE_TEXT: __('No namespace'),
|
||||
GROUPS: __('Groups'),
|
||||
USERS: __('Users'),
|
||||
};
|
||||
|
||||
const filterByName = (data, searchTerm = '') => {
|
||||
if (!searchTerm) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return data.filter((d) => d.humanName.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'NamespaceSelectDeprecated',
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlSearchBoxByType,
|
||||
GlIntersectionObserver,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
groupNamespaces: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
userNamespaces: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
defaultText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: i18n.DEFAULT_TEXT,
|
||||
},
|
||||
includeHeaders: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
emptyNamespaceTitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: i18n.DEFAULT_EMPTY_NAMESPACE_TEXT,
|
||||
},
|
||||
includeEmptyNamespace: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
hasNextPageOfGroups: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isSearchLoading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
shouldFilterNamespaces: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
selectedNamespace: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasUserNamespaces() {
|
||||
return this.userNamespaces.length;
|
||||
},
|
||||
hasGroupNamespaces() {
|
||||
return this.groupNamespaces.length;
|
||||
},
|
||||
filteredGroupNamespaces() {
|
||||
if (!this.shouldFilterNamespaces) return this.groupNamespaces;
|
||||
if (!this.hasGroupNamespaces) return [];
|
||||
return filterByName(this.groupNamespaces, this.searchTerm);
|
||||
},
|
||||
filteredUserNamespaces() {
|
||||
if (!this.shouldFilterNamespaces) return this.userNamespaces;
|
||||
if (!this.hasUserNamespaces) return [];
|
||||
return filterByName(this.userNamespaces, this.searchTerm);
|
||||
},
|
||||
selectedNamespaceText() {
|
||||
return this.selectedNamespace?.humanName || this.defaultText;
|
||||
},
|
||||
filteredEmptyNamespaceTitle() {
|
||||
const { includeEmptyNamespace, emptyNamespaceTitle, searchTerm } = this;
|
||||
|
||||
if (!includeEmptyNamespace) {
|
||||
return '';
|
||||
}
|
||||
if (!searchTerm) {
|
||||
return emptyNamespaceTitle;
|
||||
}
|
||||
|
||||
return emptyNamespaceTitle.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchTerm() {
|
||||
this.$emit('search', this.searchTerm);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleSelect(item) {
|
||||
this.selectedNamespace = item;
|
||||
this.searchTerm = '';
|
||||
this.$emit('select', item);
|
||||
},
|
||||
handleSelectEmptyNamespace() {
|
||||
this.handleSelect({ id: EMPTY_NAMESPACE_ID, humanName: this.emptyNamespaceTitle });
|
||||
},
|
||||
},
|
||||
i18n,
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-dropdown
|
||||
:text="selectedNamespaceText"
|
||||
:block="fullWidth"
|
||||
data-qa-selector="namespaces_list"
|
||||
@show="$emit('show')"
|
||||
>
|
||||
<template #header>
|
||||
<gl-search-box-by-type
|
||||
v-model.trim="searchTerm"
|
||||
:is-loading="isSearchLoading"
|
||||
data-qa-selector="namespaces_list_search"
|
||||
/>
|
||||
</template>
|
||||
<div v-if="filteredEmptyNamespaceTitle">
|
||||
<gl-dropdown-item
|
||||
data-qa-selector="namespaces_list_item"
|
||||
@click="handleSelectEmptyNamespace()"
|
||||
>
|
||||
{{ emptyNamespaceTitle }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider />
|
||||
</div>
|
||||
<div
|
||||
v-if="hasUserNamespaces"
|
||||
data-qa-selector="namespaces_list_users"
|
||||
data-testid="namespace-list-users"
|
||||
>
|
||||
<gl-dropdown-section-header v-if="includeHeaders">{{
|
||||
$options.i18n.USERS
|
||||
}}</gl-dropdown-section-header>
|
||||
<gl-dropdown-item
|
||||
v-for="item in filteredUserNamespaces"
|
||||
:key="item.id"
|
||||
data-qa-selector="namespaces_list_item"
|
||||
@click="handleSelect(item)"
|
||||
>{{ item.humanName }}</gl-dropdown-item
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasGroupNamespaces"
|
||||
data-qa-selector="namespaces_list_groups"
|
||||
data-testid="namespace-list-groups"
|
||||
>
|
||||
<gl-dropdown-section-header v-if="includeHeaders">{{
|
||||
$options.i18n.GROUPS
|
||||
}}</gl-dropdown-section-header>
|
||||
<gl-dropdown-item
|
||||
v-for="item in filteredGroupNamespaces"
|
||||
:key="item.id"
|
||||
data-qa-selector="namespaces_list_item"
|
||||
@click="handleSelect(item)"
|
||||
>{{ item.humanName }}</gl-dropdown-item
|
||||
>
|
||||
</div>
|
||||
<gl-loading-icon v-if="isLoading" class="gl-mb-3" size="sm" />
|
||||
<gl-intersection-observer v-if="hasNextPageOfGroups" @appear="$emit('load-more-groups')" />
|
||||
</gl-dropdown>
|
||||
</template>
|
|
@ -277,139 +277,128 @@
|
|||
.description p {
|
||||
margin-bottom: 0;
|
||||
color: $gl-text-color-secondary;
|
||||
@include str-truncated(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.projects-list {
|
||||
@include basic-list;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include gl-display-table;
|
||||
|
||||
.project-row {
|
||||
@include basic-list-stats;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $gl-padding-12 0;
|
||||
@include gl-display-table-row;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $gl-font-size;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
margin-bottom: 0;
|
||||
.project-cell {
|
||||
@include gl-display-table-cell;
|
||||
@include gl-border-b;
|
||||
@include gl-vertical-align-top;
|
||||
@include gl-py-4;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.namespace-name {
|
||||
font-weight: $gl-font-weight-normal;
|
||||
}
|
||||
.project-row:last-of-type {
|
||||
.project-cell {
|
||||
@include gl-border-none;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
flex: 0 0 auto;
|
||||
align-self: flex-start;
|
||||
|
||||
&.admin-projects,
|
||||
&.group-settings-projects {
|
||||
.project-row {
|
||||
@include basic-list-stats;
|
||||
|
||||
.description > p {
|
||||
@include gl-mb-0;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
@include gl-line-height-42;
|
||||
}
|
||||
}
|
||||
|
||||
.project-details {
|
||||
min-width: 0;
|
||||
line-height: $gl-line-height;
|
||||
|
||||
.flex-wrapper {
|
||||
min-width: 0;
|
||||
margin-top: -$gl-padding-8; // negative margin required for flex-wrap
|
||||
flex: 1 1 100%;
|
||||
|
||||
.project-title {
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
max-width: 625px;
|
||||
|
||||
p,
|
||||
.commit-row-message {
|
||||
@include gl-mb-0;
|
||||
@include str-truncated(100%);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.user-access-role {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
line-height: 1.5;
|
||||
color: $gl-text-color-secondary;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
.user-access-role {
|
||||
line-height: $gl-line-height-14;
|
||||
}
|
||||
max-height: $gl-spacing-scale-8;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-link {
|
||||
display: inline-block;
|
||||
line-height: 17px;
|
||||
vertical-align: middle;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
@include gl-text-decoration-none;
|
||||
}
|
||||
|
||||
.controls {
|
||||
@include media-breakpoint-down(xs) {
|
||||
margin-top: $gl-padding-8;
|
||||
}
|
||||
&:not(.compact) {
|
||||
.controls {
|
||||
@include media-breakpoint-up(lg) {
|
||||
@include gl-justify-content-start;
|
||||
@include gl-pr-9;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
flex: 1 1 40%;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
color: inherit;
|
||||
margin-right: $gl-padding;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
margin-right: 0;
|
||||
margin-left: $gl-padding-8;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
&:not(.with-pipeline-status) {
|
||||
.icon-wrapper:first-of-type {
|
||||
@include media-breakpoint-up(lg) {
|
||||
@include gl-ml-7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.with-pipeline-status) {
|
||||
.icon-wrapper:first-of-type {
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-left: $gl-padding-32;
|
||||
.project-details {
|
||||
p,
|
||||
.commit-row-message {
|
||||
@include gl-white-space-normal;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||
display: -webkit-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
@include media-breakpoint-up(sm) {
|
||||
@include gl-justify-content-end;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
@include media-breakpoint-down(md) {
|
||||
@include gl-mr-0;
|
||||
@include gl-ml-3;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
&:first-child {
|
||||
@include gl-ml-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-link {
|
||||
display: inline-flex;
|
||||
@include gl-display-inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
@include media-breakpoint-down(xs) {
|
||||
margin-right: $gl-padding-8;
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-right: $gl-spacing-scale-7;
|
||||
}
|
||||
}
|
||||
|
||||
&.compact {
|
||||
.project-row {
|
||||
padding: $gl-padding 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $gl-font-size;
|
||||
.description {
|
||||
@include gl-w-full;
|
||||
@include gl-display-table;
|
||||
@include gl-table-layout-fixed;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
|
@ -422,27 +411,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.updated-note {
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-top: $gl-padding-8;
|
||||
@include gl-mt-2;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
margin-left: $gl-padding-8;
|
||||
margin-right: 0;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
@include gl-ml-3;
|
||||
@include gl-mr-0;
|
||||
}
|
||||
|
||||
.user-access-role {
|
||||
|
@ -451,10 +428,6 @@
|
|||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
h2 {
|
||||
font-size: $gl-font-size;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
@include avatar-size(40px, 10px);
|
||||
min-height: 40px;
|
||||
|
@ -468,24 +441,18 @@
|
|||
|
||||
@include media-breakpoint-down(md) {
|
||||
.updated-note {
|
||||
margin-top: $gl-padding-8;
|
||||
text-align: right;
|
||||
@include gl-mt-3;
|
||||
@include gl-text-right;
|
||||
}
|
||||
}
|
||||
|
||||
.forks,
|
||||
.pipeline-status,
|
||||
.updated-note {
|
||||
display: flex;
|
||||
@include gl-display-flex;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
&:not(.explore) {
|
||||
.forks {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.explore {
|
||||
.pipeline-status,
|
||||
.updated-note {
|
||||
|
@ -496,8 +463,8 @@
|
|||
|
||||
@include media-breakpoint-down(xs) {
|
||||
.updated-note {
|
||||
margin-top: 0;
|
||||
text-align: left;
|
||||
@include gl-mt-0;
|
||||
@include gl-text-left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,16 +112,6 @@ module GroupsHelper
|
|||
s_("GroupSettings|Available only on the top-level group. Applies to all subgroups. Groups already shared with a group outside %{group} are still shared unless removed manually.").html_safe % { group: link_to_group(group) }
|
||||
end
|
||||
|
||||
def parent_group_options(current_group)
|
||||
exclude_groups = current_group.self_and_descendants.pluck_primary_key
|
||||
exclude_groups << current_group.parent_id if current_group.parent_id
|
||||
groups = GroupsFinder.new(current_user, min_access_level: Gitlab::Access::OWNER, exclude_group_ids: exclude_groups).execute.sort_by(&:human_name).map do |group|
|
||||
{ id: group.id, text: group.human_name }
|
||||
end
|
||||
|
||||
Gitlab::Json.dump(groups)
|
||||
end
|
||||
|
||||
def render_setting_to_allow_project_access_token_creation?(group)
|
||||
group.root? && current_user.can?(:admin_setting_to_allow_project_access_token_creation, group)
|
||||
end
|
||||
|
|
|
@ -478,6 +478,10 @@ module ProjectsHelper
|
|||
localized_access_names[access] || Gitlab::Access.human_access(access)
|
||||
end
|
||||
|
||||
def badge_count(number)
|
||||
format_cached_count(1000, number)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def localized_access_names
|
||||
|
|
|
@ -6,8 +6,23 @@ module Enums
|
|||
library: 0
|
||||
}.with_indifferent_access.freeze
|
||||
|
||||
PURL_TYPES = {
|
||||
composer: 1, # refered to as `packagist` in gemnasium-db
|
||||
conan: 2,
|
||||
gem: 3,
|
||||
golang: 4, # refered to as `go` in gemnasium-db
|
||||
maven: 5,
|
||||
npm: 6,
|
||||
nuget: 7,
|
||||
pypi: 8
|
||||
}.with_indifferent_access.freeze
|
||||
|
||||
def self.component_types
|
||||
COMPONENT_TYPES
|
||||
end
|
||||
|
||||
def self.purl_types
|
||||
PURL_TYPES
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
- c.body do
|
||||
= render 'shared/projects/list',
|
||||
projects: contributed_projects.sort_by(&:star_count).reverse,
|
||||
projects_limit: 5, stars: true, avatar: false
|
||||
projects_limit: 5, stars: true, avatar: false, compact_mode: true
|
||||
|
||||
- if local_assigns.has_key?(:projects) && projects.present?
|
||||
= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-py-0' }) do |c|
|
||||
|
@ -14,4 +14,4 @@
|
|||
- c.body do
|
||||
= render 'shared/projects/list',
|
||||
projects: projects.sort_by(&:star_count).reverse,
|
||||
projects_limit: 10, stars: true, avatar: false
|
||||
projects_limit: 10, stars: true, avatar: false, compact_mode: true
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- form_id = "transfer-group-form"
|
||||
- initial_data = { button_text: s_('GroupSettings|Transfer group'), group_name: @group.name, target_form_id: form_id, parent_groups: parent_group_options(group), is_paid_group: group.paid?.to_s }
|
||||
- initial_data = { button_text: s_('GroupSettings|Transfer group'), group_name: @group.name, group_id: @group.id, target_form_id: form_id, is_paid_group: group.paid?.to_s }
|
||||
|
||||
.sub-section
|
||||
%h4.warning-title= s_('GroupSettings|Transfer group')
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
- if any_projects?(projects)
|
||||
- load_pipeline_status(projects) if pipeline_status
|
||||
- load_max_project_member_accesses(projects) # Prime cache used in shared/projects/project view rendered below
|
||||
%ul.projects-list{ class: css_classes }
|
||||
%ul.projects-list.gl-text-secondary.gl-w-full.gl-my-2{ class: css_classes }
|
||||
- projects.each_with_index do |project, i|
|
||||
- css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
|
||||
= render "shared/projects/project", project: project, skip_namespace: skip_namespace,
|
||||
|
|
|
@ -8,102 +8,108 @@
|
|||
- access = max_project_member_access(project)
|
||||
- compact_mode = false unless local_assigns[:compact_mode] == true
|
||||
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
|
||||
- css_class = '' unless local_assigns[:css_class]
|
||||
- css_class += " gl-display-flex!"
|
||||
- css_class = "gl-sm-display-flex gl-align-items-center gl-vertical-align-middle!" if project.description.blank? && !show_last_commit_as_description
|
||||
- cache_key = project_list_cache_key(project, pipeline_status: pipeline_status)
|
||||
- updated_tooltip = time_ago_with_tooltip(project.last_activity_date)
|
||||
- show_pipeline_status_icon = pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
|
||||
- last_pipeline = project.last_pipeline if show_pipeline_status_icon
|
||||
- css_controls_class = compact_mode ? [] : ["flex-lg-row", "justify-content-lg-between"]
|
||||
- css_controls_class << "with-pipeline-status" if show_pipeline_status_icon && last_pipeline.present?
|
||||
- avatar_container_class = project.creator && use_creator_avatar ? '' : 'rect-avatar'
|
||||
- css_controls_class = "with-pipeline-status" if show_pipeline_status_icon && last_pipeline.present?
|
||||
- css_controls_container_class = compact_mode ? "" : "gl-lg-flex-direction-row gl-justify-content-space-between"
|
||||
- css_metadata_classes = "gl-display-flex gl-align-items-center gl-mr-5 gl-reset-color! icon-wrapper has-tooltip"
|
||||
|
||||
%li.project-row.gl-align-items-center{ class: css_class }
|
||||
%li.project-row
|
||||
= cache(cache_key) do
|
||||
- if avatar
|
||||
.flex-grow-0.flex-shrink-0{ class: avatar_container_class }
|
||||
.project-cell.gl-w-11
|
||||
= link_to project_path(project), class: dom_class(project) do
|
||||
- if project.creator && use_creator_avatar
|
||||
= render Pajamas::AvatarComponent.new(project.creator, size: 48, alt: '', class: 'gl-mr-5')
|
||||
- else
|
||||
= render Pajamas::AvatarComponent.new(project, size: 48, alt: '', class: 'gl-mr-5')
|
||||
.project-details.d-sm-flex.flex-sm-fill.align-items-center{ data: { qa_selector: 'project_content', qa_project_name: project.name } }
|
||||
.flex-wrapper
|
||||
.d-flex.align-items-center.flex-wrap.project-title
|
||||
%h2.d-flex.gl-mt-3
|
||||
= link_to project_path(project), class: 'text-plain js-prefetch-document' do
|
||||
%span.project-full-name.gl-mr-3><
|
||||
%span.namespace-name
|
||||
- if project.namespace && !skip_namespace
|
||||
= project.namespace.human_name
|
||||
\/
|
||||
%span.project-name<
|
||||
= project.name
|
||||
.project-cell{ class: css_class }
|
||||
.project-details.gl-pr-9.gl-sm-pr-0.gl-w-full.gl-display-flex.gl-flex-direction-column{ data: { qa_selector: 'project_content', qa_project_name: project.name } }
|
||||
.gl-display-flex.gl-align-items-center.gl-flex-wrap-wrap
|
||||
%h2.gl-font-base.gl-line-height-20.gl-my-0
|
||||
= link_to project_path(project), class: 'text-plain gl-mr-3 js-prefetch-document' do
|
||||
%span.namespace-name.gl-font-weight-normal
|
||||
- if project.namespace && !skip_namespace
|
||||
= project.namespace.human_name
|
||||
\/
|
||||
%span.project-name<
|
||||
= project.name
|
||||
|
||||
%span.metadata-info.visibility-icon.gl-mr-3.gl-mt-3.text-secondary.has-tooltip{ data: { container: 'body', placement: 'top' }, title: visibility_icon_description(project) }
|
||||
= visibility_level_icon(project.visibility_level)
|
||||
%span.gl-mr-3.has-tooltip{ data: { container: 'body', placement: 'top' }, title: visibility_icon_description(project) }
|
||||
= visibility_level_icon(project.visibility_level)
|
||||
|
||||
- if explore_projects_tab? && project_license_name(project)
|
||||
%span.metadata-info.d-inline-flex.align-items-center.gl-mr-3.gl-mt-3
|
||||
= sprite_icon('scale', size: 14, css_class: 'gl-mr-2')
|
||||
= project_license_name(project)
|
||||
- if explore_projects_tab? && project_license_name(project)
|
||||
%span.gl-display-inline-flex.gl-align-items-center.gl-mr-3
|
||||
= sprite_icon('scale', size: 14, css_class: 'gl-mr-2')
|
||||
= project_license_name(project)
|
||||
|
||||
- if !explore_projects_tab? && access&.nonzero?
|
||||
-# haml-lint:disable UnnecessaryStringOutput
|
||||
= ' ' # prevent haml from eating the space between elements
|
||||
.metadata-info.gl-mt-3
|
||||
%span.user-access-role.gl-display-block{ data: { qa_selector: 'user_role_content' } }= localized_project_human_access(access)
|
||||
- if !explore_projects_tab? && access&.nonzero?
|
||||
-# haml-lint:disable UnnecessaryStringOutput
|
||||
= ' ' # prevent haml from eating the space between elements
|
||||
%span.user-access-role.gl-display-block.gl-m-0{ data: { qa_selector: 'user_role_content' } }= Gitlab::Access.human_access(access)
|
||||
|
||||
- if !explore_projects_tab?
|
||||
.metadata-info.gl-mt-3
|
||||
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: project
|
||||
- if !explore_projects_tab?
|
||||
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: project
|
||||
|
||||
- if show_last_commit_as_description
|
||||
.description.d-none.d-sm-block.gl-mr-3
|
||||
= link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
|
||||
- elsif project.description.present?
|
||||
.description.d-none.d-sm-block.gl-mr-3
|
||||
= markdown_field(project, :description)
|
||||
- if show_last_commit_as_description
|
||||
.description.gl-display-none.gl-sm-display-block.gl-overflow-hidden.gl-mr-3.gl-mt-2
|
||||
= link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
|
||||
- elsif project.description.present?
|
||||
.description.gl-display-none.gl-sm-display-block.gl-overflow-hidden.gl-mr-3.gl-mt-2
|
||||
= markdown_field(project, :description)
|
||||
|
||||
- if project.topics.any?
|
||||
.gl-mt-2
|
||||
= render "shared/projects/topics", project: project.present(current_user: current_user)
|
||||
- if project.topics.any?
|
||||
.gl-mt-2
|
||||
= render "shared/projects/topics", project: project.present(current_user: current_user)
|
||||
|
||||
= render_if_exists 'shared/projects/removed', project: project
|
||||
= render_if_exists 'shared/projects/removed', project: project
|
||||
|
||||
.controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0.text-secondary{ class: css_controls_class.join(" ") }
|
||||
.icon-container.d-flex.align-items-center
|
||||
.gl-display-flex.gl-mt-3{ class: "#{css_class} gl-sm-display-none!" }
|
||||
.controls.gl-display-flex.gl-align-items-center
|
||||
- if show_pipeline_status_icon && last_pipeline.present?
|
||||
- pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref)
|
||||
%span.icon-wrapper.pipeline-status
|
||||
%span.icon-wrapper.pipeline-status.gl-mr-5
|
||||
= render 'ci/status/icon', status: last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
|
||||
|
||||
= render_if_exists 'shared/projects/archived', project: project
|
||||
- if stars
|
||||
= link_to project_starrers_path(project),
|
||||
class: "d-flex align-items-center icon-wrapper stars has-tooltip",
|
||||
title: _('Stars'), data: { container: 'body', placement: 'top' } do
|
||||
= sprite_icon('star', size: 14, css_class: 'gl-mr-2')
|
||||
= number_with_delimiter(project.star_count)
|
||||
- if forks
|
||||
= link_to project_forks_path(project),
|
||||
class: "align-items-center icon-wrapper forks has-tooltip",
|
||||
title: _('Forks'), data: { container: 'body', placement: 'top' } do
|
||||
= sprite_icon('fork', size: 14, css_class: 'gl-mr-2')
|
||||
= number_with_delimiter(project.forks_count)
|
||||
- if show_merge_request_count?(disabled: !merge_requests, compact_mode: compact_mode)
|
||||
= link_to project_merge_requests_path(project),
|
||||
class: "d-none d-xl-flex align-items-center icon-wrapper merge-requests has-tooltip",
|
||||
title: _('Merge requests'), data: { container: 'body', placement: 'top' } do
|
||||
= sprite_icon('git-merge', size: 14, css_class: 'gl-mr-2')
|
||||
= number_with_delimiter(project.open_merge_requests_count)
|
||||
- if show_issue_count?(disabled: !issues, compact_mode: compact_mode)
|
||||
= link_to project_issues_path(project),
|
||||
class: "d-none d-xl-flex align-items-center icon-wrapper issues has-tooltip",
|
||||
title: _('Issues'), data: { container: 'body', placement: 'top' } do
|
||||
= sprite_icon('issues', size: 14, css_class: 'gl-mr-2')
|
||||
= number_with_delimiter(project.open_issues_count)
|
||||
.updated-note
|
||||
= link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' } do
|
||||
= sprite_icon('star-o', size: 14, css_class: 'gl-mr-2')
|
||||
= badge_count(project.star_count)
|
||||
.updated-note.gl-ml-3.gl-sm-ml-0
|
||||
%span
|
||||
= _('Updated')
|
||||
= updated_tooltip
|
||||
|
||||
.project-cell{ class: "#{css_class} gl-xs-display-none!" }
|
||||
.project-controls.gl-display-flex.gl-flex-direction-column.gl-w-full{ class: css_controls_container_class, data: { testid: 'project_controls'} }
|
||||
.controls.gl-display-flex.gl-align-items-center{ class: css_controls_class }
|
||||
- if show_pipeline_status_icon && last_pipeline.present?
|
||||
- pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref)
|
||||
%span.icon-wrapper.pipeline-status.gl-mr-5
|
||||
= render 'ci/status/icon', status: last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
|
||||
|
||||
= render_if_exists 'shared/projects/archived', project: project
|
||||
- if stars
|
||||
= link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' } do
|
||||
= sprite_icon('star-o', size: 14, css_class: 'gl-mr-2')
|
||||
= badge_count(project.star_count)
|
||||
- if forks
|
||||
= link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' } do
|
||||
= sprite_icon('fork', size: 14, css_class: 'gl-mr-2')
|
||||
= badge_count(project.forks_count)
|
||||
- if show_merge_request_count?(disabled: !merge_requests, compact_mode: compact_mode)
|
||||
= link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' } do
|
||||
= sprite_icon('git-merge', size: 14, css_class: 'gl-mr-2')
|
||||
= badge_count(project.open_merge_requests_count)
|
||||
- if show_issue_count?(disabled: !issues, compact_mode: compact_mode)
|
||||
= link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' } do
|
||||
= sprite_icon('issues', size: 14, css_class: 'gl-mr-2')
|
||||
= badge_count(project.open_issues_count)
|
||||
.updated-note.gl-white-space-nowrap.gl-justify-content-end
|
||||
%span
|
||||
= _('Updated')
|
||||
= updated_tooltip
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddPurlTypeToSbomComponents < Gitlab::Database::Migration[2.0]
|
||||
def change
|
||||
add_column :sbom_components, :purl_type, :smallint
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveUniqueIndexOnSbomComponentsTypeAndName < Gitlab::Database::Migration[2.0]
|
||||
INDEX_NAME = 'index_sbom_components_on_component_type_and_name'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
remove_concurrent_index_by_name :sbom_components, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :sbom_components, [:component_type, :name], unique: true, name: INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUniqueIndexOnSbomComponentsTypeNameAndPurlType < Gitlab::Database::Migration[2.0]
|
||||
INDEX_NAME = 'index_sbom_components_on_component_type_name_and_purl_type'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :sbom_components, [:name, :purl_type, :component_type], unique: true, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :sbom_components, name: INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
f1f30c3581e35a92f3ede694e1eb70c6fc4dccfdb9e377b5f9046e18eaca2c54
|
|
@ -0,0 +1 @@
|
|||
33bbeaa1d94cfa936de422fcc2f0456d235dde13072f6907cd514a12956ef9aa
|
|
@ -0,0 +1 @@
|
|||
0e985bac7558768e0b97316c1362cb411fed5605c0a313c3872e86f7242f8d36
|
|
@ -21119,6 +21119,7 @@ CREATE TABLE sbom_components (
|
|||
updated_at timestamp with time zone NOT NULL,
|
||||
component_type smallint NOT NULL,
|
||||
name text NOT NULL,
|
||||
purl_type smallint,
|
||||
CONSTRAINT check_91a8f6ad53 CHECK ((char_length(name) <= 255))
|
||||
);
|
||||
|
||||
|
@ -30483,7 +30484,7 @@ CREATE INDEX index_sbom_component_versions_on_component_id ON sbom_component_ver
|
|||
|
||||
CREATE UNIQUE INDEX index_sbom_component_versions_on_component_id_and_version ON sbom_component_versions USING btree (component_id, version);
|
||||
|
||||
CREATE UNIQUE INDEX index_sbom_components_on_component_type_and_name ON sbom_components USING btree (component_type, name);
|
||||
CREATE UNIQUE INDEX index_sbom_components_on_component_type_name_and_purl_type ON sbom_components USING btree (name, purl_type, component_type);
|
||||
|
||||
CREATE INDEX index_sbom_occurrences_on_component_id ON sbom_occurrences USING btree (component_id);
|
||||
|
||||
|
|
|
@ -536,7 +536,7 @@ POST /projects/:id/repository/commits/:sha/comments
|
|||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--form "note=Nice picture man\!" --form "path=dudeism.md" --form "line=11" --form "line_type=new" \
|
||||
--form "note=Nice picture\!" --form "path=README.md" --form "line=11" --form "line_type=new" \
|
||||
"https://gitlab.example.com/api/v4/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments"
|
||||
```
|
||||
|
||||
|
@ -554,9 +554,9 @@ Example response:
|
|||
},
|
||||
"created_at" : "2016-01-19T09:44:55.600Z",
|
||||
"line_type" : "new",
|
||||
"path" : "dudeism.md",
|
||||
"path" : "README.md",
|
||||
"line" : 11,
|
||||
"note" : "Nice picture man!"
|
||||
"note" : "Nice picture!"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -223,7 +223,7 @@ Example response:
|
|||
}],
|
||||
"compare_timeout": false,
|
||||
"compare_same_ref": false,
|
||||
"web_url": "https://gitlab.example.com/thedude/gitlab-foss/-/compare/ae73cb07c9eeaf35924a10f713b364d32b2dd34f...0b4bc9a49b562e85de7cc9e834518ea6828729b9"
|
||||
"web_url": "https://gitlab.example.com/janedoe/gitlab-foss/-/compare/ae73cb07c9eeaf35924a10f713b364d32b2dd34f...0b4bc9a49b562e85de7cc9e834518ea6828729b9"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -289,7 +289,7 @@ contains spaces in its title. Observe how spaces are escaped using the `%20`
|
|||
ASCII code.
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/42/issues?title=Hello%20Dude"
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/42/issues?title=Hello%20GitLab"
|
||||
```
|
||||
|
||||
Use `%2F` for slashes (`/`).
|
||||
|
|
|
@ -18,6 +18,7 @@ In addition to this page, the following resources can help you craft and contrib
|
|||
- [Recommended word list](word_list.md)
|
||||
- [Doc style and consistency testing](../testing.md)
|
||||
- [Guidelines for UI error messages](https://design.gitlab.com/content/error-messages/)
|
||||
- [Documentation global navigation](../site_architecture/global_nav.md)
|
||||
- [GitLab Handbook style guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines)
|
||||
- [Microsoft Style Guide](https://learn.microsoft.com/en-us/style-guide/welcome/)
|
||||
- [Google Developer Documentation Style Guide](https://developers.google.com/style)
|
||||
|
|
|
@ -66,7 +66,7 @@ module API
|
|||
optional :ref, type: String, desc: 'The ref',
|
||||
documentation: { example: 'develop' }
|
||||
optional :target_url, type: String, desc: 'The target URL to associate with this status',
|
||||
documentation: { example: 'https://gitlab.example.com/thedude/gitlab-foss/builds/91' }
|
||||
documentation: { example: 'https://gitlab.example.com/janedoe/gitlab-foss/builds/91' }
|
||||
optional :description, type: String, desc: 'A short description of the status'
|
||||
optional :name, type: String, desc: 'A string label to differentiate this status from the status of other systems',
|
||||
documentation: { example: 'coverage', default: 'default' }
|
||||
|
|
|
@ -10,7 +10,7 @@ module API
|
|||
expose :name, documentation: { type: 'string', example: 'default' }
|
||||
expose :target_url, documentation: {
|
||||
type: 'string',
|
||||
example: 'https://gitlab.example.com/thedude/gitlab-foss/builds/91'
|
||||
example: 'https://gitlab.example.com/janedoe/gitlab-foss/builds/91'
|
||||
}
|
||||
expose :description, documentation: { type: 'string' }
|
||||
expose :created_at, documentation: { type: 'dateTime', example: '2016-01-19T09:05:50.355Z' }
|
||||
|
|
|
@ -61,23 +61,19 @@ module Gitlab
|
|||
end
|
||||
|
||||
def parse_components
|
||||
data['components']&.each do |component_data|
|
||||
type = component_data['type']
|
||||
next unless supported_component_type?(type)
|
||||
|
||||
data['components']&.each_with_index do |component_data, index|
|
||||
component = ::Gitlab::Ci::Reports::Sbom::Component.new(
|
||||
type: type,
|
||||
type: component_data['type'],
|
||||
name: component_data['name'],
|
||||
purl: component_data['purl'],
|
||||
version: component_data['version']
|
||||
)
|
||||
|
||||
report.add_component(component)
|
||||
report.add_component(component) if component.ingestible?
|
||||
rescue ::Sbom::PackageUrl::InvalidPackageURL
|
||||
report.add_error("/components/#{index}/purl is invalid")
|
||||
end
|
||||
end
|
||||
|
||||
def supported_component_type?(type)
|
||||
::Enums::Sbom.component_types.include?(type.to_sym)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,11 +7,34 @@ module Gitlab
|
|||
class Component
|
||||
attr_reader :component_type, :name, :version
|
||||
|
||||
def initialize(type:, name:, version:)
|
||||
def initialize(type:, name:, purl:, version:)
|
||||
@component_type = type
|
||||
@name = name
|
||||
@purl = purl
|
||||
@version = version
|
||||
end
|
||||
|
||||
def ingestible?
|
||||
supported_component_type? && supported_purl_type?
|
||||
end
|
||||
|
||||
def purl
|
||||
return unless @purl
|
||||
|
||||
::Sbom::PackageUrl.parse(@purl)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def supported_component_type?
|
||||
::Enums::Sbom.component_types.include?(component_type.to_sym)
|
||||
end
|
||||
|
||||
def supported_purl_type?
|
||||
return true unless purl
|
||||
|
||||
::Enums::Sbom.purl_types.include?(purl.type.to_sym)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,6 +12,10 @@ module Gitlab
|
|||
@errors = []
|
||||
end
|
||||
|
||||
def valid?
|
||||
errors.empty?
|
||||
end
|
||||
|
||||
def add_error(error)
|
||||
errors << error
|
||||
end
|
||||
|
|
|
@ -90,10 +90,10 @@ module Sbom
|
|||
@subpath = subpath
|
||||
end
|
||||
|
||||
# Creates a new PackageURL from a string.
|
||||
# Creates a new PackageUrl from a string.
|
||||
# @param [String] string The package URL string.
|
||||
# @raise [InvalidPackageURL] If the string is not a valid package URL.
|
||||
# @return [PackageURL]
|
||||
# @return [PackageUrl]
|
||||
def self.parse(string)
|
||||
Decoder.new(string).decode!
|
||||
end
|
||||
|
|
|
@ -95,7 +95,7 @@ module Sbom
|
|||
# - The left side lowercased is the type: `type`
|
||||
# - The right side is the remainder: `namespace/name@version`
|
||||
@type, @string = partition(@string, '/', from: :left)
|
||||
raise InvalidPackageURL, 'invalid or missing package type' if @type.empty?
|
||||
raise InvalidPackageURL, 'invalid or missing package type' if @type.blank?
|
||||
end
|
||||
|
||||
def decode_version!
|
||||
|
@ -123,7 +123,7 @@ module Sbom
|
|||
def decode_namespace!
|
||||
# If there is anything remaining, this is the namespace.
|
||||
# The namespace may contain multiple segments delimited by `/`.
|
||||
@namespace = decode_segments(@string, &:empty?) unless @string.empty?
|
||||
@namespace = decode_segments(@string, &:empty?) if @string.present?
|
||||
end
|
||||
|
||||
def decode_segment(segment)
|
||||
|
|
|
@ -27216,9 +27216,6 @@ msgstr ""
|
|||
msgid "No milestone"
|
||||
msgstr ""
|
||||
|
||||
msgid "No namespace"
|
||||
msgstr ""
|
||||
|
||||
msgid "No other labels with such name or description"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ module QA
|
|||
def self.included(base)
|
||||
super
|
||||
|
||||
base.view "app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue" do
|
||||
base.view "app/assets/javascripts/groups_projects/components/transfer_locations.vue" do
|
||||
element :namespaces_list
|
||||
element :namespaces_list_groups
|
||||
element :namespaces_list_item
|
||||
|
|
|
@ -39,7 +39,34 @@ module Helper
|
|||
|
||||
def ast(source, file: '', version: nil)
|
||||
version ||= ruby_version
|
||||
puts RuboCop::AST::ProcessedSource.new(source, version).ast.to_s
|
||||
|
||||
ast = RuboCop::AST::ProcessedSource.new(source, version).ast
|
||||
return ast if ast
|
||||
|
||||
warn "Syntax error in `#{source}`."
|
||||
end
|
||||
|
||||
def pattern(string)
|
||||
RuboCop::NodePattern.new(string)
|
||||
end
|
||||
|
||||
def help!
|
||||
puts <<~HELP
|
||||
|
||||
Use `ast(source_string, version: nil)` method to parse code and return its AST.
|
||||
Use `pattern(string)` to compile RuboCop's node patterns.
|
||||
|
||||
See https://docs.rubocop.org/rubocop-ast/node_pattern.html.
|
||||
|
||||
Examples:
|
||||
node = ast('puts :hello')
|
||||
|
||||
pat = pattern('`(sym :hello)')
|
||||
pat.match(node) # => true
|
||||
|
||||
HELP
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def ruby_version
|
||||
|
@ -56,11 +83,12 @@ def start_irb
|
|||
|
||||
include Helper # rubocop:disable Style/MixinUsage
|
||||
|
||||
puts "Ruby version: #{ruby_version}"
|
||||
puts
|
||||
puts "Use `ast(source_string, version: nil)` method to parse code and output AST. For example:"
|
||||
puts " ast('puts :hello')"
|
||||
puts
|
||||
puts <<~BANNER
|
||||
Ruby version: #{ruby_version}
|
||||
|
||||
Type `help!` for instructions and examples.
|
||||
|
||||
BANNER
|
||||
|
||||
IRB.start
|
||||
end
|
||||
|
@ -103,12 +131,12 @@ elsif options.interactive
|
|||
start_irb
|
||||
end
|
||||
elsif options.eval
|
||||
Helper.ast(options.eval)
|
||||
puts Helper.ast(options.eval)
|
||||
elsif files.any?
|
||||
files.each do |file|
|
||||
if File.file?(file)
|
||||
source = File.read(file)
|
||||
Helper.ast(source, file: file)
|
||||
puts Helper.ast(source, file: file)
|
||||
else
|
||||
warn "Skipping non-file #{file.inspect}"
|
||||
end
|
||||
|
|
|
@ -3,15 +3,29 @@
|
|||
FactoryBot.define do
|
||||
factory :ci_reports_sbom_component, class: '::Gitlab::Ci::Reports::Sbom::Component' do
|
||||
type { "library" }
|
||||
|
||||
sequence(:name) { |n| "component-#{n}" }
|
||||
sequence(:version) { |n| "v0.0.#{n}" }
|
||||
|
||||
transient do
|
||||
purl_type { 'npm' }
|
||||
end
|
||||
|
||||
purl do
|
||||
::Sbom::PackageUrl.new(
|
||||
type: purl_type,
|
||||
name: name,
|
||||
version: version
|
||||
).to_s
|
||||
end
|
||||
|
||||
skip_create
|
||||
|
||||
initialize_with do
|
||||
::Gitlab::Ci::Reports::Sbom::Component.new(
|
||||
type: type,
|
||||
name: name,
|
||||
purl: purl,
|
||||
version: version
|
||||
)
|
||||
end
|
||||
|
|
|
@ -8,6 +8,12 @@ FactoryBot.define do
|
|||
source { association :ci_reports_sbom_source }
|
||||
end
|
||||
|
||||
trait :invalid do
|
||||
after(:build) do |report, options|
|
||||
report.add_error('This report is invalid because it contains errors.')
|
||||
end
|
||||
end
|
||||
|
||||
after(:build) do |report, options|
|
||||
options.components.each { |component| report.add_component(component) }
|
||||
report.set_source(options.source)
|
||||
|
|
|
@ -151,7 +151,7 @@ RSpec.describe 'Dashboard Projects' do
|
|||
it 'shows that the last pipeline passed' do
|
||||
visit dashboard_projects_path
|
||||
|
||||
page.within('.controls') do
|
||||
page.within('[data-testid="project_controls"]') do
|
||||
expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
|
||||
expect(page).to have_css('.ci-status-link')
|
||||
expect(page).to have_css('.ci-status-icon-success')
|
||||
|
@ -163,7 +163,7 @@ RSpec.describe 'Dashboard Projects' do
|
|||
it 'does not show the pipeline status' do
|
||||
visit dashboard_projects_path
|
||||
|
||||
page.within('.controls') do
|
||||
page.within('[data-testid="project_controls"]') do
|
||||
expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
|
||||
expect(page).not_to have_css('.ci-status-link')
|
||||
expect(page).not_to have_css('.ci-status-icon-success')
|
||||
|
|
|
@ -150,13 +150,15 @@ RSpec.describe 'Edit group settings' do
|
|||
it 'can successfully transfer the group' do
|
||||
visit edit_group_path(selected_group)
|
||||
|
||||
page.within('.js-group-transfer-form') do
|
||||
namespace_select.find('button').click
|
||||
namespace_select.find('.dropdown-menu p', text: target_group_name, match: :first).click
|
||||
|
||||
click_button 'Transfer group'
|
||||
page.within('[data-testid="transfer-locations-dropdown"]') do
|
||||
click_button _('Select parent group')
|
||||
fill_in _('Search'), with: target_group_name
|
||||
wait_for_requests
|
||||
click_button target_group_name
|
||||
end
|
||||
|
||||
click_button s_('GroupSettings|Transfer group')
|
||||
|
||||
page.within(confirm_modal) do
|
||||
expect(page).to have_text "You are going to transfer #{selected_group.name} to another namespace. Are you ABSOLUTELY sure?"
|
||||
|
||||
|
@ -169,16 +171,16 @@ RSpec.describe 'Edit group settings' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'from a subgroup' do
|
||||
context 'when transfering from a subgroup' do
|
||||
let(:selected_group) { create(:group, path: 'foo-subgroup', parent: group) }
|
||||
|
||||
context 'to no parent group' do
|
||||
context 'when transfering to no parent group' do
|
||||
let(:target_group_name) { 'No parent group' }
|
||||
|
||||
it_behaves_like 'can transfer the group'
|
||||
end
|
||||
|
||||
context 'to a different parent group' do
|
||||
context 'when transfering to a parent group' do
|
||||
let(:target_group) { create(:group, path: 'foo-parentgroup') }
|
||||
let(:target_group_name) { target_group.name }
|
||||
|
||||
|
@ -190,14 +192,11 @@ RSpec.describe 'Edit group settings' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'from a root group' do
|
||||
context 'when transfering from a root group to a parent group' do
|
||||
let(:selected_group) { create(:group, path: 'foo-rootgroup') }
|
||||
let(:target_group_name) { group.name }
|
||||
|
||||
context 'to a parent group' do
|
||||
let(:target_group_name) { group.name }
|
||||
|
||||
it_behaves_like 'can transfer the group'
|
||||
end
|
||||
it_behaves_like 'can transfer the group'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import getGroupTransferLocationsResponse from 'test_fixtures/api/groups/transfer_locations.json';
|
||||
import httpStatus from '~/lib/utils/http_status';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { updateGroup } from '~/api/groups_api';
|
||||
import { DEFAULT_PER_PAGE } from '~/api';
|
||||
import { updateGroup, getGroupTransferLocations } from '~/api/groups_api';
|
||||
|
||||
const mockApiVersion = 'v4';
|
||||
const mockUrlRoot = '/gitlab';
|
||||
const mockGroupId = '99';
|
||||
|
||||
describe('GroupsApi', () => {
|
||||
let originalGon;
|
||||
|
@ -27,7 +30,6 @@ describe('GroupsApi', () => {
|
|||
});
|
||||
|
||||
describe('updateGroup', () => {
|
||||
const mockGroupId = '99';
|
||||
const mockData = { attr: 'value' };
|
||||
const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}`;
|
||||
|
||||
|
@ -43,4 +45,25 @@ describe('GroupsApi', () => {
|
|||
expect(res.data).toMatchObject({ id: mockGroupId, ...mockData });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGroupTransferLocations', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(axios, 'get');
|
||||
});
|
||||
|
||||
it('retrieves transfer locations from the correct URL and returns them in the response data', async () => {
|
||||
const params = { page: 1 };
|
||||
const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}/transfer_locations`;
|
||||
|
||||
mock.onGet(expectedUrl).replyOnce(200, { data: getGroupTransferLocationsResponse });
|
||||
|
||||
await expect(getGroupTransferLocations(mockGroupId, params)).resolves.toMatchObject({
|
||||
data: { data: getGroupTransferLocationsResponse },
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(expectedUrl, {
|
||||
params: { ...params, per_page: DEFAULT_PER_PAGE },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,6 +32,26 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
|
|||
end
|
||||
end
|
||||
|
||||
describe API::Groups, type: :request do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
describe 'transfer_locations' do
|
||||
let_it_be(:groups) { create_list(:group, 4) }
|
||||
let_it_be(:transfer_from_group) { create(:group) }
|
||||
|
||||
before_all do
|
||||
groups.each { |group| group.add_owner(user) }
|
||||
transfer_from_group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'api/groups/transfer_locations.json' do
|
||||
get api("/groups/#{transfer_from_group.id}/transfer_locations", user)
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe GraphQL::Query, type: :request do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import { GlAlert, GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import Component from '~/groups/components/transfer_group_form.vue';
|
||||
import TransferLocationsForm, { i18n } from '~/groups/components/transfer_group_form.vue';
|
||||
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
|
||||
import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
|
||||
import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
|
||||
import { getGroupTransferLocations } from '~/api/groups_api';
|
||||
|
||||
jest.mock('~/api/groups_api', () => ({
|
||||
getGroupTransferLocations: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Transfer group form', () => {
|
||||
let wrapper;
|
||||
|
@ -22,25 +27,25 @@ describe('Transfer group form', () => {
|
|||
];
|
||||
|
||||
const defaultProps = {
|
||||
groupNamespaces,
|
||||
paidGroupHelpLink,
|
||||
isPaidGroup: false,
|
||||
confirmationPhrase,
|
||||
confirmButtonText,
|
||||
};
|
||||
|
||||
const createComponent = (propsData = {}) =>
|
||||
shallowMountExtended(Component, {
|
||||
const createComponent = (propsData = {}) => {
|
||||
wrapper = shallowMountExtended(TransferLocationsForm, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...propsData,
|
||||
},
|
||||
stubs: { GlSprintf },
|
||||
});
|
||||
};
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
|
||||
const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect);
|
||||
const findTransferLocations = () => wrapper.findComponent(TransferLocations);
|
||||
const findHiddenInput = () => wrapper.find('[name="new_parent_group_id"]');
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -49,21 +54,17 @@ describe('Transfer group form', () => {
|
|||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders the namespace select component', () => {
|
||||
expect(findNamespaceSelect().exists()).toBe(true);
|
||||
});
|
||||
it('renders the transfer locations dropdown and passes correct props', () => {
|
||||
findTransferLocations().props('groupTransferLocationsApiMethod')();
|
||||
|
||||
it('sets the namespace select properties', () => {
|
||||
expect(findNamespaceSelect().props()).toMatchObject({
|
||||
defaultText: 'Select parent group',
|
||||
fullWidth: false,
|
||||
includeHeaders: false,
|
||||
emptyNamespaceTitle: 'No parent group',
|
||||
includeEmptyNamespace: true,
|
||||
groupNamespaces,
|
||||
expect(getGroupTransferLocations).toHaveBeenCalled();
|
||||
expect(findTransferLocations().props()).toMatchObject({
|
||||
value: null,
|
||||
label: i18n.dropdownLabel,
|
||||
additionalDropdownItems: TransferLocationsForm.additionalDropdownItems,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -90,10 +91,15 @@ describe('Transfer group form', () => {
|
|||
});
|
||||
|
||||
describe('with a selected project', () => {
|
||||
const [firstGroup] = groupNamespaces;
|
||||
const [selectedItem] = groupNamespaces;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
findNamespaceSelect().vm.$emit('select', firstGroup);
|
||||
createComponent();
|
||||
findTransferLocations().vm.$emit('input', selectedItem);
|
||||
});
|
||||
|
||||
it('sets `value` prop on `TransferLocations` component', () => {
|
||||
expect(findTransferLocations().props('value')).toEqual(selectedItem);
|
||||
});
|
||||
|
||||
it('sets the confirm danger disabled property to false', () => {
|
||||
|
@ -102,7 +108,7 @@ describe('Transfer group form', () => {
|
|||
|
||||
it('sets the hidden input field', () => {
|
||||
expect(findHiddenInput().exists()).toBe(true);
|
||||
expect(parseInt(findHiddenInput().attributes('value'), 10)).toBe(firstGroup.id);
|
||||
expect(findHiddenInput().attributes('value')).toBe(String(selectedItem.id));
|
||||
});
|
||||
|
||||
it('emits "confirm" event when the danger modal is confirmed', () => {
|
||||
|
@ -116,15 +122,15 @@ describe('Transfer group form', () => {
|
|||
|
||||
describe('isPaidGroup = true', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ isPaidGroup: true });
|
||||
createComponent({ isPaidGroup: true });
|
||||
});
|
||||
|
||||
it('disables the transfer button', () => {
|
||||
expect(findConfirmDanger().props()).toMatchObject({ disabled: true });
|
||||
});
|
||||
|
||||
it('hides the namespace selector button', () => {
|
||||
expect(findNamespaceSelect().exists()).toBe(false);
|
||||
it('hides the transfer locations dropdown', () => {
|
||||
expect(findTransferLocations().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_
|
|||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { __ } from '~/locale';
|
||||
import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
|
||||
import { getTransferLocations } from '~/api/projects_api';
|
||||
import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
|
||||
|
@ -31,6 +32,10 @@ describe('TransferLocations', () => {
|
|||
groupTransferLocationsApiMethod: getTransferLocations,
|
||||
value: null,
|
||||
};
|
||||
const additionalDropdownItem = {
|
||||
id: -1,
|
||||
humanName: __('No parent group'),
|
||||
};
|
||||
|
||||
// Mock requests
|
||||
const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse);
|
||||
|
@ -93,9 +98,13 @@ describe('TransferLocations', () => {
|
|||
.findByTestId('group-transfer-locations')
|
||||
.findAllComponents(GlDropdownItem)
|
||||
.wrappers.map((dropdownItem) => dropdownItem.text());
|
||||
const findDropdownItemByText = (text) =>
|
||||
wrapper
|
||||
.findAllComponents(GlDropdownItem)
|
||||
.wrappers.find((dropdownItem) => dropdownItem.text() === text);
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
|
||||
const searchEmitInput = () => findSearch().vm.$emit('input', 'foo');
|
||||
const searchEmitInput = (searchTerm = 'foo') => findSearch().vm.$emit('input', searchTerm);
|
||||
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
|
||||
const intersectionObserverEmitAppear = () => findIntersectionObserver().vm.$emit('appear');
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
|
@ -105,6 +114,15 @@ describe('TransferLocations', () => {
|
|||
});
|
||||
|
||||
describe('when `GlDropdown` is opened', () => {
|
||||
it('shows loading icon', async () => {
|
||||
getTransferLocations.mockReturnValueOnce(new Promise(() => {}));
|
||||
createComponent();
|
||||
findDropdown().vm.$emit('show');
|
||||
await nextTick();
|
||||
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('fetches and renders user and group transfer locations', async () => {
|
||||
mockResolvedGetTransferLocations();
|
||||
createComponent();
|
||||
|
@ -118,6 +136,49 @@ describe('TransferLocations', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('when `showUserTransferLocations` prop is `false`', () => {
|
||||
it('does not fetch user transfer locations', async () => {
|
||||
mockResolvedGetTransferLocations();
|
||||
createComponent({
|
||||
propsData: {
|
||||
showUserTransferLocations: false,
|
||||
},
|
||||
});
|
||||
await showDropdown();
|
||||
|
||||
expect(wrapper.findByTestId('user-transfer-locations').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `additionalDropdownItems` prop is passed', () => {
|
||||
it('displays additional dropdown items', async () => {
|
||||
mockResolvedGetTransferLocations();
|
||||
createComponent({
|
||||
propsData: {
|
||||
additionalDropdownItems: [additionalDropdownItem],
|
||||
},
|
||||
});
|
||||
await showDropdown();
|
||||
|
||||
expect(findDropdownItemByText(additionalDropdownItem.humanName).exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when loading', () => {
|
||||
it('does not display additional dropdown items', async () => {
|
||||
getTransferLocations.mockReturnValueOnce(new Promise(() => {}));
|
||||
createComponent({
|
||||
propsData: {
|
||||
additionalDropdownItems: [additionalDropdownItem],
|
||||
},
|
||||
});
|
||||
findDropdown().vm.$emit('show');
|
||||
await nextTick();
|
||||
|
||||
expect(findDropdownItemByText(additionalDropdownItem.humanName)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when transfer locations have already been fetched', () => {
|
||||
beforeEach(async () => {
|
||||
mockResolvedGetTransferLocations();
|
||||
|
@ -187,12 +248,12 @@ describe('TransferLocations', () => {
|
|||
describe('when search is typed in', () => {
|
||||
const transferLocationsResponseSearch = [transferLocationsResponsePage1[0]];
|
||||
|
||||
const arrange = async () => {
|
||||
const arrange = async ({ propsData, searchTerm } = {}) => {
|
||||
mockResolvedGetTransferLocations();
|
||||
createComponent();
|
||||
createComponent({ propsData });
|
||||
await showDropdown();
|
||||
mockResolvedGetTransferLocations({ data: transferLocationsResponseSearch });
|
||||
searchEmitInput();
|
||||
searchEmitInput(searchTerm);
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
|
@ -215,6 +276,29 @@ describe('TransferLocations', () => {
|
|||
transferLocationsResponseSearch.map((transferLocation) => transferLocation.full_name),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not display additional dropdown items if they do not match the search', async () => {
|
||||
await arrange({
|
||||
propsData: {
|
||||
additionalDropdownItems: [additionalDropdownItem],
|
||||
},
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findDropdownItemByText(additionalDropdownItem.humanName)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('displays additional dropdown items if they match the search', async () => {
|
||||
await arrange({
|
||||
propsData: {
|
||||
additionalDropdownItems: [additionalDropdownItem],
|
||||
},
|
||||
searchTerm: 'No par',
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findDropdownItemByText(additionalDropdownItem.humanName).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are no more pages', () => {
|
||||
|
@ -280,4 +364,14 @@ describe('TransferLocations', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `label` prop is passed', () => {
|
||||
it('renders label', () => {
|
||||
const label = 'Foo bar';
|
||||
|
||||
createComponent({ propsData: { label } });
|
||||
|
||||
expect(wrapper.findByRole('group', { name: label }).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
export const groupNamespaces = [
|
||||
{ id: 1, name: 'Group 1', humanName: 'Group 1' },
|
||||
{ id: 2, name: 'Subgroup 1', humanName: 'Group 1 / Subgroup 1' },
|
||||
];
|
||||
|
||||
export const userNamespaces = [{ id: 3, name: 'User namespace 1', humanName: 'User namespace 1' }];
|
|
@ -1,236 +0,0 @@
|
|||
import { nextTick } from 'vue';
|
||||
import {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlSearchBoxByType,
|
||||
GlIntersectionObserver,
|
||||
GlLoadingIcon,
|
||||
} from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import NamespaceSelect, {
|
||||
i18n,
|
||||
EMPTY_NAMESPACE_ID,
|
||||
} from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
|
||||
import { userNamespaces, groupNamespaces } from './mock_data';
|
||||
|
||||
const FLAT_NAMESPACES = [...userNamespaces, ...groupNamespaces];
|
||||
const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST';
|
||||
const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE };
|
||||
|
||||
describe('NamespaceSelectDeprecated', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props = {}) =>
|
||||
shallowMountExtended(NamespaceSelect, {
|
||||
propsData: {
|
||||
userNamespaces,
|
||||
groupNamespaces,
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
// We have to "full" mount GlDropdown so that slot children will render
|
||||
GlDropdown,
|
||||
},
|
||||
});
|
||||
|
||||
const wrappersText = (arr) => arr.wrappers.map((w) => w.text());
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findDropdownText = () => findDropdown().props('text');
|
||||
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
|
||||
const findGroupDropdownItems = () =>
|
||||
wrapper.findByTestId('namespace-list-groups').findAllComponents(GlDropdownItem);
|
||||
const findDropdownItemsTexts = () => findDropdownItems().wrappers.map((x) => x.text());
|
||||
const findSectionHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
|
||||
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
|
||||
const search = (term) => findSearchBox().vm.$emit('input', term);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
it('renders the dropdown', () => {
|
||||
expect(findDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders each dropdown item', () => {
|
||||
expect(findDropdownItemsTexts()).toEqual(FLAT_NAMESPACES.map((x) => x.humanName));
|
||||
});
|
||||
|
||||
it('renders default dropdown text', () => {
|
||||
expect(findDropdownText()).toBe(i18n.DEFAULT_TEXT);
|
||||
});
|
||||
|
||||
it('splits group and user namespaces', () => {
|
||||
const headers = findSectionHeaders();
|
||||
expect(wrappersText(headers)).toEqual([i18n.USERS, i18n.GROUPS]);
|
||||
});
|
||||
|
||||
it('does not render wrapper as full width', () => {
|
||||
expect(findDropdown().attributes('block')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('with defaultText, it overrides dropdown text', () => {
|
||||
const textOverride = 'Select an option';
|
||||
|
||||
wrapper = createComponent({ defaultText: textOverride });
|
||||
|
||||
expect(findDropdownText()).toBe(textOverride);
|
||||
});
|
||||
|
||||
it('with includeHeaders=false, hides group/user headers', () => {
|
||||
wrapper = createComponent({ includeHeaders: false });
|
||||
|
||||
expect(findSectionHeaders()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('with fullWidth=true, sets the dropdown to full width', () => {
|
||||
wrapper = createComponent({ fullWidth: true });
|
||||
|
||||
expect(findDropdown().attributes('block')).toBe('true');
|
||||
});
|
||||
|
||||
describe('with search', () => {
|
||||
it.each`
|
||||
term | includeEmptyNamespace | shouldFilterNamespaces | expectedItems
|
||||
${''} | ${false} | ${true} | ${[...userNamespaces, ...groupNamespaces]}
|
||||
${'sub'} | ${false} | ${true} | ${[groupNamespaces[1]]}
|
||||
${'User'} | ${false} | ${true} | ${[...userNamespaces]}
|
||||
${'User'} | ${true} | ${true} | ${[...userNamespaces]}
|
||||
${'namespace'} | ${true} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]}
|
||||
${'sub'} | ${false} | ${false} | ${[...userNamespaces, ...groupNamespaces]}
|
||||
`(
|
||||
'with term=$term, includeEmptyNamespace=$includeEmptyNamespace, and shouldFilterNamespaces=$shouldFilterNamespaces should show $expectedItems.length',
|
||||
async ({ term, includeEmptyNamespace, shouldFilterNamespaces, expectedItems }) => {
|
||||
wrapper = createComponent({
|
||||
includeEmptyNamespace,
|
||||
emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
|
||||
shouldFilterNamespaces,
|
||||
});
|
||||
|
||||
search(term);
|
||||
|
||||
await nextTick();
|
||||
|
||||
const expected = expectedItems.map((x) => x.humanName);
|
||||
|
||||
expect(findDropdownItemsTexts()).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('when search is typed in', () => {
|
||||
it('emits `search` event', async () => {
|
||||
wrapper = createComponent();
|
||||
|
||||
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.emitted('search')).toEqual([['foo']]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a selected namespace', () => {
|
||||
const selectedGroupIndex = 1;
|
||||
const selectedItem = groupNamespaces[selectedGroupIndex];
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
|
||||
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'foo');
|
||||
findGroupDropdownItems().at(selectedGroupIndex).vm.$emit('click');
|
||||
});
|
||||
|
||||
it('sets the dropdown text', () => {
|
||||
expect(findDropdownText()).toBe(selectedItem.humanName);
|
||||
});
|
||||
|
||||
it('emits the `select` event when a namespace is selected', () => {
|
||||
const args = [selectedItem];
|
||||
expect(wrapper.emitted('select')).toEqual([args]);
|
||||
});
|
||||
|
||||
it('clears search', () => {
|
||||
expect(wrapper.findComponent(GlSearchBoxByType).props('value')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an empty namespace option', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({
|
||||
includeEmptyNamespace: true,
|
||||
emptyNamespaceTitle: EMPTY_NAMESPACE_TITLE,
|
||||
});
|
||||
});
|
||||
|
||||
it('includes the empty namespace', () => {
|
||||
const first = findDropdownItems().at(0);
|
||||
|
||||
expect(first.text()).toBe(EMPTY_NAMESPACE_TITLE);
|
||||
});
|
||||
|
||||
it('emits the `select` event when a namespace is selected', () => {
|
||||
findDropdownItems().at(0).vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('select')).toEqual([[EMPTY_NAMESPACE_ITEM]]);
|
||||
});
|
||||
|
||||
it.each`
|
||||
desc | term | shouldShow
|
||||
${'should hide empty option'} | ${'group'} | ${false}
|
||||
${'should show empty option'} | ${'Empty'} | ${true}
|
||||
`('when search for $term, $desc', async ({ term, shouldShow }) => {
|
||||
search(term);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findDropdownItemsTexts().includes(EMPTY_NAMESPACE_TITLE)).toBe(shouldShow);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `hasNextPageOfGroups` prop is `true`', () => {
|
||||
it('renders `GlIntersectionObserver` and emits `load-more-groups` event when bottom is reached', () => {
|
||||
wrapper = createComponent({ hasNextPageOfGroups: true });
|
||||
|
||||
const intersectionObserver = wrapper.findComponent(GlIntersectionObserver);
|
||||
|
||||
intersectionObserver.vm.$emit('appear');
|
||||
|
||||
expect(intersectionObserver.exists()).toBe(true);
|
||||
expect(wrapper.emitted('load-more-groups')).toEqual([[]]);
|
||||
});
|
||||
|
||||
describe('when `isLoading` prop is `true`', () => {
|
||||
it('renders a loading icon', () => {
|
||||
wrapper = createComponent({ hasNextPageOfGroups: true, isLoading: true });
|
||||
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `isSearchLoading` prop is `true`', () => {
|
||||
it('sets `isLoading` prop to `true`', () => {
|
||||
wrapper = createComponent({ isSearchLoading: true });
|
||||
|
||||
expect(wrapper.findComponent(GlSearchBoxByType).props('isLoading')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dropdown is opened', () => {
|
||||
it('emits `show` event', () => {
|
||||
wrapper = createComponent();
|
||||
|
||||
findDropdown().vm.$emit('show');
|
||||
|
||||
expect(wrapper.emitted('show')).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -287,39 +287,6 @@ RSpec.describe GroupsHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#parent_group_options' do
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:group) { create(:group, name: 'group') }
|
||||
let_it_be(:group2) { create(:group, name: 'group2') }
|
||||
|
||||
before do
|
||||
group.add_owner(current_user)
|
||||
group2.add_owner(current_user)
|
||||
end
|
||||
|
||||
it 'includes explicitly owned groups except self' do
|
||||
expect(parent_group_options(group2)).to eq([{ id: group.id, text: group.human_name }].to_json)
|
||||
end
|
||||
|
||||
it 'excludes parent group' do
|
||||
subgroup = create(:group, parent: group2)
|
||||
|
||||
expect(parent_group_options(subgroup)).to eq([{ id: group.id, text: group.human_name }].to_json)
|
||||
end
|
||||
|
||||
it 'includes subgroups with inherited ownership' do
|
||||
subgroup = create(:group, parent: group)
|
||||
|
||||
expect(parent_group_options(group2)).to eq([{ id: group.id, text: group.human_name }, { id: subgroup.id, text: subgroup.human_name }].to_json)
|
||||
end
|
||||
|
||||
it 'excludes own subgroups' do
|
||||
create(:group, parent: group2)
|
||||
|
||||
expect(parent_group_options(group2)).to eq([{ id: group.id, text: group.human_name }].to_json)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#can_disable_group_emails?' do
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:group) { create(:group, name: 'group') }
|
||||
|
|
|
@ -100,16 +100,53 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx do
|
|||
]
|
||||
end
|
||||
|
||||
before do
|
||||
allow(report).to receive(:add_component)
|
||||
end
|
||||
|
||||
it 'adds each component, ignoring unused attributes' do
|
||||
expect(report).to receive(:add_component)
|
||||
.with(an_object_having_attributes(name: "activesupport", version: "5.1.4", component_type: "library"))
|
||||
.with(
|
||||
an_object_having_attributes(
|
||||
name: "activesupport",
|
||||
version: "5.1.4",
|
||||
component_type: "library",
|
||||
purl: an_object_having_attributes(type: "gem")
|
||||
)
|
||||
)
|
||||
expect(report).to receive(:add_component)
|
||||
.with(an_object_having_attributes(name: "byebug", version: "10.0.0", component_type: "library"))
|
||||
.with(
|
||||
an_object_having_attributes(
|
||||
name: "byebug",
|
||||
version: "10.0.0",
|
||||
component_type: "library",
|
||||
purl: an_object_having_attributes(type: "gem")
|
||||
)
|
||||
)
|
||||
expect(report).to receive(:add_component)
|
||||
.with(an_object_having_attributes(name: "minimal-component", version: nil, component_type: "library"))
|
||||
|
||||
parse!
|
||||
end
|
||||
|
||||
context 'when a component has an invalid purl' do
|
||||
before do
|
||||
components.push(
|
||||
{
|
||||
"name" => "invalid-component",
|
||||
"version" => "v0.0.1",
|
||||
"purl" => "pkg:nil",
|
||||
"type" => "library"
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'adds an error to the report' do
|
||||
expect(report).to receive(:add_error).with("/components/#{components.size - 1}/purl is invalid")
|
||||
|
||||
parse!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when report has metadata properties' do
|
||||
|
|
|
@ -1,23 +1,67 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Reports::Sbom::Component do
|
||||
let(:attributes) do
|
||||
{
|
||||
type: 'library',
|
||||
name: 'component-name',
|
||||
version: 'v0.0.1'
|
||||
}
|
||||
end
|
||||
let(:component_type) { 'library' }
|
||||
let(:name) { 'component-name' }
|
||||
let(:purl_type) { 'npm' }
|
||||
let(:purl) { Sbom::PackageUrl.new(type: purl_type, name: name, version: version).to_s }
|
||||
let(:version) { 'v0.0.1' }
|
||||
|
||||
subject { described_class.new(**attributes) }
|
||||
|
||||
it 'has correct attributes' do
|
||||
expect(subject).to have_attributes(
|
||||
component_type: attributes[:type],
|
||||
name: attributes[:name],
|
||||
version: attributes[:version]
|
||||
subject(:component) do
|
||||
described_class.new(
|
||||
type: component_type,
|
||||
name: name,
|
||||
purl: purl,
|
||||
version: version
|
||||
)
|
||||
end
|
||||
|
||||
it 'has correct attributes' do
|
||||
expect(component).to have_attributes(
|
||||
component_type: component_type,
|
||||
name: name,
|
||||
purl: an_object_having_attributes(type: purl_type),
|
||||
version: version
|
||||
)
|
||||
end
|
||||
|
||||
describe '#ingestible?' do
|
||||
subject { component.ingestible? }
|
||||
|
||||
context 'when component_type is invalid' do
|
||||
let(:component_type) { 'invalid' }
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
|
||||
context 'when purl_type is invalid' do
|
||||
let(:purl_type) { 'invalid' }
|
||||
|
||||
it { is_expected.to be(false) }
|
||||
end
|
||||
|
||||
context 'when component_type is valid' do
|
||||
where(:component_type) { ::Enums::Sbom.component_types.keys.map(&:to_s) }
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be(true) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when purl_type is valid' do
|
||||
where(:purl_type) { ::Enums::Sbom.purl_types.keys.map(&:to_s) }
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be(true) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no purl' do
|
||||
let(:purl) { nil }
|
||||
|
||||
it { is_expected.to be(true) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,6 +5,21 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::Ci::Reports::Sbom::Report do
|
||||
subject(:report) { described_class.new }
|
||||
|
||||
describe '#valid?' do
|
||||
context 'when there are no errors' do
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'when report contains errors' do
|
||||
before do
|
||||
report.add_error('error1')
|
||||
report.add_error('error2')
|
||||
end
|
||||
|
||||
it { is_expected.not_to be_valid }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#add_error' do
|
||||
it 'appends errors to a list' do
|
||||
report.add_error('error1')
|
||||
|
|
|
@ -33,10 +33,12 @@ RSpec.describe Sbom::PackageUrl::Decoder do
|
|||
end
|
||||
|
||||
context 'when an invalid package URL string is passed' do
|
||||
let(:url) { 'invalid' }
|
||||
where(:url) { ['invalid', 'pkg:nil'] }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { decode }.to raise_error(Sbom::PackageUrl::InvalidPackageURL)
|
||||
with_them do
|
||||
it 'raises an error' do
|
||||
expect { decode }.to raise_error(Sbom::PackageUrl::InvalidPackageURL)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue