Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/review-apps/dast-api.gitlab-ci.yml14
-rw-r--r--.gitlab/ci/review-apps/main.gitlab-ci.yml1
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum6
-rw-r--r--Gemfile.lock8
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue1
-rw-r--r--app/assets/javascripts/groups/components/app.vue14
-rw-r--r--app/assets/javascripts/groups/components/overview_tabs.vue113
-rw-r--r--app/assets/javascripts/groups/constants.js23
-rw-r--r--app/assets/javascripts/groups/init_overview_tabs.js2
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue71
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue117
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue114
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue70
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_old.vue117
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue10
-rw-r--r--app/helpers/groups_helper.rb3
-rw-r--r--app/views/registrations/welcome/show.html.haml4
-rw-r--r--config/feature_flags/development/gl_avatar_for_all_user_avatars.yml8
-rw-r--r--db/docs/project_wiki_repository_states.yml9
-rw-r--r--db/migrate/20220928201920_create_project_wiki_repository_states.rb44
-rw-r--r--db/schema_migrations/202209282019201
-rw-r--r--db/structure.sql26
-rw-r--r--doc/administration/object_storage.md4
-rw-r--r--doc/api/groups.md4
-rw-r--r--doc/api/projects.md8
-rw-r--r--doc/ci/docker/using_docker_images.md4
-rw-r--r--doc/integration/vault.md188
-rw-r--r--doc/update/index.md2
-rw-r--r--doc/user/project/milestones/index.md3
-rw-r--r--doc/user/project/quick_actions.md3
-rw-r--r--lib/banzai/filter/markdown_engines/common_mark.rb2
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml1
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--qa/qa/page/component/groups_filter.rb2
-rw-r--r--spec/features/boards/boards_spec.rb1
-rw-r--r--spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb1
-rw-r--r--spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb1
-rw-r--r--spec/features/projects/user_sorts_projects_spec.rb22
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js2
-rw-r--r--spec/frontend/groups/components/app_spec.js44
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js145
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js134
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js127
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js143
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js103
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js103
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js119
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js25
-rw-r--r--spec/helpers/groups_helper_spec.rb5
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb6
54 files changed, 862 insertions, 1245 deletions
diff --git a/.gitlab/ci/review-apps/dast-api.gitlab-ci.yml b/.gitlab/ci/review-apps/dast-api.gitlab-ci.yml
new file mode 100644
index 00000000000..e2f32f120af
--- /dev/null
+++ b/.gitlab/ci/review-apps/dast-api.gitlab-ci.yml
@@ -0,0 +1,14 @@
+include:
+ - template: DAST-API.gitlab-ci.yml
+
+dast_api:
+ variables:
+ DAST_API_PROFILE: Passive
+ DAST_API_GRAPHQL: /api/graphql
+ DAST_API_TARGET_URL: ${CI_ENVIRONMENT_URL}
+ DAST_API_OVERRIDES_ENV: "{\"headers\":{\"Authorization\":\"Bearer $REVIEW_APPS_ROOT_TOKEN\"}}"
+ needs: ["review-deploy"]
+ # Uncomment resource_group if DAST_API_PROFILE is changed to an active scan
+ # resource_group: dast_api_scan
+ rules:
+ - !reference [".reports:rules:schedule-dast", rules]
diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml
index 77e0bbff6a7..4c0a3579c92 100644
--- a/.gitlab/ci/review-apps/main.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml
@@ -14,6 +14,7 @@ include:
- local: .gitlab/ci/review-apps/rules.gitlab-ci.yml
- local: .gitlab/ci/review-apps/qa.gitlab-ci.yml
- local: .gitlab/ci/review-apps/dast.gitlab-ci.yml
+ - local: .gitlab/ci/review-apps/dast-api.gitlab-ci.yml
.base-before_script: &base-before_script
- source ./scripts/utils.sh
diff --git a/Gemfile b/Gemfile
index 25d21017bff..8a8cf9e94b1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -357,7 +357,7 @@ gem 'warning', '~> 1.3.0'
group :development do
gem 'lefthook', '~> 1.1.1', require: false
gem 'rubocop'
- gem 'solargraph', '~> 0.46.0', require: false
+ gem 'solargraph', '~> 0.47.2', require: false
gem 'letter_opener_web', '~> 2.0.0'
gem 'lookbook', '~> 1.0', '>= 1.0.8'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index c22dc2c8804..1216b526045 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -458,7 +458,7 @@
{"name":"redis-namespace","version":"1.9.0","platform":"ruby","checksum":"0923961f38cf15b86cb57d92507e0a3b32480729eb5033249f5de8b12e0d8612"},
{"name":"redis-rack","version":"2.1.4","platform":"ruby","checksum":"0872eecb303e483c3863d6bd0d47323d230640d41c1a4ac4a2c7596ec0b1774c"},
{"name":"redis-store","version":"1.9.1","platform":"ruby","checksum":"7b4c7438d46f7b7ce8f67fc0eda3a04fc67d32d28cf606cc98a5df4d2b77071d"},
-{"name":"regexp_parser","version":"2.5.0","platform":"ruby","checksum":"a076d2d35ab8d11feab5fecf8aa09ec6df68c2429810748cba079f7b021ecde5"},
+{"name":"regexp_parser","version":"2.6.0","platform":"ruby","checksum":"f163ba463a45ca2f2730e0902f2475bb0eefcd536dfc2f900a86d1e5a7d7a556"},
{"name":"regexp_property_values","version":"1.0.0","platform":"java","checksum":"5e26782b01241616855c4ee7bb8a62fce9387e484f2d3eaf04f2a0633708222e"},
{"name":"regexp_property_values","version":"1.0.0","platform":"ruby","checksum":"162499dc0bba1e66d334273a059f207a61981cc8cc69d2ca743594e7886d080f"},
{"name":"representable","version":"3.0.4","platform":"ruby","checksum":"07d43917dea4712ecebd19c1909e769deed863ad444d23ceb6461519e2cba962"},
@@ -544,7 +544,7 @@
{"name":"slack-messenger","version":"2.3.4","platform":"ruby","checksum":"49c611d2be5b0f9c250a3a957b9cc09b9c07b81dacb9843642d87b6fa35609c1"},
{"name":"snaky_hash","version":"2.0.0","platform":"ruby","checksum":"fe8b2e39e8ff69320f7812af73ea06401579e29ff1734a7009567391600687de"},
{"name":"snowplow-tracker","version":"0.6.1","platform":"ruby","checksum":"9cec52fd060619f4974b3dc1f7d9a2776c5e31b668a6ead53145b9780e312314"},
-{"name":"solargraph","version":"0.46.0","platform":"ruby","checksum":"1da9fd8c364501f18b0454e54506e7098bc38dae719219713fe5f246dfc91465"},
+{"name":"solargraph","version":"0.47.2","platform":"ruby","checksum":"87ca4b799b9155c2c31c15954c483e952fdacd800f52d6709b901dd447bcac6a"},
{"name":"sorted_set","version":"1.0.3","platform":"java","checksum":"996283f2e5c6e838825bcdcee31d6306515ae5f24bcb0ee4ce09dfff32919b8c"},
{"name":"sorted_set","version":"1.0.3","platform":"ruby","checksum":"4f2b8bee6e8c59cbd296228c0f1f81679357177a8b6859dcc2a99e86cce6372f"},
{"name":"spamcheck","version":"1.0.0","platform":"ruby","checksum":"dfeea085184091353e17d729d2f3d714b07cba36aaf64c32dfc35ce9b466fc9c"},
@@ -576,7 +576,7 @@
{"name":"text","version":"1.3.1","platform":"ruby","checksum":"2fbbbc82c1ce79c4195b13018a87cbb00d762bda39241bb3cdc32792759dd3f4"},
{"name":"thor","version":"1.2.1","platform":"ruby","checksum":"b1752153dc9c6b8d3fcaa665e9e1a00a3e73f28da5e238b81c404502e539d446"},
{"name":"thrift","version":"0.16.0","platform":"ruby","checksum":"d023286ea89e30444c9f1c28dd76107f87d8aaf85fe1742da1d8cd3b5417dcce"},
-{"name":"tilt","version":"2.0.10","platform":"ruby","checksum":"9b664f0e9ae2b500cfa00f9c65c34abc6ff1799cf0034a8c0a0412d520fac866"},
+{"name":"tilt","version":"2.0.11","platform":"ruby","checksum":"7b180fc472cbdeb186c85d31c0f2d1e61a2c0d77e1d9fd0ca28482a9d972d6a0"},
{"name":"timecop","version":"0.9.1","platform":"ruby","checksum":"374b543f0961dbd487e96d09ac812d4fdfeb603ec705bbff241ba060d0a9f534"},
{"name":"timeliness","version":"0.3.10","platform":"ruby","checksum":"c357233ce19dc53148e8b29dfddde134689f18f52b32928e9dfe12ebcf4a773f"},
{"name":"timfel-krb5-auth","version":"0.8.3","platform":"ruby","checksum":"ab388c9d747fa3cd95baf2cc1c03253e372d8c680adcc543670f4f099854bb80"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 8b023661438..2a42fa30826 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1144,7 +1144,7 @@ GEM
redis-store (>= 1.2, < 2)
redis-store (1.9.1)
redis (>= 4, < 5)
- regexp_parser (2.5.0)
+ regexp_parser (2.6.0)
regexp_property_values (1.0.0)
representable (3.0.4)
declarative (< 0.1.0)
@@ -1333,7 +1333,7 @@ GEM
version_gem (~> 1.1)
snowplow-tracker (0.6.1)
contracts (~> 0.7, <= 0.11)
- solargraph (0.46.0)
+ solargraph (0.47.2)
backport (~> 1.2)
benchmark
bundler (>= 1.17.2)
@@ -1402,7 +1402,7 @@ GEM
text (1.3.1)
thor (1.2.1)
thrift (0.16.0)
- tilt (2.0.10)
+ tilt (2.0.11)
timecop (0.9.1)
timeliness (0.3.10)
timfel-krb5-auth (0.8.3)
@@ -1783,7 +1783,7 @@ DEPENDENCIES
simplecov-lcov (~> 0.8.0)
slack-messenger (~> 2.3.4)
snowplow-tracker (~> 0.6.1)
- solargraph (~> 0.46.0)
+ solargraph (~> 0.47.2)
spamcheck (~> 1.0.0)
spring (~> 2.1.0)
spring-commands-rspec (~> 1.0.4)
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 956d62c29f3..93beb014099 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -397,7 +397,6 @@ export default {
:img-size="avatarSize"
class="js-no-trigger user-avatar-link"
tooltip-placement="bottom"
- :enforce-gl-avatar="true"
>
<span class="js-assignee-tooltip">
<span class="gl-font-weight-bold gl-display-block">{{ __('Assignee') }}</span>
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 6caf5aa8e94..d74cb2d8175 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -97,6 +97,7 @@ export default {
eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
eventHub.$on(`${this.action}updatePagination`, this.updatePagination);
eventHub.$on(`${this.action}updateGroups`, this.updateGroups);
+ eventHub.$on(`${this.action}fetchFilteredAndSortedGroups`, this.fetchFilteredAndSortedGroups);
},
mounted() {
this.fetchAllGroups();
@@ -111,6 +112,7 @@ export default {
eventHub.$off(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
eventHub.$off(`${this.action}updatePagination`, this.updatePagination);
eventHub.$off(`${this.action}updateGroups`, this.updateGroups);
+ eventHub.$off(`${this.action}fetchFilteredAndSortedGroups`, this.fetchFilteredAndSortedGroups);
},
methods: {
hideModal() {
@@ -153,6 +155,18 @@ export default {
this.updateGroups(res, Boolean(this.filterGroupsBy));
});
},
+ fetchFilteredAndSortedGroups({ filterGroupsBy, sortBy }) {
+ this.isLoading = true;
+
+ return this.fetchGroups({
+ filterGroupsBy,
+ sortBy,
+ updatePagination: true,
+ }).then((res) => {
+ this.isLoading = false;
+ this.updateGroups(res, Boolean(filterGroupsBy));
+ });
+ },
fetchPage({ page, filterGroupsBy, sortBy, archived }) {
this.isLoading = true;
diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue
index 325e42af0f8..84e992b6365 100644
--- a/app/assets/javascripts/groups/components/overview_tabs.vue
+++ b/app/assets/javascripts/groups/components/overview_tabs.vue
@@ -1,6 +1,6 @@
<script>
-import { GlTabs, GlTab } from '@gitlab/ui';
-import { isString } from 'lodash';
+import { GlTabs, GlTab, GlSearchBoxByType, GlSorting, GlSortingItem } from '@gitlab/ui';
+import { isString, debounce } from 'lodash';
import { __ } from '~/locale';
import GroupsStore from '../store/groups_store';
import GroupsService from '../service/groups_service';
@@ -8,12 +8,16 @@ import {
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
ACTIVE_TAB_SHARED,
ACTIVE_TAB_ARCHIVED,
+ OVERVIEW_TABS_SORTING_ITEMS,
} from '../constants';
+import eventHub from '../event_hub';
import GroupsApp from './app.vue';
+const [SORTING_ITEM_NAME] = OVERVIEW_TABS_SORTING_ITEMS;
+
export default {
- components: { GlTabs, GlTab, GroupsApp },
- inject: ['endpoints'],
+ components: { GlTabs, GlTab, GroupsApp, GlSearchBoxByType, GlSorting, GlSortingItem },
+ inject: ['endpoints', 'initialSort'],
data() {
return {
tabs: [
@@ -43,9 +47,35 @@ export default {
},
],
activeTabIndex: 0,
+ sort: SORTING_ITEM_NAME,
+ isAscending: true,
+ search: '',
};
},
+ computed: {
+ activeTab() {
+ return this.tabs[this.activeTabIndex];
+ },
+ sortQueryStringValue() {
+ return this.isAscending ? this.sort.asc : this.sort.desc;
+ },
+ },
+ watch: {
+ search: debounce(async function debouncedSearch() {
+ this.handleSearchOrSortChange();
+ }, 250),
+ },
mounted() {
+ this.search = this.$route.query?.filter || '';
+
+ const sortQueryStringValue = this.$route.query?.sort || this.initialSort;
+ const sort =
+ OVERVIEW_TABS_SORTING_ITEMS.find((sortOption) =>
+ [sortOption.asc, sortOption.desc].includes(sortQueryStringValue),
+ ) || SORTING_ITEM_NAME;
+ this.sort = sort;
+ this.isAscending = sort.asc === sortQueryStringValue;
+
const activeTabIndex = this.tabs.findIndex((tab) => tab.key === this.$route.name);
if (activeTabIndex === -1) {
@@ -72,14 +102,56 @@ export default {
? this.$route.params.group.split('/')
: this.$route.params.group;
- this.$router.push({ name: tab.key, params: { group: groupParam } });
+ this.$router.push({ name: tab.key, params: { group: groupParam }, query: this.$route.query });
+ },
+ handleSearchOrSortChange() {
+ // Update query string
+ const query = {};
+ if (this.sortQueryStringValue !== this.initialSort) {
+ query.sort = this.isAscending ? this.sort.asc : this.sort.desc;
+ }
+ if (this.search) {
+ query.filter = this.search;
+ }
+ this.$router.push({ query });
+
+ // Reset `lazy` prop so that groups/projects are fetched with updated `sort` and `filter` params when switching tabs
+ this.tabs.forEach((tab, index) => {
+ if (index === this.activeTabIndex) {
+ return;
+ }
+ // eslint-disable-next-line no-param-reassign
+ tab.lazy = true;
+ });
+
+ // Update data
+ eventHub.$emit(`${this.activeTab.key}fetchFilteredAndSortedGroups`, {
+ filterGroupsBy: this.search,
+ sortBy: this.sortQueryStringValue,
+ });
+ },
+ handleSortDirectionChange() {
+ this.isAscending = !this.isAscending;
+
+ this.handleSearchOrSortChange();
+ },
+ handleSortingItemClick(sortingItem) {
+ if (sortingItem === this.sort) {
+ return;
+ }
+
+ this.sort = sortingItem;
+
+ this.handleSearchOrSortChange();
},
},
i18n: {
[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]: __('Subgroups and projects'),
[ACTIVE_TAB_SHARED]: __('Shared projects'),
[ACTIVE_TAB_ARCHIVED]: __('Archived projects'),
+ searchPlaceholder: __('Search'),
},
+ OVERVIEW_TABS_SORTING_ITEMS,
};
</script>
@@ -99,5 +171,36 @@ export default {
:render-empty-state="renderEmptyState"
/>
</gl-tab>
+ <template #tabs-end>
+ <li class="gl-flex-grow-1 gl-align-self-center gl-w-full gl-lg-w-auto gl-py-2">
+ <div class="gl-lg-display-flex gl-justify-content-end gl-mx-n2 gl-my-n2">
+ <div class="gl-p-2 gl-lg-form-input-md gl-w-full">
+ <gl-search-box-by-type
+ v-model="search"
+ :placeholder="$options.i18n.searchPlaceholder"
+ data-qa-selector="groups_filter_field"
+ />
+ </div>
+ <div class="gl-p-2 gl-w-full gl-lg-w-auto">
+ <gl-sorting
+ class="gl-w-full"
+ dropdown-class="gl-w-full"
+ data-testid="group_sort_by_dropdown"
+ :text="sort.label"
+ :is-ascending="isAscending"
+ @sortDirectionChange="handleSortDirectionChange"
+ >
+ <gl-sorting-item
+ v-for="sortingItem in $options.OVERVIEW_TABS_SORTING_ITEMS"
+ :key="sortingItem.label"
+ :active="sortingItem === sort"
+ @click="handleSortingItemClick(sortingItem)"
+ >{{ sortingItem.label }}</gl-sorting-item
+ >
+ </gl-sorting>
+ </div>
+ </div>
+ </li>
+ </template>
</gl-tabs>
</template>
diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js
index 223c2975c11..33bfcade336 100644
--- a/app/assets/javascripts/groups/constants.js
+++ b/app/assets/javascripts/groups/constants.js
@@ -62,3 +62,26 @@ export const VISIBILITY_TYPE_ICON = {
[VISIBILITY_LEVEL_INTERNAL_STRING]: 'shield',
[VISIBILITY_LEVEL_PRIVATE_STRING]: 'lock',
};
+
+export const OVERVIEW_TABS_SORTING_ITEMS = [
+ {
+ label: __('Name'),
+ asc: 'name_asc',
+ desc: 'name_desc',
+ },
+ {
+ label: __('Created'),
+ asc: 'created_asc',
+ desc: 'created_desc',
+ },
+ {
+ label: __('Updated'),
+ asc: 'latest_activity_asc',
+ desc: 'latest_activity_desc',
+ },
+ {
+ label: __('Stars'),
+ asc: 'stars_asc',
+ desc: 'stars_desc',
+ },
+];
diff --git a/app/assets/javascripts/groups/init_overview_tabs.js b/app/assets/javascripts/groups/init_overview_tabs.js
index 4fa3682c729..664d07ca13d 100644
--- a/app/assets/javascripts/groups/init_overview_tabs.js
+++ b/app/assets/javascripts/groups/init_overview_tabs.js
@@ -51,6 +51,7 @@ export const initGroupOverviewTabs = () => {
subgroupsAndProjectsEndpoint,
sharedProjectsEndpoint,
archivedProjectsEndpoint,
+ initialSort,
} = el.dataset;
return new Vue({
@@ -70,6 +71,7 @@ export const initGroupOverviewTabs = () => {
[ACTIVE_TAB_SHARED]: sharedProjectsEndpoint,
[ACTIVE_TAB_ARCHIVED]: archivedProjectsEndpoint,
},
+ initialSort,
},
render(createElement) {
return createElement(OverviewTabs);
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 22fe3fe440e..f2f3d7ebeab 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -122,14 +122,12 @@ export default {
:link-href="commit.author.webPath"
:img-src="commit.author.avatarUrl"
:img-size="32"
- :img-css-classes="'gl-mr-0!' /* NOTE: this is needed only while we migrate user-avatar-image to GlAvatar (7731 epics) */"
class="gl-my-2 gl-mr-4"
/>
<user-avatar-image
v-else
class="gl-my-2 gl-mr-4"
:img-src="commit.authorGravatar || $options.defaultAvatarUrl"
- :css-classes="'gl-mr-0!' /* NOTE: this is needed only while we migrate user-avatar-image to GlAvatar (7731 epics) */"
:size="32"
/>
<div
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index c1e618620d8..6552a874c3a 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -5,29 +5,29 @@
Sample configuration:
- <user-avatar-image
+ <user-avatar
lazy
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
+ :size="24"
/>
*/
+import { GlTooltip, GlAvatar } from '@gitlab/ui';
+import { isObject } from 'lodash';
import defaultAvatarUrl from 'images/no_avatar.png';
import { __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import UserAvatarImageNew from './user_avatar_image_new.vue';
-import UserAvatarImageOld from './user_avatar_image_old.vue';
+import { placeholderImage } from '~/lazy_loader';
export default {
name: 'UserAvatarImage',
components: {
- UserAvatarImageNew,
- UserAvatarImageOld,
+ GlTooltip,
+ GlAvatar,
},
- mixins: [glFeatureFlagMixin()],
props: {
lazy: {
type: Boolean,
@@ -51,8 +51,7 @@ export default {
},
size: {
type: [Number, Object],
- required: false,
- default: 20,
+ required: true,
},
tooltipText: {
type: String,
@@ -64,22 +63,52 @@ export default {
required: false,
default: 'top',
},
- enforceGlAvatar: {
- type: Boolean,
- required: false,
+ },
+ computed: {
+ // API response sends null when gravatar is disabled and
+ // we provide an empty string when we use it inside user avatar link.
+ // In both cases we should render the defaultAvatarUrl
+ sanitizedSource() {
+ let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
+ // Only adds the width to the URL if its not a base64 data image
+ if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
+ baseSrc += `?width=${this.maximumSize}`;
+ return baseSrc;
+ },
+ maximumSize() {
+ if (isObject(this.size)) {
+ return Math.max(...Object.values(this.size));
+ }
+
+ return this.size;
+ },
+ resultantSrcAttribute() {
+ return this.lazy ? placeholderImage : this.sanitizedSource;
},
},
};
</script>
<template>
- <user-avatar-image-new
- v-if="glFeatures.glAvatarForAllUserAvatars || enforceGlAvatar"
- v-bind="$props"
- >
- <slot></slot>
- </user-avatar-image-new>
- <user-avatar-image-old v-else v-bind="$props">
- <slot></slot>
- </user-avatar-image-old>
+ <span ref="userAvatar">
+ <gl-avatar
+ :class="{
+ lazy: lazy,
+ [cssClasses]: true,
+ }"
+ :src="resultantSrcAttribute"
+ :data-src="sanitizedSource"
+ :size="size"
+ :alt="imgAlt"
+ />
+
+ <gl-tooltip
+ v-if="tooltipText || $scopedSlots.default"
+ :target="() => $refs.userAvatar"
+ :placement="tooltipPlacement"
+ boundary="window"
+ >
+ <slot>{{ tooltipText }}</slot>
+ </gl-tooltip>
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue
deleted file mode 100644
index 6bd66981860..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar that
- does not need to link to the user's profile. The image and an optional
- tooltip can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar
- lazy
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :tooltip-text="tooltipText"
- tooltip-placement="top"
- />
-
- */
-
-import { GlTooltip, GlAvatar } from '@gitlab/ui';
-import { isObject } from 'lodash';
-import defaultAvatarUrl from 'images/no_avatar.png';
-import { __ } from '~/locale';
-import { placeholderImage } from '~/lazy_loader';
-
-export default {
- name: 'UserAvatarImageNew',
- components: {
- GlTooltip,
- GlAvatar,
- },
- props: {
- lazy: {
- type: Boolean,
- required: false,
- default: false,
- },
- imgSrc: {
- type: String,
- required: false,
- default: defaultAvatarUrl,
- },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
- imgAlt: {
- type: String,
- required: false,
- default: __('user avatar'),
- },
- size: {
- type: [Number, Object],
- required: false,
- default: 20,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- },
- computed: {
- // API response sends null when gravatar is disabled and
- // we provide an empty string when we use it inside user avatar link.
- // In both cases we should render the defaultAvatarUrl
- sanitizedSource() {
- let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
- // Only adds the width to the URL if its not a base64 data image
- if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
- baseSrc += `?width=${this.maximumSize}`;
- return baseSrc;
- },
- maximumSize() {
- if (isObject(this.size)) {
- return Math.max(...Object.values(this.size));
- }
-
- return this.size;
- },
- resultantSrcAttribute() {
- return this.lazy ? placeholderImage : this.sanitizedSource;
- },
- },
-};
-</script>
-
-<template>
- <span ref="userAvatar">
- <gl-avatar
- :class="{
- lazy: lazy,
- [cssClasses]: true,
- }"
- :src="resultantSrcAttribute"
- :data-src="sanitizedSource"
- :size="size"
- :alt="imgAlt"
- />
-
- <gl-tooltip
- v-if="
- tooltipText ||
- $slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
- "
- :target="() => $refs.userAvatar"
- :placement="tooltipPlacement"
- boundary="window"
- >
- <slot>{{ tooltipText }}</slot>
- </gl-tooltip>
- </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue
deleted file mode 100644
index 6e8c200d5c3..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar that
- does not need to link to the user's profile. The image and an optional
- tooltip can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar-image
- lazy
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :tooltip-text="tooltipText"
- tooltip-placement="top"
- />
-
- */
-
-import { GlTooltip } from '@gitlab/ui';
-import defaultAvatarUrl from 'images/no_avatar.png';
-import { __ } from '~/locale';
-import { placeholderImage } from '~/lazy_loader';
-
-export default {
- name: 'UserAvatarImageOld',
- components: {
- GlTooltip,
- },
- props: {
- lazy: {
- type: Boolean,
- required: false,
- default: false,
- },
- imgSrc: {
- type: String,
- required: false,
- default: defaultAvatarUrl,
- },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
- imgAlt: {
- type: String,
- required: false,
- default: __('user avatar'),
- },
- size: {
- type: Number,
- required: false,
- default: 20,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- },
- computed: {
- // API response sends null when gravatar is disabled and
- // we provide an empty string when we use it inside user avatar link.
- // In both cases we should render the defaultAvatarUrl
- sanitizedSource() {
- let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
- // Only adds the width to the URL if its not a base64 data image
- if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
- baseSrc += `?width=${this.size}`;
- return baseSrc;
- },
- resultantSrcAttribute() {
- return this.lazy ? placeholderImage : this.sanitizedSource;
- },
- avatarSizeClass() {
- return `s${this.size}`;
- },
- },
-};
-</script>
-
-<template>
- <span>
- <img
- ref="userAvatarImage"
- :class="{
- lazy: lazy,
- [avatarSizeClass]: true,
- [cssClasses]: true,
- }"
- :src="resultantSrcAttribute"
- :width="size"
- :height="size"
- :alt="imgAlt"
- :data-src="sanitizedSource"
- class="avatar"
- />
- <gl-tooltip
- v-if="
- tooltipText ||
- $slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
- "
- :target="() => $refs.userAvatarImage"
- :placement="tooltipPlacement"
- boundary="window"
- >
- <slot>{{ tooltipText }}</slot>
- </gl-tooltip>
- </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index f80abed4d69..1a81da3eb0d 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -9,7 +9,7 @@
:link-href="userProfileUrl"
:img-src="userAvatarSrc"
:img-alt="tooltipText"
- :img-size="20"
+ :img-size="32"
:tooltip-text="tooltipText"
:tooltip-placement="top"
:username="username"
@@ -17,17 +17,18 @@
*/
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import UserAvatarLinkNew from './user_avatar_link_new.vue';
-import UserAvatarLinkOld from './user_avatar_link_old.vue';
+import { GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
+import UserAvatarImage from './user_avatar_image.vue';
export default {
- name: 'UserAvatarLink',
+ name: 'UserAvatarLinkNew',
components: {
- UserAvatarLinkNew,
- UserAvatarLinkOld,
+ UserAvatarImage,
+ GlAvatarLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagMixin()],
props: {
lazy: {
type: Boolean,
@@ -56,8 +57,7 @@ export default {
},
imgSize: {
type: [Number, Object],
- required: false,
- default: 20,
+ required: true,
},
tooltipText: {
type: String,
@@ -74,29 +74,43 @@ export default {
required: false,
default: '',
},
- enforceGlAvatar: {
- type: Boolean,
- required: false,
+ },
+ computed: {
+ shouldShowUsername() {
+ return this.username.length > 0;
+ },
+ avatarTooltipText() {
+ return this.shouldShowUsername ? '' : this.tooltipText;
},
},
};
</script>
<template>
- <user-avatar-link-new
- v-if="glFeatures.glAvatarForAllUserAvatars || enforceGlAvatar"
- v-bind="$props"
- >
- <slot></slot>
- <template #avatar-badge>
- <slot name="avatar-badge"></slot>
- </template>
- </user-avatar-link-new>
+ <gl-avatar-link :href="linkHref" class="user-avatar-link">
+ <user-avatar-image
+ :img-src="imgSrc"
+ :img-alt="imgAlt"
+ :css-classes="imgCssClasses"
+ :size="imgSize"
+ :tooltip-text="avatarTooltipText"
+ :tooltip-placement="tooltipPlacement"
+ :lazy="lazy"
+ >
+ <slot></slot>
+ </user-avatar-image>
+
+ <span
+ v-if="shouldShowUsername"
+ v-gl-tooltip
+ :title="tooltipText"
+ :tooltip-placement="tooltipPlacement"
+ class="gl-ml-3"
+ data-testid="user-avatar-link-username"
+ >
+ {{ username }}
+ </span>
- <user-avatar-link-old v-else v-bind="$props">
- <slot></slot>
- <template #avatar-badge>
- <slot name="avatar-badge"></slot>
- </template>
- </user-avatar-link-old>
+ <slot name="avatar-badge"></slot>
+ </gl-avatar-link>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue
deleted file mode 100644
index 83551c689c4..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar wrapped in
- a clickable link (likely to the user's profile). The link, image, and
- tooltip can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar-link
- :link-href="userProfileUrl"
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :img-size="20"
- :tooltip-text="tooltipText"
- :tooltip-placement="top"
- :username="username"
- />
-
-*/
-
-import { GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
-import UserAvatarImage from './user_avatar_image.vue';
-
-export default {
- name: 'UserAvatarLinkNew',
- components: {
- UserAvatarImage,
- GlAvatarLink,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- lazy: {
- type: Boolean,
- required: false,
- default: false,
- },
- linkHref: {
- type: String,
- required: false,
- default: '',
- },
- imgSrc: {
- type: String,
- required: false,
- default: '',
- },
- imgAlt: {
- type: String,
- required: false,
- default: '',
- },
- imgCssClasses: {
- type: String,
- required: false,
- default: '',
- },
- imgSize: {
- type: [Number, Object],
- required: false,
- default: 20,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- username: {
- type: String,
- required: false,
- default: '',
- },
- enforceGlAvatar: {
- type: Boolean,
- required: false,
- },
- },
- computed: {
- shouldShowUsername() {
- return this.username.length > 0;
- },
- avatarTooltipText() {
- return this.shouldShowUsername ? '' : this.tooltipText;
- },
- },
-};
-</script>
-
-<template>
- <gl-avatar-link :href="linkHref" class="user-avatar-link">
- <user-avatar-image
- :img-src="imgSrc"
- :img-alt="imgAlt"
- :css-classes="imgCssClasses"
- :size="imgSize"
- :tooltip-text="avatarTooltipText"
- :tooltip-placement="tooltipPlacement"
- :lazy="lazy"
- :enforce-gl-avatar="enforceGlAvatar"
- >
- <slot></slot>
- </user-avatar-image>
-
- <span
- v-if="shouldShowUsername"
- v-gl-tooltip
- :title="tooltipText"
- :tooltip-placement="tooltipPlacement"
- class="gl-ml-3"
- data-testid="user-avatar-link-username"
- >
- {{ username }}
- </span>
-
- <slot name="avatar-badge"></slot>
- </gl-avatar-link>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_old.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_old.vue
deleted file mode 100644
index c2e46e61e1b..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_old.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar wrapped in
- a clickable link (likely to the user's profile). The link, image, and
- tooltip can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar-link
- :link-href="userProfileUrl"
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :img-size="20"
- :tooltip-text="tooltipText"
- :tooltip-placement="top"
- :username="username"
- />
-
-*/
-
-import { GlLink, GlTooltipDirective } from '@gitlab/ui';
-import UserAvatarImage from './user_avatar_image.vue';
-
-export default {
- name: 'UserAvatarLinkOld',
- components: {
- GlLink,
- UserAvatarImage,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- lazy: {
- type: Boolean,
- required: false,
- default: false,
- },
- linkHref: {
- type: String,
- required: false,
- default: '',
- },
- imgSrc: {
- type: String,
- required: false,
- default: '',
- },
- imgAlt: {
- type: String,
- required: false,
- default: '',
- },
- imgCssClasses: {
- type: String,
- required: false,
- default: '',
- },
- imgSize: {
- type: Number,
- required: false,
- default: 20,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- username: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- shouldShowUsername() {
- return this.username.length > 0;
- },
- avatarTooltipText() {
- return this.shouldShowUsername ? '' : this.tooltipText;
- },
- },
-};
-</script>
-
-<template>
- <span>
- <gl-link :href="linkHref" class="user-avatar-link">
- <user-avatar-image
- :img-src="imgSrc"
- :img-alt="imgAlt"
- :css-classes="imgCssClasses"
- :size="imgSize"
- :tooltip-text="avatarTooltipText"
- :tooltip-placement="tooltipPlacement"
- :lazy="lazy"
- >
- <slot></slot>
- </user-avatar-image>
-
- <span
- v-if="shouldShowUsername"
- v-gl-tooltip
- :title="tooltipText"
- :tooltip-placement="tooltipPlacement"
- data-testid="user-avatar-link-username"
- >
- {{ username }}
- </span>
- <slot name="avatar-badge"></slot>
- </gl-link>
- </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
index 9da298ad705..231f5ff3d1f 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
@@ -1,6 +1,5 @@
<script>
import { GlButton } from '@gitlab/ui';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { sprintf, __ } from '~/locale';
import UserAvatarLink from './user_avatar_link.vue';
@@ -9,7 +8,6 @@ export default {
UserAvatarLink,
GlButton,
},
- mixins: [glFeatureFlagMixin()],
props: {
items: {
type: Array,
@@ -22,8 +20,7 @@ export default {
},
imgSize: {
type: [Number, Object],
- required: false,
- default: 20,
+ required: true,
},
emptyText: {
type: String,
@@ -59,9 +56,6 @@ export default {
return sprintf(__('%{count} more'), { count });
},
- imgCssClasses() {
- return this.glFeatures.glAvatarForAllUserAvatars ? 'gl-mr-3' : '';
- },
},
methods: {
expand() {
@@ -85,7 +79,7 @@ export default {
:img-alt="item.name"
:tooltip-text="item.name"
:img-size="imgSize"
- :img-css-classes="imgCssClasses"
+ img-css-classes="gl-mr-3"
/>
<template v-if="hasBreakpoint">
<gl-button v-if="hasHiddenItems" variant="link" @click="expand">
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 267371c6b35..6b00c213875 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -177,7 +177,8 @@ module GroupsHelper
subgroups_and_projects_endpoint: group_children_path(group, format: :json),
shared_projects_endpoint: group_shared_projects_path(group, format: :json),
archived_projects_endpoint: group_children_path(group, format: :json, archived: 'only'),
- current_group_visibility: group.visibility
+ current_group_visibility: group.visibility,
+ initial_sort: project_list_sort_by
}.merge(subgroups_and_projects_list_app_data(group))
end
diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml
index 5cff0f562e5..fe455f4a0bc 100644
--- a/app/views/registrations/welcome/show.html.haml
+++ b/app/views/registrations/welcome/show.html.haml
@@ -19,7 +19,9 @@
%p.gl-text-center= html_escape(_('%{gitlab_experience_text}. Don\'t worry, this information isn\'t shared outside of your self-managed GitLab instance.')) % { gitlab_experience_text: gitlab_experience_text }
= gitlab_ui_form_for(current_user,
url: users_sign_up_welcome_path(glm_tracking_params),
- html: { class: 'card gl-w-full! gl-p-5 js-users-signup-welcome', 'aria-live' => 'assertive' }) do |f|
+ html: { class: 'card gl-w-full! gl-p-5 js-users-signup-welcome',
+ 'aria-live' => 'assertive',
+ data: { testid: 'welcome-form' } }) do |f|
.devise-errors
= render 'devise/shared/error_messages', resource: current_user
.row
diff --git a/config/feature_flags/development/gl_avatar_for_all_user_avatars.yml b/config/feature_flags/development/gl_avatar_for_all_user_avatars.yml
deleted file mode 100644
index a3fee67a7f6..00000000000
--- a/config/feature_flags/development/gl_avatar_for_all_user_avatars.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: gl_avatar_for_all_user_avatars
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81437
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353477
-milestone: '14.9'
-type: development
-group: group::foundations
-default_enabled: false
diff --git a/db/docs/project_wiki_repository_states.yml b/db/docs/project_wiki_repository_states.yml
new file mode 100644
index 00000000000..b074eca3c89
--- /dev/null
+++ b/db/docs/project_wiki_repository_states.yml
@@ -0,0 +1,9 @@
+---
+table_name: project_wiki_repository_states
+classes:
+- ProjectWikiRepositoryState
+feature_categories:
+- geo_replication
+description: Separate table for project wikis containing Geo verification metadata.
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/99168
+milestone: '15.5'
diff --git a/db/migrate/20220928201920_create_project_wiki_repository_states.rb b/db/migrate/20220928201920_create_project_wiki_repository_states.rb
new file mode 100644
index 00000000000..17ad5cf6b7a
--- /dev/null
+++ b/db/migrate/20220928201920_create_project_wiki_repository_states.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+class CreateProjectWikiRepositoryStates < Gitlab::Database::Migration[2.0]
+ VERIFICATION_STATE_INDEX_NAME = "index_project_wiki_repository_states_on_verification_state"
+ PENDING_VERIFICATION_INDEX_NAME = "index_project_wiki_repository_states_pending_verification"
+ FAILED_VERIFICATION_INDEX_NAME = "index_project_wiki_repository_states_failed_verification"
+ NEEDS_VERIFICATION_INDEX_NAME = "index_project_wiki_repository_states_needs_verification"
+
+ enable_lock_retries!
+
+ def up
+ create_table :project_wiki_repository_states, id: false do |t|
+ t.datetime_with_timezone :verification_started_at
+ t.datetime_with_timezone :verification_retry_at
+ t.datetime_with_timezone :verified_at
+ t.references :project, primary_key: true, default: nil, index: false, foreign_key: { on_delete: :cascade }
+ t.integer :verification_state, default: 0, limit: 2, null: false
+ t.integer :verification_retry_count, limit: 2
+ t.binary :verification_checksum, using: 'verification_checksum::bytea'
+ t.text :verification_failure, limit: 255
+
+ t.index :verification_state,
+ name: VERIFICATION_STATE_INDEX_NAME
+
+ t.index :verified_at,
+ where: "(verification_state = 0)",
+ order: { verified_at: 'ASC NULLS FIRST' },
+ name: PENDING_VERIFICATION_INDEX_NAME
+
+ t.index :verification_retry_at,
+ where: "(verification_state = 3)",
+ order: { verification_retry_at: 'ASC NULLS FIRST' },
+ name: FAILED_VERIFICATION_INDEX_NAME
+
+ t.index :verification_state,
+ where: "(verification_state = 0 OR verification_state = 3)",
+ name: NEEDS_VERIFICATION_INDEX_NAME
+ end
+ end
+
+ def down
+ drop_table :project_wiki_repository_states
+ end
+end
diff --git a/db/schema_migrations/20220928201920 b/db/schema_migrations/20220928201920
new file mode 100644
index 00000000000..e77f9abf6a0
--- /dev/null
+++ b/db/schema_migrations/20220928201920
@@ -0,0 +1 @@
+b2492ebefc3738dfe706379ef664d3f28315102acc1c0681ba67e6aae62861d7 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index e33f31226d8..d34e91f2100 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -20153,6 +20153,18 @@ CREATE SEQUENCE project_topics_id_seq
ALTER SEQUENCE project_topics_id_seq OWNED BY project_topics.id;
+CREATE TABLE project_wiki_repository_states (
+ verification_started_at timestamp with time zone,
+ verification_retry_at timestamp with time zone,
+ verified_at timestamp with time zone,
+ project_id bigint NOT NULL,
+ verification_state smallint DEFAULT 0 NOT NULL,
+ verification_retry_count smallint,
+ verification_checksum bytea,
+ verification_failure text,
+ CONSTRAINT check_119f134b68 CHECK ((char_length(verification_failure) <= 255))
+);
+
CREATE TABLE projects (
id integer NOT NULL,
name character varying,
@@ -26184,6 +26196,9 @@ ALTER TABLE ONLY project_statistics
ALTER TABLE ONLY project_topics
ADD CONSTRAINT project_topics_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY project_wiki_repository_states
+ ADD CONSTRAINT project_wiki_repository_states_pkey PRIMARY KEY (project_id);
+
ALTER TABLE ONLY projects
ADD CONSTRAINT projects_pkey PRIMARY KEY (id);
@@ -29957,6 +29972,14 @@ CREATE INDEX index_project_topics_on_topic_id ON project_topics USING btree (top
CREATE UNIQUE INDEX index_project_user_callouts_feature ON user_project_callouts USING btree (user_id, feature_name, project_id);
+CREATE INDEX index_project_wiki_repository_states_failed_verification ON project_wiki_repository_states USING btree (verification_retry_at NULLS FIRST) WHERE (verification_state = 3);
+
+CREATE INDEX index_project_wiki_repository_states_needs_verification ON project_wiki_repository_states USING btree (verification_state) WHERE ((verification_state = 0) OR (verification_state = 3));
+
+CREATE INDEX index_project_wiki_repository_states_on_verification_state ON project_wiki_repository_states USING btree (verification_state);
+
+CREATE INDEX index_project_wiki_repository_states_pending_verification ON project_wiki_repository_states USING btree (verified_at NULLS FIRST) WHERE (verification_state = 0);
+
CREATE INDEX index_projects_aimed_for_deletion ON projects USING btree (marked_for_deletion_at) WHERE ((marked_for_deletion_at IS NOT NULL) AND (pending_delete = false));
CREATE INDEX index_projects_api_created_at_id_desc ON projects USING btree (created_at, id DESC);
@@ -34240,6 +34263,9 @@ ALTER TABLE ONLY packages_debian_project_distributions
ALTER TABLE ONLY packages_rubygems_metadata
ADD CONSTRAINT fk_rails_95a3f5ce78 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
+ALTER TABLE ONLY project_wiki_repository_states
+ ADD CONSTRAINT fk_rails_9647227ce1 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY packages_pypi_metadata
ADD CONSTRAINT fk_rails_9698717cdd FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md
index c2e27375221..0e85635b3d2 100644
--- a/doc/administration/object_storage.md
+++ b/doc/administration/object_storage.md
@@ -247,8 +247,8 @@ The connection settings match those provided by [fog-aws](https://github.com/fog
| `aws_signature_version` | AWS signature version to use. `2` or `4` are valid options. Digital Ocean Spaces and other providers may need `2`. | `4` |
| `enable_signature_v4_streaming` | Set to `true` to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be `false`. | `true` |
| `region` | AWS region. | |
-| `host` | S3 compatible host for when not using AWS. For example, `localhost` or `storage.example.com`. HTTPS and port 443 is assumed. | `s3.amazonaws.com` |
-| `endpoint` | Can be used when configuring an S3 compatible service such as [MinIO](https://min.io), by entering a URL such as `http://127.0.0.1:9000`. This takes precedence over `host`. | (optional) |
+| `host` | DEPRECATED: Use `endpoint` instead. S3 compatible host for when not using AWS. For example, `localhost` or `storage.example.com`. HTTPS and port 443 is assumed. | `s3.amazonaws.com` |
+| `endpoint` | Can be used when configuring an S3 compatible service such as [MinIO](https://min.io), by entering a URL such as `http://127.0.0.1:9000`. This takes precedence over `host`. Always use `endpoint` for consolidated form. | (optional) |
| `path_style` | Set to `true` to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Set to `true` for using [MinIO](https://min.io). Leave as `false` for AWS S3. | `false`. |
| `use_iam_profile` | Set to `true` to use IAM profile instead of access keys. | `false` |
| `aws_credentials_refresh_threshold_seconds` | Sets the [automatic refresh threshold](https://github.com/fog/fog-aws#controlling-credential-refresh-time-with-iam-authentication) when using temporary credentials in IAM. | `15` |
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 56f1b7f7d7e..c1effc78a4d 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -287,7 +287,7 @@ Parameters:
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, `similarity` (1), or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of authorized projects matching the search criteria |
-| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication where only simple fields are returned. |
| `owned` | boolean | no | Limit by projects owned by the current user |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` |
@@ -370,7 +370,7 @@ Parameters:
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of authorized projects matching the search criteria |
-| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+| `simple` | boolean | no | Return only limited fields for each project. This is a no-op without authentication where only simple fields are returned. |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` |
| `with_merge_requests_enabled` | boolean | no | Limit by projects with merge requests feature enabled. Default is `false` |
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 6bba45418eb..167434a1b32 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -61,7 +61,7 @@ GET /projects
| `repository_storage` | string | **{dotted-circle}** No | Limit results to projects stored on `repository_storage`. _(administrators only)_ |
| `search_namespaces` | boolean | **{dotted-circle}** No | Include ancestor namespaces when matching search criteria. Default is `false`. |
| `search` | string | **{dotted-circle}** No | Return list of projects matching the search criteria. |
-| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
+| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication where only simple fields are returned. |
| `sort` | string | **{dotted-circle}** No | Return projects sorted in `asc` or `desc` order. Default is `desc`. |
| `starred` | boolean | **{dotted-circle}** No | Limit by projects starred by the current user. |
| `statistics` | boolean | **{dotted-circle}** No | Include project statistics. Only available to Reporter or higher level role members. |
@@ -336,7 +336,7 @@ GET /users/:user_id/projects
| `order_by` | string | **{dotted-circle}** No | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at`. |
| `owned` | boolean | **{dotted-circle}** No | Limit by projects explicitly owned by the current user. |
| `search` | string | **{dotted-circle}** No | Return list of projects matching the search criteria. |
-| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
+| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication where only simple fields are returned. |
| `sort` | string | **{dotted-circle}** No | Return projects sorted in `asc` or `desc` order. Default is `desc`. |
| `starred` | boolean | **{dotted-circle}** No | Limit by projects starred by the current user. |
| `statistics` | boolean | **{dotted-circle}** No | Include project statistics. Only available to Reporter or higher level role members. |
@@ -591,7 +591,7 @@ GET /users/:user_id/starred_projects
| `order_by` | string | **{dotted-circle}** No | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at`. |
| `owned` | boolean | **{dotted-circle}** No | Limit by projects explicitly owned by the current user. |
| `search` | string | **{dotted-circle}** No | Return list of projects matching the search criteria. |
-| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
+| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication where only simple fields are returned. |
| `sort` | string | **{dotted-circle}** No | Return projects sorted in `asc` or `desc` order. Default is `desc`. |
| `starred` | boolean | **{dotted-circle}** No | Limit by projects starred by the current user. |
| `statistics` | boolean | **{dotted-circle}** No | Include project statistics. Only available to Reporter or higher level role members. |
@@ -1476,7 +1476,7 @@ GET /projects/:id/forks
| `order_by` | string | **{dotted-circle}** No | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at`. |
| `owned` | boolean | **{dotted-circle}** No | Limit by projects explicitly owned by the current user. |
| `search` | string | **{dotted-circle}** No | Return list of projects matching the search criteria. |
-| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication as then _only_ simple fields are returned. |
+| `simple` | boolean | **{dotted-circle}** No | Return only limited fields for each project. This is a no-op without authentication where only simple fields are returned. |
| `sort` | string | **{dotted-circle}** No | Return projects sorted in `asc` or `desc` order. Default is `desc`. |
| `starred` | boolean | **{dotted-circle}** No | Limit by projects starred by the current user. |
| `statistics` | boolean | **{dotted-circle}** No | Include project statistics. Only available to Reporter or higher level role members. |
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index fa6142802e6..e4011acde27 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -282,8 +282,8 @@ Use one of the following methods to determine the value for `DOCKER_AUTH_CONFIG`
configuration JSON manually. Open a terminal and execute the following command:
```shell
- # The use of "-n" - prevents encoding a newline in the password.
- echo -n "my_username:my_password" | base64
+ # The use of printf (as opposed to echo) prevents encoding a newline in the password.
+ printf "my_username:my_password" | openssl base64 -A
# Example output to copy
bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=
diff --git a/doc/integration/vault.md b/doc/integration/vault.md
index 192e5f3a8ba..9440435aaa4 100644
--- a/doc/integration/vault.md
+++ b/doc/integration/vault.md
@@ -7,131 +7,151 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Vault Authentication with GitLab OpenID Connect **(FREE)**
[Vault](https://www.vaultproject.io/) is a secrets management application offered by HashiCorp.
-It allows you to store and manage sensitive information such as secret environment variables, encryption keys, and authentication tokens.
-Vault offers Identity-based Access, which means Vault users can authenticate through several of their preferred cloud providers.
+It allows you to store and manage sensitive information such as secret environment
+variables, encryption keys, and authentication tokens.
-This document explains how Vault users can authenticate themselves through GitLab by utilizing our OpenID authentication feature.
-The following assumes you already have Vault installed and running.
+Vault offers Identity-based Access, which means Vault users can authenticate
+through several of their preferred cloud providers.
-1. **Get the OpenID Connect client ID and secret from GitLab:**
+The following content explains how Vault users can authenticate themselves through
+GitLab by using our OpenID authentication feature.
- First you must create a GitLab application to obtain an application ID and secret for authenticating into Vault.
- To do this, sign in to GitLab and follow these steps:
+## Prerequisites
- 1. In the top-right corner, select your avatar.
- 1. Select **Edit profile**.
- 1. On the left sidebar, select **Applications**.
- 1. Fill out the application **Name** and [**Redirect URI**](https://www.vaultproject.io/docs/auth/jwt#redirect-uris).
- 1. Select the **OpenID** scope.
- 1. Select **Save application**.
- 1. Copy client ID and secret, or keep the page open for reference.
+1. [Install Vault](https://www.vaultproject.io/docs/install).
+1. Run Vault.
- ![GitLab OAuth provider](img/gitlab_oauth_vault_v12_6.png)
+## Get the OpenID Connect client ID and secret from GitLab
-1. **Enable OIDC auth on Vault:**
+First you must create a GitLab application to obtain an application ID and secret
+for authenticating into Vault. To do this, sign in to GitLab and follow these steps:
- OpenID Connect is not enabled in Vault by default. This needs to be enabled in the terminal.
+1. In the top-right corner, select your avatar.
+1. Select **Edit profile**.
+1. On the left sidebar, select **Applications**.
+1. Fill out the application **Name** and [**Redirect URI**](https://www.vaultproject.io/docs/auth/jwt#redirect-uris).
+1. Select the **OpenID** scope.
+1. Select **Save application**.
+1. Copy the **Client ID** and **Client Secret**, or keep the page open for reference.
- Open a terminal session and run the following command to enable the OpenID Connect authentication provider in Vault:
+![GitLab OAuth provider](img/gitlab_oauth_vault_v12_6.png)
- ```shell
- vault auth enable oidc
- ```
+## Enable OpenID Connect on Vault
- You should see the following output in the terminal:
+OpenID Connect (OIDC) is not enabled in Vault by default.
- ```plaintext
- Success! Enabled oidc auth method at: oidc/
- ```
+To enable the OIDC authentication provider in Vault, open a terminal session
+and run the following command:
-1. **Write the OIDC configuration:**
+```shell
+vault auth enable oidc
+```
- Next, Vault needs to be given the application ID and secret generated by GitLab.
+You should see the following output in the terminal:
- In the terminal session, run the following command to give Vault access to the GitLab application you've just created with an OpenID scope. This allows Vault to authenticate through GitLab.
+```plaintext
+Success! Enabled oidc auth method at: oidc/
+```
- Replace `your_application_id` and `your_secret` in the example below with the application ID and secret generated for your app:
+## Write the OIDC configuration
- ```shell
- $ vault write auth/oidc/config \
- oidc_discovery_url="https://gitlab.com" \
- oidc_client_id="your_application_id" \
- oidc_client_secret="your_secret" \
- default_role="demo" \
- bound_issuer="localhost"
- ```
+To give Vault the application ID and secret generated by GitLab and allow
+Vault to authenticate through GitLab, run the following command in the terminal:
- You should see the following output in the terminal:
+```shell
+$ vault write auth/oidc/config \
+ oidc_discovery_url="https://gitlab.com" \
+ oidc_client_id="<your_application_id>" \
+ oidc_client_secret="<your_secret>" \
+ default_role="demo" \
+ bound_issuer="localhost"
+```
- ```shell
- Success! Data written to: auth/oidc/config
- ```
+Replace `<your_application_id>` and `<your_secret>` with the application ID
+and secret generated for your app.
-1. **Write the OIDC Role Configuration:**
+You should see the following output in the terminal:
- Now that Vault has a GitLab application ID and secret, it needs to know the [**Redirect URIs**](https://www.vaultproject.io/docs/auth/jwt#redirect-uris) and scopes given to GitLab during the application creation process. The redirect URIs need to match where your Vault instance is running. The `oidc_scopes` field needs to include the `openid`. Similarly to the previous step, replace `your_application_id` with the generated application ID from GitLab:
+```shell
+Success! Data written to: auth/oidc/config
+```
- This configuration is saved under the name of the role you are creating. In this case, we are creating a `demo` role. Later, we show how you can access this role through the Vault CLI.
+## Write the OIDC role configuration
- WARNING:
- If you're using a public GitLab instance (GitLab.com or any other instance publicly
- accessible), it's paramount to specify the `bound_claims` to allow access only to
- members of your group/project. Otherwise, anyone with a public account can access
- your Vault instance.
+You must tell Vault the [**Redirect URIs**](https://www.vaultproject.io/docs/auth/jwt#redirect-uris)
+and scopes given to GitLab when you created the application.
- ```shell
- vault write auth/oidc/role/demo -<<EOF
- {
- "user_claim": "sub",
- "allowed_redirect_uris": "your_vault_instance_redirect_uris",
- "bound_audiences": "your_application_id",
- "oidc_scopes": "openid",
- "role_type": "oidc",
- "policies": "demo",
- "ttl": "1h",
- "bound_claims": { "groups": ["yourGroup/yourSubgrup"] }
- }
- EOF
- ```
+Run the following command in the terminal:
+
+```shell
+vault write auth/oidc/role/demo -<<EOF
+{
+ "user_claim": "sub",
+ "allowed_redirect_uris": "<your_vault_instance_redirect_uris>",
+ "bound_audiences": "<your_application_id>",
+ "oidc_scopes": "<openid>",
+ "role_type": "oidc",
+ "policies": "demo",
+ "ttl": "1h",
+ "bound_claims": { "groups": ["<yourGroup/yourSubgrup>"] }
+}
+EOF
+```
+
+Replace:
+
+- `<your_vault_instance_redirect_uris>` with redirect URIs that match where your
+ Vault instance is running.
+- `<your_application_id>` with the application ID generated for your app.
-1. **Sign in to Vault:**
+The `oidc_scopes` field must include `openid`.
- 1. Go to your Vault UI (example: [http://127.0.0.1:8200/ui/vault/auth?with=oidc](http://127.0.0.1:8200/ui/vault/auth?with=oidc)).
- 1. If the `OIDC` method is not currently selected, open the dropdown and select it.
- 1. Select **Sign in With GitLab**, which opens a modal window:
+This configuration is saved under the name of the role you are creating. In this
+example, we are creating a `demo` role.
- ![Sign into Vault with GitLab](img/sign_into_vault_with_gitlab_v12_6.png)
+WARNING:
+If you're using a public GitLab instance, such as GitLab.com, you must specify
+the `bound_claims` to allow access only to members of your group or project.
+Otherwise, anyone with a public account can access your Vault instance.
- 1. Select **Authorize** to allow Vault to sign in through GitLab. This redirects you back to your Vault UI as a signed-in user.
+## Sign in to Vault
- ![Authorize Vault to connect with GitLab](img/authorize_vault_with_gitlab_v12_6.png)
+1. Go to your Vault UI. For example: [http://127.0.0.1:8200/ui/vault/auth?with=oidc](http://127.0.0.1:8200/ui/vault/auth?with=oidc).
+1. If the `OIDC` method is not selected, open the dropdown list and select it.
+1. Select **Sign in With GitLab**, which opens a modal window:
-1. **Sign in using the Vault CLI** (optional):
+ ![Sign into Vault with GitLab](img/sign_into_vault_with_gitlab_v12_6.png)
- Vault also allows you to sign in via their CLI.
+1. To allow Vault to sign in through GitLab, select **Authorize**. This redirects you back to your Vault UI as a signed-in user.
- After writing the same configurations from above, you can run the command below in your terminal to sign in with the role configuration created in step 4 above:
+ ![Authorize Vault to connect with GitLab](img/authorize_vault_with_gitlab_v12_6.png)
+
+## Sign in using the Vault CLI (optional)
+
+You can also sign into Vault using the [Vault CLI](https://www.vaultproject.io/docs/commands).
+
+1. To sign in with the role configuration you created in the previous example,
+ run the following command in your terminal:
```shell
vault login -method=oidc port=8250 role=demo
```
- Here's a short explanation of what this command does:
+ This command sets:
+
+ - `role=demo` so Vault knows which configuration we'd like to sign in with.
+ - `-method=oidc` to set Vault to use the `OIDC` sign-in method.
+ - `port=8250` to set the port that GitLab should redirect to. This port
+ number must match the port given to GitLab when listing
+ [Redirect URIs](https://www.vaultproject.io/docs/auth/jwt#redirect-uris).
- 1. In the **Write the OIDC Role Configuration** (step 4), we created a role called
- `demo`. We set `role=demo` so Vault knows which configuration we'd like to
- sign in with.
- 1. To set Vault to use the `OIDC` sign-in method, we set `-method=oidc`.
- 1. To set the port that GitLab should redirect to, we set `port=8250` or
- another port number that matches the port given to GitLab when listing
- [Redirect URIs](https://www.vaultproject.io/docs/auth/jwt#redirect-uris).
+ After running this command, you should see a link in the terminal.
- After running the command, it presents a link in the terminal.
- Select the link in the terminal and a browser tab opens that confirms you're signed into Vault via OIDC:
+1. Open this link in a web browser:
![Signed into Vault via OIDC](img/signed_into_vault_via_oidc_v12_6.png)
- The terminal outputs:
+ You should see in the terminal:
```plaintext
Success! You are now authenticated. The token information displayed below
diff --git a/doc/update/index.md b/doc/update/index.md
index 8990cdb7482..06609f03952 100644
--- a/doc/update/index.md
+++ b/doc/update/index.md
@@ -392,6 +392,8 @@ The following table, while not exhaustive, shows some examples of the supported
upgrade paths.
Additional steps between the mentioned versions are possible. We list the minimally necessary steps only.
+For a dynamic view of examples of supported upgrade paths, try the [Upgrade Path tool](https://gitlab-com.gitlab.io/support/toolbox/upgrade-path/). The Upgrade Path tool is maintained by the [GitLab Support team](https://about.gitlab.com/handbook/support/#about-the-support-team). Share feedback and help improve the tool by raising an issue or MR in the [upgrade-path project](https://gitlab.com/gitlab-com/support/toolbox/upgrade-path).
+
| Target version | Your version | Supported upgrade path | Note |
| -------------- | ------------ | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `15.1.0` | `14.6.2` | `14.6.2` -> `14.9.5` -> `14.10.5` -> `15.0.2` -> `15.1.0` | Three intermediate versions are required: `14.9` and `14.10`, `15.0`, then `15.1.0`. |
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index d5cca7a8d18..b02461f5d9d 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -180,8 +180,9 @@ Prerequisites:
To promote a project milestone:
1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the left sidebar, select **Issues > Milestones**.
1. Either:
- - Select **Promote to Group Milestone** (**{level-up}**).
+ - Select **Promote to Group Milestone** (**{level-up}**) next to the milestone you want to promote.
- Select the milestone title, and then select **Promote**.
1. Select **Promote Milestone**.
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index e5408cd9bde..b77401a1098 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -118,7 +118,8 @@ threads. Some quick actions might not be available to all subscription tiers.
| `/unapprove` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Unapprove the merge request. ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8103) in GitLab 14.3 |
| `/unassign @user1 @user2` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Remove specific assignees. |
| `/unassign` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove all assignees. |
-| `/unassign_reviewer @user1 @user2` or `/remove_reviewer @user1 @user2` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove specific reviewers. |
+| `/unassign_reviewer @user1 @user2` or `/remove_reviewer @user1 @user2` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove specific reviewers.
+| `/unassign_reviewer me` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove yourself as a reviewer. |
| `/unassign_reviewer` or `/remove_reviewer` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Remove all reviewers. |
| `/unlabel ~label1 ~label2` or `/remove_label ~label1 ~label2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Remove specified labels. |
| `/unlabel` or `/remove_label` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Remove all labels. |
diff --git a/lib/banzai/filter/markdown_engines/common_mark.rb b/lib/banzai/filter/markdown_engines/common_mark.rb
index cf368e28beb..7abfadc612b 100644
--- a/lib/banzai/filter/markdown_engines/common_mark.rb
+++ b/lib/banzai/filter/markdown_engines/common_mark.rb
@@ -56,3 +56,5 @@ module Banzai
end
end
end
+
+Banzai::Filter::MarkdownEngines::CommonMark.prepend_mod
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index 8b62dfda677..0f93f6df54d 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -449,6 +449,7 @@ projects: :gitlab_main
projects_sync_events: :gitlab_main
project_statistics: :gitlab_main
project_topics: :gitlab_main
+project_wiki_repository_states: :gitlab_main
prometheus_alert_events: :gitlab_main
prometheus_alerts: :gitlab_main
prometheus_metrics: :gitlab_main
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 2cd0ee7072d..d59b00f7797 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -55,7 +55,6 @@ module Gitlab
push_frontend_feature_flag(:security_auto_fix)
push_frontend_feature_flag(:new_header_search)
push_frontend_feature_flag(:source_editor_toolbar)
- push_frontend_feature_flag(:gl_avatar_for_all_user_avatars)
push_frontend_feature_flag(:gl_listbox_for_sort_dropdowns)
end
diff --git a/qa/qa/page/component/groups_filter.rb b/qa/qa/page/component/groups_filter.rb
index ff61c91f0f6..28b8ece918d 100644
--- a/qa/qa/page/component/groups_filter.rb
+++ b/qa/qa/page/component/groups_filter.rb
@@ -9,7 +9,7 @@ module QA
def self.included(base)
super
- base.view 'app/views/shared/groups/_search_form.html.haml' do
+ base.view 'app/assets/javascripts/groups/components/overview_tabs.vue' do
element :groups_filter_field
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index f279af90aa3..a09c9d258dc 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -23,7 +23,6 @@ RSpec.describe 'Project issue boards', :js do
project.add_maintainer(user2)
sign_in(user)
- stub_feature_flags(gl_avatar_for_all_user_avatars: false)
set_cookie('sidebar_collapsed', 'true')
end
diff --git a/spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb b/spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb
index 5aae5abaf10..1fa8f533869 100644
--- a/spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb
+++ b/spec/features/issues/user_scrolls_to_deeplinked_note_spec.rb
@@ -10,7 +10,6 @@ RSpec.describe 'User scrolls to deep-linked note' do
context 'on issue page', :js do
it 'on comment' do
- stub_feature_flags(gl_avatar_for_all_user_avatars: false)
visit project_issue_path(project, issue, anchor: "note_#{comment_1.id}")
wait_for_requests
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index 8225fcbfd89..09ac71d8ae8 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -25,7 +25,6 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do
before do
project.add_maintainer(user)
sign_in user
- stub_feature_flags(gl_avatar_for_all_user_avatars: false)
set_cookie('sidebar_collapsed', 'true')
end
diff --git a/spec/features/projects/user_sorts_projects_spec.rb b/spec/features/projects/user_sorts_projects_spec.rb
index 15f7dae502d..49b9052ed3c 100644
--- a/spec/features/projects/user_sorts_projects_spec.rb
+++ b/spec/features/projects/user_sorts_projects_spec.rb
@@ -24,7 +24,6 @@ RSpec.describe 'User sorts projects and order persists' do
end
it "is set on the group_canonical_path" do
- stub_feature_flags(group_overview_tabs_vue: false)
visit(group_canonical_path(group))
within '[data-testid=group_sort_by_dropdown]' do
@@ -33,7 +32,6 @@ RSpec.describe 'User sorts projects and order persists' do
end
it "is set on the details_group_path" do
- stub_feature_flags(group_overview_tabs_vue: false)
visit(details_group_path(group))
within '[data-testid=group_sort_by_dropdown]' do
@@ -42,7 +40,7 @@ RSpec.describe 'User sorts projects and order persists' do
end
end
- context "from explore projects" do
+ context "from explore projects", :js do
before do
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
sign_in(user)
@@ -51,10 +49,10 @@ RSpec.describe 'User sorts projects and order persists' do
first(:link, 'Updated date').click
end
- it_behaves_like "sort order persists across all views", 'Updated date', 'Updated date'
+ it_behaves_like "sort order persists across all views", 'Updated date', 'Updated'
end
- context 'from dashboard projects' do
+ context 'from dashboard projects', :js do
before do
stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
sign_in(user)
@@ -68,31 +66,29 @@ RSpec.describe 'User sorts projects and order persists' do
context 'from group homepage', :js do
before do
- stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
- stub_feature_flags(group_overview_tabs_vue: false)
sign_in(user)
visit(group_canonical_path(group))
within '[data-testid=group_sort_by_dropdown]' do
find('button.gl-dropdown-toggle').click
- first(:button, 'Last created').click
+ first(:button, 'Created').click
+ wait_for_requests
end
end
- it_behaves_like "sort order persists across all views", "Created date", "Last created"
+ it_behaves_like "sort order persists across all views", "Created date", "Created"
end
context 'from group details', :js do
before do
- stub_feature_flags(gl_listbox_for_sort_dropdowns: false)
- stub_feature_flags(group_overview_tabs_vue: false)
sign_in(user)
visit(details_group_path(group))
within '[data-testid=group_sort_by_dropdown]' do
find('button.gl-dropdown-toggle').click
- first(:button, 'Most stars').click
+ first(:button, 'Stars').click
+ wait_for_requests
end
end
- it_behaves_like "sort order persists across all views", "Stars", "Most stars"
+ it_behaves_like "sort order persists across all views", "Stars", "Stars"
end
end
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 440f169be86..75d55376d09 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -82,7 +82,7 @@ describe('diffs/components/commit_item', () => {
const imgElement = avatarElement.find('img');
expect(avatarElement.attributes('href')).toBe(commit.author.web_url);
- expect(imgElement.classes()).toContain('s32');
+ expect(imgElement.classes()).toContain('gl-avatar-s32');
expect(imgElement.attributes('alt')).toBe(commit.author.name);
expect(imgElement.attributes('src')).toBe(commit.author.avatar_url);
});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 33d76a8571d..75f70bbf19d 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -440,6 +440,10 @@ describe('AppComponent', () => {
expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', expect.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', expect.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith(
+ 'fetchFilteredAndSortedGroups',
+ expect.any(Function),
+ );
});
it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', async () => {
@@ -468,6 +472,46 @@ describe('AppComponent', () => {
expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', expect.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', expect.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ 'fetchFilteredAndSortedGroups',
+ expect.any(Function),
+ );
+ });
+ });
+
+ describe('when `fetchFilteredAndSortedGroups` event is emitted', () => {
+ const search = 'Foo bar';
+ const sort = 'created_asc';
+ const emitFetchFilteredAndSortedGroups = () => {
+ eventHub.$emit('fetchFilteredAndSortedGroups', {
+ filterGroupsBy: search,
+ sortBy: sort,
+ });
+ };
+ let setPaginationInfoSpy;
+
+ beforeEach(() => {
+ setPaginationInfoSpy = jest.spyOn(GroupsStore.prototype, 'setPaginationInfo');
+ createShallowComponent();
+ });
+
+ it('renders loading icon', async () => {
+ emitFetchFilteredAndSortedGroups();
+ await nextTick();
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('calls API with expected params', () => {
+ emitFetchFilteredAndSortedGroups();
+
+ expect(getGroupsSpy).toHaveBeenCalledWith(undefined, undefined, search, sort, undefined);
+ });
+
+ it('updates pagination', () => {
+ emitFetchFilteredAndSortedGroups();
+
+ expect(setPaginationInfoSpy).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index 379f509db82..be352301f43 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -1,4 +1,4 @@
-import { GlTab } from '@gitlab/ui';
+import { GlSorting, GlSortingItem, GlTab } from '@gitlab/ui';
import { nextTick } from 'vue';
import { createLocalVue } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -9,25 +9,38 @@ import GroupFolderComponent from '~/groups/components/group_folder.vue';
import GroupsStore from '~/groups/store/groups_store';
import GroupsService from '~/groups/service/groups_service';
import { createRouter } from '~/groups/init_overview_tabs';
+import eventHub from '~/groups/event_hub';
import {
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
ACTIVE_TAB_SHARED,
ACTIVE_TAB_ARCHIVED,
+ OVERVIEW_TABS_SORTING_ITEMS,
} from '~/groups/constants';
import axios from '~/lib/utils/axios_utils';
const localVue = createLocalVue();
localVue.component('GroupFolder', GroupFolderComponent);
const router = createRouter();
+const [SORTING_ITEM_NAME, , SORTING_ITEM_UPDATED] = OVERVIEW_TABS_SORTING_ITEMS;
describe('OverviewTabs', () => {
let wrapper;
let axiosMock;
- const endpoints = {
- subgroups_and_projects: '/groups/foobar/-/children.json',
- shared: '/groups/foobar/-/shared_projects.json',
- archived: '/groups/foobar/-/children.json?archived=only',
+ const defaultProvide = {
+ endpoints: {
+ subgroups_and_projects: '/groups/foobar/-/children.json',
+ shared: '/groups/foobar/-/shared_projects.json',
+ archived: '/groups/foobar/-/children.json?archived=only',
+ },
+ newSubgroupPath: '/groups/new',
+ newProjectPath: 'projects/new',
+ newSubgroupIllustration: '',
+ newProjectIllustration: '',
+ emptySubgroupIllustration: '',
+ canCreateSubgroups: false,
+ canCreateProjects: false,
+ initialSort: 'name_asc',
};
const routerMock = {
@@ -36,18 +49,13 @@ describe('OverviewTabs', () => {
const createComponent = async ({
route = { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: 'foo/bar/baz' } },
+ provide = {},
} = {}) => {
wrapper = mountExtended(OverviewTabs, {
router,
provide: {
- endpoints,
- newSubgroupPath: '/groups/new',
- newProjectPath: 'projects/new',
- newSubgroupIllustration: '',
- newProjectIllustration: '',
- emptySubgroupIllustration: '',
- canCreateSubgroups: false,
- canCreateProjects: false,
+ ...defaultProvide,
+ ...provide,
},
localVue,
mocks: { $route: route, $router: routerMock },
@@ -81,7 +89,7 @@ describe('OverviewTabs', () => {
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
store: new GroupsStore({ showSchemaMarkup: true }),
- service: new GroupsService(endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
+ service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
hideProjects: false,
renderEmptyState: true,
});
@@ -102,7 +110,7 @@ describe('OverviewTabs', () => {
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_SHARED,
store: new GroupsStore(),
- service: new GroupsService(endpoints[ACTIVE_TAB_SHARED]),
+ service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SHARED]),
hideProjects: false,
renderEmptyState: false,
});
@@ -125,7 +133,7 @@ describe('OverviewTabs', () => {
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_ARCHIVED,
store: new GroupsStore(),
- service: new GroupsService(endpoints[ACTIVE_TAB_ARCHIVED]),
+ service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_ARCHIVED]),
hideProjects: false,
renderEmptyState: false,
});
@@ -197,4 +205,109 @@ describe('OverviewTabs', () => {
expect(routerMock.push).toHaveBeenCalledWith(expectedRoute);
});
});
+
+ describe('searching and sorting', () => {
+ const setup = async () => {
+ jest.spyOn(eventHub, '$emit');
+ await createComponent();
+
+ // Click through tabs so they are all loaded
+ await findTab(OverviewTabs.i18n[ACTIVE_TAB_SHARED]).trigger('click');
+ await findTab(OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED]).trigger('click');
+ await findTab(OverviewTabs.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]).trigger('click');
+ };
+
+ const sharedAssertions = ({ search, sort }) => {
+ it('sets `lazy` prop to `true` for all of the non-active tabs so they are reloaded after sort or search is applied', () => {
+ expect(findTabPanels().at(0).vm.$attrs.lazy).toBe(false);
+ expect(findTabPanels().at(1).vm.$attrs.lazy).toBe(true);
+ expect(findTabPanels().at(2).vm.$attrs.lazy).toBe(true);
+ });
+
+ it('emits `fetchFilteredAndSortedGroups` event from `eventHub`', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ `${ACTIVE_TAB_SUBGROUPS_AND_PROJECTS}fetchFilteredAndSortedGroups`,
+ {
+ filterGroupsBy: search,
+ sortBy: sort,
+ },
+ );
+ });
+ };
+
+ describe('when search is typed in', () => {
+ const search = 'Foo bar';
+
+ beforeEach(async () => {
+ await setup();
+ await wrapper.findByPlaceholderText(OverviewTabs.i18n.searchPlaceholder).setValue(search);
+ });
+
+ it('updates query string with `filter` key', () => {
+ expect(routerMock.push).toHaveBeenCalledWith({ query: { filter: search } });
+ });
+
+ sharedAssertions({ search, sort: defaultProvide.initialSort });
+ });
+
+ describe('when sort is changed', () => {
+ beforeEach(async () => {
+ await setup();
+ wrapper.findAllComponents(GlSortingItem).at(2).vm.$emit('click');
+ await nextTick();
+ });
+
+ it('updates query string with `sort` key', () => {
+ expect(routerMock.push).toHaveBeenCalledWith({
+ query: { sort: SORTING_ITEM_UPDATED.asc },
+ });
+ });
+
+ sharedAssertions({ search: '', sort: SORTING_ITEM_UPDATED.asc });
+ });
+
+ describe('when sort direction is changed', () => {
+ beforeEach(async () => {
+ await setup();
+ await wrapper
+ .findByRole('button', { name: 'Sorting Direction: Ascending' })
+ .trigger('click');
+ });
+
+ it('updates query string with `sort` key', () => {
+ expect(routerMock.push).toHaveBeenCalledWith({
+ query: { sort: SORTING_ITEM_NAME.desc },
+ });
+ });
+
+ sharedAssertions({ search: '', sort: SORTING_ITEM_NAME.desc });
+ });
+
+ describe('when `filter` and `sort` query strings are set', () => {
+ beforeEach(async () => {
+ await createComponent({
+ route: {
+ name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ params: { group: 'foo/bar/baz' },
+ query: { filter: 'Foo bar', sort: SORTING_ITEM_UPDATED.desc },
+ },
+ });
+ });
+
+ it('sets value of search input', () => {
+ expect(
+ wrapper.findByPlaceholderText(OverviewTabs.i18n.searchPlaceholder).element.value,
+ ).toBe('Foo bar');
+ });
+
+ it('sets sort dropdown', () => {
+ expect(wrapper.findComponent(GlSorting).props()).toMatchObject({
+ text: SORTING_ITEM_UPDATED.label,
+ isAscending: false,
+ });
+
+ expect(wrapper.findAllComponents(GlSortingItem).at(2).vm.$attrs.active).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
index 01494cb6a24..6fe60f3c2e6 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -7,7 +7,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
<user-avatar-link-stub
class="gl-my-2 gl-mr-4"
imgalt=""
- imgcssclasses="gl-mr-0!"
+ imgcssclasses=""
imgsize="32"
imgsrc="https://test.com"
linkhref="/test"
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js
deleted file mode 100644
index f87737ca86a..00000000000
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_new_spec.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlAvatar, GlTooltip } from '@gitlab/ui';
-import defaultAvatarUrl from 'images/no_avatar.png';
-import { placeholderImage } from '~/lazy_loader';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image_new.vue';
-
-jest.mock('images/no_avatar.png', () => 'default-avatar-url');
-
-const PROVIDED_PROPS = {
- size: 32,
- imgSrc: 'myavatarurl.com',
- imgAlt: 'mydisplayname',
- cssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
-};
-
-describe('User Avatar Image Component', () => {
- let wrapper;
-
- const findAvatar = () => wrapper.findComponent(GlAvatar);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('Initialization', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- },
- });
- });
-
- it('should render `GlAvatar` and provide correct properties to it', () => {
- expect(findAvatar().attributes('data-src')).toBe(
- `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- );
- expect(findAvatar().props()).toMatchObject({
- src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- alt: PROVIDED_PROPS.imgAlt,
- size: PROVIDED_PROPS.size,
- });
- });
-
- it('should add correct CSS classes', () => {
- const classes = wrapper.findComponent(GlAvatar).classes();
- expect(classes).toContain(PROVIDED_PROPS.cssClasses);
- expect(classes).not.toContain('lazy');
- });
- });
-
- describe('Initialization when lazy', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- lazy: true,
- },
- });
- });
-
- it('should add lazy attributes', () => {
- expect(findAvatar().classes()).toContain('lazy');
- expect(findAvatar().attributes()).toMatchObject({
- src: placeholderImage,
- 'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- });
- });
-
- it('should use maximum number when size is provided as an object', () => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- size: { default: 16, md: 64, lg: 24 },
- lazy: true,
- },
- });
-
- expect(findAvatar().attributes('data-src')).toBe(`${PROVIDED_PROPS.imgSrc}?width=${64}`);
- });
- });
-
- describe('Initialization without src', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- imgSrc: null,
- },
- });
- });
-
- it('should have default avatar image', () => {
- expect(findAvatar().props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`);
- });
- });
-
- describe('Dynamic tooltip content', () => {
- const slots = {
- default: ['Action!'],
- };
-
- describe('when `tooltipText` is provided and no default slot', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: { ...PROVIDED_PROPS },
- });
- });
-
- it('renders the tooltip with `tooltipText` as content', () => {
- expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
- });
- });
-
- describe('when `tooltipText` and default slot is provided', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: { ...PROVIDED_PROPS },
- slots,
- });
- });
-
- it('does not render `tooltipText` inside the tooltip', () => {
- expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText);
- });
-
- it('renders the content provided via default slot', () => {
- expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js
deleted file mode 100644
index 2c1be6ec47e..00000000000
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_old_spec.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlTooltip } from '@gitlab/ui';
-import defaultAvatarUrl from 'images/no_avatar.png';
-import { placeholderImage } from '~/lazy_loader';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image_old.vue';
-
-jest.mock('images/no_avatar.png', () => 'default-avatar-url');
-
-const PROVIDED_PROPS = {
- size: 32,
- imgSrc: 'myavatarurl.com',
- imgAlt: 'mydisplayname',
- cssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
-};
-
-const DEFAULT_PROPS = {
- size: 20,
-};
-
-describe('User Avatar Image Component', () => {
- let wrapper;
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('Initialization', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- },
- });
- });
-
- it('should have <img> as a child element', () => {
- const imageElement = wrapper.find('img');
-
- expect(imageElement.exists()).toBe(true);
- expect(imageElement.attributes('src')).toBe(
- `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- );
- expect(imageElement.attributes('data-src')).toBe(
- `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- );
- expect(imageElement.attributes('alt')).toBe(PROVIDED_PROPS.imgAlt);
- });
-
- it('should properly render img css', () => {
- const classes = wrapper.find('img').classes();
- expect(classes).toEqual(['avatar', 's32', PROVIDED_PROPS.cssClasses]);
- expect(classes).not.toContain('lazy');
- });
- });
-
- describe('Initialization when lazy', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- lazy: true,
- },
- });
- });
-
- it('should add lazy attributes', () => {
- const imageElement = wrapper.find('img');
-
- expect(imageElement.classes()).toContain('lazy');
- expect(imageElement.attributes('src')).toBe(placeholderImage);
- expect(imageElement.attributes('data-src')).toBe(
- `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
- );
- });
- });
-
- describe('Initialization without src', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage);
- });
-
- it('should have default avatar image', () => {
- const imageElement = wrapper.find('img');
-
- expect(imageElement.attributes('src')).toBe(
- `${defaultAvatarUrl}?width=${DEFAULT_PROPS.size}`,
- );
- });
- });
-
- describe('Dynamic tooltip content', () => {
- const slots = {
- default: ['Action!'],
- };
-
- describe('when `tooltipText` is provided and no default slot', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: { ...PROVIDED_PROPS },
- });
- });
-
- it('renders the tooltip with `tooltipText` as content', () => {
- expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
- });
- });
-
- describe('when `tooltipText` and default slot is provided', () => {
- beforeEach(() => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: { ...PROVIDED_PROPS },
- slots,
- });
- });
-
- it('does not render `tooltipText` inside the tooltip', () => {
- expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText);
- });
-
- it('renders the content provided via default slot', () => {
- expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
index 6ad2ef226c2..d63b13981ac 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -1,7 +1,10 @@
import { shallowMount } from '@vue/test-utils';
+import { GlAvatar, GlTooltip } from '@gitlab/ui';
+import defaultAvatarUrl from 'images/no_avatar.png';
+import { placeholderImage } from '~/lazy_loader';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import UserAvatarImageNew from '~/vue_shared/components/user_avatar/user_avatar_image_new.vue';
-import UserAvatarImageOld from '~/vue_shared/components/user_avatar/user_avatar_image_old.vue';
+
+jest.mock('images/no_avatar.png', () => 'default-avatar-url');
const PROVIDED_PROPS = {
size: 32,
@@ -15,37 +18,117 @@ const PROVIDED_PROPS = {
describe('User Avatar Image Component', () => {
let wrapper;
- const createWrapper = (props = {}, { glAvatarForAllUserAvatars } = {}) => {
- wrapper = shallowMount(UserAvatarImage, {
- propsData: {
- ...PROVIDED_PROPS,
- ...props,
- },
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars,
- },
- },
- });
- };
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
afterEach(() => {
wrapper.destroy();
});
- describe.each([
- [false, true, true],
- [true, false, true],
- [true, true, true],
- [false, false, false],
- ])(
- 'when glAvatarForAllUserAvatars=%s and enforceGlAvatar=%s',
- (glAvatarForAllUserAvatars, enforceGlAvatar, isUsingNewVersion) => {
- it(`will render ${isUsingNewVersion ? 'new' : 'old'} version`, () => {
- createWrapper({ enforceGlAvatar }, { glAvatarForAllUserAvatars });
- expect(wrapper.findComponent(UserAvatarImageNew).exists()).toBe(isUsingNewVersion);
- expect(wrapper.findComponent(UserAvatarImageOld).exists()).toBe(!isUsingNewVersion);
- });
- },
- );
+ describe('Initialization', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ },
+ });
+ });
+
+ it('should render `GlAvatar` and provide correct properties to it', () => {
+ expect(findAvatar().attributes('data-src')).toBe(
+ `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
+ );
+ expect(findAvatar().props()).toMatchObject({
+ src: `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
+ alt: PROVIDED_PROPS.imgAlt,
+ size: PROVIDED_PROPS.size,
+ });
+ });
+
+ it('should add correct CSS classes', () => {
+ const classes = wrapper.findComponent(GlAvatar).classes();
+ expect(classes).toContain(PROVIDED_PROPS.cssClasses);
+ expect(classes).not.toContain('lazy');
+ });
+ });
+
+ describe('Initialization when lazy', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ lazy: true,
+ },
+ });
+ });
+
+ it('should add lazy attributes', () => {
+ expect(findAvatar().classes()).toContain('lazy');
+ expect(findAvatar().attributes()).toMatchObject({
+ src: placeholderImage,
+ 'data-src': `${PROVIDED_PROPS.imgSrc}?width=${PROVIDED_PROPS.size}`,
+ });
+ });
+
+ it('should use maximum number when size is provided as an object', () => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ size: { default: 16, md: 64, lg: 24 },
+ lazy: true,
+ },
+ });
+
+ expect(findAvatar().attributes('data-src')).toBe(`${PROVIDED_PROPS.imgSrc}?width=${64}`);
+ });
+ });
+
+ describe('Initialization without src', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...PROVIDED_PROPS,
+ imgSrc: null,
+ },
+ });
+ });
+
+ it('should have default avatar image', () => {
+ expect(findAvatar().props('src')).toBe(`${defaultAvatarUrl}?width=${PROVIDED_PROPS.size}`);
+ });
+ });
+
+ describe('Dynamic tooltip content', () => {
+ const slots = {
+ default: ['Action!'],
+ };
+
+ describe('when `tooltipText` is provided and no default slot', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: { ...PROVIDED_PROPS },
+ });
+ });
+
+ it('renders the tooltip with `tooltipText` as content', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).toBe(PROVIDED_PROPS.tooltipText);
+ });
+ });
+
+ describe('when `tooltipText` and default slot is provided', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: { ...PROVIDED_PROPS },
+ slots,
+ });
+ });
+
+ it('does not render `tooltipText` inside the tooltip', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).not.toBe(PROVIDED_PROPS.tooltipText);
+ });
+
+ it('renders the content provided via default slot', () => {
+ expect(wrapper.findComponent(GlTooltip).text()).toContain(slots.default[0]);
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js
deleted file mode 100644
index f485a14cfea..00000000000
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_new_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { GlAvatarLink } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link_new.vue';
-
-describe('User Avatar Link Component', () => {
- let wrapper;
-
- const findUserName = () => wrapper.findByTestId('user-avatar-link-username');
-
- const defaultProps = {
- linkHref: `${TEST_HOST}/myavatarurl.com`,
- imgSize: 32,
- imgSrc: `${TEST_HOST}/myavatarurl.com`,
- imgAlt: 'mydisplayname',
- imgCssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
- username: 'username',
- };
-
- const createWrapper = (props, slots) => {
- wrapper = shallowMountExtended(UserAvatarLink, {
- propsData: {
- ...defaultProps,
- ...props,
- ...slots,
- },
- });
- };
-
- beforeEach(() => {
- createWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should render GlLink with correct props', () => {
- const link = wrapper.findComponent(GlAvatarLink);
- expect(link.exists()).toBe(true);
- expect(link.attributes('href')).toBe(defaultProps.linkHref);
- });
-
- it('should render UserAvatarImage and provide correct props to it', () => {
- expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true);
- expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({
- cssClasses: defaultProps.imgCssClasses,
- imgAlt: defaultProps.imgAlt,
- imgSrc: defaultProps.imgSrc,
- lazy: false,
- size: defaultProps.imgSize,
- tooltipPlacement: defaultProps.tooltipPlacement,
- tooltipText: '',
- enforceGlAvatar: false,
- });
- });
-
- describe('when username provided', () => {
- beforeEach(() => {
- createWrapper({ username: defaultProps.username });
- });
-
- it('should render provided username', () => {
- expect(findUserName().text()).toBe(defaultProps.username);
- });
-
- it('should provide the tooltip data for the username', () => {
- expect(findUserName().attributes()).toEqual(
- expect.objectContaining({
- title: defaultProps.tooltipText,
- 'tooltip-placement': defaultProps.tooltipPlacement,
- }),
- );
- });
- });
-
- describe('when username is NOT provided', () => {
- beforeEach(() => {
- createWrapper({ username: '' });
- });
-
- it('should NOT render username', () => {
- expect(findUserName().exists()).toBe(false);
- });
- });
-
- describe('avatar-badge slot', () => {
- const badge = '<span>User badge</span>';
-
- beforeEach(() => {
- createWrapper(defaultProps, {
- 'avatar-badge': badge,
- });
- });
-
- it('should render provided `avatar-badge` slot content', () => {
- expect(wrapper.html()).toContain(badge);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js
deleted file mode 100644
index cf7a1025dba..00000000000
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_old_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { GlLink } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link_old.vue';
-
-describe('User Avatar Link Component', () => {
- let wrapper;
-
- const findUserName = () => wrapper.find('[data-testid="user-avatar-link-username"]');
-
- const defaultProps = {
- linkHref: `${TEST_HOST}/myavatarurl.com`,
- imgSize: 32,
- imgSrc: `${TEST_HOST}/myavatarurl.com`,
- imgAlt: 'mydisplayname',
- imgCssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
- username: 'username',
- };
-
- const createWrapper = (props, slots) => {
- wrapper = shallowMountExtended(UserAvatarLink, {
- propsData: {
- ...defaultProps,
- ...props,
- ...slots,
- },
- });
- };
-
- beforeEach(() => {
- createWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should render GlLink with correct props', () => {
- const link = wrapper.findComponent(GlLink);
- expect(link.exists()).toBe(true);
- expect(link.attributes('href')).toBe(defaultProps.linkHref);
- });
-
- it('should render UserAvatarImage and povide correct props to it', () => {
- expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true);
- expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({
- cssClasses: defaultProps.imgCssClasses,
- imgAlt: defaultProps.imgAlt,
- imgSrc: defaultProps.imgSrc,
- lazy: false,
- size: defaultProps.imgSize,
- tooltipPlacement: defaultProps.tooltipPlacement,
- tooltipText: '',
- enforceGlAvatar: false,
- });
- });
-
- describe('when username provided', () => {
- beforeEach(() => {
- createWrapper({ username: defaultProps.username });
- });
-
- it('should render provided username', () => {
- expect(findUserName().text()).toBe(defaultProps.username);
- });
-
- it('should provide the tooltip data for the username', () => {
- expect(findUserName().attributes()).toEqual(
- expect.objectContaining({
- title: defaultProps.tooltipText,
- 'tooltip-placement': defaultProps.tooltipPlacement,
- }),
- );
- });
- });
-
- describe('when username is NOT provided', () => {
- beforeEach(() => {
- createWrapper({ username: '' });
- });
-
- it('should NOT render username', () => {
- expect(findUserName().exists()).toBe(false);
- });
- });
-
- describe('avatar-badge slot', () => {
- const badge = '<span>User badge</span>';
-
- beforeEach(() => {
- createWrapper(defaultProps, {
- 'avatar-badge': badge,
- });
- });
-
- it('should render provided `avatar-badge` slot content', () => {
- expect(wrapper.html()).toContain(badge);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
index fd3f59008ec..df7ce449678 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -1,51 +1,102 @@
-import { shallowMount } from '@vue/test-utils';
+import { GlAvatarLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { TEST_HOST } from 'spec/test_constants';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import UserAvatarLinkNew from '~/vue_shared/components/user_avatar/user_avatar_link_new.vue';
-import UserAvatarLinkOld from '~/vue_shared/components/user_avatar/user_avatar_link_old.vue';
-
-const PROVIDED_PROPS = {
- size: 32,
- imgSrc: 'myavatarurl.com',
- imgAlt: 'mydisplayname',
- cssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
-};
describe('User Avatar Link Component', () => {
let wrapper;
- const createWrapper = (props = {}, { glAvatarForAllUserAvatars } = {}) => {
- wrapper = shallowMount(UserAvatarLink, {
+ const findUserName = () => wrapper.findByTestId('user-avatar-link-username');
+
+ const defaultProps = {
+ linkHref: `${TEST_HOST}/myavatarurl.com`,
+ imgSize: 32,
+ imgSrc: `${TEST_HOST}/myavatarurl.com`,
+ imgAlt: 'mydisplayname',
+ imgCssClasses: 'myextraavatarclass',
+ tooltipText: 'tooltip text',
+ tooltipPlacement: 'bottom',
+ username: 'username',
+ };
+
+ const createWrapper = (props, slots) => {
+ wrapper = shallowMountExtended(UserAvatarLink, {
propsData: {
- ...PROVIDED_PROPS,
+ ...defaultProps,
...props,
- },
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars,
- },
+ ...slots,
},
});
};
+ beforeEach(() => {
+ createWrapper();
+ });
+
afterEach(() => {
wrapper.destroy();
});
- describe.each([
- [false, true, true],
- [true, false, true],
- [true, true, true],
- [false, false, false],
- ])(
- 'when glAvatarForAllUserAvatars=%s and enforceGlAvatar=%s',
- (glAvatarForAllUserAvatars, enforceGlAvatar, isUsingNewVersion) => {
- it(`will render ${isUsingNewVersion ? 'new' : 'old'} version`, () => {
- createWrapper({ enforceGlAvatar }, { glAvatarForAllUserAvatars });
- expect(wrapper.findComponent(UserAvatarLinkNew).exists()).toBe(isUsingNewVersion);
- expect(wrapper.findComponent(UserAvatarLinkOld).exists()).toBe(!isUsingNewVersion);
+ it('should render GlLink with correct props', () => {
+ const link = wrapper.findComponent(GlAvatarLink);
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(defaultProps.linkHref);
+ });
+
+ it('should render UserAvatarImage and provide correct props to it', () => {
+ expect(wrapper.findComponent(UserAvatarImage).exists()).toBe(true);
+ expect(wrapper.findComponent(UserAvatarImage).props()).toEqual({
+ cssClasses: defaultProps.imgCssClasses,
+ imgAlt: defaultProps.imgAlt,
+ imgSrc: defaultProps.imgSrc,
+ lazy: false,
+ size: defaultProps.imgSize,
+ tooltipPlacement: defaultProps.tooltipPlacement,
+ tooltipText: '',
+ });
+ });
+
+ describe('when username provided', () => {
+ beforeEach(() => {
+ createWrapper({ username: defaultProps.username });
+ });
+
+ it('should render provided username', () => {
+ expect(findUserName().text()).toBe(defaultProps.username);
+ });
+
+ it('should provide the tooltip data for the username', () => {
+ expect(findUserName().attributes()).toEqual(
+ expect.objectContaining({
+ title: defaultProps.tooltipText,
+ 'tooltip-placement': defaultProps.tooltipPlacement,
+ }),
+ );
+ });
+ });
+
+ describe('when username is NOT provided', () => {
+ beforeEach(() => {
+ createWrapper({ username: '' });
+ });
+
+ it('should NOT render username', () => {
+ expect(findUserName().exists()).toBe(false);
+ });
+ });
+
+ describe('avatar-badge slot', () => {
+ const badge = '<span>User badge</span>';
+
+ beforeEach(() => {
+ createWrapper(defaultProps, {
+ 'avatar-badge': badge,
});
- },
- );
+ });
+
+ it('should render provided `avatar-badge` slot content', () => {
+ expect(wrapper.html()).toContain(badge);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
index b9accbf0373..1ad6d043399 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
@@ -153,29 +153,4 @@ describe('UserAvatarList', () => {
});
});
});
-
- describe('additional styling for the image', () => {
- it('should not add CSS class when feature flag `glAvatarForAllUserAvatars` is disabled', () => {
- factory({
- propsData: { items: createList(1) },
- });
-
- const link = wrapper.findComponent(UserAvatarLink);
- expect(link.props('imgCssClasses')).not.toBe('gl-mr-3');
- });
-
- it('should add CSS class when feature flag `glAvatarForAllUserAvatars` is enabled', () => {
- factory({
- propsData: { items: createList(1) },
- provide: {
- glFeatures: {
- glAvatarForAllUserAvatars: true,
- },
- },
- });
-
- const link = wrapper.findComponent(UserAvatarLink);
- expect(link.props('imgCssClasses')).toBe('gl-mr-3');
- });
- });
});
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 0b6e51c26f5..a38483a956d 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -531,12 +531,14 @@ RSpec.describe GroupsHelper do
describe '#group_overview_tabs_app_data' do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
+ let_it_be(:initial_sort) { 'created_asc' }
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).with(user, :create_subgroup, group) { true }
allow(helper).to receive(:can?).with(user, :create_projects, group) { true }
+ allow(helper).to receive(:project_list_sort_by).and_return(initial_sort)
end
it 'returns expected hash' do
@@ -545,7 +547,8 @@ RSpec.describe GroupsHelper do
subgroups_and_projects_endpoint: including("/groups/#{group.path}/-/children.json"),
shared_projects_endpoint: including("/groups/#{group.path}/-/shared_projects.json"),
archived_projects_endpoint: including("/groups/#{group.path}/-/children.json?archived=only"),
- current_group_visibility: group.visibility
+ current_group_visibility: group.visibility,
+ initial_sort: initial_sort
}.merge(helper.group_overview_tabs_app_data(group))
)
end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index 749554f7786..e5c30769531 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -96,9 +96,9 @@ module StubGitlabCalls
def stub_commonmark_sourcepos_disabled
render_options = Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS
- allow_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark)
- .to receive(:render_options)
- .and_return(render_options)
+ allow_next_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) do |instance|
+ allow(instance).to receive(:render_options).and_return(render_options)
+ end
end
private