diff --git a/src/app/features/my-projects/my-projects.component.ts b/src/app/features/my-projects/my-projects.component.ts index 0e2e32b83..236d0b332 100644 --- a/src/app/features/my-projects/my-projects.component.ts +++ b/src/app/features/my-projects/my-projects.component.ts @@ -11,6 +11,7 @@ import { debounceTime, distinctUntilChanged, filter, tap } from 'rxjs'; import { ChangeDetectionStrategy, Component, + computed, DestroyRef, effect, inject, @@ -25,7 +26,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { MyProjectsTableComponent, SelectComponent, SubHeaderComponent } from '@osf/shared/components'; import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants'; import { ResourceType, SortOrder } from '@osf/shared/enums'; -import { IS_MEDIUM, parseQueryFilterParams } from '@osf/shared/helpers'; +import { IS_MEDIUM } from '@osf/shared/helpers'; import { MyResourcesItem, MyResourcesSearchFilters, QueryParams, TableParameters } from '@osf/shared/models'; import { BookmarksSelectors, @@ -39,6 +40,8 @@ import { } from '@osf/shared/stores'; import { CustomDialogService, ProjectRedirectDialogService } from '@shared/services'; +import { MyProjectsQueryService } from './services/my-projects-query.service'; +import { MyProjectsTableParamsService } from './services/my-projects-table-params.service'; import { CreateProjectDialogComponent } from './components'; import { MY_PROJECTS_TABS } from './constants'; import { MyProjectsTab } from './enums'; @@ -67,7 +70,10 @@ export class MyProjectsComponent implements OnInit { readonly router = inject(Router); readonly route = inject(ActivatedRoute); readonly projectRedirectDialogService = inject(ProjectRedirectDialogService); + readonly queryService = inject(MyProjectsQueryService); + readonly tableParamsService = inject(MyProjectsTableParamsService); + readonly bookmarksPageSize = 100; readonly isLoading = signal(false); readonly isTablet = toSignal(inject(IS_MEDIUM)); readonly tabOptions = MY_PROJECTS_TABS; @@ -92,8 +98,8 @@ export class MyProjectsComponent implements OnInit { readonly totalRegistrationsCount = select(MyResourcesSelectors.getTotalRegistrations); readonly totalPreprintsCount = select(MyResourcesSelectors.getTotalPreprints); readonly totalBookmarksCount = select(MyResourcesSelectors.getTotalBookmarks); - readonly bookmarksCollectionId = select(BookmarksSelectors.getBookmarksCollectionId); + readonly isBookmarks = computed(() => this.selectedTab() === MyProjectsTab.Bookmarks); readonly actions = createDispatchMap({ getBookmarksCollectionId: GetBookmarksCollectionId, @@ -108,6 +114,7 @@ export class MyProjectsComponent implements OnInit { this.setupQueryParamsEffect(); this.setupSearchSubscription(); this.setupTotalRecordsEffect(); + this.setupBookmarksCollectionEffect(); this.setupCleanup(); } @@ -115,19 +122,64 @@ export class MyProjectsComponent implements OnInit { this.actions.getBookmarksCollectionId(); } - setupCleanup(): void { + onPageChange(event: TablePageEvent): void { + const current = this.queryService.getRawParams(); + this.queryService.handlePageChange(event.first, event.rows, current, this.selectedTab()); + } + + onSort(event: SortEvent): void { + if (event.field) { + const current = this.queryService.getRawParams(); + this.queryService.handleSort(event.field, event.order as SortOrder, current, this.selectedTab()); + } + } + + onTabChange(tabIndex: number): void { + this.actions.clearMyProjects(); + this.selectedTab.set(tabIndex); + const current = this.queryService.getRawParams(); + this.queryService.handleTabSwitch(current, this.selectedTab()); + } + + createProject(): void { + const dialogWidth = this.isTablet() ? '850px' : '95vw'; + + this.customDialogService + .open(CreateProjectDialogComponent, { + header: 'myProjects.header.createProject', + width: dialogWidth, + }) + .onClose.pipe( + filter((result) => result?.project.id), + tap((result) => this.projectRedirectDialogService.showProjectRedirectDialog(result.project.id)), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(); + } + + navigateToProject(project: MyResourcesItem): void { + this.activeProject.set(project); + this.router.navigate([project.id]); + } + + navigateToRegistry(registry: MyResourcesItem): void { + this.activeProject.set(registry); + this.router.navigate([registry.id]); + } + + private setupCleanup(): void { this.destroyRef.onDestroy(() => { this.actions.clearMyProjects(); }); } - setupSearchSubscription(): void { + private setupSearchSubscription(): void { this.searchControl.valueChanges .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) .subscribe((searchValue) => this.handleSearch(searchValue ?? '')); } - setupTotalRecordsEffect(): void { + private setupTotalRecordsEffect(): void { effect(() => { const totalRecords = this.getTotalRecordsForCurrentTab(); untracked(() => { @@ -136,7 +188,22 @@ export class MyProjectsComponent implements OnInit { }); } - getTotalRecordsForCurrentTab(): number { + private setupBookmarksCollectionEffect(): void { + effect(() => { + const collectionId = this.bookmarksCollectionId(); + const params = this.queryParams(); + + if (collectionId && this.isBookmarks() && params) { + untracked(() => { + const queryParams = this.queryService.toQueryModel(params); + this.updateComponentState(queryParams); + this.fetchDataForCurrentTab(queryParams); + }); + } + }); + } + + private getTotalRecordsForCurrentTab(): number { switch (this.selectedTab()) { case MyProjectsTab.Projects: return this.totalProjectsCount(); @@ -151,49 +218,65 @@ export class MyProjectsComponent implements OnInit { } } - setupQueryParamsEffect(): void { + private setupQueryParamsEffect(): void { effect(() => { const params = this.queryParams(); if (!params) return; - const { page, size, search, sortColumn, sortOrder } = parseQueryFilterParams(params); + const raw = params; + + if (!this.queryService.hasTabInUrl(raw)) { + untracked(() => { + const current = this.queryParams() || {}; + this.queryService.updateParams({ page: 1 }, current, this.selectedTab()); + }); + return; + } + + untracked(() => { + const tabFromUrl = this.queryService.getTabFromUrl(raw); + if (tabFromUrl !== null && this.selectedTab() !== tabFromUrl) { + this.selectedTab.set(tabFromUrl); + } - this.updateComponentState({ page, size, search, sortColumn, sortOrder }); - this.fetchDataForCurrentTab({ - page, - size, - search, - sortColumn, - sortOrder, + if (!this.isBookmarks()) { + const queryParams = this.queryService.toQueryModel(raw); + this.updateComponentState(queryParams); + this.fetchDataForCurrentTab(queryParams); + } }); }); } - updateComponentState(params: QueryParams): void { + private updateComponentState(params: QueryParams): void { untracked(() => { const size = params.size || DEFAULT_TABLE_PARAMS.rows; this.currentPage.set(params.page ?? 1); this.currentPageSize.set(size); - this.searchControl.setValue(params.search || ''); + this.searchControl.setValue(params.search || '', { emitEvent: false }); this.sortColumn.set(params.sortColumn); this.sortOrder.set(params.sortOrder ?? SortOrder.Asc); - this.updateTableParams({ + const totalRecords = this.getTotalRecordsForCurrentTab(); + const tableParams = this.tableParamsService.buildTableParams(size, totalRecords, this.isBookmarks()); + tableParams.firstRowIndex = ((params.page ?? 1) - 1) * size; + + this.tableParams.set({ + ...tableParams, rows: size, - firstRowIndex: ((params.page ?? 1) - 1) * size, }); }); } - updateTableParams(updates: Partial): void { + private updateTableParams(updates: Partial): void { this.tableParams.update((current) => ({ ...current, ...updates, })); } - fetchDataForCurrentTab(params: QueryParams): void { + private fetchDataForCurrentTab(params: QueryParams): void { this.isLoading.set(true); const filters = this.createFilters(params); const pageNumber = params.page ?? 1; @@ -215,7 +298,7 @@ export class MyProjectsComponent implements OnInit { action$ = this.actions.getMyBookmarks( this.bookmarksCollectionId(), pageNumber, - pageSize, + this.bookmarksPageSize, filters, ResourceType.Null ); @@ -233,7 +316,7 @@ export class MyProjectsComponent implements OnInit { }); } - createFilters(params: QueryParams): MyResourcesSearchFilters { + private createFilters(params: QueryParams): MyResourcesSearchFilters { return { searchValue: params.search || '', searchFields: @@ -243,108 +326,8 @@ export class MyProjectsComponent implements OnInit { }; } - handleSearch(searchValue: string): void { - const currentParams = this.queryParams() || {}; - this.updateQueryParams({ - search: searchValue, - page: 1, - sortColumn: currentParams['sortColumn'], - sortOrder: currentParams['sortOrder'] === 'desc' ? SortOrder.Desc : SortOrder.Asc, - }); - } - - updateQueryParams(updates: Partial): void { - const currentParams = this.queryParams() || {}; - const queryParams: Record = {}; - - if ('page' in updates || currentParams['page']) { - queryParams['page'] = updates.page?.toString() ?? currentParams['page']; - } - if ('size' in updates || currentParams['size']) { - queryParams['size'] = updates.size?.toString() ?? currentParams['size']; - } - - if ('search' in updates || currentParams['search']) { - const search = updates.search ?? currentParams['search']; - if (search) { - queryParams['search'] = search; - } - } - - if ('sortColumn' in updates) { - if (updates.sortColumn) { - queryParams['sortColumn'] = updates.sortColumn; - queryParams['sortOrder'] = updates.sortOrder === SortOrder.Desc ? 'desc' : 'asc'; - } - } else if (currentParams['sortColumn']) { - queryParams['sortColumn'] = currentParams['sortColumn']; - queryParams['sortOrder'] = currentParams['sortOrder']; - } - - this.router.navigate([], { - relativeTo: this.route, - queryParams, - }); - } - - onPageChange(event: TablePageEvent): void { - const page = Math.floor(event.first / event.rows) + 1; - const currentParams = this.queryParams() || {}; - - this.updateQueryParams({ - page, - size: event.rows, - sortColumn: currentParams['sortColumn'], - sortOrder: currentParams['sortOrder'] === 'desc' ? SortOrder.Desc : SortOrder.Asc, - }); - } - - onSort(event: SortEvent): void { - if (event.field) { - this.updateQueryParams({ - sortColumn: event.field, - sortOrder: event.order as SortOrder, - }); - } - } - - onTabChange(tabIndex: number): void { - this.actions.clearMyProjects(); - this.selectedTab.set(tabIndex); - const currentParams = this.queryParams() || {}; - - this.updateQueryParams({ - page: 1, - size: currentParams['size'], - search: '', - sortColumn: undefined, - sortOrder: undefined, - }); - } - - createProject(): void { - const dialogWidth = this.isTablet() ? '850px' : '95vw'; - - this.customDialogService - .open(CreateProjectDialogComponent, { - header: 'myProjects.header.createProject', - width: dialogWidth, - }) - .onClose.pipe( - filter((result) => result?.project.id), - tap((result) => this.projectRedirectDialogService.showProjectRedirectDialog(result.project.id)), - takeUntilDestroyed(this.destroyRef) - ) - .subscribe(); - } - - navigateToProject(project: MyResourcesItem): void { - this.activeProject.set(project); - this.router.navigate([project.id]); - } - - navigateToRegistry(registry: MyResourcesItem): void { - this.activeProject.set(registry); - this.router.navigate([registry.id]); + private handleSearch(searchValue: string): void { + const current = this.queryService.getRawParams(); + this.queryService.handleSearch(searchValue, current, this.selectedTab()); } } diff --git a/src/app/features/my-projects/services/my-projects-query.service.ts b/src/app/features/my-projects/services/my-projects-query.service.ts new file mode 100644 index 000000000..378408f3b --- /dev/null +++ b/src/app/features/my-projects/services/my-projects-query.service.ts @@ -0,0 +1,125 @@ +import { inject, Injectable } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { SortOrder } from '@osf/shared/enums'; +import { parseQueryFilterParams } from '@osf/shared/helpers'; +import { QueryParams } from '@osf/shared/models'; + +import { MyProjectsTab } from '../enums'; + +@Injectable({ providedIn: 'root' }) +export class MyProjectsQueryService { + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + + updateParams(updates: Partial, currentParams: Record, selectedTab: MyProjectsTab): void { + const queryParams: Record = {}; + + queryParams['tab'] = String(selectedTab); + + if ('page' in updates || currentParams['page']) { + queryParams['page'] = updates.page?.toString() ?? currentParams['page']; + } + + const isBookmarks = selectedTab === MyProjectsTab.Bookmarks; + if (!isBookmarks && ('size' in updates || currentParams['size'])) { + queryParams['size'] = updates.size?.toString() ?? currentParams['size']; + } + + if ('search' in updates || currentParams['search']) { + const search = updates.search ?? currentParams['search']; + if (search) { + queryParams['search'] = search; + } + } + + if ('sortColumn' in updates) { + if (updates.sortColumn) { + queryParams['sortColumn'] = updates.sortColumn; + queryParams['sortOrder'] = updates.sortOrder === SortOrder.Desc ? 'desc' : 'asc'; + } + } else if (currentParams['sortColumn']) { + queryParams['sortColumn'] = currentParams['sortColumn']; + queryParams['sortOrder'] = currentParams['sortOrder']; + } + + this.router.navigate([], { + relativeTo: this.route, + queryParams, + }); + } + + getRawParams(): Record { + return (this.route.snapshot?.queryParams as Record) || {}; + } + + toQueryModel(raw: Record): QueryParams { + return parseQueryFilterParams(raw); + } + + hasTabInUrl(raw: Record): boolean { + return 'tab' in raw; + } + + getTabFromUrl(raw: Record): number | null { + const tabParam = Number(raw['tab']); + return !Number.isNaN(tabParam) ? tabParam : null; + } + + handleSearch(searchValue: string, currentParams: Record, selectedTab: MyProjectsTab): void { + const updates = this.buildSearchUpdates(searchValue, currentParams); + this.updateParams(updates, currentParams, selectedTab); + } + + handlePageChange( + first: number, + rows: number, + currentParams: Record, + selectedTab: MyProjectsTab + ): void { + const updates = this.buildPageUpdates(first, rows, currentParams); + this.updateParams(updates, currentParams, selectedTab); + } + + handleSort(field: string, order: SortOrder, currentParams: Record, selectedTab: MyProjectsTab): void { + const updates = this.buildSortUpdates(field, order); + this.updateParams(updates, currentParams, selectedTab); + } + + handleTabSwitch(currentParams: Record, selectedTab: MyProjectsTab): void { + const updates = this.buildTabSwitchUpdates(); + this.updateParams(updates, currentParams, selectedTab); + } + + private buildSearchUpdates(search: string, current: Record): Partial { + return { + search, + page: 1, + sortColumn: current['sortColumn'], + sortOrder: current['sortOrder'] === 'desc' ? SortOrder.Desc : SortOrder.Asc, + }; + } + + private buildPageUpdates(first: number, rows: number, current: Record): Partial { + const page = Math.floor(first / rows) + 1; + return { + page, + size: rows, + sortColumn: current['sortColumn'], + sortOrder: current['sortOrder'] === 'desc' ? SortOrder.Desc : SortOrder.Asc, + }; + } + + private buildSortUpdates(field: string, order: SortOrder): Partial { + return { sortColumn: field, sortOrder: order }; + } + + private buildTabSwitchUpdates(): Partial { + return { + page: 1, + search: '', + sortColumn: undefined, + sortOrder: undefined, + }; + } +} diff --git a/src/app/features/my-projects/services/my-projects-table-params.service.ts b/src/app/features/my-projects/services/my-projects-table-params.service.ts new file mode 100644 index 000000000..8ee82183a --- /dev/null +++ b/src/app/features/my-projects/services/my-projects-table-params.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; + +import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants'; +import { TableParameters } from '@osf/shared/models'; + +@Injectable({ providedIn: 'root' }) +export class MyProjectsTableParamsService { + buildTableParams(baseRows: number, totalRecords: number, isBookmarks: boolean): TableParameters { + if (isBookmarks) { + return { + ...DEFAULT_TABLE_PARAMS, + paginator: false, + rows: totalRecords, + rowsPerPageOptions: [], + totalRecords, + firstRowIndex: 0, + }; + } + + return { + ...DEFAULT_TABLE_PARAMS, + paginator: true, + rows: baseRows, + totalRecords, + firstRowIndex: 0, + }; + } +} diff --git a/src/app/shared/components/my-projects-table/my-projects-table.component.html b/src/app/shared/components/my-projects-table/my-projects-table.component.html index 9e0685657..7ea9b8def 100644 --- a/src/app/shared/components/my-projects-table/my-projects-table.component.html +++ b/src/app/shared/components/my-projects-table/my-projects-table.component.html @@ -7,7 +7,7 @@ [rows]="tableParams().rows" [first]="tableParams().firstRowIndex" [rowsPerPageOptions]="tableParams().rowsPerPageOptions" - [paginator]="true" + [paginator]="tableParams().paginator" [totalRecords]="tableParams().totalRecords" paginatorDropdownAppendTo="body" [resizableColumns]="true" diff --git a/src/app/shared/stores/my-resources/my-resources.state.ts b/src/app/shared/stores/my-resources/my-resources.state.ts index 0a0e85fd3..64337b8fe 100644 --- a/src/app/shared/stores/my-resources/my-resources.state.ts +++ b/src/app/shared/stores/my-resources/my-resources.state.ts @@ -141,13 +141,6 @@ export class MyResourcesState { action.pageNumber, action.pageSize ), - preprints: this.myResourcesService.getMyBookmarks( - action.bookmarksId, - ResourceType.Preprint, - action.filters, - action.pageNumber, - action.pageSize - ), registrations: this.myResourcesService.getMyBookmarks( action.bookmarksId, ResourceType.Registration, @@ -157,9 +150,8 @@ export class MyResourcesState { ), }).pipe( tap((results) => { - const allData = [...results.projects.data, ...results.preprints.data, ...results.registrations.data]; - const totalCount = - results.projects.meta.total + results.preprints.meta.total + results.registrations.meta.total; + const allData = [...results.projects.data, ...results.registrations.data]; + const totalCount = results.projects.meta.total + results.registrations.meta.total; ctx.patchState({ bookmarks: {