Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot
2024-02-07 06:06:55 +00:00
parent 253eddd458
commit 680d17e351
27 changed files with 535 additions and 128 deletions

View File

@ -2780,7 +2780,6 @@ Style/InlineDisableAnnotation:
- 'spec/controllers/projects/releases_controller_spec.rb'
- 'spec/controllers/projects/runners_controller_spec.rb'
- 'spec/db/docs_spec.rb'
- 'spec/deprecation_warnings.rb'
- 'spec/factories/design_management/designs.rb'
- 'spec/factories/events.rb'
- 'spec/factories/go_module_commits.rb'

View File

@ -68,6 +68,7 @@ export default {
placement="right"
text-sr-only
:items="items"
data-testid="artifacts-dropdown"
/>
</template>

View File

@ -13,17 +13,19 @@ import {
FILTERED_SEARCH_TERM,
TOKEN_EMPTY_SEARCH_TERM,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
import GroupsView from '../../shared/components/groups_view.vue';
import ProjectsView from '../../shared/components/projects_view.vue';
import { onPageChange } from '../../shared/utils';
import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '~/organizations/constants';
import GroupsView from '~/organizations/shared/components/groups_view.vue';
import ProjectsView from '~/organizations/shared/components/projects_view.vue';
import NewGroupButton from '~/organizations/shared/components/new_group_button.vue';
import NewProjectButton from '~/organizations/shared/components/new_project_button.vue';
import { onPageChange } from '~/organizations/shared/utils';
import {
QUERY_PARAM_END_CURSOR,
QUERY_PARAM_START_CURSOR,
SORT_DIRECTION_ASC,
SORT_DIRECTION_DESC,
SORT_ITEM_NAME,
} from '../../shared/constants';
} from '~/organizations/shared/constants';
import { DISPLAY_LISTBOX_ITEMS, SORT_ITEMS, FILTERED_SEARCH_TERM_KEY } from '../constants';
export default {
@ -32,7 +34,13 @@ export default {
searchInputPlaceholder: s__('Organization|Search or filter list'),
displayListboxHeaderText: __('Display'),
},
components: { FilteredSearchBar, GlCollapsibleListbox, GlSorting },
components: {
FilteredSearchBar,
GlCollapsibleListbox,
GlSorting,
NewGroupButton,
NewProjectButton,
},
filteredSearch: {
tokens: [],
namespace: 'organization_groups_and_projects',
@ -156,7 +164,15 @@ export default {
<template>
<div>
<h1 class="gl-font-size-h-display">{{ $options.i18n.pageTitle }}</h1>
<div
class="page-title-holder gl-display-flex gl-sm-flex-direction-row gl-flex-direction-column gl-sm-align-items-center"
>
<h1 class="page-title gl-font-size-h-display">{{ $options.i18n.pageTitle }}</h1>
<div class="gl-display-flex gl-column-gap-3 gl-sm-ml-auto gl-mb-4 gl-sm-mb-0">
<new-group-button category="secondary" />
<new-project-button />
</div>
</div>
<div class="gl-p-5 gl-bg-gray-10 gl-border-t gl-border-b">
<div class="gl-mx-n2 gl-my-n2 gl-md-display-flex">
<div class="gl-p-2 gl-flex-grow-1">

View File

@ -32,6 +32,9 @@ export const initOrganizationsGroupsAndProjects = () => {
groupsEmptyStateSvgPath,
newGroupPath,
newProjectPath,
canCreateGroup,
canCreateProject,
hasGroups,
} = convertObjectPropsToCamelCase(JSON.parse(appData));
Vue.use(VueRouter);
@ -51,6 +54,9 @@ export const initOrganizationsGroupsAndProjects = () => {
groupsEmptyStateSvgPath,
newGroupPath,
newProjectPath,
canCreateGroup,
canCreateProject,
hasGroups,
},
render(createElement) {
return createElement(App);

View File

@ -7,6 +7,7 @@ import { DEFAULT_PER_PAGE } from '~/api';
import groupsQuery from '../graphql/queries/groups.query.graphql';
import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants';
import { formatGroups } from '../utils';
import NewGroupButton from './new_group_button.vue';
export default {
i18n: {
@ -18,19 +19,14 @@ export default {
description: s__(
'Organization|A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
),
primaryButtonText: __('New group'),
},
prev: __('Prev'),
next: __('Next'),
},
components: { GlLoadingIcon, GlEmptyState, GlKeysetPagination, GroupsList },
components: { GlLoadingIcon, GlEmptyState, GlKeysetPagination, GroupsList, NewGroupButton },
inject: {
organizationGid: {},
groupsEmptyStateSvgPath: {},
newGroupPath: {
default: null,
},
},
props: {
shouldShowEmptyStateButtons: {
@ -143,14 +139,6 @@ export default {
description: this.$options.i18n.emptyState.description,
};
if (this.shouldShowEmptyStateButtons && this.newGroupPath) {
return {
...baseProps,
primaryButtonLink: this.newGroupPath,
primaryButtonText: this.$options.i18n.emptyState.primaryButtonText,
};
}
return baseProps;
},
},
@ -186,5 +174,9 @@ export default {
/>
</div>
</div>
<gl-empty-state v-else v-bind="emptyStateProps" />
<gl-empty-state v-else v-bind="emptyStateProps">
<template v-if="shouldShowEmptyStateButtons" #actions>
<new-group-button />
</template>
</gl-empty-state>
</template>

View File

@ -0,0 +1,32 @@
<script>
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
i18n: {
newGroup: __('New group'),
},
components: {
GlButton,
},
inject: ['canCreateGroup', 'newGroupPath'],
props: {
category: {
type: String,
required: false,
default: 'primary',
},
},
computed: {
showButton() {
return this.canCreateGroup && this.newGroupPath;
},
},
};
</script>
<template>
<gl-button v-if="showButton" :href="newGroupPath" :category="category" variant="confirm">{{
$options.i18n.newGroup
}}</gl-button>
</template>

View File

@ -0,0 +1,44 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
export default {
i18n: {
newProjectButtonDisabledTooltip: s__(
'Organization|Projects are hosted/created in groups. Before creating a project, you must create a group.',
),
newProject: __('New project'),
},
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlButton,
},
inject: ['hasGroups', 'canCreateProject', 'newProjectPath'],
computed: {
showButton() {
return this.canCreateProject && this.newProjectPath;
},
tooltip() {
return this.hasGroups ? null : this.$options.i18n.newProjectButtonDisabledTooltip;
},
},
};
</script>
<template>
<span
v-if="showButton"
v-gl-tooltip
:title="tooltip"
data-testid="new-project-button-tooltip-container"
><gl-button
:href="newProjectPath"
:disabled="!hasGroups"
category="primary"
variant="confirm"
>{{ $options.i18n.newProject }}</gl-button
></span
>
</template>

View File

@ -7,6 +7,7 @@ import { createAlert } from '~/alert';
import { SORT_ITEM_NAME, SORT_DIRECTION_ASC } from '../constants';
import projectsQuery from '../graphql/queries/projects.query.graphql';
import { formatProjects } from '../utils';
import NewProjectButton from './new_project_button.vue';
export default {
i18n: {
@ -18,7 +19,6 @@ export default {
description: s__(
'GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of GitLab.',
),
primaryButtonText: __('New project'),
},
prev: __('Prev'),
next: __('Next'),
@ -28,13 +28,11 @@ export default {
GlLoadingIcon,
GlEmptyState,
GlKeysetPagination,
NewProjectButton,
},
inject: {
organizationGid: {},
projectsEmptyStateSvgPath: {},
newProjectPath: {
default: null,
},
},
props: {
shouldShowEmptyStateButtons: {
@ -149,14 +147,6 @@ export default {
description: this.$options.i18n.emptyState.description,
};
if (this.shouldShowEmptyStateButtons && this.newProjectPath) {
return {
...baseProps,
primaryButtonLink: this.newProjectPath,
primaryButtonText: this.$options.i18n.emptyState.primaryButtonText,
};
}
return baseProps;
},
},
@ -191,5 +181,9 @@ export default {
/>
</div>
</div>
<gl-empty-state v-else v-bind="emptyStateProps" />
<gl-empty-state v-else v-bind="emptyStateProps">
<template v-if="shouldShowEmptyStateButtons" #actions>
<new-project-button />
</template>
</gl-empty-state>
</template>

View File

@ -35,6 +35,9 @@ export const initOrganizationsShow = () => {
newGroupPath,
newProjectPath,
associationCounts,
canCreateProject,
canCreateGroup,
hasGroups,
} = convertObjectPropsToCamelCase(JSON.parse(appData));
Vue.use(VueRouter);
@ -54,6 +57,9 @@ export const initOrganizationsShow = () => {
groupsEmptyStateSvgPath,
newGroupPath,
newProjectPath,
canCreateProject,
canCreateGroup,
hasGroups,
},
render(createElement) {
return createElement(App, {

View File

@ -19,14 +19,6 @@ module JiraConnectHelper
def jira_connect_oauth_data(installation)
oauth_instance_url = installation.oauth_authorization_url
oauth_authorize_path = oauth_authorization_path(
client_id: Gitlab::CurrentSettings.jira_connect_application_key,
response_type: 'code',
scope: 'api',
redirect_uri: jira_connect_oauth_callbacks_url,
state: oauth_state
)
{
oauth_authorize_url: Gitlab::Utils.append_path(oauth_instance_url, oauth_authorize_path),
oauth_token_path: oauth_token_path,
@ -55,4 +47,20 @@ module JiraConnectHelper
unlink_path: jira_connect_subscription_path(subscription)
}
end
def relative_url_root
Gitlab.config.gitlab.relative_url_root.presence
end
def oauth_authorize_path
oauth_authorize_path = oauth_authorization_path(
client_id: Gitlab::CurrentSettings.jira_connect_application_key,
response_type: 'code',
scope: 'api',
redirect_uri: jira_connect_oauth_callbacks_url,
state: oauth_state
)
oauth_authorize_path.delete_prefix(relative_url_root)
end
end

View File

@ -77,7 +77,10 @@ module Organizations
projects_empty_state_svg_path: image_path('illustrations/empty-state/empty-projects-md.svg'),
groups_empty_state_svg_path: image_path('illustrations/empty-state/empty-groups-md.svg'),
new_group_path: new_group_path,
new_project_path: new_project_path
new_project_path: new_project_path,
can_create_group: can?(current_user, :create_group, organization),
can_create_project: current_user&.can_create_project?,
has_groups: has_groups?(organization)
}
end
@ -95,5 +98,9 @@ module Organizations
admin_user: admin_user_path(:id)
}
end
def has_groups?(organization)
organization.groups.exists?
end
end
end

View File

@ -557,3 +557,28 @@ If you find that `fapolicyd` is denying execution, consider the following:
```
1. Restart the service.
## `Pre-receive hook declined` error when pushing to RHEL instance with `fapolicyd` enabled
When pushing to an RHEL-based instance with `fapolicyd` enabled, you might get a `Pre-receive hook declined` error. This error can occur because `fapolicyd` can block the execution
of the Gitaly binary. To resolve this problem, either:
- Disable `fapolicyd`.
- Create an `fapolicyd` rule to permit execution of Gitaly binaries with `fapolicyd` enabled.
To create a rule to allow Gitaly binary execution:
1. Create a file at `/etc/fapolicyd/rules.d/89-gitlab.rules`.
1. Enter the following into the file:
```plaintext
allow perm=any all : ftype=application/x-executable dir=/var/opt/gitlab/gitaly/
```
1. Restart the service:
```shell
systemctl restart fapolicyd
```
The new rule takes effect after the daemon restarts.

View File

@ -127,7 +127,8 @@ GitLab Support with:
1. Your GitLab self-managed instance URL.
1. Your GitLab.com username.
1. If possible, the `X-Request-Id` response header for the failed `GET` request to `https://gitlab.com/-/jira_connect/installations`.
1. Optional. [A HAR file that captured the problem](https://support.zendesk.com/hc/en-us/articles/4408828867098-Generating-a-HAR-file-for-troubleshooting).
1. Optional. [A HAR file that captured the problem](https://support.zendesk.com/hc/en-us/articles/4408828867098-Generating-a-HAR-file-for-troubleshooting) that you have
processed with the [harcleaner](https://gitlab.com/gitlab-com/support/toolbox/harcleaner) utility.
The GitLab Support team can then look up why this is failing in the GitLab.com server logs.

View File

@ -34514,6 +34514,9 @@ msgstr ""
msgid "Organization|Perform advanced options such as deleting the organization."
msgstr ""
msgid "Organization|Projects are hosted/created in groups. Before creating a project, you must create a group."
msgstr ""
msgid "Organization|Public - The organization can be accessed without any authentication."
msgstr ""

View File

@ -63,7 +63,7 @@
"@gitlab/svgs": "3.83.0",
"@gitlab/ui": "^74.0.0",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "^0.0.1-dev-20240201215504",
"@gitlab/web-ide": "^0.0.1-dev-20240206230318",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
"@rails/actioncable": "7.0.8",
"@rails/ujs": "7.0.8",

View File

@ -37,7 +37,7 @@ gem 'chemlab', '~> 0.11', '>= 0.11.1'
gem 'chemlab-library-www-gitlab-com', '~> 0.1', '>= 0.1.1'
# dependencies for jenkins client
gem 'nokogiri', '~> 1.16'
gem 'nokogiri', '~> 1.16', '>= 1.16.2'
gem 'deprecation_toolkit', '~> 2.2.0', require: false

View File

@ -213,7 +213,7 @@ GEM
multi_json (1.15.0)
multi_xml (0.6.0)
netrc (0.11.0)
nokogiri (1.16.0)
nokogiri (1.16.2)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
octokit (8.1.0)
@ -362,7 +362,7 @@ DEPENDENCIES
gitlab_quality-test_tooling (~> 1.11.0)
influxdb-client (~> 3.0)
knapsack (~> 4.0)
nokogiri (~> 1.16)
nokogiri (~> 1.16, >= 1.16.2)
octokit (~> 8.1.0)
parallel (~> 1.24)
parallel_tests (~> 4.4)

View File

@ -96,6 +96,10 @@ module QA
element 'pipeline-container'
end
view 'app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue' do
element 'artifacts-dropdown'
end
view 'app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue' do
element 'apply-suggestion-dropdown'
element 'commit-message-field'
@ -499,7 +503,7 @@ module QA
def click_artifacts_dropdown_button
wait_for_requests
within_element('pipeline-container') do
within_element('artifacts-dropdown') do
click_element('base-dropdown-toggle')
end
end

View File

@ -7,13 +7,11 @@ return if Gitlab::Utils.to_boolean(ENV['SILENCE_DEPRECATIONS'], default: false)
# to developers to ease upgrading to newer Ruby versions.
Warning[:deprecated] = true
# rubocop:disable Layout/LineLength
# rubocop:disable Layout/LineLength -- Avoid multiline (x modifier) Regexp to keep it readable
case RUBY_VERSION[/\d+\.\d+/, 0]
when '3.2'
warn "#{__FILE__}:#{__LINE__}: warning: Ignored warnings for Ruby < 3.2 are no longer necessary."
else
when '3.1'
require 'warning'
# Ignore Ruby warnings until Ruby 3.2.
# These warnings only happen in Ruby 3.1 and are gone in Ruby 3.2.
# ... ruby/3.1.3/lib/ruby/gems/3.1.0/gems/rspec-parameterized-table_syntax-1.0.0/lib/rspec/parameterized/table_syntax.rb:38: warning: Refinement#include is deprecated and will be removed in Ruby 3.2
Warning.ignore(%r{rspec-parameterized-table_syntax-1\.0\.0/lib/rspec/parameterized/table_syntax\.rb:\d+: warning: Refinement#include is deprecated})

View File

@ -2,6 +2,8 @@ import { GlCollapsibleListbox, GlSorting } from '@gitlab/ui';
import App from '~/organizations/groups_and_projects/components/app.vue';
import GroupsView from '~/organizations/shared/components/groups_view.vue';
import ProjectsView from '~/organizations/shared/components/projects_view.vue';
import NewGroupButton from '~/organizations/shared/components/new_group_button.vue';
import NewProjectButton from '~/organizations/shared/components/new_project_button.vue';
import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '~/organizations/constants';
import { SORT_ITEMS } from '~/organizations/groups_and_projects/constants';
import {
@ -35,10 +37,19 @@ describe('GroupsAndProjectsApp', () => {
});
};
const findPageTitle = () => wrapper.findByText('Groups and projects');
const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar);
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findSort = () => wrapper.findComponent(GlSorting);
const findProjectsView = () => wrapper.findComponent(ProjectsView);
const findNewGroupButton = () => wrapper.findComponent(NewGroupButton);
const findNewProjectButton = () => wrapper.findComponent(NewProjectButton);
it('renders page title as Groups and projects', () => {
createComponent();
expect(findPageTitle().exists()).toBe(true);
});
describe.each`
display | expectedComponent | expectedDisplayListboxSelectedProp
@ -101,6 +112,20 @@ describe('GroupsAndProjectsApp', () => {
});
});
describe('actions', () => {
beforeEach(() => {
createComponent();
});
it('renders NewProjectButton', () => {
expect(findNewProjectButton().exists()).toBe(true);
});
it('renders NewGroupButton with correct props', () => {
expect(findNewGroupButton().props()).toStrictEqual({ category: 'secondary' });
});
});
it('renders sort dropdown with sort items and correct props', () => {
createComponent();

View File

@ -3,6 +3,7 @@ import Vue from 'vue';
import { GlEmptyState, GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
import GroupsView from '~/organizations/shared/components/groups_view.vue';
import { SORT_DIRECTION_ASC, SORT_ITEM_NAME } from '~/organizations/shared/constants';
import NewGroupButton from '~/organizations/shared/components/new_group_button.vue';
import { formatGroups } from '~/organizations/shared/utils';
import groupsQuery from '~/organizations/shared/graphql/queries/groups.query.graphql';
import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
@ -67,6 +68,7 @@ describe('GroupsView', () => {
};
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findNewGroupButton = () => wrapper.findComponent(NewGroupButton);
afterEach(() => {
mockApollo = null;
@ -81,51 +83,47 @@ describe('GroupsView', () => {
});
describe('when API call is successful', () => {
describe('when there are no groups', () => {
const emptyHandler = jest.fn().mockResolvedValue({
data: {
organization: {
id: defaultProvide.organizationGid,
groups: {
nodes: [],
pageInfo: pageInfoEmpty,
describe.each`
shouldShowEmptyStateButtons
${false}
${true}
`(
'when there are no groups and `shouldShowEmptyStateButtons` is `$shouldShowEmptyStateButtons`',
({ shouldShowEmptyStateButtons }) => {
const emptyHandler = jest.fn().mockResolvedValue({
data: {
organization: {
id: defaultProvide.organizationGid,
groups: {
nodes: [],
pageInfo: pageInfoEmpty,
},
},
},
},
});
it('renders empty state without buttons by default', async () => {
createComponent({ handler: emptyHandler });
await waitForPromises();
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
title: "You don't have any groups yet.",
description:
'A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
svgHeight: 144,
svgPath: defaultProvide.groupsEmptyStateSvgPath,
primaryButtonLink: null,
primaryButtonText: null,
});
});
describe('when `shouldShowEmptyStateButtons` is `true` and `groupsEmptyStateSvgPath` is set', () => {
it('renders empty state with buttons', async () => {
it(`renders empty state ${
shouldShowEmptyStateButtons ? 'with' : 'without'
} buttons`, async () => {
createComponent({
handler: emptyHandler,
propsData: { shouldShowEmptyStateButtons: true },
propsData: { shouldShowEmptyStateButtons },
});
await waitForPromises();
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
primaryButtonLink: defaultProvide.newGroupPath,
primaryButtonText: 'New group',
title: "You don't have any groups yet.",
description:
'A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
svgHeight: 144,
svgPath: defaultProvide.groupsEmptyStateSvgPath,
});
expect(findNewGroupButton().exists()).toBe(shouldShowEmptyStateButtons);
});
});
});
},
);
describe('when there are groups', () => {
beforeEach(() => {

View File

@ -0,0 +1,80 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import NewGroupButton from '~/organizations/shared/components/new_group_button.vue';
describe('NewGroupButton', () => {
let wrapper;
const defaultProvide = {
canCreateGroup: false,
newGroupPath: '',
};
const defaultProps = {
category: 'primary',
};
function createComponent({ provide = {}, props = {} } = {}) {
wrapper = shallowMount(NewGroupButton, {
provide: {
...defaultProvide,
...provide,
},
propsData: {
...defaultProps,
...props,
},
});
}
const findGlButton = () => wrapper.findComponent(GlButton);
describe.each`
canCreateGroup | newGroupPath
${false} | ${null}
${false} | ${'/asdf'}
${true} | ${null}
`(
'when `canCreateGroup` is $canCreateGroup and `newGroupPath` is $newGroupPath',
({ canCreateGroup, newGroupPath }) => {
beforeEach(() => {
createComponent({ provide: { canCreateGroup, newGroupPath } });
});
it('renders nothing', () => {
expect(wrapper.html()).toBe('');
});
},
);
describe('when `canCreateGroup` is true and `newGroupPath` is /asdf', () => {
const newGroupPath = '/asdf';
describe('with no category', () => {
beforeEach(() => {
createComponent({
provide: { canCreateGroup: true, newGroupPath },
props: { category: undefined },
});
});
it('renders GlButton correctly', () => {
expect(findGlButton().attributes('href')).toBe(newGroupPath);
expect(findGlButton().props('category')).toBe(defaultProps.category);
});
});
describe('with set category', () => {
const category = 'secondary';
beforeEach(() => {
createComponent({ provide: { canCreateGroup: true, newGroupPath }, props: { category } });
});
it('renders GlButton correctly', () => {
expect(findGlButton().attributes('href')).toBe(newGroupPath);
expect(findGlButton().props('category')).toBe(category);
});
});
});
});

View File

@ -0,0 +1,77 @@
import { GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NewProjectButton from '~/organizations/shared/components/new_project_button.vue';
describe('NewProjectButton', () => {
let wrapper;
const defaultProvide = {
canCreateProject: false,
newProjectPath: '',
hasGroups: false,
};
function createComponent({ provide = {} } = {}) {
wrapper = shallowMountExtended(NewProjectButton, {
provide: {
...defaultProvide,
...provide,
},
});
}
const findTooltipContainer = () => wrapper.findByTestId('new-project-button-tooltip-container');
const findGlButton = () => wrapper.findComponent(GlButton);
describe.each`
canCreateProject | newProjectPath
${false} | ${null}
${false} | ${'/asdf'}
${true} | ${null}
`(
'when `canCreateProject` is $canCreateProject and `newProjectPath` is $newProjectPath',
({ canCreateProject, newProjectPath }) => {
beforeEach(() => {
createComponent({ provide: { canCreateProject, newProjectPath } });
});
it('renders nothing', () => {
expect(wrapper.html()).toBe('');
});
},
);
describe('when `canCreateProject` is true and `newProjectPath` is /asdf', () => {
const newProjectPath = '/asdf';
beforeEach(() => {
createComponent({ provide: { canCreateProject: true, newProjectPath } });
});
it('renders GlButton correctly', () => {
expect(findGlButton().attributes('href')).toBe(newProjectPath);
});
});
describe.each`
hasGroups | disabled | tooltip
${false} | ${'true'} | ${'Projects are hosted/created in groups. Before creating a project, you must create a group.'}
${true} | ${undefined} | ${undefined}
`(
'when `canCreateProject` is true , `newProjectPath` is /asdf, and hasGroups is $hasGroups',
({ hasGroups, disabled, tooltip }) => {
beforeEach(() => {
createComponent({
provide: { canCreateProject: true, newProjectPath: '/asdf', hasGroups },
});
});
it(`renders GlButton as ${disabled ? 'disabled' : 'not disabled'} with ${
tooltip ? 'tooltip' : 'no tooltip'
}`, () => {
expect(findGlButton().attributes('disabled')).toBe(disabled);
expect(findTooltipContainer().attributes('title')).toBe(tooltip);
});
},
);
});

View File

@ -2,6 +2,7 @@ import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { GlLoadingIcon, GlEmptyState, GlKeysetPagination } from '@gitlab/ui';
import ProjectsView from '~/organizations/shared/components/projects_view.vue';
import NewProjectButton from '~/organizations/shared/components/new_project_button.vue';
import projectsQuery from '~/organizations/shared/graphql/queries/projects.query.graphql';
import { formatProjects } from '~/organizations/shared/utils';
import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
@ -66,6 +67,7 @@ describe('ProjectsView', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findProjectsList = () => wrapper.findComponent(ProjectsList);
const findNewProjectButton = () => wrapper.findComponent(NewProjectButton);
afterEach(() => {
mockApollo = null;
@ -80,51 +82,47 @@ describe('ProjectsView', () => {
});
describe('when API call is successful', () => {
describe('when there are no projects', () => {
const emptyHandler = jest.fn().mockResolvedValue({
data: {
organization: {
id: defaultProvide.organizationGid,
projects: {
nodes: [],
pageInfo: pageInfoEmpty,
describe.each`
shouldShowEmptyStateButtons
${false}
${true}
`(
'when there are no projects and `shouldShowEmptyStateButtons` is `$shouldShowEmptyStateButtons`',
({ shouldShowEmptyStateButtons }) => {
const emptyHandler = jest.fn().mockResolvedValue({
data: {
organization: {
id: defaultProvide.organizationGid,
projects: {
nodes: [],
pageInfo: pageInfoEmpty,
},
},
},
},
});
it('renders empty state without buttons by default', async () => {
createComponent({ handler: emptyHandler });
await waitForPromises();
expect(findEmptyState().props()).toMatchObject({
title: "You don't have any projects yet.",
description:
'Projects are where you can store your code, access issues, wiki, and other features of GitLab.',
svgHeight: 144,
svgPath: defaultProvide.projectsEmptyStateSvgPath,
primaryButtonLink: null,
primaryButtonText: null,
});
});
describe('when `shouldShowEmptyStateButtons` is `true` and `projectsEmptyStateSvgPath` is set', () => {
it('renders empty state with buttons', async () => {
it(`renders empty state ${
shouldShowEmptyStateButtons ? 'with' : 'without'
} buttons`, async () => {
createComponent({
handler: emptyHandler,
propsData: { shouldShowEmptyStateButtons: true },
propsData: { shouldShowEmptyStateButtons },
});
await waitForPromises();
expect(findEmptyState().props()).toMatchObject({
primaryButtonLink: defaultProvide.newProjectPath,
primaryButtonText: 'New project',
title: "You don't have any projects yet.",
description:
'Projects are where you can store your code, access issues, wiki, and other features of GitLab.',
svgHeight: 144,
svgPath: defaultProvide.projectsEmptyStateSvgPath,
});
expect(findNewProjectButton().exists()).toBe(shouldShowEmptyStateButtons);
});
});
});
},
);
describe('when there are projects', () => {
beforeEach(() => {

View File

@ -72,6 +72,22 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do
oauth_token_path: '/oauth/token'
)
end
context 'with relative_url_root' do
let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://gitlab.example.com/gitlab') }
before do
stub_config_setting(relative_url_root: '/gitlab')
allow(Rails.application.routes).to receive(:default_url_options).and_return(script_name: '/gitlab')
end
it 'points urls to the self-managed instance' do
expect(parsed_oauth_metadata).to include(
oauth_authorize_url: start_with('https://gitlab.example.com/gitlab/oauth/authorize?'),
oauth_token_path: '/gitlab/oauth/token'
)
end
end
end
end

View File

@ -3,6 +3,9 @@
require 'spec_helper'
RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
include Devise::Test::ControllerHelpers
let_it_be(:user) { build_stubbed(:user) }
let_it_be(:organization_detail) { build_stubbed(:organization_detail, description_html: '<em>description</em>') }
let_it_be(:organization) { organization_detail.organization }
let_it_be(:organization_gid) { 'gid://gitlab/Organizations::Organization/1' }
@ -27,6 +30,31 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
allow(helper).to receive(:image_path).with(groups_empty_state_svg_path).and_return(groups_empty_state_svg_path)
allow(helper).to receive(:image_path).with(projects_empty_state_svg_path).and_return(projects_empty_state_svg_path)
allow(helper).to receive(:preview_markdown_organizations_path).and_return(preview_markdown_organizations_path)
allow(helper).to receive(:current_user).and_return(user)
end
shared_examples 'includes that the user can create a group' do |method|
it 'returns expected json' do
expect(
Gitlab::Json.parse(helper.send(method, organization))
).to include('can_create_group' => true)
end
end
shared_examples 'includes that the user can create a project' do |method|
it 'returns expected json' do
expect(
Gitlab::Json.parse(helper.send(method, organization))
).to include('can_create_project' => true)
end
end
shared_examples 'includes that the organization has groups' do |method|
it 'returns expected json' do
expect(
Gitlab::Json.parse(helper.send(method, organization))
).to include('has_groups' => true)
end
end
describe '#organization_layout_nav' do
@ -68,13 +96,38 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
.and_return(groups_and_projects_organization_path)
end
it 'returns expected json' do
context 'when the user can create a group' do
before do
allow(helper).to receive(:can?).with(user, :create_group, organization).and_return(true)
end
include_examples 'includes that the user can create a group', 'organization_show_app_data'
end
context 'when the user can create a project' do
before do
allow(user).to receive(:can_create_project?).and_return(true)
end
include_examples 'includes that the user can create a project', 'organization_show_app_data'
end
context 'when the organization has groups' do
before do
allow(helper).to receive(:has_groups?).and_return(true)
end
include_examples 'includes that the organization has groups', 'organization_show_app_data'
end
it "includes all other non-conditional data" do
expect(organization).to receive(:avatar_url).with(size: 128).and_return('avatar.jpg')
expect(
Gitlab::Json.parse(
helper.organization_show_app_data(organization)
)
).to eq(
).to include(
{
'organization_gid' => organization_gid,
'organization' => {
@ -99,12 +152,36 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
end
describe '#organization_groups_and_projects_app_data' do
it 'returns expected json' do
context 'when the user can create a group' do
before do
allow(helper).to receive(:can?).with(user, :create_group, organization).and_return(true)
end
include_examples 'includes that the user can create a group', 'organization_groups_and_projects_app_data'
end
context 'when the user can create a project' do
before do
allow(user).to receive(:can_create_project?).and_return(true)
end
include_examples 'includes that the user can create a project', 'organization_groups_and_projects_app_data'
end
context 'when the organization has groups' do
before do
allow(helper).to receive(:has_groups?).and_return(true)
end
include_examples 'includes that the organization has groups', 'organization_groups_and_projects_app_data'
end
it "includes all other non-conditional data" do
expect(
Gitlab::Json.parse(
helper.organization_groups_and_projects_app_data(organization)
)
).to eq(
).to include(
{
'organization_gid' => organization_gid,
'new_group_path' => new_group_path,

View File

@ -1339,10 +1339,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235"
integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g==
"@gitlab/web-ide@^0.0.1-dev-20240201215504":
version "0.0.1-dev-20240201215504"
resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20240201215504.tgz#e1c7fd4c2be48e956a6c54dd752541a7ccb5bf3b"
integrity sha512-nw5BApPI6l6F4CKiLM0RRbXvM1QACxFZc15ZJh4/R+8FaZ3pVQ1CXc/CVvLwrjJVphO5rfm98A7FV7Cjn1zwhA==
"@gitlab/web-ide@^0.0.1-dev-20240206230318":
version "0.0.1-dev-20240206230318"
resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20240206230318.tgz#3dd52302fa521550f1f3b8f98e6041ef35ac1ff0"
integrity sha512-/R1+NA99j2Ns5/WwP1jx6jRkEt0QLdcZ05Fvylrci1NSIXa7andWyIweCY2NpERBJrwXXFZ2MBKf8hEULI/roQ==
"@graphql-eslint/eslint-plugin@3.20.1":
version "3.20.1"