diff options
Diffstat (limited to 'app/assets/javascripts/organizations/groups_and_projects/components')
3 files changed, 248 insertions, 43 deletions
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue index 2b42c821cd5..10471cc1fdd 100644 --- a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue +++ b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue @@ -1,53 +1,127 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; -import { __, s__ } from '~/locale'; -import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { createAlert } from '~/alert'; -import projectsQuery from '../graphql/queries/projects.query.graphql'; +import { GlCollapsibleListbox, GlSorting, GlSortingItem } from '@gitlab/ui'; +import { isEqual } from 'lodash'; +import { s__, __ } from '~/locale'; +import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import { + filterToQueryObject, + processFilters, + urlQueryToFilter, + prepareTokens, +} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; +import { + FILTERED_SEARCH_TERM, + TOKEN_EMPTY_SEARCH_TERM, +} from '~/vue_shared/components/filtered_search_bar/constants'; +import { + DISPLAY_QUERY_GROUPS, + DISPLAY_QUERY_PROJECTS, + DISPLAY_LISTBOX_ITEMS, + SORT_DIRECTION_ASC, + SORT_DIRECTION_DESC, + SORT_ITEMS, + SORT_ITEM_CREATED, + FILTERED_SEARCH_TERM_KEY, +} from '../constants'; +import GroupsPage from './groups_page.vue'; +import ProjectsPage from './projects_page.vue'; export default { i18n: { pageTitle: __('Groups and projects'), - errorMessage: s__( - 'Organization|An error occurred loading the projects. Please refresh the page to try again.', - ), + searchInputPlaceholder: s__('Organization|Search or filter list'), + displayListboxHeaderText: __('Display'), }, - components: { - ProjectsList, - GlLoadingIcon, + components: { FilteredSearchBar, GlCollapsibleListbox, GlSorting, GlSortingItem }, + filteredSearch: { + tokens: [], + namespace: 'organization_groups_and_projects', + recentSearchesStorageKey: 'organization_groups_and_projects', }, - data() { - return { - projects: [], - }; - }, - apollo: { - projects: { - query: projectsQuery, - update(data) { - return data.organization.projects.nodes; - }, - error(error) { - createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); - }, + displayListboxItems: DISPLAY_LISTBOX_ITEMS, + sortItems: SORT_ITEMS, + computed: { + routerView() { + const { display } = this.$route.query; + + switch (display) { + case DISPLAY_QUERY_GROUPS: + return GroupsPage; + + case DISPLAY_QUERY_PROJECTS: + return ProjectsPage; + + default: + return GroupsPage; + } + }, + activeSortItem() { + return this.$options.sortItems.find((sortItem) => sortItem.name === this.sortName); + }, + sortName() { + return this.$route.query.sort_name || SORT_ITEM_CREATED.name; + }, + isAscending() { + return this.$route.query.sort_direction !== SORT_DIRECTION_DESC; + }, + sortText() { + return this.activeSortItem.text; + }, + filteredSearchValue() { + const tokens = prepareTokens( + urlQueryToFilter(this.$route.query, { + filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY, + filterNamesAllowList: [FILTERED_SEARCH_TERM], + }), + ); + + return tokens.length ? tokens : [TOKEN_EMPTY_SEARCH_TERM]; + }, + displayListboxSelected() { + const { display } = this.$route.query; + + return [DISPLAY_QUERY_GROUPS, DISPLAY_QUERY_PROJECTS].includes(display) + ? display + : DISPLAY_QUERY_GROUPS; }, }, - computed: { - formattedProjects() { - return this.projects.map(({ id, nameWithNamespace, accessLevel, ...project }) => ({ - ...project, - id: getIdFromGraphQLId(id), - name: nameWithNamespace, - permissions: { - projectAccess: { - accessLevel: accessLevel.integerValue, - }, - }, - })); - }, - isLoading() { - return this.$apollo.queries.projects?.loading; + methods: { + pushQuery(query) { + const currentQuery = this.$route.query; + + if (isEqual(currentQuery, query)) { + return; + } + + this.$router.push({ query }); + }, + onDisplayListboxSelect(display) { + this.pushQuery({ display }); + }, + onSortItemClick(sortItem) { + if (this.$route.query.sort_name === sortItem.name) { + return; + } + + this.pushQuery({ ...this.$route.query, sort_name: sortItem.name }); + }, + onSortDirectionChange(isAscending) { + this.pushQuery({ + ...this.$route.query, + sort_direction: isAscending ? SORT_DIRECTION_ASC : SORT_DIRECTION_DESC, + }); + }, + onFilter(filters) { + const { display, sort_name, sort_direction } = this.$route.query; + + this.pushQuery({ + display, + sort_name, + sort_direction, + ...filterToQueryObject(processFilters(filters), { + filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY, + }), + }); }, }, }; @@ -56,7 +130,49 @@ export default { <template> <div> <h1 class="gl-font-size-h-display">{{ $options.i18n.pageTitle }}</h1> - <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" /> - <projects-list v-else :projects="formattedProjects" show-project-icon /> + <div class="gl-p-5 gl-bg-gray-10 gl-border-t gl-border-b"> + <div class="gl-mx-n2 gl-my-n2 gl-md-display-flex"> + <div class="gl-p-2 gl-flex-grow-1"> + <filtered-search-bar + :namespace="$options.filteredSearch.namespace" + :tokens="$options.filteredSearch.tokens" + :initial-filter-value="filteredSearchValue" + sync-filter-and-sort + :recent-searches-storage-key="$options.filteredSearch.recentSearchesStorageKey" + :search-input-placeholder="$options.i18n.searchInputPlaceholder" + @onFilter="onFilter" + /> + </div> + <div class="gl-p-2"> + <gl-collapsible-listbox + :selected="displayListboxSelected" + :items="$options.displayListboxItems" + :header-text="$options.i18n.displayListboxHeaderText" + block + toggle-class="gl-md-w-30" + @select="onDisplayListboxSelect" + /> + </div> + <div class="gl-p-2"> + <gl-sorting + class="gl-display-flex" + dropdown-class="gl-w-full" + :text="sortText" + :is-ascending="isAscending" + @sortDirectionChange="onSortDirectionChange" + > + <gl-sorting-item + v-for="sortItem in $options.sortItems" + :key="sortItem.name" + :active="activeSortItem.name === sortItem.name" + @click="onSortItemClick(sortItem)" + > + {{ sortItem.text }} + </gl-sorting-item> + </gl-sorting> + </div> + </div> + </div> + <component :is="routerView" /> </div> </template> diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue new file mode 100644 index 00000000000..20db38403f7 --- /dev/null +++ b/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue @@ -0,0 +1,43 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import { createAlert } from '~/alert'; +import { s__ } from '~/locale'; +import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue'; +import groupsQuery from '../graphql/queries/groups.query.graphql'; +import { formatGroups } from '../utils'; + +export default { + i18n: { + errorMessage: s__( + 'Organization|An error occurred loading the groups. Please refresh the page to try again.', + ), + }, + components: { GlLoadingIcon, GroupsList }, + data() { + return { + groups: [], + }; + }, + apollo: { + groups: { + query: groupsQuery, + update(data) { + return formatGroups(data.organization.groups.nodes); + }, + error(error) { + createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); + }, + }, + }, + computed: { + isLoading() { + return this.$apollo.queries.groups.loading; + }, + }, +}; +</script> + +<template> + <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" /> + <groups-list v-else :groups="groups" show-group-icon /> +</template> diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue new file mode 100644 index 00000000000..d6958ee996e --- /dev/null +++ b/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue @@ -0,0 +1,46 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; +import { createAlert } from '~/alert'; +import projectsQuery from '../graphql/queries/projects.query.graphql'; +import { formatProjects } from '../utils'; + +export default { + i18n: { + errorMessage: s__( + 'Organization|An error occurred loading the projects. Please refresh the page to try again.', + ), + }, + components: { + ProjectsList, + GlLoadingIcon, + }, + data() { + return { + projects: [], + }; + }, + apollo: { + projects: { + query: projectsQuery, + update(data) { + return formatProjects(data.organization.projects.nodes); + }, + error(error) { + createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); + }, + }, + }, + computed: { + isLoading() { + return this.$apollo.queries.projects.loading; + }, + }, +}; +</script> + +<template> + <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" /> + <projects-list v-else :projects="projects" show-project-icon /> +</template> |