mirror of
https://github.com/gitlabhq/gitlabhq.git
synced 2025-07-23 02:54:40 +00:00
Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
@ -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'
|
||||
|
@ -68,6 +68,7 @@ export default {
|
||||
placement="right"
|
||||
text-sr-only
|
||||
:items="items"
|
||||
data-testid="artifacts-dropdown"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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, {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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 ""
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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})
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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(() => {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
@ -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(() => {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
Reference in New Issue
Block a user