Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(openchallenges): fix bugs related search dropdown filters on search page #2688

Merged
merged 32 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
01ee477
save some testing work
rrchai May 17, 2024
11fb2b2
Merge branch 'main' into fix-bugs-filter
tschaffter May 17, 2024
fdbc453
Revert "save some testing work"
rrchai May 17, 2024
59612b8
Merge remote-tracking branch 'upstream/main' into fix-bugs-filter
rrchai May 17, 2024
39f04a2
move logic for loadedPages to the child filter component
rrchai May 17, 2024
d796eb9
removee dupliciated codes
rrchai May 17, 2024
54ba2a7
removee dupliciated codes x 2
rrchai May 17, 2024
2580011
set up a custom loader template
rrchai May 18, 2024
b9ffd71
add loader template
rrchai May 18, 2024
4984c9b
remove loader on empty
rrchai May 18, 2024
d725d24
disable loader animation by default
rrchai May 18, 2024
e5dbdd5
wait for only search term changes
rrchai May 18, 2024
273a0b4
change back to check all object changes
rrchai May 18, 2024
2d9a67e
expand params to match item size displayed with scroll height
rrchai May 18, 2024
ba2dd3d
add some space at the bottom
rrchai May 18, 2024
88e3247
save temp codes
rrchai May 18, 2024
5069a44
add the selected param in url for dropdown filters
rrchai May 20, 2024
7d21260
manage labels for invalid selected values
rrchai May 20, 2024
4eacef9
set deplays to 400ms
rrchai May 20, 2024
40a90ba
fix test
rrchai May 21, 2024
184f55f
remove unused attr on dropdown filter
rrchai May 21, 2024
6108c07
adjust bottom space
rrchai May 21, 2024
b62f544
test adding error on invalid value
rrchai May 23, 2024
e2f8e82
Revert "test adding error on invalid value"
rrchai May 23, 2024
84e984e
fix issue that the selected values are not shown when seasrch term ch…
rrchai May 23, 2024
043b4da
avoid negative pages
rrchai May 23, 2024
7a6e104
Merge remote-tracking branch 'upstream/main' into fix-bugs-filter
rrchai May 23, 2024
baed0e3
update text
rrchai May 23, 2024
099a4f0
set pageSize to 50 & displaying items to 10
rrchai May 24, 2024
14fc74a
change icon to avatar for loader
rrchai May 24, 2024
d8b637f
Merge branch 'main' into fix-bugs-filter
rrchai May 30, 2024
96145d8
Merge branch 'main' into fix-bugs-filter
rrchai Jun 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ export type ChallengeSearchDropdown =
| 'inputDataTypes'
| 'organizations'
| 'platforms';

export const CHALLENGE_SEARCH_DROPDOWNS: ChallengeSearchDropdown[] = [
'inputDataTypes',
'organizations',
'platforms',
];
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ <h2>Challenges</h2>
>
<openchallenges-checkbox-filter
[options]="incentivesFilter.options"
[selectedOptions]="selectedIncentives"
[selectedOptions]="selectedValues['incentives']"
(selectionChange)="onParamChange({ incentives: $event })"
inputId="{{ incentivesFilter.query }}"
/>
Expand All @@ -54,7 +54,7 @@ <h2>Challenges</h2>
>
<openchallenges-checkbox-filter
[options]="statusFilter.options"
[selectedOptions]="selectedStatus"
[selectedOptions]="selectedValues['status']"
(selectionChange)="onParamChange({ status: $event })"
inputId="{{ statusFilter.query }}"
/>
Expand All @@ -67,7 +67,7 @@ <h2>Challenges</h2>
>
<openchallenges-checkbox-filter
[options]="categoriesFilter.options"
[selectedOptions]="selectedCategories"
[selectedOptions]="selectedValues['categories']"
(selectionChange)="onParamChange({ categories: $event })"
inputId="{{ categoriesFilter.query }}"
/>
Expand All @@ -80,13 +80,14 @@ <h2>Challenges</h2>
>
<openchallenges-search-dropdown-filter
[options]="dropdownFilters['platforms'].options"
[selectedOptions]="selectedPlatforms"
[optionsPerPage]="defaultDropdownOptionSize"
[selectedOptions]="selectedValues['platforms']"
placeholder="{{ dropdownFilters['platforms'].label.toLowerCase() + '(s)' }} "
[showAvatar]="dropdownFilters['platforms'].showAvatar"
[filterByApiClient]="true"
(selectionChange)="onParamChange({ platforms: $event })"
(searchChange)="onSearchChange('platforms', $event)"
(lazyLoad)="onLazyLoad('platforms', $event)"
(pageChange)="onLazyLoad('platforms', $event)"
/>
</p-panel>
<p-divider></p-divider>
Expand All @@ -97,13 +98,14 @@ <h2>Challenges</h2>
>
<openchallenges-search-dropdown-filter
[options]="dropdownFilters['inputDataTypes'].options"
[selectedOptions]="selectedInputDataTypes"
[optionsPerPage]="defaultDropdownOptionSize"
[selectedOptions]="selectedValues['inputDataTypes']"
placeholder="{{ dropdownFilters['inputDataTypes'].label.toLowerCase() + '(s)' }} "
[showAvatar]="dropdownFilters['inputDataTypes'].showAvatar"
[filterByApiClient]="true"
(selectionChange)="onParamChange({ inputDataTypes: $event })"
(searchChange)="onSearchChange('inputDataTypes', $event)"
(lazyLoad)="onLazyLoad('inputDataTypes', $event)"
(pageChange)="onLazyLoad('inputDataTypes', $event)"
/>
</p-panel>
<p-divider></p-divider>
Expand All @@ -114,13 +116,14 @@ <h2>Challenges</h2>
>
<openchallenges-search-dropdown-filter
[options]="dropdownFilters['organizations'].options"
[selectedOptions]="selectedOrgs"
[optionsPerPage]="defaultDropdownOptionSize"
[selectedOptions]="selectedValues['organizations']"
placeholder="{{ dropdownFilters['organizations'].label.toLowerCase() + '(s)' }} "
[showAvatar]="dropdownFilters['organizations'].showAvatar"
[filterByApiClient]="true"
(selectionChange)="onParamChange({ organizations: $event })"
(searchChange)="onSearchChange('organizations', $event)"
(lazyLoad)="onLazyLoad('organizations', $event)"
(pageChange)="onLazyLoad('organizations', $event)"
/>
</p-panel>
<p-divider></p-divider>
Expand Down Expand Up @@ -162,7 +165,7 @@ <h2>Challenges</h2>
>
<openchallenges-checkbox-filter
[options]="submissionTypesFilter.options"
[selectedOptions]="selectedSubmissionTypes"
[selectedOptions]="selectedValues['submissionTypes']"
(selectionChange)="onParamChange({ submissionTypes: $event })"
inputId="{{ submissionTypesFilter.query }}"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ import { SeoService } from '@sagebionetworks/shared/util';
import { getSeoData } from './challenge-search-seo-data';
import { HttpParams } from '@angular/common/http';
import { ChallengeSearchDataService } from './challenge-search-data.service';
import { MultiSelectLazyLoadEvent } from 'primeng/multiselect';
import { ChallengeSearchDropdown } from './challenge-search-dropdown';
import {
ChallengeSearchDropdown,
CHALLENGE_SEARCH_DROPDOWNS,
} from './challenge-search-dropdown';

@Component({
selector: 'openchallenges-challenge-search',
Expand Down Expand Up @@ -133,6 +135,7 @@ export class ChallengeSearchComponent
// set default values
defaultPageNumber = 0;
defaultPageSize = 24;
defaultDropdownOptionSize = 5;
tschaffter marked this conversation as resolved.
Show resolved Hide resolved
tschaffter marked this conversation as resolved.
Show resolved Hide resolved
defaultSelectedYear = undefined;
defaultSortedBy: ChallengeSort = 'relevance';

Expand All @@ -148,16 +151,17 @@ export class ChallengeSearchComponent

// set dropdown filter placeholders
dropdownFilters!: { [key: string]: FilterPanel };
loadedPages!: { [key: string]: Set<number> };

// define selected filter values
selectedCategories!: ChallengeCategory[];
selectedIncentives!: ChallengeIncentive[];
selectedInputDataTypes!: number[];
selectedOrgs!: number[];
selectedPlatforms!: string[];
selectedStatus!: ChallengeStatus[];
selectedSubmissionTypes!: ChallengeSubmissionType[];
selectedValues = {
categories: [] as ChallengeCategory[],
incentives: [] as ChallengeIncentive[],
inputDataTypes: [] as number[],
organizations: [] as number[],
platforms: [] as string[],
status: [] as ChallengeStatus[],
submissionTypes: [] as ChallengeSubmissionType[],
};

constructor(
private activatedRoute: ActivatedRoute,
Expand Down Expand Up @@ -200,36 +204,40 @@ export class ChallengeSearchComponent
}

// update selected filter values based on params in url
this.selectedCategories = this.splitParam(params['categories']);
this.selectedIncentives = this.splitParam(params['incentives']);
this.selectedInputDataTypes = this.splitParam(
params['inputDataTypes']
).map((idString) => +idString);

this.searchedTerms = params['searchTerms'];
this.selectedOrgs = this.splitParam(params['organizations']).map(
(idString) => +idString
);
this.selectedPageNumber = +params['pageNumber'] || this.defaultPageNumber;
this.selectedPageSize = this.defaultPageSize; // no available pageSize options for users
this.selectedPlatforms = this.splitParam(params['platforms']);
this.selectedStatus = this.splitParam(params['status']);
this.selectedSubmissionTypes = this.splitParam(params['submissionTypes']);
this.sortedBy = params['sort'] || this.defaultSortedBy;

this.selectedValues['categories'] = this.splitParam(params['categories']);
this.selectedValues['incentives'] = this.splitParam(params['incentives']);
this.selectedValues['inputDataTypes'] = this.splitParam(
params['inputDataTypes']
).map((idString) => +idString);
this.selectedValues['organizations'] = this.splitParam(
params['organizations']
).map((idString) => +idString);
this.selectedValues['platforms'] = this.splitParam(params['platforms']);
this.selectedValues['status'] = this.splitParam(params['status']);
this.selectedValues['submissionTypes'] = this.splitParam(
params['submissionTypes']
);

const defaultQuery: ChallengeSearchQuery = {
categories: this.selectedCategories,
incentives: this.selectedIncentives,
inputDataTypes: this.selectedInputDataTypes,
categories: this.selectedValues['categories'],
incentives: this.selectedValues['incentives'],
inputDataTypes: this.selectedValues['inputDataTypes'],
maxStartDate: this.selectedMaxStartDate,
minStartDate: this.selectedMinStartDate,
organizations: this.selectedOrgs,
organizations: this.selectedValues['organizations'],
pageNumber: this.selectedPageNumber,
pageSize: this.selectedPageSize,
platforms: this.selectedPlatforms,
platforms: this.selectedValues['platforms'],
searchTerms: this.searchedTerms,
sort: this.sortedBy,
status: this.selectedStatus,
submissionTypes: this.selectedSubmissionTypes,
status: this.selectedValues['status'],
submissionTypes: this.selectedValues['submissionTypes'],
};

this.query.next(defaultQuery);
Expand All @@ -240,12 +248,7 @@ export class ChallengeSearchComponent
this.totalChallengesCount = page.totalElements;
});

// update loaded pages and dropdown filters
this.loadedPages = {
inputDataTypes: new Set(),
organizations: new Set(),
platforms: new Set(),
};
// update dropdown filters
this.dropdownFilters = {
inputDataTypes: challengeInputDataTypesFilterPanel,
organizations: challengeOrganizationsFilterPanel,
Expand Down Expand Up @@ -330,6 +333,14 @@ export class ChallengeSearchComponent
// this.selectedPageSize = this.defaultPageSize;
this.paginator.resetPageNumber();
}

// update selected filter values if specific parameters change
Object.keys(filteredQuery).forEach((key) => {
if (key in this.selectedValues && filteredQuery[key] !== undefined) {
(this.selectedValues as any)[key] = filteredQuery[key];
}
});

// update params of URL
const currentParams = new HttpParams({
fromString: this._location.path().split('?')[1] ?? '',
Expand Down Expand Up @@ -365,52 +376,68 @@ export class ChallengeSearchComponent
if (searchType === 'challenges') {
this.challengeSearchTerms.next(searched);
} else {
this.loadedPages[searchType].clear();
this.dropdownFilters[searchType].options = [];
this.challengeSearchDataService.setSearchQuery(searchType, {
// reset options except selections when search term is applied
const selectedOptions = this.dropdownFilters[searchType].options.filter(
(option) => {
if (Array.isArray(option.value)) {
return option.value.some((item) =>
(this.selectedValues[searchType] as FilterValue[]).includes(item)
);
} else {
return (this.selectedValues[searchType] as FilterValue[]).includes(
option.value
);
}
}
);
this.dropdownFilters[searchType].options = selectedOptions;
this.challengeSearchDataService.setEdamConceptSearchQuery({
searchTerms: searched,
});
}
}

onLazyLoad(
dropdown: ChallengeSearchDropdown,
event: MultiSelectLazyLoadEvent
): void {
const size = this.defaultPageSize;
const startPage = Math.floor(event.first / size);
const endPage = Math.floor(event.last / size);
onLazyLoad(dropdown: ChallengeSearchDropdown, page: number): void {
const query: any = { pageNumber: page };

// load next page as scrolling down
for (let page = startPage; page <= endPage; page++) {
if (!this.loadedPages[dropdown].has(page)) {
this.loadedPages[dropdown].add(page);
this.challengeSearchDataService.setSearchQuery(dropdown, {
pageNumber: page,
pageSize: size,
});
}
if (page === 0) {
// reset ids and slugs params of dropdown search query
query.ids = undefined;
query.slugs = undefined;
}
// load next page as scrolling down
this.challengeSearchDataService.setSearchQuery(dropdown, query);
}

private setDropdownSelections(): void {
this.challengeSearchDataService.setSearchQuery('inputDataTypes', {
ids: this.selectedValues['inputDataTypes'],
});
this.challengeSearchDataService.setSearchQuery('organizations', {
ids: this.selectedValues['organizations'],
});
this.challengeSearchDataService.setSearchQuery('platforms', {
slugs: this.selectedValues['platforms'],
});
}

private loadInitialDropdownData(): void {
const dropdowns = [
'inputDataTypes',
'organizations',
'platforms',
] as ChallengeSearchDropdown[];
dropdowns.forEach((dropdown) => {
// query the dropdown filter option(s) pre-selected in url param (only initially)
this.setDropdownSelections();

// fetch and update dropdown options with new data for each dropdown category
CHALLENGE_SEARCH_DROPDOWNS.forEach((dropdown) => {
const extraDefaultParams =
dropdown === 'inputDataTypes' ? { sections: [EdamSection.Data] } : {};

this.challengeSearchDataService
.fetchData(dropdown, {
pageSize: this.defaultPageSize, // set constant pageSize to match lazyLoad
pageSize: this.defaultDropdownOptionSize, // set constant pageSize to match lazyLoad
...extraDefaultParams,
})
.pipe(takeUntil(this.destroy))
.subscribe((newOptions) => {
// update filter options by taking unique filter values
// update filter options by appending new unique filter values to the bottom
this.dropdownFilters[dropdown].options = unionWith(
this.dropdownFilters[dropdown].options,
newOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
[(ngModel)]="selectedOptions"
(onChange)="onChange(selectedOptions)"
[placeholder]="placeholder"
[maxSelectedLabels]="2"
selectedItemsLabel="{0} items selected"
[overlayOptions]="overlayOptions"
[filter]="filter"
(onFilter)="onSearch($event)"
[virtualScroll]="true"
[virtualScrollItemSize]="50"
[virtualScrollOptions]="scrollOptions"
[virtualScrollItemSize]="itemHeight"
tschaffter marked this conversation as resolved.
Show resolved Hide resolved
[virtualScrollOptions]="scrollerOptions"
[lazy]="lazy"
(onLazyLoad)="onLazyLoad($event)"
>
Expand All @@ -29,7 +27,7 @@
</div>
</ng-template>

<!-- loader template with skeleton -->
<!-- custom loader with skeleton -->
<ng-template pTemplate="loader">
<div *ngIf="showAvatar; else noIconLoader" class="loader-with-icon-container">
tschaffter marked this conversation as resolved.
Show resolved Hide resolved
<div *ngFor="let _ of [].constructor(5)" class="skeleton-with-icon">
Expand All @@ -44,32 +42,17 @@
</ng-template>
</ng-template>

<!-- Empty results templates -->
<!-- custom message for empty results -->
tschaffter marked this conversation as resolved.
Show resolved Hide resolved
<ng-template pTemplate="empty">
<div *ngIf="options.length === 0 && isLoading; else noResults">
<div *ngIf="showAvatar; else noIconLoader" class="loader-with-icon-container">
<div *ngFor="let _ of [].constructor(5)" class="skeleton-with-icon">
<p-skeleton class="circle" shape="circle" size="2rem"></p-skeleton>
<p-skeleton width="12rem" height="1rem"></p-skeleton>
</div>
</div>
<ng-template #noIconLoader>
<div class="loader-container">
<p-skeleton
*ngFor="let _ of [].constructor(5)"
class="skeleton"
width="14rem"
height="1.3rem"
borderRadius="16px"
></p-skeleton>
</div>
</ng-template>
</div>
<ng-template #noResults>
<div *ngIf="options.length === 0 && !isLoading">
<span>No results found</span>
</div>
</ng-template>
<ng-container *ngIf="!isLoading">
<span>No results found</span>
</ng-container>
</ng-template>

<!-- custom label for the selected values -->
<ng-template pTemplate="selectedItems" let-option>
<div *ngIf="validSelectionCount > 0">{{ validSelectionCount }} item(s) selected</div>
<div *ngIf="validSelectionCount === 0">{{ placeholder }}</div>
</ng-template>

<!-- custom list items -->
Expand Down
Loading
Loading