Commit 01b259d2 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '353203-cannot-backfill-the-search-box-after-refresh-container-registry' into 'master'

Delay rendering search box

See merge request gitlab-org/gitlab!80994
parents e09a291d ec1ee3e6
...@@ -13,9 +13,8 @@ import getContainerRepositoriesQuery from 'shared_queries/container_registry/get ...@@ -13,9 +13,8 @@ import getContainerRepositoriesQuery from 'shared_queries/container_registry/get
import createFlash from '~/flash'; import createFlash from '~/flash';
import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue'; import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants'; import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import DeleteImage from '../components/delete_image.vue'; import DeleteImage from '../components/delete_image.vue';
import RegistryHeader from '../components/list_page/registry_header.vue'; import RegistryHeader from '../components/list_page/registry_header.vue';
...@@ -61,8 +60,8 @@ export default { ...@@ -61,8 +60,8 @@ export default {
GlSkeletonLoader, GlSkeletonLoader,
RegistryHeader, RegistryHeader,
DeleteImage, DeleteImage,
RegistrySearch,
CleanupPolicyEnabledAlert, CleanupPolicyEnabledAlert,
PersistedSearch,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -130,8 +129,7 @@ export default { ...@@ -130,8 +129,7 @@ export default {
containerRepositoriesCount: 0, containerRepositoriesCount: 0,
itemToDelete: {}, itemToDelete: {},
deleteAlertType: null, deleteAlertType: null,
filter: [], sorting: null,
sorting: { orderBy: 'UPDATED', sort: 'desc' },
name: null, name: null,
mutationLoading: false, mutationLoading: false,
fetchBaseQuery: false, fetchBaseQuery: false,
...@@ -154,7 +152,7 @@ export default { ...@@ -154,7 +152,7 @@ export default {
queryVariables() { queryVariables() {
return { return {
name: this.name, name: this.name,
sort: this.sortBy, sort: this.sorting,
fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath, fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath,
isGroupPage: this.config.isGroupPage, isGroupPage: this.config.isGroupPage,
first: GRAPHQL_PAGE_SIZE, first: GRAPHQL_PAGE_SIZE,
...@@ -182,24 +180,6 @@ export default { ...@@ -182,24 +180,6 @@ export default {
? DELETE_IMAGE_SUCCESS_MESSAGE ? DELETE_IMAGE_SUCCESS_MESSAGE
: DELETE_IMAGE_ERROR_MESSAGE; : DELETE_IMAGE_ERROR_MESSAGE;
}, },
sortBy() {
const { orderBy, sort } = this.sorting;
return `${orderBy}_${sort}`.toUpperCase();
},
},
mounted() {
const { sorting, filters } = extractFilterAndSorting(this.$route.query);
this.filter = [...filters];
this.name = filters[0]?.value.data;
this.sorting = { ...this.sorting, ...sorting };
// If the two graphql calls - which are not batched - resolve togheter we will have a race
// condition when apollo sets the cache, with this we give the 'base' call an headstart
this.fetchBaseQuery = true;
setTimeout(() => {
this.fetchAdditionalDetails = true;
}, 200);
}, },
methods: { methods: {
deleteImage(item) { deleteImage(item) {
...@@ -258,18 +238,20 @@ export default { ...@@ -258,18 +238,20 @@ export default {
this.track('confirm_delete'); this.track('confirm_delete');
this.mutationLoading = true; this.mutationLoading = true;
}, },
updateSorting(value) { handleSearchUpdate({ sort, filters }) {
this.sorting = { this.sorting = sort;
...this.sorting,
...value, const search = filters.find((i) => i.type === FILTERED_SEARCH_TERM);
};
},
doFilter() {
const search = this.filter.find((i) => i.type === FILTERED_SEARCH_TERM);
this.name = search?.value?.data; this.name = search?.value?.data;
},
updateUrlQueryString(query) { if (!this.fetchBaseQuery && !this.fetchAdditionalDetails) {
this.$router.push({ query }); // If the two graphql calls - which are not batched - resolve together we will have a race
// condition when apollo sets the cache, with this we give the 'base' call an headstart
this.fetchBaseQuery = true;
setTimeout(() => {
this.fetchAdditionalDetails = true;
}, 200);
}
}, },
}, },
}; };
...@@ -332,16 +314,12 @@ export default { ...@@ -332,16 +314,12 @@ export default {
/> />
</template> </template>
</registry-header> </registry-header>
<persisted-search
<registry-search class="gl-mb-5"
:filter="filter"
:sorting="sorting"
:tokens="[]"
:sortable-fields="$options.searchConfig" :sortable-fields="$options.searchConfig"
@sorting:changed="updateSorting" :default-order="$options.searchConfig[0].orderBy"
@filter:changed="filter = $event" default-sort="desc"
@filter:submit="doFilter" @update="handleSearchUpdate"
@query:changed="updateUrlQueryString"
/> />
<div v-if="isLoading" class="gl-mt-5"> <div v-if="isLoading" class="gl-mt-5">
......
...@@ -23,7 +23,7 @@ import deleteContainerRepositoryMutation from '~/packages_and_registries/contain ...@@ -23,7 +23,7 @@ import deleteContainerRepositoryMutation from '~/packages_and_registries/contain
import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql'; import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
import component from '~/packages_and_registries/container_registry/explorer/pages/list.vue'; import component from '~/packages_and_registries/container_registry/explorer/pages/list.vue';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { $toast } from 'jest/packages_and_registries/shared/mocks'; import { $toast } from 'jest/packages_and_registries/shared/mocks';
...@@ -55,11 +55,15 @@ describe('List Page', () => { ...@@ -55,11 +55,15 @@ describe('List Page', () => {
const findDeleteAlert = () => wrapper.findComponent(GlAlert); const findDeleteAlert = () => wrapper.findComponent(GlAlert);
const findImageList = () => wrapper.findComponent(ImageList); const findImageList = () => wrapper.findComponent(ImageList);
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch); const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]'); const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
const findDeleteImage = () => wrapper.findComponent(DeleteImage); const findDeleteImage = () => wrapper.findComponent(DeleteImage);
const findCleanupAlert = () => wrapper.findComponent(CleanupPolicyEnabledAlert); const findCleanupAlert = () => wrapper.findComponent(CleanupPolicyEnabledAlert);
const fireFirstSortUpdate = () => {
findPersistedSearch().vm.$emit('update', { sort: 'UPDATED_DESC', filters: [] });
};
const waitForApolloRequestRender = async () => { const waitForApolloRequestRender = async () => {
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
await waitForPromises(); await waitForPromises();
...@@ -117,7 +121,7 @@ describe('List Page', () => { ...@@ -117,7 +121,7 @@ describe('List Page', () => {
it('contains registry header', async () => { it('contains registry header', async () => {
mountComponent(); mountComponent();
fireFirstSortUpdate();
await waitForApolloRequestRender(); await waitForApolloRequestRender();
expect(findRegistryHeader().exists()).toBe(true); expect(findRegistryHeader().exists()).toBe(true);
...@@ -167,7 +171,7 @@ describe('List Page', () => { ...@@ -167,7 +171,7 @@ describe('List Page', () => {
describe('isLoading is true', () => { describe('isLoading is true', () => {
it('shows the skeleton loader', async () => { it('shows the skeleton loader', async () => {
mountComponent(); mountComponent();
fireFirstSortUpdate();
await nextTick(); await nextTick();
expect(findSkeletonLoader().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(true);
...@@ -187,7 +191,7 @@ describe('List Page', () => { ...@@ -187,7 +191,7 @@ describe('List Page', () => {
it('title has the metadataLoading props set to true', async () => { it('title has the metadataLoading props set to true', async () => {
mountComponent(); mountComponent();
fireFirstSortUpdate();
await nextTick(); await nextTick();
expect(findRegistryHeader().props('metadataLoading')).toBe(true); expect(findRegistryHeader().props('metadataLoading')).toBe(true);
...@@ -244,6 +248,7 @@ describe('List Page', () => { ...@@ -244,6 +248,7 @@ describe('List Page', () => {
describe('unfiltered state', () => { describe('unfiltered state', () => {
it('quick start is visible', async () => { it('quick start is visible', async () => {
mountComponent(); mountComponent();
fireFirstSortUpdate();
await waitForApolloRequestRender(); await waitForApolloRequestRender();
...@@ -252,6 +257,7 @@ describe('List Page', () => { ...@@ -252,6 +257,7 @@ describe('List Page', () => {
it('list component is visible', async () => { it('list component is visible', async () => {
mountComponent(); mountComponent();
fireFirstSortUpdate();
await waitForApolloRequestRender(); await waitForApolloRequestRender();
...@@ -264,7 +270,7 @@ describe('List Page', () => { ...@@ -264,7 +270,7 @@ describe('List Page', () => {
.fn() .fn()
.mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock); .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
mountComponent({ detailsResolver }); mountComponent({ detailsResolver });
fireFirstSortUpdate();
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
await waitForPromises(); await waitForPromises();
...@@ -274,7 +280,7 @@ describe('List Page', () => { ...@@ -274,7 +280,7 @@ describe('List Page', () => {
it('does not block the list ui to show', async () => { it('does not block the list ui to show', async () => {
const detailsResolver = jest.fn().mockRejectedValue(); const detailsResolver = jest.fn().mockRejectedValue();
mountComponent({ detailsResolver }); mountComponent({ detailsResolver });
fireFirstSortUpdate();
await waitForApolloRequestRender(); await waitForApolloRequestRender();
expect(findImageList().exists()).toBe(true); expect(findImageList().exists()).toBe(true);
...@@ -285,6 +291,7 @@ describe('List Page', () => { ...@@ -285,6 +291,7 @@ describe('List Page', () => {
const detailsResolver = jest.fn().mockImplementation(() => new Promise(() => {})); const detailsResolver = jest.fn().mockImplementation(() => new Promise(() => {}));
mountComponent({ detailsResolver }); mountComponent({ detailsResolver });
fireFirstSortUpdate();
await waitForApolloRequestRender(); await waitForApolloRequestRender();
expect(findImageList().props('metadataLoading')).toBe(true); expect(findImageList().props('metadataLoading')).toBe(true);
...@@ -293,6 +300,7 @@ describe('List Page', () => { ...@@ -293,6 +300,7 @@ describe('List Page', () => {
describe('delete image', () => { describe('delete image', () => {
const selectImageForDeletion = async () => { const selectImageForDeletion = async () => {
fireFirstSortUpdate();
await waitForApolloRequestRender(); await waitForApolloRequestRender();
findImageList().vm.$emit('delete', deletedContainerRepository); findImageList().vm.$emit('delete', deletedContainerRepository);
...@@ -346,27 +354,27 @@ describe('List Page', () => { ...@@ -346,27 +354,27 @@ describe('List Page', () => {
describe('search and sorting', () => { describe('search and sorting', () => {
const doSearch = async () => { const doSearch = async () => {
await waitForApolloRequestRender(); await waitForApolloRequestRender();
findRegistrySearch().vm.$emit('filter:changed', [ findPersistedSearch().vm.$emit('update', {
{ type: FILTERED_SEARCH_TERM, value: { data: 'centos6' } }, sort: 'UPDATED_DESC',
]); filters: [{ type: FILTERED_SEARCH_TERM, value: { data: 'centos6' } }],
});
findRegistrySearch().vm.$emit('filter:submit'); findPersistedSearch().vm.$emit('filter:submit');
await waitForPromises(); await waitForPromises();
}; };
it('has a search box element', async () => { it('has a persisted search box element', async () => {
mountComponent(); mountComponent();
fireFirstSortUpdate();
await waitForApolloRequestRender(); await waitForApolloRequestRender();
const registrySearch = findRegistrySearch(); const registrySearch = findPersistedSearch();
expect(registrySearch.exists()).toBe(true); expect(registrySearch.exists()).toBe(true);
expect(registrySearch.props()).toMatchObject({ expect(registrySearch.props()).toMatchObject({
filter: [], defaultOrder: 'UPDATED',
sorting: { orderBy: 'UPDATED', sort: 'desc' }, defaultSort: 'desc',
sortableFields: SORT_FIELDS, sortableFields: SORT_FIELDS,
tokens: [],
}); });
}); });
...@@ -376,7 +384,7 @@ describe('List Page', () => { ...@@ -376,7 +384,7 @@ describe('List Page', () => {
await waitForApolloRequestRender(); await waitForApolloRequestRender();
findRegistrySearch().vm.$emit('sorting:changed', { sort: 'asc' }); findPersistedSearch().vm.$emit('update', { sort: 'UPDATED_DESC', filters: [] });
await nextTick(); await nextTick();
expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' })); expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' }));
...@@ -416,7 +424,7 @@ describe('List Page', () => { ...@@ -416,7 +424,7 @@ describe('List Page', () => {
.fn() .fn()
.mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock); .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
mountComponent({ resolver, detailsResolver }); mountComponent({ resolver, detailsResolver });
fireFirstSortUpdate();
await waitForApolloRequestRender(); await waitForApolloRequestRender();
findImageList().vm.$emit('prev-page'); findImageList().vm.$emit('prev-page');
...@@ -436,7 +444,7 @@ describe('List Page', () => { ...@@ -436,7 +444,7 @@ describe('List Page', () => {
.fn() .fn()
.mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock); .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
mountComponent({ resolver, detailsResolver }); mountComponent({ resolver, detailsResolver });
fireFirstSortUpdate();
await waitForApolloRequestRender(); await waitForApolloRequestRender();
findImageList().vm.$emit('next-page'); findImageList().vm.$emit('next-page');
...@@ -455,6 +463,7 @@ describe('List Page', () => { ...@@ -455,6 +463,7 @@ describe('List Page', () => {
describe('modal', () => { describe('modal', () => {
beforeEach(() => { beforeEach(() => {
mountComponent(); mountComponent();
fireFirstSortUpdate();
}); });
it('exists', () => { it('exists', () => {
...@@ -472,6 +481,7 @@ describe('List Page', () => { ...@@ -472,6 +481,7 @@ describe('List Page', () => {
describe('tracking', () => { describe('tracking', () => {
beforeEach(() => { beforeEach(() => {
mountComponent(); mountComponent();
fireFirstSortUpdate();
}); });
const testTrackingCall = (action) => { const testTrackingCall = (action) => {
...@@ -502,62 +512,6 @@ describe('List Page', () => { ...@@ -502,62 +512,6 @@ describe('List Page', () => {
}); });
}); });
describe('url query string handling', () => {
const defaultQueryParams = {
search: [1, 2],
sort: 'asc',
orderBy: 'CREATED',
};
const queryChangePayload = 'foo';
it('query:updated event pushes the new query to the router', async () => {
const push = jest.fn();
mountComponent({ mocks: { $router: { push } } });
await nextTick();
findRegistrySearch().vm.$emit('query:changed', queryChangePayload);
expect(push).toHaveBeenCalledWith({ query: queryChangePayload });
});
it('graphql API call has the variables set from the URL', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
mountComponent({ query: defaultQueryParams, resolver });
await nextTick();
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({
name: 1,
sort: 'CREATED_ASC',
}),
);
});
it.each`
sort | orderBy | search | payload
${'ASC'} | ${undefined} | ${undefined} | ${{ sort: 'UPDATED_ASC' }}
${undefined} | ${'bar'} | ${undefined} | ${{ sort: 'BAR_DESC' }}
${'ASC'} | ${'bar'} | ${undefined} | ${{ sort: 'BAR_ASC' }}
${undefined} | ${undefined} | ${undefined} | ${{}}
${undefined} | ${undefined} | ${['one']} | ${{ name: 'one' }}
${undefined} | ${undefined} | ${['one', 'two']} | ${{ name: 'one' }}
${undefined} | ${'UPDATED'} | ${['one', 'two']} | ${{ name: 'one', sort: 'UPDATED_DESC' }}
${'ASC'} | ${'UPDATED'} | ${['one', 'two']} | ${{ name: 'one', sort: 'UPDATED_ASC' }}
`(
'with sort equal to $sort, orderBy equal to $orderBy, search set to $search API call has the variables set as $payload',
async ({ sort, orderBy, search, payload }) => {
const resolver = jest.fn().mockResolvedValue({ sort, orderBy });
mountComponent({ query: { sort, orderBy, search }, resolver });
await nextTick();
expect(resolver).toHaveBeenCalledWith(expect.objectContaining(payload));
},
);
});
describe('cleanup is on alert', () => { describe('cleanup is on alert', () => {
it('exist when showCleanupPolicyOnAlert is true and has the correct props', async () => { it('exist when showCleanupPolicyOnAlert is true and has the correct props', async () => {
mountComponent({ mountComponent({
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment