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:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 12:40:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 12:40:42 +0300
commitee664acb356f8123f4f6b00b73c1e1cf0866c7fb (patch)
treef8479f94a28f66654c6a4f6fb99bad6b4e86a40e /app/assets/javascripts/groups
parent62f7d5c5b69180e82ae8196b7b429eeffc8e7b4f (diff)
Add latest changes from gitlab-org/gitlab@15-5-stable-eev15.5.0-rc42
Diffstat (limited to 'app/assets/javascripts/groups')
-rw-r--r--app/assets/javascripts/groups/components/app.vue28
-rw-r--r--app/assets/javascripts/groups/components/groups.vue20
-rw-r--r--app/assets/javascripts/groups/components/new_top_level_group_alert.vue40
-rw-r--r--app/assets/javascripts/groups/components/overview_tabs.vue177
-rw-r--r--app/assets/javascripts/groups/components/transfer_group_form.vue2
-rw-r--r--app/assets/javascripts/groups/constants.js25
-rw-r--r--app/assets/javascripts/groups/init_overview_tabs.js2
-rw-r--r--app/assets/javascripts/groups/settings/components/access_dropdown.vue4
8 files changed, 236 insertions, 62 deletions
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 0bd7371d39b..15f5a3518a5 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { mergeUrlParams, getParameterByName } from '~/lib/utils/url_utility';
import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
@@ -51,7 +51,6 @@ export default {
isModalVisible: false,
isLoading: true,
isSearchEmpty: false,
- searchEmptyMessage: '',
targetGroup: null,
targetParentGroup: null,
showEmptyState: false,
@@ -88,15 +87,12 @@ export default {
},
},
created() {
- this.searchEmptyMessage = this.hideProjects
- ? COMMON_STR.GROUP_SEARCH_EMPTY
- : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
-
eventHub.$on(`${this.action}fetchPage`, this.fetchPage);
eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren);
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 +107,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() {
@@ -132,7 +129,7 @@ export default {
this.isLoading = false;
window.scrollTo({ top: 0, behavior: 'smooth' });
- createFlash({ message: COMMON_STR.FAILURE });
+ createAlert({ message: COMMON_STR.FAILURE });
});
},
fetchAllGroups() {
@@ -153,6 +150,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;
@@ -218,7 +227,7 @@ export default {
if (err.status === 403) {
message = COMMON_STR.LEAVE_FORBIDDEN;
}
- createFlash({ message });
+ createAlert({ message });
this.targetGroup.isBeingRemoved = false;
});
},
@@ -245,7 +254,7 @@ export default {
const hasGroups = groups && groups.length > 0;
if (this.renderEmptyState) {
- this.isSearchEmpty = this.filterGroupsBy !== null && !hasGroups;
+ this.isSearchEmpty = fromSearch && !hasGroups;
} else {
this.isSearchEmpty = !hasGroups;
}
@@ -280,7 +289,6 @@ export default {
v-else
:groups="groups"
:search-empty="isSearchEmpty"
- :search-empty-message="searchEmptyMessage"
:page-info="pageInfo"
:action="action"
/>
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index 3a05c308a2a..43aa0753082 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -1,11 +1,18 @@
<script>
+import { GlEmptyState } from '@gitlab/ui';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { getParameterByName } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import eventHub from '../event_hub';
export default {
+ i18n: {
+ emptyStateTitle: __('No results found'),
+ emptyStateDescription: __('Edit your search and try again'),
+ },
components: {
PaginationLinks,
+ GlEmptyState,
},
props: {
groups: {
@@ -20,10 +27,6 @@ export default {
type: Boolean,
required: true,
},
- searchEmptyMessage: {
- type: String,
- required: true,
- },
action: {
type: String,
required: false,
@@ -43,12 +46,11 @@ export default {
<template>
<div class="groups-list-tree-container" data-qa-selector="groups_list_tree_container">
- <div
+ <gl-empty-state
v-if="searchEmpty"
- class="has-no-search-results gl-font-style-italic gl-text-center gl-text-gray-600 gl-p-5"
- >
- {{ searchEmptyMessage }}
- </div>
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDescription"
+ />
<template v-else>
<group-folder :groups="groups" :action="action" />
<pagination-links
diff --git a/app/assets/javascripts/groups/components/new_top_level_group_alert.vue b/app/assets/javascripts/groups/components/new_top_level_group_alert.vue
new file mode 100644
index 00000000000..c6af6cdb59f
--- /dev/null
+++ b/app/assets/javascripts/groups/components/new_top_level_group_alert.vue
@@ -0,0 +1,40 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export default {
+ name: 'NewTopLevelGroupAlert',
+ components: {
+ GlAlert,
+ UserCalloutDismisser,
+ },
+ i18n: {
+ titleText: s__("Groups|You're creating a new top-level group"),
+ bodyText: s__(
+ 'Groups|Members, projects, trials, and paid subscriptions are tied to a specific top-level group. If you are already a member of a top-level group, you can create a subgroup so your new work is part of your existing top-level group. Do you want to create a subgroup instead?',
+ ),
+ primaryBtnText: s__('Groups|Learn more about subgroups'),
+ },
+ subgroupsDocsPath: helpPagePath('user/group/subgroups/index'),
+};
+</script>
+
+<template>
+ <user-callout-dismisser feature-name="new_top_level_group_alert">
+ <template #default="{ dismiss, shouldShowCallout }">
+ <gl-alert
+ v-if="shouldShowCallout"
+ ref="newTopLevelAlert"
+ data-testid="new-top-level-alert"
+ :title="$options.i18n.titleText"
+ :primary-button-text="$options.i18n.primaryBtnText"
+ :primary-button-link="$options.subgroupsDocsPath"
+ @dismiss="dismiss"
+ >
+ {{ $options.i18n.bodyText }}
+ </gl-alert>
+ </template>
+ </user-callout-dismisser>
+</template>
diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue
index 325e42af0f8..d0c5846ac88 100644
--- a/app/assets/javascripts/groups/components/overview_tabs.vue
+++ b/app/assets/javascripts/groups/components/overview_tabs.vue
@@ -1,58 +1,77 @@
<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 { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
import GroupsStore from '../store/groups_store';
import GroupsService from '../service/groups_service';
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() {
+ const tabs = [
+ {
+ title: this.$options.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS],
+ key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ renderEmptyState: true,
+ lazy: this.$route.name !== ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
+ store: new GroupsStore({ showSchemaMarkup: true }),
+ },
+ {
+ title: this.$options.i18n[ACTIVE_TAB_SHARED],
+ key: ACTIVE_TAB_SHARED,
+ renderEmptyState: false,
+ lazy: this.$route.name !== ACTIVE_TAB_SHARED,
+ service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]),
+ store: new GroupsStore(),
+ },
+ {
+ title: this.$options.i18n[ACTIVE_TAB_ARCHIVED],
+ key: ACTIVE_TAB_ARCHIVED,
+ renderEmptyState: false,
+ lazy: this.$route.name !== ACTIVE_TAB_ARCHIVED,
+ service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]),
+ store: new GroupsStore(),
+ },
+ ];
return {
- tabs: [
- {
- title: this.$options.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS],
- key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
- renderEmptyState: true,
- lazy: false,
- service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
- store: new GroupsStore({ showSchemaMarkup: true }),
- },
- {
- title: this.$options.i18n[ACTIVE_TAB_SHARED],
- key: ACTIVE_TAB_SHARED,
- renderEmptyState: false,
- lazy: true,
- service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]),
- store: new GroupsStore(),
- },
- {
- title: this.$options.i18n[ACTIVE_TAB_ARCHIVED],
- key: ACTIVE_TAB_ARCHIVED,
- renderEmptyState: false,
- lazy: true,
- service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]),
- store: new GroupsStore(),
- },
- ],
- activeTabIndex: 0,
+ tabs,
+ activeTabIndex: tabs.findIndex((tab) => tab.key === this.$route.name),
+ sort: SORTING_ITEM_NAME,
+ isAscending: true,
+ search: '',
};
},
+ computed: {
+ activeTab() {
+ return this.tabs[this.activeTabIndex];
+ },
+ sortQueryStringValue() {
+ return this.isAscending ? this.sort.asc : this.sort.desc;
+ },
+ },
mounted() {
- const activeTabIndex = this.tabs.findIndex((tab) => tab.key === this.$route.name);
-
- if (activeTabIndex === -1) {
- return;
- }
+ this.search = this.$route.query?.filter || '';
- this.activeTabIndex = activeTabIndex;
+ 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;
},
methods: {
handleTabInput(tabIndex) {
@@ -72,14 +91,64 @@ 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();
+ },
+ handleSearchInput(value) {
+ this.search = value;
+
+ this.debouncedSearch();
},
+ debouncedSearch: debounce(async function debouncedSearch() {
+ this.handleSearchOrSortChange();
+ }, DEBOUNCE_DELAY),
},
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 +168,37 @@ 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
+ :value="search"
+ :placeholder="$options.i18n.searchPlaceholder"
+ data-qa-selector="groups_filter_field"
+ @input="handleSearchInput"
+ />
+ </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/components/transfer_group_form.vue b/app/assets/javascripts/groups/components/transfer_group_form.vue
index 7e7282a27b0..e28459811d7 100644
--- a/app/assets/javascripts/groups/components/transfer_group_form.vue
+++ b/app/assets/javascripts/groups/components/transfer_group_form.vue
@@ -2,7 +2,7 @@
import { GlFormGroup } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
+import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
export const i18n = {
confirmationMessage: __(
diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js
index 223c2975c11..6fb12cd6270 100644
--- a/app/assets/javascripts/groups/constants.js
+++ b/app/assets/javascripts/groups/constants.js
@@ -24,8 +24,6 @@ export const COMMON_STR = {
EDIT_BTN_TITLE: s__('GroupsTree|Edit'),
REMOVE_BTN_TITLE: s__('GroupsTree|Delete'),
OPTIONS_DROPDOWN_TITLE: s__('GroupsTree|Options'),
- GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'),
- GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'),
};
export const ITEM_TYPE = {
@@ -62,3 +60,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/groups/settings/components/access_dropdown.vue b/app/assets/javascripts/groups/settings/components/access_dropdown.vue
index 28f059fa23e..db8e424e166 100644
--- a/app/assets/javascripts/groups/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/groups/settings/components/access_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__, n__ } from '~/locale';
import { getSubGroups } from '../api/access_dropdown_api';
import { LEVEL_TYPES } from '../constants';
@@ -98,7 +98,7 @@ export default {
this.consolidateData(groupsResponse.data);
this.setSelected({ initial });
})
- .catch(() => createFlash({ message: __('Failed to load groups.') }))
+ .catch(() => createAlert({ message: __('Failed to load groups.') }))
.finally(() => {
this.initialLoading = false;
this.loading = false;