Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1abf48c10c
commit
cc803c04b8
27 changed files with 581 additions and 199 deletions
|
@ -1,11 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButton } from '@gitlab/ui';
|
import { GlButton, GlLink } from '@gitlab/ui';
|
||||||
import ExperimentTracking from '~/experimentation/experiment_tracking';
|
import ExperimentTracking from '~/experimentation/experiment_tracking';
|
||||||
import { s__ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
import eventHub from '../event_hub';
|
import eventHub from '../event_hub';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { GlButton },
|
components: { GlButton, GlLink },
|
||||||
props: {
|
props: {
|
||||||
displayText: {
|
displayText: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -37,6 +37,42 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
triggerElement: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'button',
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isButton() {
|
||||||
|
return this.triggerElement === 'button';
|
||||||
|
},
|
||||||
|
componentAttributes() {
|
||||||
|
const baseAttributes = {
|
||||||
|
class: this.classes,
|
||||||
|
'data-qa-selector': 'invite_members_button',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.event && this.label) {
|
||||||
|
return {
|
||||||
|
...baseAttributes,
|
||||||
|
'data-track-event': this.event,
|
||||||
|
'data-track-label': this.label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseAttributes;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.trackExperimentOnShow();
|
this.trackExperimentOnShow();
|
||||||
|
@ -57,12 +93,15 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<gl-button
|
<gl-button
|
||||||
:class="classes"
|
v-if="isButton"
|
||||||
:icon="icon"
|
v-bind="componentAttributes"
|
||||||
:variant="variant"
|
:variant="variant"
|
||||||
data-qa-selector="invite_members_button"
|
:icon="icon"
|
||||||
@click="openModal"
|
@click="openModal"
|
||||||
>
|
>
|
||||||
{{ displayText }}
|
{{ displayText }}
|
||||||
</gl-button>
|
</gl-button>
|
||||||
|
<gl-link v-else v-bind="componentAttributes" data-is-link="true" @click="openModal">
|
||||||
|
{{ displayText }}
|
||||||
|
</gl-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { mapState, mapActions } from 'vuex';
|
import { mapState, mapActions } from 'vuex';
|
||||||
import { __, s__ } from '~/locale';
|
import { __, s__ } from '~/locale';
|
||||||
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
||||||
|
import UrlSync from '~/vue_shared/components/url_sync.vue';
|
||||||
import getTableHeaders from '../utils';
|
import getTableHeaders from '../utils';
|
||||||
import PackageTypeToken from './tokens/package_type_token.vue';
|
import PackageTypeToken from './tokens/package_type_token.vue';
|
||||||
|
|
||||||
|
@ -16,7 +17,7 @@ export default {
|
||||||
operators: [{ value: '=', description: __('is'), default: 'true' }],
|
operators: [{ value: '=', description: __('is'), default: 'true' }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
components: { RegistrySearch },
|
components: { RegistrySearch, UrlSync },
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
isGroupPage: (state) => state.config.isGroupPage,
|
isGroupPage: (state) => state.config.isGroupPage,
|
||||||
|
@ -38,13 +39,18 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<registry-search
|
<url-sync>
|
||||||
:filter="filter"
|
<template #default="{ updateQuery }">
|
||||||
:sorting="sorting"
|
<registry-search
|
||||||
:tokens="$options.tokens"
|
:filter="filter"
|
||||||
:sortable-fields="sortableFields"
|
:sorting="sorting"
|
||||||
@sorting:changed="updateSorting"
|
:tokens="$options.tokens"
|
||||||
@filter:changed="setFilter"
|
:sortable-fields="sortableFields"
|
||||||
@filter:submit="$emit('update')"
|
@sorting:changed="updateSorting"
|
||||||
/>
|
@filter:changed="setFilter"
|
||||||
|
@filter:submit="$emit('update')"
|
||||||
|
@query:changed="updateQuery"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</url-sync>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { historyReplaceState } from '~/lib/utils/common_utils';
|
||||||
import { s__ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
|
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
|
||||||
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
||||||
|
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
|
||||||
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
|
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
|
||||||
import PackageSearch from './package_search.vue';
|
import PackageSearch from './package_search.vue';
|
||||||
import PackageTitle from './package_title.vue';
|
import PackageTitle from './package_title.vue';
|
||||||
|
@ -42,11 +43,21 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
const queryParams = getQueryParams(window.document.location.search);
|
||||||
|
const { sorting, filters } = extractFilterAndSorting(queryParams);
|
||||||
|
this.setSorting(sorting);
|
||||||
|
this.setFilter(filters);
|
||||||
this.requestPackagesList();
|
this.requestPackagesList();
|
||||||
this.checkDeleteAlert();
|
this.checkDeleteAlert();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['requestPackagesList', 'requestDeletePackage', 'setSelectedType']),
|
...mapActions([
|
||||||
|
'requestPackagesList',
|
||||||
|
'requestDeletePackage',
|
||||||
|
'setSelectedType',
|
||||||
|
'setSorting',
|
||||||
|
'setFilter',
|
||||||
|
]),
|
||||||
onPageChanged(page) {
|
onPageChanged(page) {
|
||||||
return this.requestPackagesList({ page });
|
return this.requestPackagesList({ page });
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,3 +7,23 @@ export const keyValueToFilterToken = (type, data) => ({ type, value: { data } })
|
||||||
|
|
||||||
export const searchArrayToFilterTokens = (search) =>
|
export const searchArrayToFilterTokens = (search) =>
|
||||||
search.map((s) => keyValueToFilterToken(FILTERED_SEARCH_TERM, s));
|
search.map((s) => keyValueToFilterToken(FILTERED_SEARCH_TERM, s));
|
||||||
|
|
||||||
|
export const extractFilterAndSorting = (queryObject) => {
|
||||||
|
const { type, search, sort, orderBy } = queryObject;
|
||||||
|
const filters = [];
|
||||||
|
const sorting = {};
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
filters.push(keyValueToFilterToken('type', type));
|
||||||
|
}
|
||||||
|
if (search) {
|
||||||
|
filters.push(...searchArrayToFilterTokens(search));
|
||||||
|
}
|
||||||
|
if (sort) {
|
||||||
|
sorting.sort = sort;
|
||||||
|
}
|
||||||
|
if (orderBy) {
|
||||||
|
sorting.orderBy = orderBy;
|
||||||
|
}
|
||||||
|
return { filters, sorting };
|
||||||
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ import initIssuableSidebar from '~/init_issuable_sidebar';
|
||||||
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
|
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
|
||||||
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
|
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
|
||||||
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
||||||
|
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
|
||||||
import { IssuableType } from '~/issuable_show/constants';
|
import { IssuableType } from '~/issuable_show/constants';
|
||||||
import Issue from '~/issue';
|
import Issue from '~/issue';
|
||||||
import '~/notes/index';
|
import '~/notes/index';
|
||||||
|
@ -36,6 +37,7 @@ export default function initShowIssue() {
|
||||||
initSentryErrorStackTraceApp();
|
initSentryErrorStackTraceApp();
|
||||||
initRelatedMergeRequestsApp();
|
initRelatedMergeRequestsApp();
|
||||||
initInviteMembersModal();
|
initInviteMembersModal();
|
||||||
|
initInviteMembersTrigger();
|
||||||
|
|
||||||
import(/* webpackChunkName: 'design_management' */ '~/design_management')
|
import(/* webpackChunkName: 'design_management' */ '~/design_management')
|
||||||
.then((module) => module.default())
|
.then((module) => module.default())
|
||||||
|
|
|
@ -6,6 +6,7 @@ import initIssuableSidebar from '~/init_issuable_sidebar';
|
||||||
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
|
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
|
||||||
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
|
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
|
||||||
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
||||||
|
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
|
||||||
import { handleLocationHash } from '~/lib/utils/common_utils';
|
import { handleLocationHash } from '~/lib/utils/common_utils';
|
||||||
import StatusBox from '~/merge_request/components/status_box.vue';
|
import StatusBox from '~/merge_request/components/status_box.vue';
|
||||||
import initSourcegraph from '~/sourcegraph';
|
import initSourcegraph from '~/sourcegraph';
|
||||||
|
@ -22,6 +23,7 @@ export default function initMergeRequestShow() {
|
||||||
initInviteMemberModal();
|
initInviteMemberModal();
|
||||||
initInviteMemberTrigger();
|
initInviteMemberTrigger();
|
||||||
initInviteMembersModal();
|
initInviteMembersModal();
|
||||||
|
initInviteMembersTrigger();
|
||||||
|
|
||||||
const el = document.querySelector('.js-mr-status-box');
|
const el = document.querySelector('.js-mr-status-box');
|
||||||
// eslint-disable-next-line no-new
|
// eslint-disable-next-line no-new
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { get } from 'lodash';
|
||||||
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
|
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
||||||
|
import { extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
|
||||||
import Tracking from '~/tracking';
|
import Tracking from '~/tracking';
|
||||||
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
||||||
import DeleteImage from '../components/delete_image.vue';
|
import DeleteImage from '../components/delete_image.vue';
|
||||||
|
@ -82,6 +83,9 @@ export default {
|
||||||
searchConfig: SORT_FIELDS,
|
searchConfig: SORT_FIELDS,
|
||||||
apollo: {
|
apollo: {
|
||||||
baseImages: {
|
baseImages: {
|
||||||
|
skip() {
|
||||||
|
return !this.fetchBaseQuery;
|
||||||
|
},
|
||||||
query: getContainerRepositoriesQuery,
|
query: getContainerRepositoriesQuery,
|
||||||
variables() {
|
variables() {
|
||||||
return this.queryVariables;
|
return this.queryVariables;
|
||||||
|
@ -125,15 +129,19 @@ export default {
|
||||||
sorting: { orderBy: 'UPDATED', sort: 'desc' },
|
sorting: { orderBy: 'UPDATED', sort: 'desc' },
|
||||||
name: null,
|
name: null,
|
||||||
mutationLoading: false,
|
mutationLoading: false,
|
||||||
|
fetchBaseQuery: false,
|
||||||
fetchAdditionalDetails: false,
|
fetchAdditionalDetails: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
images() {
|
images() {
|
||||||
return this.baseImages.map((image, index) => ({
|
if (this.baseImages) {
|
||||||
...image,
|
return this.baseImages.map((image, index) => ({
|
||||||
...get(this.additionalDetails, index, {}),
|
...image,
|
||||||
}));
|
...get(this.additionalDetails, index, {}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
},
|
},
|
||||||
graphqlResource() {
|
graphqlResource() {
|
||||||
return this.config.isGroupPage ? 'group' : 'project';
|
return this.config.isGroupPage ? 'group' : 'project';
|
||||||
|
@ -172,8 +180,15 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
const { sorting, filters } = extractFilterAndSorting(this.$route.query);
|
||||||
|
|
||||||
|
this.filter = [...filters];
|
||||||
|
this.name = filters[0]?.value.data;
|
||||||
|
this.sorting = { ...this.sorting, ...sorting };
|
||||||
|
|
||||||
// If the two graphql calls - which are not batched - resolve togheter we will have a race
|
// If the two graphql calls - which are not batched - resolve togheter we will have a race
|
||||||
// condition when apollo sets the cache, with this we give the 'base' call an headstart
|
// condition when apollo sets the cache, with this we give the 'base' call an headstart
|
||||||
|
this.fetchBaseQuery = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.fetchAdditionalDetails = true;
|
this.fetchAdditionalDetails = true;
|
||||||
}, 200);
|
}, 200);
|
||||||
|
@ -245,6 +260,9 @@ export default {
|
||||||
const search = this.filter.find((i) => i.type === FILTERED_SEARCH_TERM);
|
const search = this.filter.find((i) => i.type === FILTERED_SEARCH_TERM);
|
||||||
this.name = search?.value?.data;
|
this.name = search?.value?.data;
|
||||||
},
|
},
|
||||||
|
updateUrlQueryString(query) {
|
||||||
|
this.$router.push({ query });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -304,6 +322,7 @@ export default {
|
||||||
@sorting:changed="updateSorting"
|
@sorting:changed="updateSorting"
|
||||||
@filter:changed="filter = $event"
|
@filter:changed="filter = $event"
|
||||||
@filter:submit="doFilter"
|
@filter:submit="doFilter"
|
||||||
|
@query:changed="updateUrlQueryString"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="isLoading" class="gl-mt-5">
|
<div v-if="isLoading" class="gl-mt-5">
|
||||||
|
|
|
@ -53,10 +53,10 @@
|
||||||
%ul.dropdown-footer-list
|
%ul.dropdown-footer-list
|
||||||
%li
|
%li
|
||||||
- if directly_invite_members?
|
- if directly_invite_members?
|
||||||
= link_to invite_text,
|
.js-invite-members-trigger{ data: { trigger_element: 'anchor',
|
||||||
project_project_members_path(@project),
|
display_text: invite_text,
|
||||||
title: invite_text,
|
event: 'click_invite_members',
|
||||||
data: { 'is-link': true, 'track-event': 'click_invite_members', 'track-label': track_label }
|
label: track_label } }
|
||||||
- else
|
- else
|
||||||
.js-invite-member-trigger{ data: { display_text: invite_text, event: 'click_invite_members_version_b', label: track_label } }
|
.js-invite-member-trigger{ data: { display_text: invite_text, event: 'click_invite_members_version_b', label: track_label } }
|
||||||
- else
|
- else
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Change assignee dropdown invite to utilize invite modal
|
||||||
|
merge_request: 57002
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Connect Registries searches to URL
|
||||||
|
merge_request: 57251
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -6,8 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
|
|
||||||
# PostgreSQL replication and failover with Omnibus GitLab **(PREMIUM SELF)**
|
# PostgreSQL replication and failover with Omnibus GitLab **(PREMIUM SELF)**
|
||||||
|
|
||||||
This document focuses on configuration supported with [GitLab Premium](https://about.gitlab.com/pricing/), using the Omnibus GitLab package.
|
If you're a Free user of GitLab self-managed, consider using a cloud-hosted solution.
|
||||||
If you're a Community Edition or Starter user, consider using a cloud hosted solution.
|
|
||||||
This document doesn't cover installations from source.
|
This document doesn't cover installations from source.
|
||||||
|
|
||||||
If a setup with replication and failover isn't what you were looking for, see
|
If a setup with replication and failover isn't what you were looking for, see
|
||||||
|
|
|
@ -215,7 +215,7 @@ Example response:
|
||||||
|
|
||||||
## Update an issue board
|
## Update an issue board
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5954) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.1.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5954) in GitLab 11.1.
|
||||||
|
|
||||||
Updates a project issue board.
|
Updates a project issue board.
|
||||||
|
|
||||||
|
@ -228,10 +228,10 @@ PUT /projects/:id/boards/:board_id
|
||||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||||
| `board_id` | integer | yes | The ID of a board |
|
| `board_id` | integer | yes | The ID of a board |
|
||||||
| `name` | string | no | The new name of the board |
|
| `name` | string | no | The new name of the board |
|
||||||
| `assignee_id` **(STARTER)** | integer | no | The assignee the board should be scoped to |
|
| `assignee_id` **(PREMIUM)** | integer | no | The assignee the board should be scoped to |
|
||||||
| `milestone_id` **(STARTER)** | integer | no | The milestone the board should be scoped to |
|
| `milestone_id` **(PREMIUM)** | integer | no | The milestone the board should be scoped to |
|
||||||
| `labels` **(STARTER)** | string | no | Comma-separated list of label names which the board should be scoped to |
|
| `labels` **(PREMIUM)** | string | no | Comma-separated list of label names which the board should be scoped to |
|
||||||
| `weight` **(STARTER)** | integer | no | The weight range from 0 to 9, to which the board should be scoped to |
|
| `weight` **(PREMIUM)** | integer | no | The weight range from 0 to 9, to which the board should be scoped to |
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/boards/1?name=new_name&milestone_id=43&assignee_id=1&labels=Doing&weight=4"
|
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/boards/1?name=new_name&milestone_id=43&assignee_id=1&labels=Doing&weight=4"
|
||||||
|
|
|
@ -183,20 +183,52 @@ file.
|
||||||
|
|
||||||
To add a redirect:
|
To add a redirect:
|
||||||
|
|
||||||
1. Create a merge request in one of the internal docs projects (`gitlab`,
|
1. In the repository (`gitlab`, `gitlab-runner`, `omnibus-gitlab`, or `charts`),
|
||||||
`gitlab-runner`, `omnibus-gitlab`, or `charts`), depending on the location of
|
create a new documentation file. Don't delete the old one. The easiest
|
||||||
the file that's being moved, renamed, or removed.
|
way is to copy it. For example:
|
||||||
1. To move or rename the documentation file, create a new file with the new
|
|
||||||
name or location, but don't delete the existing documentation file.
|
```shell
|
||||||
1. In the original documentation file, add the redirect code for
|
cp doc/user/search/old_file.md doc/api/new_file.md
|
||||||
`/help`. Use the following template exactly, and change only the links and
|
```
|
||||||
date. Use relative paths and `.md` for a redirect to another documentation
|
|
||||||
page. Use the full URL (with `https://`) to redirect to a different project or
|
1. Add the redirect code to the old documentation file by running the
|
||||||
site:
|
following Rake task. The first argument is the path of the old file,
|
||||||
|
and the second argument is the path of the new file:
|
||||||
|
|
||||||
|
- To redirect to a page in the same project, use relative paths and
|
||||||
|
the `.md` extension. Both old and new paths start from the same location.
|
||||||
|
In the following example, both paths are relative to `doc/`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bundle exec rake "gitlab:docs:redirect[doc/user/search/old_file.md, doc/api/new_file.md]"
|
||||||
|
```
|
||||||
|
|
||||||
|
- To redirect to a page in a different project or site, use the full URL (with `https://`) :
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bundle exec rake "gitlab:docs:redirect[doc/user/search/old_file.md, https://example.com]"
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can omit the arguments, and you'll be asked to enter
|
||||||
|
their values:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bundle exec rake gitlab:docs:redirect
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't want to use the Rake task, you can use the following template.
|
||||||
|
However, the file paths must be relative to the `doc` or `docs` directory.
|
||||||
|
|
||||||
|
Replace the value of `redirect_to` with the new file path and `YYYY-MM-DD`
|
||||||
|
with the date the file should be removed.
|
||||||
|
|
||||||
|
Redirect files that link to docs in internal documentation projects
|
||||||
|
are removed after three months. Redirect files that link to external sites are
|
||||||
|
removed after one year:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
---
|
---
|
||||||
redirect_to: '../path/to/file/index.md'
|
redirect_to: '../newpath/to/file/index.md'
|
||||||
---
|
---
|
||||||
|
|
||||||
This document was moved to [another location](../path/to/file/index.md).
|
This document was moved to [another location](../path/to/file/index.md).
|
||||||
|
@ -205,27 +237,24 @@ To add a redirect:
|
||||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
|
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
|
||||||
```
|
```
|
||||||
|
|
||||||
Redirect files linking to docs in any of the internal documentations projects
|
|
||||||
are removed after three months. Redirect files linking to external sites are
|
|
||||||
removed after one year.
|
|
||||||
|
|
||||||
1. If the documentation page being moved has any Disqus comments, follow the steps
|
1. If the documentation page being moved has any Disqus comments, follow the steps
|
||||||
described in [Redirections for pages with Disqus comments](#redirections-for-pages-with-disqus-comments).
|
described in [Redirections for pages with Disqus comments](#redirections-for-pages-with-disqus-comments).
|
||||||
1. If a documentation page you're removing includes images that aren't used
|
1. Open a merge request with your changes. If a documentation page
|
||||||
|
you're removing includes images that aren't used
|
||||||
with any other documentation pages, be sure to use your merge request to delete
|
with any other documentation pages, be sure to use your merge request to delete
|
||||||
those images from the repository.
|
those images from the repository.
|
||||||
1. Assign the merge request to a technical writer for review and merge.
|
1. Assign the merge request to a technical writer for review and merge.
|
||||||
1. Search for links to the original documentation file. You must find and update all
|
1. Search for links to the old documentation file. You must find and update all
|
||||||
links that point to the original documentation file:
|
links that point to the old documentation file:
|
||||||
|
|
||||||
- In <https://gitlab.com/gitlab-com/www-gitlab-com>, search for full URLs:
|
- In <https://gitlab.com/gitlab-com/www-gitlab-com>, search for full URLs:
|
||||||
`grep -r "docs.gitlab.com/ee/path/to/file.html" .`
|
`grep -r "docs.gitlab.com/ee/path/to/file.html" .`
|
||||||
- In <https://gitlab.com/gitlab-org/gitlab-docs/-/tree/master/content/_data>,
|
- In <https://gitlab.com/gitlab-org/gitlab-docs/-/tree/master/content/_data>,
|
||||||
search the navigation bar configuration files for the path with `.html`:
|
search the navigation bar configuration files for the path with `.html`:
|
||||||
`grep -r "path/to/file.html" .`
|
`grep -r "path/to/file.html" .`
|
||||||
- In any of the four internal projects. This includes searching for links in the docs
|
- In any of the four internal projects, search for links in the docs
|
||||||
and codebase. Search for all variations, including full URL and just the path.
|
and codebase. Search for all variations, including full URL and just the path.
|
||||||
In macOS for example, go to the root directory of the `gitlab` project and run:
|
For example, go to the root directory of the `gitlab` project and run:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
grep -r "docs.gitlab.com/ee/path/to/file.html" .
|
grep -r "docs.gitlab.com/ee/path/to/file.html" .
|
||||||
|
|
|
@ -302,7 +302,7 @@ end
|
||||||
|
|
||||||
Adding foreign key to `projects`:
|
Adding foreign key to `projects`:
|
||||||
|
|
||||||
We can use the `add_concurrenct_foreign_key` method in this case, as this helper method
|
We can use the `add_concurrent_foreign_key` method in this case, as this helper method
|
||||||
has the lock retries built into it.
|
has the lock retries built into it.
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
|
|
|
@ -6,93 +6,93 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
|
|
||||||
# Jira DVCS connector
|
# Jira DVCS connector
|
||||||
|
|
||||||
If you're using GitLab.com and Jira Cloud, use the
|
Use the Jira DVCS (distributed version control system) connector if you self-host
|
||||||
[GitLab for Jira app](connect-app.md) unless you have a specific need for the DVCS Connector.
|
either your Jira instance or your GitLab instance, and you want to sync information
|
||||||
|
between them. If you use Jira Cloud and GitLab.com, you should use the
|
||||||
|
[GitLab for Jira app](connect-app.md) unless you specifically need the DVCS connector.
|
||||||
|
|
||||||
When configuring Jira DVCS Connector:
|
When you configure the Jira DVCS connector, make sure your GitLab and Jira instances
|
||||||
|
are accessible.
|
||||||
|
|
||||||
- If you are using self-managed GitLab, make sure your GitLab instance is accessible by Jira.
|
- **Self-managed GitLab**: Your GitLab instance must be accessible by Jira.
|
||||||
- If you're connecting to Jira Cloud, ensure your instance is accessible through the internet.
|
- **Jira Cloud**: Your instance must be accessible through the internet.
|
||||||
- If you are using Jira Server, make sure your instance is accessible however your network is set up.
|
- **Jira Server**: Your network must allow access to your instance.
|
||||||
|
|
||||||
## GitLab account configuration for DVCS
|
## Configure a GitLab application for DVCS
|
||||||
|
|
||||||
NOTE:
|
We recommend you create and use a `jira` user in GitLab, and use the account only
|
||||||
To ensure that regular user account maintenance doesn't impact your integration,
|
for integration work. A separate account ensures regular account maintenance does not affect
|
||||||
create and use a single-purpose `jira` user in GitLab.
|
your integration.
|
||||||
|
|
||||||
1. In GitLab, create a new application to allow Jira to connect with your GitLab account.
|
1. In GitLab, [create a user](../../user/profile/account/create_accounts.md) for Jira to
|
||||||
1. Sign in to the GitLab account that you want Jira to use to connect to GitLab.
|
use to connect to GitLab. For Jira to access all projects,
|
||||||
1. In the top-right corner, select your avatar.
|
a user with [Administrator](../../user/permissions.md) permissions must
|
||||||
1. Select **Edit profile**.
|
create the user.
|
||||||
|
1. In the top right corner, click the account's avatar, and select **Edit profile**.
|
||||||
1. In the left sidebar, select **Applications**.
|
1. In the left sidebar, select **Applications**.
|
||||||
1. In the **Name** field, enter a descriptive name for the integration, such as `Jira`.
|
1. In the **Name** field, enter a descriptive name for the integration, such as `Jira`.
|
||||||
1. In the **Redirect URI** field, enter `https://<gitlab.example.com>/login/oauth/callback`,
|
1. In the **Redirect URI** field, enter the URI appropriate for your version of GitLab,
|
||||||
replacing `<gitlab.example.com>` with your GitLab instance domain. For example, if you are using GitLab.com,
|
replacing `<gitlab.example.com>` with your GitLab instance domain:
|
||||||
this would be `https://gitlab.com/login/oauth/callback`.
|
- *For GitLab versions 11.3 and later,* use `https://<gitlab.example.com>/login/oauth/callback`.
|
||||||
|
If you use GitLab.com, the URL is `https://gitlab.com/login/oauth/callback`.
|
||||||
|
- *For GitLab versions 11.2 and earlier,* use
|
||||||
|
`https://<gitlab.example.com>/-/jira/login/oauth/callback`.
|
||||||
|
|
||||||
NOTE:
|
1. For **Scopes**, select `api` and clear any other checkboxes.
|
||||||
If using a GitLab version earlier than 11.3, the `Redirect URI` must be
|
1. Select **Submit**.
|
||||||
`https://<gitlab.example.com>/-/jira/login/oauth/callback`. If you want Jira
|
1. GitLab displays the generated **Application ID**
|
||||||
to have access to all projects, GitLab recommends that an administrator create the
|
and **Secret** values. Copy these values, as you need them to configure Jira.
|
||||||
application.
|
|
||||||
|
|
||||||
![GitLab application setup](img/jira_dev_panel_gl_setup_1.png)
|
## Configure Jira for DVCS
|
||||||
|
|
||||||
1. Check **API** in the **Scopes** section, and clear any other checkboxes.
|
If you use Jira Cloud and GitLab.com, use the [GitLab for Jira app](connect-app.md)
|
||||||
1. Click **Save application**. GitLab displays the generated **Application ID**
|
unless you specifically need the DVCS Connector.
|
||||||
and **Secret** values. Copy these values, which you use in Jira.
|
|
||||||
|
|
||||||
## Jira DVCS Connector setup
|
Configure this connection when you want to import all GitLab commits and branches,
|
||||||
|
for the groups you specify, into Jira. This import takes a few minutes and, after
|
||||||
If you're using GitLab.com and Jira Cloud, use the
|
it completes, refreshes every 60 minutes:
|
||||||
[GitLab for Jira app](connect-app.md) unless you have a specific need for the DVCS Connector.
|
|
||||||
|
|
||||||
1. Ensure you have completed the [GitLab configuration](#gitlab-account-configuration-for-dvcs).
|
|
||||||
1. If you're using Jira Server, go to **Settings (gear) > Applications > DVCS accounts**.
|
|
||||||
If you're using Jira Cloud, go to **Settings (gear) > Products > DVCS accounts**.
|
|
||||||
1. Click **Link GitHub Enterprise account** to start creating a new integration.
|
|
||||||
(We're pretending to be GitHub in this integration, until there's additional platform support in Jira.)
|
|
||||||
1. Complete the form:
|
|
||||||
|
|
||||||
1. Select **GitHub Enterprise** for the **Host** field.
|
|
||||||
|
|
||||||
1. In the **Team or User Account** field, enter either:
|
|
||||||
|
|
||||||
|
1. Ensure you have completed the [GitLab configuration](#configure-a-gitlab-application-for-dvcs).
|
||||||
|
1. Go to your DVCS account:
|
||||||
|
- *For Jira Server,* go to **Settings (gear) > Applications > DVCS accounts**.
|
||||||
|
- *For Jira Cloud,* go to **Settings (gear) > Products > DVCS accounts**.
|
||||||
|
1. To create a new integration, select the appropriate value for **Host**:
|
||||||
|
- *For Jira versions 8.14 and later:* Select **GitLab** or
|
||||||
|
<!-- vale gitlab.Substitutions = NO -->
|
||||||
|
**GitLab Self-Hosted**.
|
||||||
|
<!-- vale gitlab.Substitutions = YES -->
|
||||||
|
- *For Jira versions 8.13 and earlier:* Select **GitHub Enterprise**.
|
||||||
|
1. For **Team or User Account**, enter either:
|
||||||
- The relative path of a top-level GitLab group that you have access to.
|
- The relative path of a top-level GitLab group that you have access to.
|
||||||
- The relative path of your personal namespace.
|
- The relative path of your personal namespace.
|
||||||
|
|
||||||
![Creation of Jira DVCS integration](img/jira_dev_panel_jira_setup_2.png)
|
1. In the **Host URL** field, enter the URI appropriate for your version of GitLab,
|
||||||
|
replacing `<gitlab.example.com>` with your GitLab instance domain:
|
||||||
1. In the **Host URL** field, enter `https://<gitlab.example.com>/`,
|
- *For GitLab versions 11.3 and later,* use `https://<gitlab.example.com>/`.
|
||||||
replacing `<gitlab.example.com>` with your GitLab instance domain. For example, if you are using GitLab.com,
|
- *For GitLab versions 11.2 and earlier,* use
|
||||||
this would be `https://gitlab.com/`.
|
`https://<gitlab.example.com>/-/jira`.
|
||||||
|
|
||||||
NOTE:
|
|
||||||
If using a GitLab version earlier than 11.3 the **Host URL** value should be `https://<gitlab.example.com>/-/jira`
|
|
||||||
|
|
||||||
1. For the **Client ID** field, use the **Application ID** value from the previous section.
|
|
||||||
|
|
||||||
1. For the **Client Secret** field, use the **Secret** value from the previous section.
|
|
||||||
|
|
||||||
|
1. For **Client ID**, use the **Application ID** value from the previous section.
|
||||||
|
1. For **Client Secret**, use the **Secret** value from the previous section.
|
||||||
1. Ensure that the rest of the checkboxes are checked.
|
1. Ensure that the rest of the checkboxes are checked.
|
||||||
|
1. Select **Add** to complete and create the integration.
|
||||||
|
|
||||||
1. Click **Add** to complete and create the integration.
|
To connect additional GitLab projects from other GitLab top-level groups, or
|
||||||
|
personal namespaces, repeat the previous steps with additional Jira DVCS accounts.
|
||||||
|
|
||||||
Jira takes up to a few minutes to know about (import behind the scenes) all the commits and branches
|
After you configure the integration, read more about [how to test and use it](index.md#usage).
|
||||||
for all the projects in the GitLab group you specified in the previous step. These are refreshed
|
|
||||||
every 60 minutes.
|
|
||||||
|
|
||||||
In the future, we plan on implementing real-time integration. If you need
|
## Refresh data imported to Jira
|
||||||
to refresh the data manually, you can do this from the `Applications -> DVCS
|
|
||||||
accounts` screen where you initially set up the integration:
|
|
||||||
|
|
||||||
![Refresh GitLab information in Jira](img/jira_dev_panel_manual_refresh.png)
|
Jira imports the commits and branches every 60 minutes for your projects. You
|
||||||
|
can refresh the data manually from the Jira interface:
|
||||||
|
|
||||||
To connect additional GitLab projects from other GitLab top-level groups (or personal namespaces), repeat the previous
|
1. Sign in to your Jira instance as the user you configured the integration with.
|
||||||
steps with additional Jira DVCS accounts.
|
1. Go to **Settings (gear) > Applications**.
|
||||||
|
1. Select **DVCS accounts**.
|
||||||
Now that the integration is configured, read more about how to test and use it in [Usage](index.md#usage).
|
1. In the table, for the repository you want to refresh, in the **Last Activity**
|
||||||
|
column, select the icon:
|
||||||
|
![Refresh GitLab information in Jira](img/jira_dev_panel_manual_refresh.png)
|
||||||
|
|
||||||
## Troubleshooting your DVCS connection
|
## Troubleshooting your DVCS connection
|
||||||
|
|
||||||
|
@ -100,39 +100,46 @@ Refer to the items in this section if you're having problems with your DVCS conn
|
||||||
|
|
||||||
### Jira cannot access GitLab server
|
### Jira cannot access GitLab server
|
||||||
|
|
||||||
|
If you complete the **Add New Account** form, authorize access, and you receive
|
||||||
|
this error, Jira and GitLab cannot connect. No other error messages
|
||||||
|
appear in any logs:
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
Error obtaining access token. Cannot access https://gitlab.example.com from Jira.
|
Error obtaining access token. Cannot access https://gitlab.example.com from Jira.
|
||||||
```
|
```
|
||||||
|
|
||||||
This error message is generated in Jira, after completing the **Add New Account**
|
### SSL and TLS problems
|
||||||
form and authorizing access. It indicates a connectivity issue from Jira to
|
|
||||||
GitLab. No other error messages appear in any logs.
|
|
||||||
|
|
||||||
If there was an issue with SSL/TLS, this error message is generated.
|
Problems with SSL and TLS can cause this error message:
|
||||||
|
|
||||||
- The [GitLab Jira integration](../../user/project/integrations/jira.md) requires GitLab to connect to Jira. Any
|
```plaintext
|
||||||
TLS issues that arise from a private certificate authority or self-signed
|
Error obtaining access token. Cannot access https://gitlab.example.com from Jira.
|
||||||
certificate [are resolved on the GitLab server](https://docs.gitlab.com/omnibus/settings/ssl.html#other-certificate-authorities),
|
```
|
||||||
|
|
||||||
|
- The [GitLab Jira integration](../../user/project/integrations/jira.md) requires
|
||||||
|
GitLab to connect to Jira. Any TLS issues that arise from a private certificate
|
||||||
|
authority or self-signed certificate are resolved
|
||||||
|
[on the GitLab server](https://docs.gitlab.com/omnibus/settings/ssl.html#other-certificate-authorities),
|
||||||
as GitLab is the TLS client.
|
as GitLab is the TLS client.
|
||||||
- The Jira Development panel integration requires Jira to connect to GitLab, which
|
- The Jira Development panel integration requires Jira to connect to GitLab, which
|
||||||
causes Jira to be the TLS client. If your GitLab server's certificate is not
|
causes Jira to be the TLS client. If your GitLab server's certificate is not
|
||||||
issued by a public certificate authority, the Java Truststore on Jira's server
|
issued by a public certificate authority, the Java Truststore on Jira's server
|
||||||
needs to have the appropriate certificate added to it (such as your organization's
|
must have the appropriate certificate (such as your organization's
|
||||||
root certificate).
|
root certificate) added to it .
|
||||||
|
|
||||||
Refer to Atlassian's documentation and Atlassian Support for assistance setting up Jira correctly:
|
Refer to Atlassian's documentation and Atlassian Support for assistance setting up Jira correctly:
|
||||||
|
|
||||||
- [Adding a certificate to the trust store](https://confluence.atlassian.com/kb/how-to-import-a-public-ssl-certificate-into-a-jvm-867025849.html).
|
- [Add a certificate](https://confluence.atlassian.com/kb/how-to-import-a-public-ssl-certificate-into-a-jvm-867025849.html)
|
||||||
- Simplest approach is to use [`keytool`](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html).
|
to the trust store.
|
||||||
|
- The simplest approach is [`keytool`](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html).
|
||||||
- Add additional roots to Java's default Truststore (`cacerts`) to allow Jira to
|
- Add additional roots to Java's default Truststore (`cacerts`) to allow Jira to
|
||||||
also trust public certificate authorities.
|
also trust public certificate authorities.
|
||||||
- If the integration stops working after upgrading Jira's Java runtime, this
|
- If the integration stops working after upgrading Jira's Java runtime, the
|
||||||
might be because the `cacerts` Truststore got replaced.
|
`cacerts` Truststore may have been replaced during the upgrade.
|
||||||
|
|
||||||
- [Troubleshooting connectivity up to and including TLS handshaking](https://confluence.atlassian.com/kb/unable-to-connect-to-ssl-services-due-to-pkix-path-building-failed-error-779355358.html),
|
- Troubleshooting connectivity [up to and including TLS handshaking](https://confluence.atlassian.com/kb/unable-to-connect-to-ssl-services-due-to-pkix-path-building-failed-error-779355358.html),
|
||||||
using the a java class called `SSLPoke`.
|
using the a java class called `SSLPoke`.
|
||||||
|
- Download the class from Atlassian's knowledge base to a directory on Jira's server, such as `/tmp`.
|
||||||
- Download the class from Atlassian's knowledge base to Jira's server, for example to `/tmp`.
|
|
||||||
- Use the same Java runtime as Jira.
|
- Use the same Java runtime as Jira.
|
||||||
- Pass all networking-related parameters that Jira is called with, such as proxy
|
- Pass all networking-related parameters that Jira is called with, such as proxy
|
||||||
settings or an alternative root Truststore (`-Djavax.net.ssl.trustStore`):
|
settings or an alternative root Truststore (`-Djavax.net.ssl.trustStore`):
|
||||||
|
@ -154,38 +161,42 @@ The requested scope is invalid, unknown, or malformed.
|
||||||
|
|
||||||
Potential resolutions:
|
Potential resolutions:
|
||||||
|
|
||||||
- Verify the URL shown in the browser after being redirected from Jira in step 5 of [Jira DVCS Connector Setup](#jira-dvcs-connector-setup) includes `scope=api` in the query string.
|
1. Verify that the URL shown in the browser after being redirected from Jira in the
|
||||||
- If `scope=api` is missing from the URL, return to [GitLab account configuration](#gitlab-account-configuration-for-dvcs) and ensure the application you created in step 1 has the `api` box checked under scopes.
|
[Jira DVCS connector setup](#configure-jira-for-dvcs) includes `scope=api` in
|
||||||
|
the query string.
|
||||||
|
1. If `scope=api` is missing from the URL, edit the
|
||||||
|
[GitLab account configuration](#configure-a-gitlab-application-for-dvcs). Review
|
||||||
|
the **Scopes** field and ensure the `api` check box is selected.
|
||||||
|
|
||||||
### Jira error adding account and no repositories listed
|
### Jira error adding account and no repositories listed
|
||||||
|
|
||||||
```plaintext
|
After you complete the **Add New Account** form in Jira and authorize access, you might
|
||||||
Error!
|
encounter these issues:
|
||||||
Failed adding the account: [Error retrieving list of repositories]
|
|
||||||
```
|
|
||||||
|
|
||||||
This error message is generated in Jira after completing the **Add New Account**
|
- An `Error! Failed adding the account: [Error retrieving list of repositories]` error.
|
||||||
form in Jira and authorizing access. Attempting to click **Try Again** returns
|
- An `Account is already integrated with JIRA` error when you click **Try Again**.
|
||||||
`Account is already integrated with JIRA.` The account is set up in the DVCS
|
- An account is visible in the DVCS accounts view, but no repositories are listed.
|
||||||
accounts view, but no repositories are listed.
|
|
||||||
|
|
||||||
Potential resolutions:
|
To resolve this issue:
|
||||||
|
|
||||||
- If you're using GitLab versions 11.10-12.7, upgrade to GitLab 12.8.10 or later
|
- If you're using GitLab Free or GitLab Starter, be sure you're using
|
||||||
to resolve an identified [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/37012).
|
GitLab 13.4 or later.
|
||||||
- If you're using GitLab Free, be sure you're using GitLab 13.4 or later.
|
- *If you're using GitLab versions 11.10-12.7,* upgrade to GitLab 12.8.10 or later
|
||||||
|
to resolve [an identified issue](https://gitlab.com/gitlab-org/gitlab/-/issues/37012).
|
||||||
|
|
||||||
[Contact GitLab Support](https://about.gitlab.com/support/) if none of these reasons apply.
|
[Contact GitLab Support](https://about.gitlab.com/support/) if none of these reasons apply.
|
||||||
|
|
||||||
### Fixing synchronization issues
|
### Fix synchronization issues
|
||||||
|
|
||||||
If Jira displays incorrect information (such as deleted branches), you may need to
|
If Jira displays incorrect information, such as deleted branches, you may need to
|
||||||
resynchronize the information. To do so:
|
resynchronize the information. To do so:
|
||||||
|
|
||||||
1. In Jira, go to **Jira Administration > Applications > DVCS accounts**.
|
1. In Jira, go to **Jira Administration > Applications > DVCS accounts**.
|
||||||
1. At the account (group or subgroup) level, Jira displays an option to
|
1. At the account (group or subgroup) level, Jira displays an option to
|
||||||
**Refresh repositories** in the `...` (ellipsis) menu.
|
**Refresh repositories** in the **{ellipsis_h}** (ellipsis) menu.
|
||||||
1. For each project, there's a sync button displayed next to the **last activity** date.
|
1. For each project, there's a sync button displayed next to the **last activity** date.
|
||||||
To perform a *soft resync*, click the button, or complete a *full sync* by shift clicking
|
- To perform a *soft resync*, click the button.
|
||||||
the button. For more information, see
|
- To complete a *full sync*, shift-click the button.
|
||||||
[Atlassian's documentation](https://support.atlassian.com/jira-cloud-administration/docs/synchronize-jira-cloud-to-bitbucket/).
|
|
||||||
|
For more information, read
|
||||||
|
[Atlassian's documentation](https://support.atlassian.com/jira-cloud-administration/docs/synchronize-jira-cloud-to-bitbucket/).
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 35 KiB |
Binary file not shown.
Before Width: | Height: | Size: 17 KiB |
|
@ -19,8 +19,8 @@ After pushing your changes to a new branch, you can:
|
||||||
- [Discuss](../../../discussions/index.md) your implementation with your team
|
- [Discuss](../../../discussions/index.md) your implementation with your team
|
||||||
- Preview changes submitted to a new branch with [Review Apps](../../../../ci/review_apps/index.md).
|
- Preview changes submitted to a new branch with [Review Apps](../../../../ci/review_apps/index.md).
|
||||||
|
|
||||||
With [GitLab Starter](https://about.gitlab.com/pricing/), you can also request
|
You can also request [approval](../../merge_requests/merge_request_approvals.md)
|
||||||
[approval](../../merge_requests/merge_request_approvals.md) from your managers.
|
from your managers.
|
||||||
|
|
||||||
For more information on managing branches using the GitLab UI, see:
|
For more information on managing branches using the GitLab UI, see:
|
||||||
|
|
||||||
|
|
57
lib/tasks/gitlab/docs/redirect.rake
Normal file
57
lib/tasks/gitlab/docs/redirect.rake
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
require 'date'
|
||||||
|
require 'pathname'
|
||||||
|
|
||||||
|
# https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page
|
||||||
|
namespace :gitlab do
|
||||||
|
namespace :docs do
|
||||||
|
desc 'GitLab | Docs | Create a doc redirect'
|
||||||
|
task :redirect, [:old_path, :new_path] do |_, args|
|
||||||
|
if args.old_path
|
||||||
|
old_path = args.old_path
|
||||||
|
else
|
||||||
|
puts '=> Enter the path of the OLD file:'
|
||||||
|
old_path = STDIN.gets.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
if args.new_path
|
||||||
|
new_path = args.new_path
|
||||||
|
else
|
||||||
|
puts '=> Enter the path of the NEW file:'
|
||||||
|
new_path = STDIN.gets.chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# If the new path is a relative URL, find the relative path between
|
||||||
|
# the old and new paths.
|
||||||
|
# The returned path is one level deeper, so remove the leading '../'.
|
||||||
|
#
|
||||||
|
unless new_path.start_with?('http')
|
||||||
|
old_pathname = Pathname.new(old_path)
|
||||||
|
new_pathname = Pathname.new(new_path)
|
||||||
|
relative_path = new_pathname.relative_path_from(old_pathname).to_s
|
||||||
|
(_, *last) = relative_path.split('/')
|
||||||
|
new_path = last.join('/')
|
||||||
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# - If this is an external URL, move the date 1 year later.
|
||||||
|
# - If this is a relative URL, move the date 3 months later.
|
||||||
|
#
|
||||||
|
date = Time.now.utc.strftime('%Y-%m-%d')
|
||||||
|
date = new_path.start_with?('http') ? Date.parse(date) >> 12 : Date.parse(date) >> 3
|
||||||
|
|
||||||
|
puts "=> Creating new redirect from #{old_path} to #{new_path}"
|
||||||
|
File.open(old_path, 'w') do |post|
|
||||||
|
post.puts '---'
|
||||||
|
post.puts "redirect_to: '#{new_path}'"
|
||||||
|
post.puts '---'
|
||||||
|
post.puts
|
||||||
|
post.puts "This file was moved to [another location](#{new_path})."
|
||||||
|
post.puts
|
||||||
|
post.puts "<!-- This redirect file can be deleted after <#{date}>. -->"
|
||||||
|
post.puts "<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
import { GlButton } from '@gitlab/ui';
|
import { GlButton, GlLink } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import ExperimentTracking from '~/experimentation/experiment_tracking';
|
import ExperimentTracking from '~/experimentation/experiment_tracking';
|
||||||
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
|
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
|
||||||
|
@ -8,22 +8,29 @@ jest.mock('~/experimentation/experiment_tracking');
|
||||||
|
|
||||||
const displayText = 'Invite team members';
|
const displayText = 'Invite team members';
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
let triggerProps;
|
||||||
|
let findButton;
|
||||||
|
const triggerComponent = {
|
||||||
|
button: GlButton,
|
||||||
|
anchor: GlLink,
|
||||||
|
};
|
||||||
|
|
||||||
const createComponent = (props = {}) => {
|
const createComponent = (props = {}) => {
|
||||||
wrapper = shallowMount(InviteMembersTrigger, {
|
wrapper = shallowMount(InviteMembersTrigger, {
|
||||||
propsData: {
|
propsData: {
|
||||||
displayText,
|
displayText,
|
||||||
|
...triggerProps,
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('InviteMembersTrigger', () => {
|
describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement) => {
|
||||||
const findButton = () => wrapper.findComponent(GlButton);
|
triggerProps = { triggerElement };
|
||||||
|
findButton = () => wrapper.findComponent(triggerComponent[triggerElement]);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
wrapper = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('displayText', () => {
|
describe('displayText', () => {
|
||||||
|
@ -74,5 +81,19 @@ describe('InviteMembersTrigger', () => {
|
||||||
|
|
||||||
expect(ExperimentTracking).not.toHaveBeenCalledWith('_track_experiment_');
|
expect(ExperimentTracking).not.toHaveBeenCalledWith('_track_experiment_');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not add tracking attributes', () => {
|
||||||
|
createComponent();
|
||||||
|
|
||||||
|
expect(findButton().attributes('data-track-event')).toBeUndefined();
|
||||||
|
expect(findButton().attributes('data-track-label')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds tracking attributes', () => {
|
||||||
|
createComponent({ label: '_label_', event: '_event_' });
|
||||||
|
|
||||||
|
expect(findButton().attributes('data-track-event')).toBe('_event_');
|
||||||
|
expect(findButton().attributes('data-track-label')).toBe('_label_');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,8 @@ import PackageSearch from '~/packages/list/components/package_search.vue';
|
||||||
import PackageListApp from '~/packages/list/components/packages_list_app.vue';
|
import PackageListApp from '~/packages/list/components/packages_list_app.vue';
|
||||||
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
|
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
|
||||||
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
|
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
|
||||||
|
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
||||||
|
import * as packageUtils from '~/packages_and_registries/shared/utils';
|
||||||
|
|
||||||
jest.mock('~/lib/utils/common_utils');
|
jest.mock('~/lib/utils/common_utils');
|
||||||
jest.mock('~/flash');
|
jest.mock('~/flash');
|
||||||
|
@ -61,6 +63,7 @@ describe('packages_list_app', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createStore();
|
createStore();
|
||||||
|
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue({});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -72,25 +75,6 @@ describe('packages_list_app', () => {
|
||||||
expect(wrapper.element).toMatchSnapshot();
|
expect(wrapper.element).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('empty state', () => {
|
|
||||||
it('generate the correct empty list link', () => {
|
|
||||||
mountComponent();
|
|
||||||
|
|
||||||
const link = findListComponent().find(GlLink);
|
|
||||||
|
|
||||||
expect(link.attributes('href')).toBe(emptyListHelpUrl);
|
|
||||||
expect(link.text()).toBe('publish and share your packages');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('includes the right content on the default tab', () => {
|
|
||||||
mountComponent();
|
|
||||||
|
|
||||||
const heading = findEmptyState().find('h1');
|
|
||||||
|
|
||||||
expect(heading.text()).toBe('There are no packages yet');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('call requestPackagesList on page:changed', () => {
|
it('call requestPackagesList on page:changed', () => {
|
||||||
mountComponent();
|
mountComponent();
|
||||||
store.dispatch.mockClear();
|
store.dispatch.mockClear();
|
||||||
|
@ -108,10 +92,75 @@ describe('packages_list_app', () => {
|
||||||
expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
|
expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not call requestPackagesList two times on render', () => {
|
it('does call requestPackagesList only one time on render', () => {
|
||||||
mountComponent();
|
mountComponent();
|
||||||
|
|
||||||
expect(store.dispatch).toHaveBeenCalledTimes(1);
|
expect(store.dispatch).toHaveBeenCalledTimes(3);
|
||||||
|
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', expect.any(Object));
|
||||||
|
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', expect.any(Array));
|
||||||
|
expect(store.dispatch).toHaveBeenNthCalledWith(3, 'requestPackagesList');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('url query string handling', () => {
|
||||||
|
const defaultQueryParamsMock = {
|
||||||
|
search: [1, 2],
|
||||||
|
type: 'npm',
|
||||||
|
sort: 'asc',
|
||||||
|
orderBy: 'created',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('calls setSorting with the query string based sorting', () => {
|
||||||
|
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
|
||||||
|
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', {
|
||||||
|
orderBy: defaultQueryParamsMock.orderBy,
|
||||||
|
sort: defaultQueryParamsMock.sort,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls setFilter with the query string based filters', () => {
|
||||||
|
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
|
||||||
|
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', [
|
||||||
|
{ type: 'type', value: { data: defaultQueryParamsMock.type } },
|
||||||
|
{ type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[0] } },
|
||||||
|
{ type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[1] } },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls setSorting and setFilters with the results of extractFilterAndSorting', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(packageUtils, 'extractFilterAndSorting')
|
||||||
|
.mockReturnValue({ filters: ['foo'], sorting: { sort: 'desc' } });
|
||||||
|
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', { sort: 'desc' });
|
||||||
|
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', ['foo']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('empty state', () => {
|
||||||
|
it('generate the correct empty list link', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
const link = findListComponent().find(GlLink);
|
||||||
|
|
||||||
|
expect(link.attributes('href')).toBe(emptyListHelpUrl);
|
||||||
|
expect(link.text()).toBe('publish and share your packages');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes the right content on the default tab', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
const heading = findEmptyState().find('h1');
|
||||||
|
|
||||||
|
expect(heading.text()).toBe('There are no packages yet');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filter without results', () => {
|
describe('filter without results', () => {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import component from '~/packages/list/components/package_search.vue';
|
||||||
import PackageTypeToken from '~/packages/list/components/tokens/package_type_token.vue';
|
import PackageTypeToken from '~/packages/list/components/tokens/package_type_token.vue';
|
||||||
import getTableHeaders from '~/packages/list/utils';
|
import getTableHeaders from '~/packages/list/utils';
|
||||||
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
||||||
|
import UrlSync from '~/vue_shared/components/url_sync.vue';
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
localVue.use(Vuex);
|
localVue.use(Vuex);
|
||||||
|
@ -12,7 +13,8 @@ describe('Package Search', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let store;
|
let store;
|
||||||
|
|
||||||
const findRegistrySearch = () => wrapper.find(RegistrySearch);
|
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
|
||||||
|
const findUrlSync = () => wrapper.findComponent(UrlSync);
|
||||||
|
|
||||||
const createStore = (isGroupPage) => {
|
const createStore = (isGroupPage) => {
|
||||||
const state = {
|
const state = {
|
||||||
|
@ -37,6 +39,9 @@ describe('Package Search', () => {
|
||||||
wrapper = shallowMount(component, {
|
wrapper = shallowMount(component, {
|
||||||
localVue,
|
localVue,
|
||||||
store,
|
store,
|
||||||
|
stubs: {
|
||||||
|
UrlSync,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -104,4 +109,20 @@ describe('Package Search', () => {
|
||||||
|
|
||||||
expect(wrapper.emitted('update')).toEqual([[]]);
|
expect(wrapper.emitted('update')).toEqual([[]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has a UrlSync component', () => {
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
expect(findUrlSync().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('on query:changed calls updateQuery from UrlSync', () => {
|
||||||
|
jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
|
||||||
|
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
findRegistrySearch().vm.$emit('query:changed');
|
||||||
|
|
||||||
|
expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
||||||
import {
|
import {
|
||||||
getQueryParams,
|
getQueryParams,
|
||||||
keyValueToFilterToken,
|
keyValueToFilterToken,
|
||||||
searchArrayToFilterTokens,
|
searchArrayToFilterTokens,
|
||||||
|
extractFilterAndSorting,
|
||||||
} from '~/packages_and_registries/shared/utils';
|
} from '~/packages_and_registries/shared/utils';
|
||||||
|
|
||||||
describe('Packages And Registries shared utils', () => {
|
describe('Packages And Registries shared utils', () => {
|
||||||
|
@ -27,9 +29,31 @@ describe('Packages And Registries shared utils', () => {
|
||||||
const search = ['one', 'two'];
|
const search = ['one', 'two'];
|
||||||
|
|
||||||
expect(searchArrayToFilterTokens(search)).toStrictEqual([
|
expect(searchArrayToFilterTokens(search)).toStrictEqual([
|
||||||
{ type: 'filtered-search-term', value: { data: 'one' } },
|
{ type: FILTERED_SEARCH_TERM, value: { data: 'one' } },
|
||||||
{ type: 'filtered-search-term', value: { data: 'two' } },
|
{ type: FILTERED_SEARCH_TERM, value: { data: 'two' } },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('extractFilterAndSorting', () => {
|
||||||
|
it.each`
|
||||||
|
search | type | sort | orderBy | result
|
||||||
|
${['one']} | ${'myType'} | ${'asc'} | ${'foo'} | ${{ sorting: { sort: 'asc', orderBy: 'foo' }, filters: [{ type: 'type', value: { data: 'myType' } }, { type: FILTERED_SEARCH_TERM, value: { data: 'one' } }] }}
|
||||||
|
${['one']} | ${null} | ${'asc'} | ${'foo'} | ${{ sorting: { sort: 'asc', orderBy: 'foo' }, filters: [{ type: FILTERED_SEARCH_TERM, value: { data: 'one' } }] }}
|
||||||
|
${[]} | ${null} | ${'asc'} | ${'foo'} | ${{ sorting: { sort: 'asc', orderBy: 'foo' }, filters: [] }}
|
||||||
|
${null} | ${null} | ${'asc'} | ${'foo'} | ${{ sorting: { sort: 'asc', orderBy: 'foo' }, filters: [] }}
|
||||||
|
${null} | ${null} | ${null} | ${'foo'} | ${{ sorting: { orderBy: 'foo' }, filters: [] }}
|
||||||
|
${null} | ${null} | ${null} | ${null} | ${{ sorting: {}, filters: [] }}
|
||||||
|
`(
|
||||||
|
'returns sorting and filters objects in the correct form',
|
||||||
|
({ search, type, sort, orderBy, result }) => {
|
||||||
|
const queryObject = {
|
||||||
|
search,
|
||||||
|
type,
|
||||||
|
sort,
|
||||||
|
orderBy,
|
||||||
|
};
|
||||||
|
expect(extractFilterAndSorting(queryObject)).toStrictEqual(result);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
|
import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
|
||||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
@ -61,7 +62,7 @@ describe('List Page', () => {
|
||||||
const waitForApolloRequestRender = async () => {
|
const waitForApolloRequestRender = async () => {
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
await wrapper.vm.$nextTick();
|
await nextTick();
|
||||||
};
|
};
|
||||||
|
|
||||||
const mountComponent = ({
|
const mountComponent = ({
|
||||||
|
@ -70,6 +71,7 @@ describe('List Page', () => {
|
||||||
detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock),
|
detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock),
|
||||||
mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock),
|
mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock),
|
||||||
config = { isGroupPage: false },
|
config = { isGroupPage: false },
|
||||||
|
query = {},
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
localVue.use(VueApollo);
|
localVue.use(VueApollo);
|
||||||
|
|
||||||
|
@ -96,6 +98,7 @@ describe('List Page', () => {
|
||||||
$toast,
|
$toast,
|
||||||
$route: {
|
$route: {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
|
query,
|
||||||
},
|
},
|
||||||
...mocks,
|
...mocks,
|
||||||
},
|
},
|
||||||
|
@ -159,9 +162,11 @@ describe('List Page', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isLoading is true', () => {
|
describe('isLoading is true', () => {
|
||||||
it('shows the skeleton loader', () => {
|
it('shows the skeleton loader', async () => {
|
||||||
mountComponent();
|
mountComponent();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
expect(findSkeletonLoader().exists()).toBe(true);
|
expect(findSkeletonLoader().exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -177,9 +182,11 @@ describe('List Page', () => {
|
||||||
expect(findCliCommands().exists()).toBe(false);
|
expect(findCliCommands().exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('title has the metadataLoading props set to true', () => {
|
it('title has the metadataLoading props set to true', async () => {
|
||||||
mountComponent();
|
mountComponent();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
expect(findRegistryHeader().props('metadataLoading')).toBe(true);
|
expect(findRegistryHeader().props('metadataLoading')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -312,7 +319,7 @@ describe('List Page', () => {
|
||||||
await selectImageForDeletion();
|
await selectImageForDeletion();
|
||||||
|
|
||||||
findDeleteImage().vm.$emit('success');
|
findDeleteImage().vm.$emit('success');
|
||||||
await wrapper.vm.$nextTick();
|
await nextTick();
|
||||||
|
|
||||||
const alert = findDeleteAlert();
|
const alert = findDeleteAlert();
|
||||||
expect(alert.exists()).toBe(true);
|
expect(alert.exists()).toBe(true);
|
||||||
|
@ -328,7 +335,7 @@ describe('List Page', () => {
|
||||||
await selectImageForDeletion();
|
await selectImageForDeletion();
|
||||||
|
|
||||||
findDeleteImage().vm.$emit('error');
|
findDeleteImage().vm.$emit('error');
|
||||||
await wrapper.vm.$nextTick();
|
await nextTick();
|
||||||
|
|
||||||
const alert = findDeleteAlert();
|
const alert = findDeleteAlert();
|
||||||
expect(alert.exists()).toBe(true);
|
expect(alert.exists()).toBe(true);
|
||||||
|
@ -349,7 +356,7 @@ describe('List Page', () => {
|
||||||
|
|
||||||
findRegistrySearch().vm.$emit('filter:submit');
|
findRegistrySearch().vm.$emit('filter:submit');
|
||||||
|
|
||||||
await wrapper.vm.$nextTick();
|
await nextTick();
|
||||||
};
|
};
|
||||||
|
|
||||||
it('has a search box element', async () => {
|
it('has a search box element', async () => {
|
||||||
|
@ -374,7 +381,7 @@ describe('List Page', () => {
|
||||||
await waitForApolloRequestRender();
|
await waitForApolloRequestRender();
|
||||||
|
|
||||||
findRegistrySearch().vm.$emit('sorting:changed', { sort: 'asc' });
|
findRegistrySearch().vm.$emit('sorting:changed', { sort: 'asc' });
|
||||||
await wrapper.vm.$nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' }));
|
expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' }));
|
||||||
});
|
});
|
||||||
|
@ -417,7 +424,7 @@ describe('List Page', () => {
|
||||||
await waitForApolloRequestRender();
|
await waitForApolloRequestRender();
|
||||||
|
|
||||||
findImageList().vm.$emit('prev-page');
|
findImageList().vm.$emit('prev-page');
|
||||||
await wrapper.vm.$nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(resolver).toHaveBeenCalledWith(
|
expect(resolver).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ before: pageInfo.startCursor }),
|
expect.objectContaining({ before: pageInfo.startCursor }),
|
||||||
|
@ -437,7 +444,7 @@ describe('List Page', () => {
|
||||||
await waitForApolloRequestRender();
|
await waitForApolloRequestRender();
|
||||||
|
|
||||||
findImageList().vm.$emit('next-page');
|
findImageList().vm.$emit('next-page');
|
||||||
await wrapper.vm.$nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(resolver).toHaveBeenCalledWith(
|
expect(resolver).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ after: pageInfo.endCursor }),
|
expect.objectContaining({ after: pageInfo.endCursor }),
|
||||||
|
@ -458,11 +465,10 @@ describe('List Page', () => {
|
||||||
expect(findDeleteModal().exists()).toBe(true);
|
expect(findDeleteModal().exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains a description with the path of the item to delete', () => {
|
it('contains a description with the path of the item to delete', async () => {
|
||||||
findImageList().vm.$emit('delete', { path: 'foo' });
|
findImageList().vm.$emit('delete', { path: 'foo' });
|
||||||
return wrapper.vm.$nextTick().then(() => {
|
await nextTick();
|
||||||
expect(findDeleteModal().html()).toContain('foo');
|
expect(findDeleteModal().html()).toContain('foo');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -498,4 +504,60 @@ describe('List Page', () => {
|
||||||
testTrackingCall('confirm_delete');
|
testTrackingCall('confirm_delete');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('url query string handling', () => {
|
||||||
|
const defaultQueryParams = {
|
||||||
|
search: [1, 2],
|
||||||
|
sort: 'asc',
|
||||||
|
orderBy: 'CREATED',
|
||||||
|
};
|
||||||
|
const queryChangePayload = 'foo';
|
||||||
|
|
||||||
|
it('query:updated event pushes the new query to the router', async () => {
|
||||||
|
const push = jest.fn();
|
||||||
|
mountComponent({ mocks: { $router: { push } } });
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
findRegistrySearch().vm.$emit('query:changed', queryChangePayload);
|
||||||
|
|
||||||
|
expect(push).toHaveBeenCalledWith({ query: queryChangePayload });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('graphql API call has the variables set from the URL', async () => {
|
||||||
|
const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
|
||||||
|
mountComponent({ query: defaultQueryParams, resolver });
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(resolver).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
name: 1,
|
||||||
|
sort: 'CREATED_ASC',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
sort | orderBy | search | payload
|
||||||
|
${'ASC'} | ${undefined} | ${undefined} | ${{ sort: 'UPDATED_ASC' }}
|
||||||
|
${undefined} | ${'bar'} | ${undefined} | ${{ sort: 'BAR_DESC' }}
|
||||||
|
${'ASC'} | ${'bar'} | ${undefined} | ${{ sort: 'BAR_ASC' }}
|
||||||
|
${undefined} | ${undefined} | ${undefined} | ${{}}
|
||||||
|
${undefined} | ${undefined} | ${['one']} | ${{ name: 'one' }}
|
||||||
|
${undefined} | ${undefined} | ${['one', 'two']} | ${{ name: 'one' }}
|
||||||
|
${undefined} | ${'UPDATED'} | ${['one', 'two']} | ${{ name: 'one', sort: 'UPDATED_DESC' }}
|
||||||
|
${'ASC'} | ${'UPDATED'} | ${['one', 'two']} | ${{ name: 'one', sort: 'UPDATED_ASC' }}
|
||||||
|
`(
|
||||||
|
'with sort equal to $sort, orderBy equal to $orderBy, search set to $search API call has the variables set as $payload',
|
||||||
|
async ({ sort, orderBy, search, payload }) => {
|
||||||
|
const resolver = jest.fn().mockResolvedValue({ sort, orderBy });
|
||||||
|
mountComponent({ query: { sort, orderBy, search }, resolver });
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(resolver).toHaveBeenCalledWith(expect.objectContaining(payload));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
RSpec.shared_examples 'issuable invite members experiments' do
|
RSpec.shared_examples 'issuable invite members experiments' do
|
||||||
context 'when a privileged user can invite' do
|
context 'when a privileged user can invite' do
|
||||||
it 'shows a link for inviting members and follows through to the members page' do
|
it 'shows a link for inviting members and launches invite modal' do
|
||||||
project.add_maintainer(user)
|
project.add_maintainer(user)
|
||||||
visit issuable_path
|
visit issuable_path
|
||||||
|
|
||||||
|
@ -11,14 +11,14 @@ RSpec.shared_examples 'issuable invite members experiments' do
|
||||||
wait_for_requests
|
wait_for_requests
|
||||||
|
|
||||||
page.within '.dropdown-menu-user' do
|
page.within '.dropdown-menu-user' do
|
||||||
expect(page).to have_link('Invite Members', href: project_project_members_path(project))
|
expect(page).to have_link('Invite Members')
|
||||||
expect(page).to have_selector('[data-track-event="click_invite_members"]')
|
expect(page).to have_selector('[data-track-event="click_invite_members"]')
|
||||||
expect(page).to have_selector('[data-track-label="edit_assignee"]')
|
expect(page).to have_selector('[data-track-label="edit_assignee"]')
|
||||||
end
|
end
|
||||||
|
|
||||||
click_link 'Invite Members'
|
click_link 'Invite Members'
|
||||||
|
|
||||||
expect(current_path).to eq project_project_members_path(project)
|
expect(page).to have_content("You're inviting members to the")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
0
vendor/gitignore/C++.gitignore
vendored
Executable file → Normal file
0
vendor/gitignore/C++.gitignore
vendored
Executable file → Normal file
0
vendor/gitignore/Java.gitignore
vendored
Executable file → Normal file
0
vendor/gitignore/Java.gitignore
vendored
Executable file → Normal file
Loading…
Reference in a new issue