diff --git a/src/app/core/interceptors/auth.interceptor.ts b/src/app/core/interceptors/auth.interceptor.ts index aaaee6079..8b5e55217 100644 --- a/src/app/core/interceptors/auth.interceptor.ts +++ b/src/app/core/interceptors/auth.interceptor.ts @@ -6,7 +6,7 @@ export const authInterceptor: HttpInterceptorFn = ( req: HttpRequest, next: HttpHandlerFn ): Observable> => { - const authToken = '2rjFZwmdDG4rtKj7hGkEMO6XyHBM2lN7XBbsA1e8OqcFhOWu6Z7fQZiheu9RXtzSeVrgOt'; + const authToken = 'UlO9O9GNKgVzJD7pUeY53jiQTKJ4U2znXVWNvh0KZQruoENuILx0IIYf9LoDz7Duq72EIm'; // UlO9O9GNKgVzJD7pUeY53jiQTKJ4U2znXVWNvh0KZQruoENuILx0IIYf9LoDz7Duq72EIm kyrylo // 2rjFZwmdDG4rtKj7hGkEMO6XyHBM2lN7XBbsA1e8OqcFhOWu6Z7fQZiheu9RXtzSeVrgOt roman nastyuk // yZ485nN6MfhqvGrfU4Xk5BEnq0T6LM50nQ6H9VrYaMTaZUQNTuxnIwlp0Wpz879RCsK9GQ NM stage3 diff --git a/src/app/features/collections/collections.routes.ts b/src/app/features/collections/collections.routes.ts index 3e5412ab7..b34f680cc 100644 --- a/src/app/features/collections/collections.routes.ts +++ b/src/app/features/collections/collections.routes.ts @@ -3,9 +3,10 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; import { AddToCollectionState } from '@osf/features/collections/store/add-to-collection'; -import { CollectionsState } from '@osf/features/collections/store/collections'; +import { CollectionsModerationState } from '@osf/features/moderation/store/collections-moderation'; import { ConfirmLeavingGuard } from '@shared/guards'; -import { ContributorsState, ProjectsState } from '@shared/stores'; +import { BookmarksState, CitationsState, ContributorsState, NodeLinksState, ProjectsState } from '@shared/stores'; +import { CollectionsState } from '@shared/stores/collections'; export const collectionsRoutes: Routes = [ { @@ -42,10 +43,20 @@ export const collectionsRoutes: Routes = [ canDeactivate: [ConfirmLeavingGuard], }, { - path: ':id/moderation', + path: ':collectionId/moderation', loadChildren: () => import('@osf/features/moderation/collection-moderation.routes').then((m) => m.collectionModerationRoutes), }, + { + path: ':collectionId/moderation/:id', + loadComponent: () => + import( + '@osf/features/moderation/components/collection-submission-overview/collection-submission-overview.component' + ).then((mod) => mod.CollectionSubmissionOverviewComponent), + providers: [ + provideStates([NodeLinksState, CitationsState, CollectionsModerationState, CollectionsState, BookmarksState]), + ], + }, ], }, ]; diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts b/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts index b7820770a..690917789 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts +++ b/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts @@ -26,10 +26,10 @@ import { AddToCollectionSteps } from '@osf/features/collections/enums'; import { ClearAddToCollectionState, CreateCollectionSubmission, -} from '@osf/features/collections/store/add-to-collection/add-to-collection.actions'; -import { CollectionsSelectors, GetCollectionProvider } from '@osf/features/collections/store/collections'; +} from '@osf/features/collections/store/add-to-collection'; import { LoadingSpinnerComponent } from '@shared/components'; import { CanDeactivateComponent } from '@shared/models'; +import { CollectionsSelectors, GetCollectionProvider } from '@shared/stores'; import { ProjectsSelectors } from '@shared/stores/projects/projects.selectors'; import { diff --git a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.ts b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.ts index 2816d42f9..86e974b06 100644 --- a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.ts +++ b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.ts @@ -13,7 +13,7 @@ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angula import { collectionFilterTypes } from '@osf/features/collections/constants/filter-types.const'; import { AddToCollectionSteps } from '@osf/features/collections/enums'; import { CollectionFilterEntry } from '@osf/features/collections/models'; -import { CollectionsSelectors, GetCollectionDetails } from '@osf/features/collections/store/collections'; +import { CollectionsSelectors, GetCollectionDetails } from '@shared/stores'; @Component({ selector: 'osf-collection-metadata-step', diff --git a/src/app/features/collections/components/add-to-collection/select-project-step/select-project-step.component.ts b/src/app/features/collections/components/add-to-collection/select-project-step/select-project-step.component.ts index 300db8df2..8cb47647b 100644 --- a/src/app/features/collections/components/add-to-collection/select-project-step/select-project-step.component.ts +++ b/src/app/features/collections/components/add-to-collection/select-project-step/select-project-step.component.ts @@ -23,10 +23,10 @@ import { FormsModule } from '@angular/forms'; import { UserSelectors } from '@core/store/user'; import { AddToCollectionSteps } from '@osf/features/collections/enums'; -import { CollectionsSelectors, GetUserCollectionSubmissions } from '@osf/features/collections/store/collections'; import { GetProjects, SetSelectedProject } from '@osf/shared/stores'; import { CustomOption } from '@shared/models'; import { Project } from '@shared/models/projects'; +import { CollectionsSelectors, GetUserCollectionSubmissions } from '@shared/stores/collections'; import { ProjectsSelectors } from '@shared/stores/projects/projects.selectors'; @Component({ diff --git a/src/app/features/collections/components/collections-discover/collections-discover.component.ts b/src/app/features/collections/components/collections-discover/collections-discover.component.ts index 14fa8bcb0..2a4fcff3f 100644 --- a/src/app/features/collections/components/collections-discover/collections-discover.component.ts +++ b/src/app/features/collections/components/collections-discover/collections-discover.component.ts @@ -13,8 +13,9 @@ import { FormControl } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { CollectionsHelpDialogComponent, CollectionsMainContentComponent } from '@osf/features/collections/components'; -import { CollectionsFilters } from '@osf/features/collections/models'; import { CollectionsQuerySyncService } from '@osf/features/collections/services'; +import { LoadingSpinnerComponent, SearchInputComponent } from '@shared/components'; +import { CollectionsFilters } from '@shared/models'; import { ClearCollections, ClearCollectionSubmissions, @@ -24,8 +25,7 @@ import { SearchCollectionSubmissions, SetPageNumber, SetSearchValue, -} from '@osf/features/collections/store/collections'; -import { LoadingSpinnerComponent, SearchInputComponent } from '@shared/components'; +} from '@shared/stores/collections'; @Component({ selector: 'osf-collections-discover', diff --git a/src/app/features/collections/components/collections-filter-chips/collections-filter-chips.component.ts b/src/app/features/collections/components/collections-filter-chips/collections-filter-chips.component.ts index 392395f71..7523ddba6 100644 --- a/src/app/features/collections/components/collections-filter-chips/collections-filter-chips.component.ts +++ b/src/app/features/collections/components/collections-filter-chips/collections-filter-chips.component.ts @@ -18,7 +18,7 @@ import { SetStatusFilters, SetStudyDesignFilters, SetVolumeFilters, -} from '@osf/features/collections/store/collections'; +} from '@shared/stores/collections'; @Component({ selector: 'osf-collections-filter-chips', diff --git a/src/app/features/collections/components/collections-filters/collections-filters.component.ts b/src/app/features/collections/components/collections-filters/collections-filters.component.ts index ab00fba8d..cae7d8b17 100644 --- a/src/app/features/collections/components/collections-filters/collections-filters.component.ts +++ b/src/app/features/collections/components/collections-filters/collections-filters.component.ts @@ -22,7 +22,7 @@ import { SetStatusFilters, SetStudyDesignFilters, SetVolumeFilters, -} from '@osf/features/collections/store/collections'; +} from '@shared/stores/collections'; @Component({ selector: 'osf-collections-filters', diff --git a/src/app/features/collections/components/collections-main-content/collections-main-content.component.ts b/src/app/features/collections/components/collections-main-content/collections-main-content.component.ts index acd1c9976..bfcbfd1ff 100644 --- a/src/app/features/collections/components/collections-main-content/collections-main-content.component.ts +++ b/src/app/features/collections/components/collections-main-content/collections-main-content.component.ts @@ -12,7 +12,7 @@ import { FormsModule } from '@angular/forms'; import { CollectionsFilterChipsComponent } from '@osf/features/collections/components'; import { collectionsSortOptions } from '@osf/features/collections/constants'; -import { CollectionsSelectors, SetSortBy } from '@osf/features/collections/store/collections'; +import { CollectionsSelectors, SetSortBy } from '@shared/stores/collections'; import { IS_WEB } from '@shared/utils'; import { CollectionsFiltersComponent } from '../collections-filters/collections-filters.component'; @@ -38,7 +38,7 @@ export class CollectionsMainContentComponent { protected readonly sortOptions = collectionsSortOptions; protected isWeb = toSignal(inject(IS_WEB)); protected selectedSort = select(CollectionsSelectors.getSortBy); - protected collectionSubmissions = select(CollectionsSelectors.getCollectionSubmissions); + protected collectionSubmissions = select(CollectionsSelectors.getCollectionSubmissionsSearchResult); protected isCollectionSubmissionsLoading = select(CollectionsSelectors.getCollectionSubmissionsLoading); protected isFiltersOpen = signal(false); diff --git a/src/app/features/collections/components/collections-search-result-card/collections-search-result-card.component.ts b/src/app/features/collections/components/collections-search-result-card/collections-search-result-card.component.ts index f0427835f..2f5dcb38d 100644 --- a/src/app/features/collections/components/collections-search-result-card/collections-search-result-card.component.ts +++ b/src/app/features/collections/components/collections-search-result-card/collections-search-result-card.component.ts @@ -4,7 +4,7 @@ import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; import { collectionFilterNames } from '@osf/features/collections/constants'; -import { CollectionSubmission } from '@osf/features/collections/models'; +import { CollectionSubmission } from '@shared/models'; @Component({ selector: 'osf-collections-search-result-card', diff --git a/src/app/features/collections/components/collections-search-results/collections-search-results.component.ts b/src/app/features/collections/components/collections-search-results/collections-search-results.component.ts index 6511d0c90..30b43ede8 100644 --- a/src/app/features/collections/components/collections-search-results/collections-search-results.component.ts +++ b/src/app/features/collections/components/collections-search-results/collections-search-results.component.ts @@ -8,8 +8,8 @@ import { Skeleton } from 'primeng/skeleton'; import { ChangeDetectionStrategy, Component, computed } from '@angular/core'; -import { CollectionsSelectors, SetPageNumber } from '@osf/features/collections/store/collections'; import { CustomPaginatorComponent } from '@osf/shared/components'; +import { CollectionsSelectors, SetPageNumber } from '@shared/stores/collections'; import { CollectionsSearchResultCardComponent } from '../collections-search-result-card/collections-search-result-card.component'; @@ -21,7 +21,7 @@ import { CollectionsSearchResultCardComponent } from '../collections-search-resu changeDetection: ChangeDetectionStrategy.OnPush, }) export class CollectionsSearchResultsComponent { - protected searchResults = select(CollectionsSelectors.getCollectionSubmissions); + protected searchResults = select(CollectionsSelectors.getCollectionSubmissionsSearchResult); protected isCollectionDetailsLoading = select(CollectionsSelectors.getCollectionDetailsLoading); protected isCollectionSubmissionsLoading = select(CollectionsSelectors.getCollectionSubmissionsLoading); protected totalSubmissions = select(CollectionsSelectors.getTotalSubmissions); diff --git a/src/app/features/collections/models/index.ts b/src/app/features/collections/models/index.ts index 4d84b4d20..fa8df0d4a 100644 --- a/src/app/features/collections/models/index.ts +++ b/src/app/features/collections/models/index.ts @@ -1,8 +1,4 @@ export * from './collection-filter-entry.model'; export * from './collection-license-json-api.models'; -export * from './collection-submission-payload'; -export * from './collections.models'; -export * from './collections-filters.model'; -export * from './collections-json-api.models'; export * from './collections-query-params.model'; export * from './project-metadata-form.model'; diff --git a/src/app/features/collections/services/add-to-collection.service.ts b/src/app/features/collections/services/add-to-collection.service.ts index 714c743ad..3e8aaabe3 100644 --- a/src/app/features/collections/services/add-to-collection.service.ts +++ b/src/app/features/collections/services/add-to-collection.service.ts @@ -3,10 +3,8 @@ import { map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@core/services'; -import { CollectionsMapper } from '@osf/features/collections/mappers'; -import { CollectionSubmissionPayload } from '@osf/features/collections/models'; -import { LicensesMapper } from '@shared/mappers'; -import { License, LicensesResponseJsonApi } from '@shared/models'; +import { CollectionsMapper, LicensesMapper } from '@shared/mappers'; +import { CollectionSubmissionPayload, License, LicensesResponseJsonApi } from '@shared/models'; import { environment } from 'src/environments/environment'; diff --git a/src/app/features/collections/services/collections-query-sync.service.ts b/src/app/features/collections/services/collections-query-sync.service.ts index c7b43a16d..4aaaecc33 100644 --- a/src/app/features/collections/services/collections-query-sync.service.ts +++ b/src/app/features/collections/services/collections-query-sync.service.ts @@ -6,14 +6,10 @@ import { ActivatedRoute, Router } from '@angular/router'; import { collectionsSortOptions } from '@osf/features/collections/constants'; import { queryParamsKeys } from '@osf/features/collections/constants/query-params-keys.const'; -import { CollectionQueryParams, CollectionsFilters } from '@osf/features/collections/models'; -import { - CollectionsSelectors, - SetAllFilters, - SetSearchValue, - SetSortBy, -} from '@osf/features/collections/store/collections'; -import { SetPageNumber } from '@osf/features/collections/store/collections/collections.actions'; +import { CollectionQueryParams } from '@osf/features/collections/models'; +import { CollectionsFilters } from '@shared/models'; +import { CollectionsSelectors, SetAllFilters, SetSearchValue, SetSortBy } from '@shared/stores/collections'; +import { SetPageNumber } from '@shared/stores/collections/collections.actions'; @Injectable() export class CollectionsQuerySyncService { diff --git a/src/app/features/collections/services/index.ts b/src/app/features/collections/services/index.ts index f02958ed8..66b8b9238 100644 --- a/src/app/features/collections/services/index.ts +++ b/src/app/features/collections/services/index.ts @@ -1,4 +1,3 @@ export * from './add-to-collection.service'; -export * from './collections.service'; export * from './collections-query-sync.service'; export * from './project-metadata-form.service'; diff --git a/src/app/features/collections/store/add-to-collection/add-to-collection.actions.ts b/src/app/features/collections/store/add-to-collection/add-to-collection.actions.ts index 3522031b5..bf91e101b 100644 --- a/src/app/features/collections/store/add-to-collection/add-to-collection.actions.ts +++ b/src/app/features/collections/store/add-to-collection/add-to-collection.actions.ts @@ -1,4 +1,4 @@ -import { CollectionSubmissionPayload } from '@osf/features/collections/models'; +import { CollectionSubmissionPayload } from '@shared/models'; export class GetCollectionLicenses { static readonly type = '[Add To Collection] Get Collection Licenses'; diff --git a/src/app/features/moderation/collection-moderation.routes.ts b/src/app/features/moderation/collection-moderation.routes.ts index 9b9bc68a1..92ef78df2 100644 --- a/src/app/features/moderation/collection-moderation.routes.ts +++ b/src/app/features/moderation/collection-moderation.routes.ts @@ -2,9 +2,11 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; +import { CollectionsModerationState } from '@osf/features/moderation/store/collections-moderation'; import { ResourceType } from '@osf/shared/enums'; +import { CollectionsState } from '@shared/stores/collections'; -import { ModeratorsState } from './store/moderation'; +import { ModeratorsState } from './store/moderators'; import { CollectionModerationTab } from './enums'; export const collectionModerationRoutes: Routes = [ @@ -27,6 +29,7 @@ export const collectionModerationRoutes: Routes = [ (m) => m.CollectionModerationSubmissionsComponent ), data: { tab: CollectionModerationTab.AllItems }, + providers: [provideStates([CollectionsModerationState, CollectionsState])], }, { path: 'moderators', diff --git a/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.ts b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.ts index c1a3afd8a..40337fa7b 100644 --- a/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.ts +++ b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.ts @@ -17,7 +17,7 @@ import { CustomPaginatorComponent, LoadingSpinnerComponent, SearchInputComponent import { AddModeratorType } from '../../enums'; import { ModeratorAddModel, ModeratorDialogAddModel } from '../../models'; -import { ClearUsers, ModeratorsSelectors, SearchUsers } from '../../store/moderation'; +import { ClearUsers, ModeratorsSelectors, SearchUsers } from '../../store/moderators'; @Component({ selector: 'osf-add-moderator-dialog', diff --git a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.html b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.html index cfcfe0e90..f3f43114b 100644 --- a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.html +++ b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.html @@ -1,19 +1,18 @@
- -

{{ totalCount }}

{{ item.label | translate }}

-
+
@@ -27,4 +26,18 @@
- +@if (!isLoading()) { + + + @if (collectionSubmissions().length > pageSize) { + + } +} @else { + +} diff --git a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.ts b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.ts index a919b9ab4..73b4ee5db 100644 --- a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.ts +++ b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.ts @@ -1,42 +1,208 @@ +import { createDispatchMap, select } from '@ngxs/store'; + import { TranslatePipe } from '@ngx-translate/core'; +import { PaginatorState } from 'primeng/paginator'; import { SelectButton } from 'primeng/selectbutton'; -import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, effect, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; import { Primitive } from '@osf/core/helpers'; -import { IconComponent, SelectComponent } from '@osf/shared/components'; -import { ALL_SORT_OPTIONS } from '@osf/shared/constants'; +import { CollectionSubmissionsListComponent } from '@osf/features/moderation/components/collection-submissions-list/collection-submissions-list.component'; +import { + CollectionsModerationSelectors, + GetCollectionSubmissions, + GetSubmissionsReviewActions, +} from '@osf/features/moderation/store/collections-moderation'; +import { + CustomPaginatorComponent, + IconComponent, + LoadingSpinnerComponent, + SelectComponent, +} from '@osf/shared/components'; +import { COLLECTION_SUBMISSIONS_SORT_OPTIONS } from '@osf/shared/constants'; +import { + ClearCollections, + ClearCollectionSubmissions, + CollectionsSelectors, + GetCollectionDetails, + GetCollectionProvider, + SearchCollectionSubmissions, + SetPageNumber, +} from '@shared/stores'; import { SUBMISSION_REVIEW_OPTIONS } from '../../constants'; import { SubmissionReviewStatus } from '../../enums'; -import { SubmissionsListComponent } from '../submissions-list/submissions-list.component'; -import { pendingReviews } from '../test-data'; @Component({ selector: 'osf-collection-moderation-submissions', - imports: [SelectButton, TranslatePipe, FormsModule, SelectComponent, SubmissionsListComponent, IconComponent], + imports: [ + SelectButton, + TranslatePipe, + FormsModule, + SelectComponent, + CollectionSubmissionsListComponent, + IconComponent, + CustomPaginatorComponent, + LoadingSpinnerComponent, + ], templateUrl: './collection-moderation-submissions.component.html', styleUrl: './collection-moderation-submissions.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class CollectionModerationSubmissionsComponent { + private router = inject(Router); + private route = inject(ActivatedRoute); readonly submissionReviewOptions = SUBMISSION_REVIEW_OPTIONS; - sortOptions = ALL_SORT_OPTIONS; - selectedSortOption = signal(null); - selectedReviewOption = this.submissionReviewOptions[0].value; + protected collectionProvider = select(CollectionsSelectors.getCollectionProvider); + protected isCollectionProviderLoading = select(CollectionsSelectors.getCollectionProviderLoading); + protected isSubmissionsLoading = select(CollectionsModerationSelectors.getCollectionSubmissionsLoading); + protected collectionSubmissions = select(CollectionsModerationSelectors.getCollectionSubmissions); + protected totalSubmissions = select(CollectionsModerationSelectors.getCollectionSubmissionsTotalCount); + protected providerId = signal(''); + protected primaryCollectionId = computed(() => this.collectionProvider()?.primaryCollection?.id); + protected reviewStatus = signal(SubmissionReviewStatus.Pending); + protected currentPage = signal('1'); + protected pageSize = 10; + + protected isLoading = computed(() => { + return this.isCollectionProviderLoading() || this.isSubmissionsLoading(); + }); + + sortOptions = COLLECTION_SUBMISSIONS_SORT_OPTIONS; + selectedSortOption = signal(this.sortOptions[0].value); + + protected firstIndex = computed(() => (parseInt(this.currentPage()) - 1) * 10); + + protected actions = createDispatchMap({ + getCollectionProvider: GetCollectionProvider, + getCollectionDetails: GetCollectionDetails, + searchCollectionSubmissions: SearchCollectionSubmissions, + getCollectionSubmissions: GetCollectionSubmissions, + setPageNumber: SetPageNumber, + clearCollections: ClearCollections, + clearCollectionsSubmissions: ClearCollectionSubmissions, + getSubmissionsReviewActions: GetSubmissionsReviewActions, + }); + + private setupEffects(): void { + effect(() => { + const collectionId = this.primaryCollectionId(); + if (collectionId) { + this.actions.getCollectionDetails(collectionId); + } + }); + + effect(() => { + const provider = this.collectionProvider(); + const status = this.reviewStatus(); + const sortBy = this.selectedSortOption() || this.sortOptions[0].value; + const page = this.currentPage(); + + if (status && provider) { + this.actions.getCollectionSubmissions(provider.primaryCollection.id, status, page, sortBy); + } + }); - totalCount = 5; + effect(() => { + const status = this.reviewStatus(); + const sortBy = this.selectedSortOption(); + const page = this.currentPage(); - submissions = pendingReviews; + this.updateUrlParams(status, sortBy, page); + }); + + effect(() => { + const submissions = this.collectionSubmissions(); + const collectionId = this.primaryCollectionId(); + + if (submissions.length && collectionId) { + submissions.forEach((submission) => { + this.actions.getSubmissionsReviewActions(submission.id, collectionId); + }); + } + }); + } + + constructor() { + this.initializeFromQueryParams(); + this.initializeCollectionProvider(); + this.setupEffects(); + } changeReviewStatus(value: SubmissionReviewStatus) { - console.log(value); + this.reviewStatus.set(value); + this.currentPage.set('1'); } changeSort(value: Primitive) { - console.log(value); + this.selectedSortOption.set(value as string); + this.currentPage.set('1'); + } + + onPageChange(event: PaginatorState): void { + if (event.page !== undefined) { + const pageNumber = (event.page + 1).toString(); + this.currentPage.set(pageNumber); + } + } + + private initializeFromQueryParams(): void { + const queryParams = this.route.snapshot.queryParams; + const statusValues = Object.values(SubmissionReviewStatus); + + const statusParam = queryParams['status']; + if (statusParam && statusValues.includes(statusParam)) { + this.reviewStatus.set(statusParam); + } else { + this.reviewStatus.set(SubmissionReviewStatus.Pending); + } + + const sortByParam = queryParams['sortBy']; + if (sortByParam) { + this.selectedSortOption.set(sortByParam); + } + + const pageParam = queryParams['page']; + if (pageParam && !isNaN(parseInt(pageParam)) && parseInt(pageParam)) { + this.currentPage.set(pageParam); + } else { + this.currentPage.set('1'); + } + } + + private updateUrlParams(status: SubmissionReviewStatus, sortBy: string, page: string): void { + const queryParams: Record = { status }; + + if (sortBy) { + queryParams['sortBy'] = sortBy; + } + + if (page !== '1') { + queryParams['page'] = page; + } else { + queryParams['page'] = null; + } + + this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling: 'merge', + }); + } + + private initializeCollectionProvider(): void { + const id = this.route.parent?.snapshot.paramMap.get('collectionId'); + + if (!id) { + this.router.navigate(['/not-found']); + return; + } + + this.providerId.set(id); + this.actions.getCollectionProvider(id); } } diff --git a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.html b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.html new file mode 100644 index 000000000..ed06d8552 --- /dev/null +++ b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.html @@ -0,0 +1,61 @@ +@let action = currentReviewAction(); +@let attributes = currentSubmissionAttributes(); + +@if (action && attributes) { +
+
+ + + + +
+ +

+ @switch (action.toState) { + @case (SubmissionReviewStatus.Pending) { + {{ 'moderation.submissionReview.submitted' | translate }} + } + @case (SubmissionReviewStatus.Accepted) { + @if (!collectionProvider()?.reviewsWorkflow) { + {{ 'moderation.submissionReview.submitted' | translate }} + } @else { + {{ 'moderation.submissionReview.accepted' | translate }} + } + } + @case (SubmissionReviewStatus.Rejected) { + {{ 'moderation.submissionReview.rejected' | translate }} + } + @case (SubmissionReviewStatus.Removed) { + {{ 'moderation.submissionReview.withdrawn' | translate }} + } + } + {{ action.dateCreated | timeAgo }} + {{ 'moderation.submissionReview.by' | translate }} + {{ action.createdBy }} +

+ +
+ @for (attribute of attributes; track attribute.key) { + @if (!$first) { + | + } +

+ {{ attribute.label }}: {{ attribute.value }} +

+ } +
+ + @if (action.comment) { +

- {{ action.comment }}

+ } +
+} diff --git a/src/app/features/moderation/components/submission-item/submission-item.component.scss b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.scss similarity index 100% rename from src/app/features/moderation/components/submission-item/submission-item.component.scss rename to src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.scss diff --git a/src/app/features/moderation/components/submission-item/submission-item.component.spec.ts b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts similarity index 52% rename from src/app/features/moderation/components/submission-item/submission-item.component.spec.ts rename to src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts index 870d57d0d..63e58b72d 100644 --- a/src/app/features/moderation/components/submission-item/submission-item.component.spec.ts +++ b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts @@ -1,17 +1,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { SubmissionItemComponent } from './submission-item.component'; +import { CollectionSubmissionItemComponent } from './collection-submission-item.component'; describe('SubmissionItemComponent', () => { - let component: SubmissionItemComponent; - let fixture: ComponentFixture; + let component: CollectionSubmissionItemComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SubmissionItemComponent], + imports: [CollectionSubmissionItemComponent], }).compileComponents(); - fixture = TestBed.createComponent(SubmissionItemComponent); + fixture = TestBed.createComponent(CollectionSubmissionItemComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.ts b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.ts new file mode 100644 index 000000000..8ccb6af20 --- /dev/null +++ b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.ts @@ -0,0 +1,67 @@ +import { select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { DialogService } from 'primeng/dynamicdialog'; + +import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { collectionFilterNames } from '@osf/features/collections/constants'; +import { SubmissionReviewStatus } from '@osf/features/moderation/enums'; +import { IconComponent } from '@osf/shared/components'; +import { CollectionSubmission } from '@shared/models'; +import { TimeAgoPipe } from '@shared/pipes/time-ago.pipe'; +import { CollectionsSelectors } from '@shared/stores'; + +import { ReviewStatusIcon } from '../../constants'; + +@Component({ + selector: 'osf-submission-item', + imports: [TranslatePipe, IconComponent, TimeAgoPipe, Button], + templateUrl: './collection-submission-item.component.html', + styleUrl: './collection-submission-item.component.scss', + providers: [DialogService], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CollectionSubmissionItemComponent { + private router = inject(Router); + private activatedRoute = inject(ActivatedRoute); + submission = input.required(); + collectionProvider = select(CollectionsSelectors.getCollectionProvider); + + protected readonly reviewStatusIcon = ReviewStatusIcon; + + protected currentReviewAction = computed(() => { + const actions = this.submission().actions; + + if (!actions || !actions.length) return null; + + return actions[0]; + }); + + protected currentSubmissionAttributes = computed(() => { + const item = this.submission(); + if (!item) return null; + + return collectionFilterNames + .map((attribute) => ({ + ...attribute, + value: item[attribute.key as keyof CollectionSubmission] as string, + })) + .filter((attribute) => attribute.value); + }); + + protected handleNavigation() { + const currentStatus = this.activatedRoute.snapshot.queryParams['status']; + const queryParams = currentStatus ? { status: currentStatus, mode: 'moderation' } : {}; + + this.router.navigate(['../', this.submission().nodeId], { + relativeTo: this.activatedRoute, + queryParams, + }); + } + + protected readonly SubmissionReviewStatus = SubmissionReviewStatus; +} diff --git a/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.html b/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.html new file mode 100644 index 000000000..0659eace7 --- /dev/null +++ b/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.scss b/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.scss new file mode 100644 index 000000000..da0c027b5 --- /dev/null +++ b/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.scss @@ -0,0 +1,5 @@ +:host { + display: flex; + flex-direction: column; + flex: 1; +} diff --git a/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.spec.ts b/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.spec.ts new file mode 100644 index 000000000..29571332c --- /dev/null +++ b/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CollectionSubmissionOverviewComponent } from './collection-submission-overview.component'; + +describe('CollectionSubmissionOverviewComponent', () => { + let component: CollectionSubmissionOverviewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CollectionSubmissionOverviewComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CollectionSubmissionOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.ts b/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.ts new file mode 100644 index 000000000..d0a610b59 --- /dev/null +++ b/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.ts @@ -0,0 +1,32 @@ +import { ChangeDetectionStrategy, Component, computed, effect, inject } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { ProjectOverviewComponent } from '@osf/features/project/overview/project-overview.component'; +import { Mode } from '@shared/enums'; + +@Component({ + selector: 'osf-collection-submission-overview', + imports: [ProjectOverviewComponent], + templateUrl: './collection-submission-overview.component.html', + styleUrl: './collection-submission-overview.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CollectionSubmissionOverviewComponent { + private route = inject(ActivatedRoute); + private router = inject(Router); + private activatedRoute = inject(ActivatedRoute); + + readonly isModerationMode = computed(() => { + const mode = this.route.snapshot.queryParams['mode']; + + return mode === Mode.Moderation; + }); + + constructor() { + effect(() => { + if (!this.isModerationMode()) { + this.router.navigate(['../'], { relativeTo: this.activatedRoute }); + } + }); + } +} diff --git a/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.html b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.html new file mode 100644 index 000000000..5e6d11a21 --- /dev/null +++ b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.html @@ -0,0 +1,13 @@ +@if (submissions().length) { +
+ @for (item of submissions(); track $index) { +
+ +
+ } +
+} @else { +
+

{{ 'moderation.noSubmissions' | translate }}

+
+} diff --git a/src/app/features/moderation/components/submissions-list/submissions-list.component.scss b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.scss similarity index 100% rename from src/app/features/moderation/components/submissions-list/submissions-list.component.scss rename to src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.scss diff --git a/src/app/features/moderation/components/submissions-list/submissions-list.component.spec.ts b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts similarity index 51% rename from src/app/features/moderation/components/submissions-list/submissions-list.component.spec.ts rename to src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts index 6b65de3a6..4252ffb4a 100644 --- a/src/app/features/moderation/components/submissions-list/submissions-list.component.spec.ts +++ b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts @@ -1,17 +1,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { SubmissionsListComponent } from './submissions-list.component'; +import { CollectionSubmissionsListComponent } from './collection-submissions-list.component'; describe('SubmissionsListComponent', () => { - let component: SubmissionsListComponent; - let fixture: ComponentFixture; + let component: CollectionSubmissionsListComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SubmissionsListComponent], + imports: [CollectionSubmissionsListComponent], }).compileComponents(); - fixture = TestBed.createComponent(SubmissionsListComponent); + fixture = TestBed.createComponent(CollectionSubmissionsListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.ts b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.ts new file mode 100644 index 000000000..c2625b282 --- /dev/null +++ b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.ts @@ -0,0 +1,19 @@ +import { select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +import { CollectionSubmissionItemComponent } from '@osf/features/moderation/components/collection-submission-item/collection-submission-item.component'; +import { CollectionsModerationSelectors } from '@osf/features/moderation/store/collections-moderation'; + +@Component({ + selector: 'osf-submissions-list', + imports: [CollectionSubmissionItemComponent, TranslatePipe], + templateUrl: './collection-submissions-list.component.html', + styleUrl: './collection-submissions-list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CollectionSubmissionsListComponent { + submissions = select(CollectionsModerationSelectors.getCollectionSubmissions); +} diff --git a/src/app/features/moderation/components/index.ts b/src/app/features/moderation/components/index.ts index ef89edfb4..b328db41e 100644 --- a/src/app/features/moderation/components/index.ts +++ b/src/app/features/moderation/components/index.ts @@ -10,5 +10,5 @@ export { PreprintModerationSettingsComponent } from './preprint-moderation-setti export { RecentActivityListComponent } from './recent-activity-list/recent-activity-list.component'; export { RegistrySettingsComponent } from './registry-settings/registry-settings.component'; export { RegistrySubmissionsComponent } from './registry-submissions/registry-submissions.component'; -export { SubmissionItemComponent } from './submission-item/submission-item.component'; -export { SubmissionsListComponent } from './submissions-list/submissions-list.component'; +export { CollectionSubmissionItemComponent } from '@osf/features/moderation/components/collection-submission-item/collection-submission-item.component'; +export { CollectionSubmissionsListComponent } from '@osf/features/moderation/components/collection-submissions-list/collection-submissions-list.component'; diff --git a/src/app/features/moderation/components/moderators-list/moderators-list.component.ts b/src/app/features/moderation/components/moderators-list/moderators-list.component.ts index 1cdf5c759..0d930b4d1 100644 --- a/src/app/features/moderation/components/moderators-list/moderators-list.component.ts +++ b/src/app/features/moderation/components/moderators-list/moderators-list.component.ts @@ -21,7 +21,7 @@ import { ModeratorsSelectors, UpdateModerator, UpdateSearchValue, -} from '@osf/features/moderation/store/moderation'; +} from '@osf/features/moderation/store/moderators'; import { SearchInputComponent } from '@osf/shared/components'; import { ResourceType } from '@osf/shared/enums'; import { CustomConfirmationService, ToastService } from '@osf/shared/services'; @@ -48,7 +48,9 @@ export class ModeratorsListComponent implements OnInit { private readonly dialogService = inject(DialogService); private readonly toastService = inject(ToastService); - readonly providerId = toSignal(this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined)); + readonly providerId = toSignal( + this.route.parent?.params.pipe(map((params) => params['collectionId'])) ?? of(undefined) + ); readonly resourceType: Signal = toSignal( this.route.data.pipe(map((params) => params['resourceType'])) ?? of(undefined) ); diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html index 4e63c0b8a..dd993ea4a 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.html @@ -27,4 +27,4 @@ - + diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts index 9c1102cbd..c87b7a388 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts @@ -7,12 +7,12 @@ import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { Primitive } from '@osf/core/helpers'; +import { CollectionSubmissionsListComponent } from '@osf/features/moderation/components/collection-submissions-list/collection-submissions-list.component'; import { IconComponent, SelectComponent } from '@osf/shared/components'; import { ALL_SORT_OPTIONS } from '@osf/shared/constants'; import { SUBMITTED_SUBMISSION_REVIEW_OPTIONS } from '../../constants'; import { SubmissionReviewStatus } from '../../enums'; -import { SubmissionsListComponent } from '../submissions-list/submissions-list.component'; import { pubicReviews } from '../test-data'; @Component({ @@ -22,7 +22,7 @@ import { pubicReviews } from '../test-data'; TranslatePipe, FormsModule, SelectComponent, - SubmissionsListComponent, + CollectionSubmissionsListComponent, IconComponent, TitleCasePipe, ], diff --git a/src/app/features/moderation/components/submission-item/submission-item.component.html b/src/app/features/moderation/components/submission-item/submission-item.component.html deleted file mode 100644 index 6d0b41388..000000000 --- a/src/app/features/moderation/components/submission-item/submission-item.component.html +++ /dev/null @@ -1,16 +0,0 @@ -
-
- - {{ submission().name }} -
- -

- {{ 'moderation.submissionReview.submitted' | translate }} - {{ submission().dateSubmitted }} - {{ 'moderation.submissionReview.by' | translate }} - {{ submission().submittedBy }} -

-
diff --git a/src/app/features/moderation/components/submission-item/submission-item.component.ts b/src/app/features/moderation/components/submission-item/submission-item.component.ts deleted file mode 100644 index adbdf2608..000000000 --- a/src/app/features/moderation/components/submission-item/submission-item.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { TranslatePipe } from '@ngx-translate/core'; - -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; - -import { IconComponent } from '@osf/shared/components'; - -import { ReviewStatusIcon } from '../../constants'; -import { Submission } from '../../models'; - -@Component({ - selector: 'osf-submission-item', - imports: [TranslatePipe, IconComponent], - templateUrl: './submission-item.component.html', - styleUrl: './submission-item.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SubmissionItemComponent { - submission = input.required(); - - readonly reviewStatusIcon = ReviewStatusIcon; -} diff --git a/src/app/features/moderation/components/submissions-list/submissions-list.component.html b/src/app/features/moderation/components/submissions-list/submissions-list.component.html deleted file mode 100644 index e75b0b742..000000000 --- a/src/app/features/moderation/components/submissions-list/submissions-list.component.html +++ /dev/null @@ -1,7 +0,0 @@ -
- @for (item of submissions(); track $index) { -
- -
- } -
diff --git a/src/app/features/moderation/components/submissions-list/submissions-list.component.ts b/src/app/features/moderation/components/submissions-list/submissions-list.component.ts deleted file mode 100644 index fd16a47ca..000000000 --- a/src/app/features/moderation/components/submissions-list/submissions-list.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; - -import { Submission } from '../../models'; -import { SubmissionItemComponent } from '../submission-item/submission-item.component'; - -@Component({ - selector: 'osf-submissions-list', - imports: [SubmissionItemComponent], - templateUrl: './submissions-list.component.html', - styleUrl: './submissions-list.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SubmissionsListComponent { - submissions = input.required(); -} diff --git a/src/app/features/moderation/constants/submission.const.ts b/src/app/features/moderation/constants/submission.const.ts index 0bdf6f298..7063974b3 100644 --- a/src/app/features/moderation/constants/submission.const.ts +++ b/src/app/features/moderation/constants/submission.const.ts @@ -17,7 +17,7 @@ export const SUBMISSION_REVIEW_OPTIONS = [ label: 'moderation.submissionReviewStatus.rejected', }, { - value: SubmissionReviewStatus.Withdrawn, + value: SubmissionReviewStatus.Removed, icon: 'fas fa-circle-minus', label: 'moderation.submissionReviewStatus.withdrawn', }, @@ -63,6 +63,10 @@ export const ReviewStatusIcon: Record; + currentReviewAction: AsyncStateModel; +} + +export const COLLECTIONS_MODERATION_STATE_DEFAULTS: CollectionsModerationStateModel = { + collectionSubmissions: { + data: [], + isLoading: false, + error: null, + totalCount: 0, + }, + currentReviewAction: { + data: null, + isLoading: false, + error: null, + }, +}; diff --git a/src/app/features/moderation/store/collections-moderation/collections-moderation.selectors.ts b/src/app/features/moderation/store/collections-moderation/collections-moderation.selectors.ts new file mode 100644 index 000000000..01c7560a6 --- /dev/null +++ b/src/app/features/moderation/store/collections-moderation/collections-moderation.selectors.ts @@ -0,0 +1,36 @@ +import { Selector } from '@ngxs/store'; + +import { CollectionsModerationStateModel } from './collections-moderation.model'; +import { CollectionsModerationState } from './collections-moderation.state'; + +export class CollectionsModerationSelectors { + @Selector([CollectionsModerationState]) + static getCollectionSubmissions(state: CollectionsModerationStateModel) { + return state.collectionSubmissions.data; + } + + @Selector([CollectionsModerationState]) + static getCollectionSubmissionsTotalCount(state: CollectionsModerationStateModel) { + return state.collectionSubmissions.totalCount; + } + + @Selector([CollectionsModerationState]) + static getCollectionSubmissionsLoading(state: CollectionsModerationStateModel) { + return state.collectionSubmissions.isLoading; + } + + @Selector([CollectionsModerationState]) + static getCollectionSubmissionSubmitting(state: CollectionsModerationStateModel) { + return state.collectionSubmissions.isSubmitting; + } + + @Selector([CollectionsModerationState]) + static getCurrentReviewAction(state: CollectionsModerationStateModel) { + return state.currentReviewAction.data; + } + + @Selector([CollectionsModerationState]) + static getCurrentReviewActionLoading(state: CollectionsModerationStateModel) { + return state.currentReviewAction.isLoading; + } +} diff --git a/src/app/features/moderation/store/collections-moderation/collections-moderation.state.ts b/src/app/features/moderation/store/collections-moderation/collections-moderation.state.ts new file mode 100644 index 000000000..a9b57c184 --- /dev/null +++ b/src/app/features/moderation/store/collections-moderation/collections-moderation.state.ts @@ -0,0 +1,120 @@ +import { Action, State, StateContext } from '@ngxs/store'; +import { patch } from '@ngxs/store/operators'; + +import { catchError, forkJoin, map, of, switchMap, tap } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { handleSectionError } from '@core/handlers'; +import { CollectionsService } from '@shared/services'; + +import { + ClearCollectionModeration, + CreateCollectionSubmissionAction, + GetCollectionSubmissions, + GetSubmissionsReviewActions, +} from './collections-moderation.actions'; +import { COLLECTIONS_MODERATION_STATE_DEFAULTS, CollectionsModerationStateModel } from './collections-moderation.model'; + +@State({ + name: 'collectionsModeration', + defaults: COLLECTIONS_MODERATION_STATE_DEFAULTS, +}) +@Injectable() +export class CollectionsModerationState { + collectionsService = inject(CollectionsService); + + @Action(GetCollectionSubmissions) + getCollectionSubmissions(ctx: StateContext, action: GetCollectionSubmissions) { + ctx.setState(patch({ collectionSubmissions: patch({ isLoading: true }) })); + + return this.collectionsService + .fetchCollectionSubmissionsByStatus(action.collectionId, action.status, action.page, action.sortBy) + .pipe( + switchMap((res) => { + if (!res.data.length) { + return of({ + data: [], + totalCount: res.totalCount, + }); + } + + const actionRequests = res.data.map((submission) => + this.collectionsService.fetchCollectionSubmissionsActions(submission.nodeId, action.collectionId) + ); + + return forkJoin(actionRequests).pipe( + map((actions) => ({ + data: res.data.map((submission, i) => ({ ...submission, actions: actions[i] })), + totalCount: res.totalCount, + })) + ); + }), + tap((res) => { + ctx.patchState({ + collectionSubmissions: { + data: res.data, + isLoading: false, + error: null, + totalCount: res.totalCount, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'collectionSubmissions', error)) + ); + } + + @Action(GetSubmissionsReviewActions) + getCurrentReviewAction(ctx: StateContext, action: GetSubmissionsReviewActions) { + ctx.patchState({ + currentReviewAction: { + ...ctx.getState().currentReviewAction, + isLoading: true, + }, + }); + + return this.collectionsService.fetchCollectionSubmissionsActions(action.submissionId, action.collectionId).pipe( + tap((res) => { + ctx.patchState({ + currentReviewAction: { + data: res[0] || null, + isLoading: false, + error: null, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'currentReviewAction', error)) + ); + } + + @Action(CreateCollectionSubmissionAction) + createCollectionSubmissionAction( + ctx: StateContext, + action: CreateCollectionSubmissionAction + ) { + const state = ctx.getState(); + ctx.patchState({ + collectionSubmissions: { + ...state.collectionSubmissions, + isSubmitting: true, + }, + }); + + return this.collectionsService.createCollectionSubmissionAction(action.payload).pipe( + tap(() => { + ctx.patchState({ + collectionSubmissions: { + ...state.collectionSubmissions, + isSubmitting: false, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'collectionSubmissions', error)) + ); + } + + @Action(ClearCollectionModeration) + clearCollectionModeration(ctx: StateContext) { + ctx.setState(COLLECTIONS_MODERATION_STATE_DEFAULTS); + } +} diff --git a/src/app/features/moderation/store/collections-moderation/index.ts b/src/app/features/moderation/store/collections-moderation/index.ts new file mode 100644 index 000000000..12bb7c820 --- /dev/null +++ b/src/app/features/moderation/store/collections-moderation/index.ts @@ -0,0 +1,4 @@ +export * from './collections-moderation.actions'; +export * from './collections-moderation.model'; +export * from './collections-moderation.selectors'; +export * from './collections-moderation.state'; diff --git a/src/app/features/moderation/store/moderation/index.ts b/src/app/features/moderation/store/moderators/index.ts similarity index 100% rename from src/app/features/moderation/store/moderation/index.ts rename to src/app/features/moderation/store/moderators/index.ts diff --git a/src/app/features/moderation/store/moderation/moderators.actions.ts b/src/app/features/moderation/store/moderators/moderators.actions.ts similarity index 100% rename from src/app/features/moderation/store/moderation/moderators.actions.ts rename to src/app/features/moderation/store/moderators/moderators.actions.ts diff --git a/src/app/features/moderation/store/moderation/moderators.model.ts b/src/app/features/moderation/store/moderators/moderators.model.ts similarity index 100% rename from src/app/features/moderation/store/moderation/moderators.model.ts rename to src/app/features/moderation/store/moderators/moderators.model.ts diff --git a/src/app/features/moderation/store/moderation/moderators.selectors.ts b/src/app/features/moderation/store/moderators/moderators.selectors.ts similarity index 100% rename from src/app/features/moderation/store/moderation/moderators.selectors.ts rename to src/app/features/moderation/store/moderators/moderators.selectors.ts diff --git a/src/app/features/moderation/store/moderation/moderators.state.ts b/src/app/features/moderation/store/moderators/moderators.state.ts similarity index 100% rename from src/app/features/moderation/store/moderation/moderators.state.ts rename to src/app/features/moderation/store/moderators/moderators.state.ts diff --git a/src/app/features/preprints/components/filters/preprints-resources/preprints-resources.component.ts b/src/app/features/preprints/components/filters/preprints-resources/preprints-resources.component.ts index ba8f0a726..a41ca2655 100644 --- a/src/app/features/preprints/components/filters/preprints-resources/preprints-resources.component.ts +++ b/src/app/features/preprints/components/filters/preprints-resources/preprints-resources.component.ts @@ -11,7 +11,6 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { Primitive } from '@core/helpers'; -import { SetSortBy } from '@osf/features/collections/store/collections'; import { GetResourcesByLink } from '@osf/features/my-profile/store'; import { PreprintsFilterChipsComponent, PreprintsResourcesFiltersComponent } from '@osf/features/preprints/components'; import { PreprintsDiscoverSelectors } from '@osf/features/preprints/store/preprints-discover'; @@ -20,6 +19,7 @@ import { PreprintsResourcesFiltersOptionsSelectors } from '@osf/features/preprin import { ResourceCardComponent } from '@osf/shared/components'; import { searchSortingOptions } from '@osf/shared/constants'; import { IS_WEB, IS_XSMALL } from '@osf/shared/utils'; +import { SetSortBy } from '@shared/stores/collections'; @Component({ selector: 'osf-preprints-resources', diff --git a/src/app/features/project/overview/components/linked-resources/linked-resources.component.html b/src/app/features/project/overview/components/linked-resources/linked-resources.component.html index a80536c42..911a45961 100644 --- a/src/app/features/project/overview/components/linked-resources/linked-resources.component.html +++ b/src/app/features/project/overview/components/linked-resources/linked-resources.component.html @@ -1,11 +1,13 @@

{{ 'project.overview.linkedProjects.title' | translate }}

- + @if (!isCollectionsRoute()) { + + }
@if (isLinkedResourcesLoading()) { diff --git a/src/app/features/project/overview/components/linked-resources/linked-resources.component.ts b/src/app/features/project/overview/components/linked-resources/linked-resources.component.ts index 6e44c70e0..adbac7eae 100644 --- a/src/app/features/project/overview/components/linked-resources/linked-resources.component.ts +++ b/src/app/features/project/overview/components/linked-resources/linked-resources.component.ts @@ -7,7 +7,7 @@ import { DialogService } from 'primeng/dynamicdialog'; import { Skeleton } from 'primeng/skeleton'; import { NgClass } from '@angular/common'; -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { DeleteNodeLinkDialogComponent, LinkResourceDialogComponent } from '@osf/features/project/overview/components'; @@ -26,6 +26,7 @@ import { IS_XSMALL } from '@shared/utils'; export class LinkedResourcesComponent { private dialogService = inject(DialogService); private translateService = inject(TranslateService); + isCollectionsRoute = input(false); protected linkedResources = select(NodeLinksSelectors.getLinkedResources); protected isLinkedResourcesLoading = select(NodeLinksSelectors.getLinkedResourcesLoading); protected isMobile = toSignal(inject(IS_XSMALL)); diff --git a/src/app/features/project/overview/components/overview-components/overview-components.component.html b/src/app/features/project/overview/components/overview-components/overview-components.component.html index 7e59732e3..7fe4507b5 100644 --- a/src/app/features/project/overview/components/overview-components/overview-components.component.html +++ b/src/app/features/project/overview/components/overview-components/overview-components.component.html @@ -2,11 +2,13 @@

{{ 'project.overview.components.title' | translate }}

- + @if (!isCollectionsRoute()) { + + }
diff --git a/src/app/features/project/overview/components/overview-components/overview-components.component.ts b/src/app/features/project/overview/components/overview-components/overview-components.component.ts index e22259207..ed37ad138 100644 --- a/src/app/features/project/overview/components/overview-components/overview-components.component.ts +++ b/src/app/features/project/overview/components/overview-components/overview-components.component.ts @@ -8,7 +8,7 @@ import { Menu } from 'primeng/menu'; import { Skeleton } from 'primeng/skeleton'; import { NgClass } from '@angular/common'; -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { Router } from '@angular/router'; @@ -31,6 +31,7 @@ export class OverviewComponentsComponent { private dialogService = inject(DialogService); private translateService = inject(TranslateService); protected isMobile = toSignal(inject(IS_XSMALL)); + isCollectionsRoute = input(false); protected components = select(ProjectOverviewSelectors.getComponents); protected isComponentsLoading = select(ProjectOverviewSelectors.getComponentsLoading); protected readonly componentActionItems = (componentId: string) => [ diff --git a/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html b/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html index 42c7d9ab1..2bc904cf5 100644 --- a/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html +++ b/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.html @@ -2,7 +2,7 @@ @if (resource) {
- @if (visibilityToggle()) { + @if (visibilityToggle() && !isCollectionsRoute()) {
@@ -18,9 +18,23 @@
} + @if (isCollectionsRoute()) { + @if (isPublic()) { +
+ +

{{ 'project.overview.header.publicProject' | translate }}

+
+ } @else { +
+ +

{{ 'project.overview.header.privateProject' | translate }}

+
+ } + } +
- @if (resource.storage) { + @if (resource.storage && !isCollectionsRoute()) {

{{ +resource.storage.storageUsage | fileSize }}

diff --git a/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts b/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts index 063f82392..5bfeb1761 100644 --- a/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts +++ b/src/app/features/project/overview/components/overview-toolbar/overview-toolbar.component.ts @@ -53,6 +53,7 @@ export class OverviewToolbarComponent { private translateService = inject(TranslateService); private toastService = inject(ToastService); protected destroyRef = inject(DestroyRef); + isCollectionsRoute = input(false); protected isPublic = signal(false); protected isBookmarked = signal(false); diff --git a/src/app/features/project/overview/project-overview.component.html b/src/app/features/project/overview/project-overview.component.html index 2dc1de059..f83db97e8 100644 --- a/src/app/features/project/overview/project-overview.component.html +++ b/src/app/features/project/overview/project-overview.component.html @@ -1,19 +1,62 @@ @let project = currentProject(); +@let status = submissionReviewStatus(); -@if (!isProjectLoading() && project) { +@if (!isLoading() && project) {
- +
- +
-
+
+ @if (isCollectionsRoute()) { + + + + @if (status && isCollectionsRoute() && collectionProvider()) { + @switch (status) { + @case (SubmissionReviewStatus.Pending) { + + + Pending: Pending entry into {{ collectionProvider()?.name }} + + } + @case (SubmissionReviewStatus.Accepted) { + + Accepted: Accepted entry into {{ collectionProvider()?.name }} + + } + @case (SubmissionReviewStatus.Rejected) { + + Rejected: Rejected entry into {{ collectionProvider()?.name }} + + } + @case (SubmissionReviewStatus.Removed) { + + Withdrawn: Entry withdrawn from {{ collectionProvider()?.name }} + + } + } + } + } +
- - + +
@@ -21,6 +64,7 @@
diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index 10139b086..78aa5c925 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -1,19 +1,54 @@ import { createDispatchMap, select } from '@ngxs/store'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + import { ButtonModule } from 'primeng/button'; import { DialogService } from 'primeng/dynamicdialog'; +import { Message } from 'primeng/message'; import { TagModule } from 'primeng/tag'; import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, DestroyRef, HostBinding, inject, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + effect, + HostBinding, + inject, + OnInit, +} from '@angular/core'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; -import { ClearCollections } from '@osf/features/collections/store/collections'; -import { LoadingSpinnerComponent, ResourceMetadataComponent, SubHeaderComponent } from '@shared/components'; -import { ResourceType } from '@shared/enums'; +import { SubmissionReviewStatus } from '@osf/features/moderation/enums'; +import { + LoadingSpinnerComponent, + MakeDecisionDialogComponent, + ResourceMetadataComponent, + SubHeaderComponent, +} from '@shared/components'; +import { Mode, ResourceType } from '@shared/enums'; import { MapProjectOverview } from '@shared/mappers/resource-overview.mappers'; -import { ClearWiki, GetAllNodeLinks, GetBookmarksCollectionId, GetHomeWiki, GetLinkedResources } from '@shared/stores'; +import { ToastService } from '@shared/services'; +import { + ClearWiki, + CollectionsSelectors, + GetAllNodeLinks, + GetBookmarksCollectionId, + GetCollectionProvider, + GetHomeWiki, + GetLinkedResources, +} from '@shared/stores'; +import { ClearCollections } from '@shared/stores/collections'; +import { IS_XSMALL } from '@shared/utils'; + +import { + ClearCollectionModeration, + CollectionsModerationSelectors, + GetSubmissionsReviewActions, +} from '../../moderation/store/collections-moderation'; import { LinkedResourcesComponent, @@ -47,6 +82,8 @@ import { RecentActivityComponent, OverviewToolbarComponent, ResourceMetadataComponent, + TranslatePipe, + Message, ], providers: [DialogService], changeDetection: ChangeDetectionStrategy.OnPush, @@ -55,7 +92,15 @@ export class ProjectOverviewComponent implements OnInit { @HostBinding('class') classes = 'flex flex-1 flex-column w-full h-full'; private route = inject(ActivatedRoute); + private router = inject(Router); private destroyRef = inject(DestroyRef); + protected readonly toastService = inject(ToastService); + protected readonly dialogService = inject(DialogService); + protected readonly translateService = inject(TranslateService); + protected isMobile = toSignal(inject(IS_XSMALL)); + protected submissions = select(CollectionsModerationSelectors.getCollectionSubmissions); + protected collectionProvider = select(CollectionsSelectors.getCollectionProvider); + protected currentReviewAction = select(CollectionsModerationSelectors.getCurrentReviewAction); protected actions = createDispatchMap({ getProject: GetProjectById, @@ -65,9 +110,34 @@ export class ProjectOverviewComponent implements OnInit { getLinkedProjects: GetLinkedResources, getNodeLinks: GetAllNodeLinks, setProjectCustomCitation: SetProjectCustomCitation, + getCollectionProvider: GetCollectionProvider, + getCurrentReviewAction: GetSubmissionsReviewActions, clearProjectOverview: ClearProjectOverview, clearWiki: ClearWiki, clearCollections: ClearCollections, + clearCollectionModeration: ClearCollectionModeration, + }); + + readonly isCollectionsRoute = computed(() => { + return this.router.url.includes('/collections'); + }); + + readonly isModerationMode = computed(() => { + const mode = this.route.snapshot.queryParams['mode']; + + return mode === Mode.Moderation; + }); + + submissionReviewStatus = computed(() => { + return this.currentReviewAction()?.toState; + }); + + protected showDecisionButton = computed(() => { + return ( + this.isCollectionsRoute() && + this.submissionReviewStatus() !== SubmissionReviewStatus.Removed && + this.submissionReviewStatus() !== SubmissionReviewStatus.Rejected + ); }); protected currentProject = select(ProjectOverviewSelectors.getProject); @@ -79,6 +149,11 @@ export class ProjectOverviewComponent implements OnInit { return null; }); protected isProjectLoading = select(ProjectOverviewSelectors.getProjectLoading); + protected isCollectionProviderLoading = select(CollectionsSelectors.getCollectionProviderLoading); + protected isReviewActionsLoading = select(CollectionsModerationSelectors.getCurrentReviewActionLoading); + protected isLoading = computed(() => { + return this.isProjectLoading() || this.isCollectionProviderLoading() || this.isReviewActionsLoading(); + }); protected currentResource = computed(() => { if (this.currentProject()) { return { @@ -94,15 +169,16 @@ export class ProjectOverviewComponent implements OnInit { }); constructor() { + this.setupCollectionsEffects(); this.setupCleanup(); } - onCustomCitationUpdated(citation: string): void { + protected onCustomCitationUpdated(citation: string): void { this.actions.setProjectCustomCitation(citation); } ngOnInit(): void { - const projectId = this.route.parent?.snapshot.params['id']; + const projectId = this.route.snapshot.params['id'] || this.route.parent?.snapshot.params['id']; if (projectId) { this.actions.getProject(projectId); this.actions.getBookmarksId(); @@ -113,11 +189,66 @@ export class ProjectOverviewComponent implements OnInit { } } + protected handleOpenMakeDecisionDialog() { + const dialogWidth = this.isMobile() ? '95vw' : '600px'; + + this.dialogService + .open(MakeDecisionDialogComponent, { + width: dialogWidth, + focusOnShow: false, + header: this.translateService.instant('moderation.makeDecision.header'), + closeOnEscape: true, + modal: true, + closable: true, + }) + .onClose.pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((data) => { + if (data && data.action) { + this.toastService.showSuccess(`moderation.makeDecision.${data.action}Success`); + this.goBack(); + } + }); + } + + protected goBack(): void { + const currentStatus = this.route.snapshot.queryParams['status']; + const queryParams = currentStatus ? { status: currentStatus } : {}; + + this.router.navigate(['../'], { + relativeTo: this.route, + queryParams, + }); + } + + private setupCollectionsEffects(): void { + effect(() => { + if (this.isModerationMode() && this.isCollectionsRoute()) { + const collectionId = this.route.snapshot.params['collectionId']; + + this.actions.getCollectionProvider(collectionId); + } + }); + + effect(() => { + if (this.isModerationMode() && this.isCollectionsRoute()) { + const provider = this.collectionProvider(); + const resource = this.currentResource(); + + if (!provider || !resource) return; + + this.actions.getCurrentReviewAction(resource.id, provider.primaryCollection.id); + } + }); + } + private setupCleanup(): void { this.destroyRef.onDestroy(() => { this.actions.clearProjectOverview(); this.actions.clearWiki(); this.actions.clearCollections(); + this.actions.clearCollectionModeration(); }); } + + protected readonly SubmissionReviewStatus = SubmissionReviewStatus; } diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index a8b7e76be..29407add9 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -2,9 +2,11 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; +import { CollectionsModerationState } from '@osf/features/moderation/store/collections-moderation'; import { ResourceType } from '@osf/shared/enums'; import { CitationsState, + CollectionsState, ContributorsState, NodeLinksState, SubjectsState, @@ -29,7 +31,7 @@ export const projectRoutes: Routes = [ path: 'overview', loadComponent: () => import('../project/overview/project-overview.component').then((mod) => mod.ProjectOverviewComponent), - providers: [provideStates([CitationsState, NodeLinksState])], + providers: [provideStates([NodeLinksState, CitationsState, CollectionsState, CollectionsModerationState])], }, { path: 'metadata', diff --git a/src/app/features/registries/registries.routes.ts b/src/app/features/registries/registries.routes.ts index 1a921441a..ace160f71 100644 --- a/src/app/features/registries/registries.routes.ts +++ b/src/app/features/registries/registries.routes.ts @@ -5,7 +5,7 @@ import { Routes } from '@angular/router'; import { RegistriesComponent } from '@osf/features/registries/registries.component'; import { RegistriesState } from '@osf/features/registries/store'; import { RegistriesProviderSearchState } from '@osf/features/registries/store/registries-provider-search'; -import { ContributorsState, SubjectsState } from '@osf/shared/stores'; +import { CitationsState, ContributorsState, SubjectsState } from '@osf/shared/stores'; import { LicensesHandlers, ProjectsHandlers, ProvidersHandlers } from './store/handlers'; import { FilesHandlers } from './store/handlers/files.handlers'; @@ -16,7 +16,7 @@ export const registriesRoutes: Routes = [ path: '', component: RegistriesComponent, providers: [ - provideStates([RegistriesState, ContributorsState, SubjectsState, RegistriesProviderSearchState]), + provideStates([RegistriesState, CitationsState, ContributorsState, SubjectsState, RegistriesProviderSearchState]), ProvidersHandlers, ProjectsHandlers, LicensesHandlers, diff --git a/src/app/features/registry/mappers/registry-overview.mapper.ts b/src/app/features/registry/mappers/registry-overview.mapper.ts index d6f417103..1a4b93bf7 100644 --- a/src/app/features/registry/mappers/registry-overview.mapper.ts +++ b/src/app/features/registry/mappers/registry-overview.mapper.ts @@ -13,6 +13,7 @@ export function MapRegistryOverview(data: RegistryOverviewJsonApiData): Registry dateCreated: data.attributes?.date_created, dateRegistered: data.attributes?.date_registered, category: data.attributes?.category, + customCitation: data.attributes?.custom_citation, isFork: data.attributes?.fork, accessRequestsEnabled: data.attributes?.accessRequestsEnabled, nodeLicense: data.attributes.node_license diff --git a/src/app/features/registry/models/get-registry-overview-json-api.model.ts b/src/app/features/registry/models/get-registry-overview-json-api.model.ts index d0ccd9e0b..36c46d544 100644 --- a/src/app/features/registry/models/get-registry-overview-json-api.model.ts +++ b/src/app/features/registry/models/get-registry-overview-json-api.model.ts @@ -23,6 +23,7 @@ export interface RegistryOverviewJsonApiAttributes { tags: string[]; category: string; fork?: boolean; + custom_citation?: string | null; accessRequestsEnabled?: boolean; node_license?: { copyright_holders: string[]; diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index 47b95dc18..2e14600b7 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -19,6 +19,7 @@ export { LicenseComponent } from './license/license.component'; export { LineChartComponent } from './line-chart/line-chart.component'; export { ListInfoShortenerComponent } from './list-info-shortener/list-info-shortener.component'; export { LoadingSpinnerComponent } from './loading-spinner/loading-spinner.component'; +export { MakeDecisionDialogComponent } from './make-decision-dialog/make-decision-dialog.component'; export { MarkdownComponent } from './markdown/markdown.component'; export { MyProjectsTableComponent } from './my-projects-table/my-projects-table.component'; export { PasswordInputHintComponent } from './password-input-hint/password-input-hint.component'; diff --git a/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.html b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.html new file mode 100644 index 000000000..2dbdc2029 --- /dev/null +++ b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.html @@ -0,0 +1,91 @@ +@let action = currentReviewAction(); + +@if (action) { +
+

+ @switch (action.toState) { + @case (SubmissionReviewStatus.Pending) { + {{ 'moderation.submissionReview.submitted' | translate }} + } + @case (SubmissionReviewStatus.Accepted) { + {{ 'moderation.submissionReview.accepted' | translate }} + } + } + {{ action.dateCreated | timeAgo }} + {{ 'moderation.submissionReview.by' | translate }} + {{ action.createdBy }} +

+ +
+ @if (isPendingStatus()) { +
+ + +
+ } + + @if (isPreModeration() || (isHybridModeration() && isPendingStatus())) { +
+ + +
+ } + + @if ((isHybridModeration() && !isPendingStatus()) || isPostModeration()) { +
+ + +
+ } + +
+ +
+ +
+ + +
+
+
+} diff --git a/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.scss b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.spec.ts b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.spec.ts new file mode 100644 index 000000000..69fa7c0fc --- /dev/null +++ b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MakeDecisionDialogComponent } from './make-decision-dialog.component'; + +describe('MakeDecisionDialogComponent', () => { + let component: MakeDecisionDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MakeDecisionDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MakeDecisionDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.ts b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.ts new file mode 100644 index 000000000..3cc58affc --- /dev/null +++ b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.ts @@ -0,0 +1,94 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { RadioButton } from 'primeng/radiobutton'; +import { Textarea } from 'primeng/textarea'; + +import { ChangeDetectionStrategy, Component, computed, inject, OnInit } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; + +import { ModerationType, SubmissionReviewStatus } from '@osf/features/moderation/enums'; +import { + CollectionsModerationSelectors, + CreateCollectionSubmissionAction, +} from '@osf/features/moderation/store/collections-moderation'; +import { ModerationDecisionFormControls, ModerationSubmitType } from '@shared/enums'; +import { TimeAgoPipe } from '@shared/pipes/time-ago.pipe'; +import { CollectionsSelectors } from '@shared/stores'; + +@Component({ + selector: 'osf-make-decision-dialog', + imports: [Button, TranslatePipe, TimeAgoPipe, FormsModule, RadioButton, ReactiveFormsModule, Textarea], + templateUrl: './make-decision-dialog.component.html', + styleUrl: './make-decision-dialog.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MakeDecisionDialogComponent implements OnInit { + private readonly fb = inject(FormBuilder); + protected readonly config = inject(DynamicDialogConfig); + protected readonly dialogRef = inject(DynamicDialogRef); + protected readonly ModerationSubmitType = ModerationSubmitType; + protected readonly SubmissionReviewStatus = SubmissionReviewStatus; + protected readonly ModerationDecisionFormControls = ModerationDecisionFormControls; + protected collectionProvider = select(CollectionsSelectors.getCollectionProvider); + protected currentReviewAction = select(CollectionsModerationSelectors.getCurrentReviewAction); + + protected isSubmitting = select(CollectionsModerationSelectors.getCollectionSubmissionSubmitting); + protected requestForm!: FormGroup; + + protected actions = createDispatchMap({ + createSubmissionAction: CreateCollectionSubmissionAction, + }); + + protected isHybridModeration = computed(() => { + const provider = this.collectionProvider(); + return provider?.reviewsWorkflow === ModerationType.Hybrid || !provider?.reviewsWorkflow; + }); + + protected isPreModeration = computed(() => { + const provider = this.collectionProvider(); + return provider?.reviewsWorkflow === ModerationType.Pre; + }); + + protected isPostModeration = computed(() => { + const provider = this.collectionProvider(); + return provider?.reviewsWorkflow === ModerationType.Post; + }); + + protected isPendingStatus = computed(() => { + return this.currentReviewAction()?.toState === SubmissionReviewStatus.Pending; + }); + + ngOnInit() { + this.initForm(); + } + + protected handleSubmission(): void { + const targetId = this.currentReviewAction()?.targetId; + if (this.requestForm.valid && targetId) { + const formData = this.requestForm.value; + const payload = { ...formData, targetId }; + + this.actions.createSubmissionAction(payload).subscribe({ + next: () => { + this.dialogRef.close(formData); + }, + }); + } + } + + get isSubmitDisabled(): boolean { + const actionControl = this.requestForm?.get(ModerationDecisionFormControls.Action); + return !actionControl?.value || this.isSubmitting()!; + } + + private initForm(): void { + this.requestForm = this.fb.group({ + [ModerationDecisionFormControls.Action]: new FormControl('', [Validators.required]), + [ModerationDecisionFormControls.Comment]: new FormControl(''), + }); + } +} diff --git a/src/app/shared/components/resource-citations/resource-citations.component.html b/src/app/shared/components/resource-citations/resource-citations.component.html index aeacd5222..5aafdde8e 100644 --- a/src/app/shared/components/resource-citations/resource-citations.component.html +++ b/src/app/shared/components/resource-citations/resource-citations.component.html @@ -44,72 +44,77 @@

{{ citation.title }}

}
} - -

{{ 'project.overview.metadata.getMoreCitations' | translate }}

- - - {{ selectedOption.label }} - - - @if (styledCitation()) { -

{{ styledCitation()?.citation }}

+ @if (!isCollectionsRoute()) { + +

{{ 'project.overview.metadata.getMoreCitations' | translate }}

+ + + {{ selectedOption.label }} + + + @if (styledCitation()) { +

{{ styledCitation()?.citation }}

+ } + + + } - + } + } + } @else { + @if (!isCollectionsRoute()) { + +
- } + > + + + + + +
} - } @else { - -
- - - - - - -
} diff --git a/src/app/shared/components/resource-citations/resource-citations.component.ts b/src/app/shared/components/resource-citations/resource-citations.component.ts index dc7cabd9b..0a140f8a7 100644 --- a/src/app/shared/components/resource-citations/resource-citations.component.ts +++ b/src/app/shared/components/resource-citations/resource-citations.component.ts @@ -57,6 +57,7 @@ import { export class ResourceCitationsComponent { private readonly destroyRef = inject(DestroyRef); private readonly translateService = inject(TranslateService); + isCollectionsRoute = input(false); currentResource = input.required(); private readonly clipboard = inject(Clipboard); private readonly toastService = inject(ToastService); diff --git a/src/app/shared/components/resource-metadata/resource-metadata.component.html b/src/app/shared/components/resource-metadata/resource-metadata.component.html index 1252df352..bfc09ba59 100644 --- a/src/app/shared/components/resource-metadata/resource-metadata.component.html +++ b/src/app/shared/components/resource-metadata/resource-metadata.component.html @@ -5,11 +5,13 @@
@@ -167,6 +169,7 @@

{{ 'project.overview.metadata.tags' | translate }}

} diff --git a/src/app/shared/components/resource-metadata/resource-metadata.component.ts b/src/app/shared/components/resource-metadata/resource-metadata.component.ts index 533b7af33..5f09d3b11 100644 --- a/src/app/shared/components/resource-metadata/resource-metadata.component.ts +++ b/src/app/shared/components/resource-metadata/resource-metadata.component.ts @@ -22,6 +22,7 @@ import { ResourceOverview } from '@shared/models'; export class ResourceMetadataComponent { currentResource = input.required(); customCitationUpdated = output(); + isCollectionsRoute = input(false); protected readonly resourceTypes = OsfResourceTypes; diff --git a/src/app/shared/components/sub-header/sub-header.component.html b/src/app/shared/components/sub-header/sub-header.component.html index 93df97e94..797643ce6 100644 --- a/src/app/shared/components/sub-header/sub-header.component.html +++ b/src/app/shared/components/sub-header/sub-header.component.html @@ -22,8 +22,9 @@

@if (showButton()) { -
+
(false); buttonLabel = input(''); + buttonSeverity = input('primary'); title = input(''); icon = input(''); tooltip = input(''); diff --git a/src/app/shared/constants/sort-options.const.ts b/src/app/shared/constants/sort-options.const.ts index 915f9ff75..c09f67b71 100644 --- a/src/app/shared/constants/sort-options.const.ts +++ b/src/app/shared/constants/sort-options.const.ts @@ -19,3 +19,22 @@ export const ALL_SORT_OPTIONS: CustomOption[] = [ label: 'project.files.sort.lastModifiedNewest', }, ]; + +export const COLLECTION_SUBMISSIONS_SORT_OPTIONS: CustomOption[] = [ + { + value: SortType.LastModifiedNewest, + label: 'project.files.sort.lastModifiedNewest', + }, + { + value: SortType.LastModifiedOldest, + label: 'project.files.sort.lastModifiedOldest', + }, + { + value: SortType.TitleAZ, + label: 'project.files.sort.nameAZ', + }, + { + value: SortType.TitleZA, + label: 'project.files.sort.nameZA', + }, +]; diff --git a/src/app/shared/enums/index.ts b/src/app/shared/enums/index.ts index 8f8ece679..c3cec26c8 100644 --- a/src/app/shared/enums/index.ts +++ b/src/app/shared/enums/index.ts @@ -11,6 +11,9 @@ export * from './file-menu-type.enum'; export * from './filter-type.enum'; export * from './get-resources-request-type.enum'; export * from './metadata-projects.enum'; +export * from './mode.enum'; +export * from './moderation-decision-form-controls.enum'; +export * from './moderation-submit-type.enum'; export * from './profile-addons-stepper.enum'; export * from './profile-settings-key.enum'; export * from './registration-review-states.enum'; diff --git a/src/app/shared/enums/mode.enum.ts b/src/app/shared/enums/mode.enum.ts new file mode 100644 index 000000000..dddc6ab79 --- /dev/null +++ b/src/app/shared/enums/mode.enum.ts @@ -0,0 +1,3 @@ +export enum Mode { + Moderation = 'moderation', +} diff --git a/src/app/shared/enums/moderation-decision-form-controls.enum.ts b/src/app/shared/enums/moderation-decision-form-controls.enum.ts new file mode 100644 index 000000000..437c33de1 --- /dev/null +++ b/src/app/shared/enums/moderation-decision-form-controls.enum.ts @@ -0,0 +1,4 @@ +export enum ModerationDecisionFormControls { + 'Action' = 'action', + 'Comment' = 'comment', +} diff --git a/src/app/shared/enums/moderation-submit-type.enum.ts b/src/app/shared/enums/moderation-submit-type.enum.ts new file mode 100644 index 000000000..3e84faa5a --- /dev/null +++ b/src/app/shared/enums/moderation-submit-type.enum.ts @@ -0,0 +1,5 @@ +export enum ModerationSubmitType { + 'Accept' = 'accept', + 'Reject' = 'reject', + 'Withdraw' = 'remove', +} diff --git a/src/app/shared/enums/sort-type.enum.ts b/src/app/shared/enums/sort-type.enum.ts index e45dcd466..f1bce0ce3 100644 --- a/src/app/shared/enums/sort-type.enum.ts +++ b/src/app/shared/enums/sort-type.enum.ts @@ -3,4 +3,6 @@ export enum SortType { NameZA = '-name', LastModifiedOldest = 'date_modified', LastModifiedNewest = '-date_modified', + TitleAZ = 'title', + TitleZA = '-title', } diff --git a/src/app/features/collections/mappers/collections.mapper.ts b/src/app/shared/mappers/collections/collections.mapper.ts similarity index 62% rename from src/app/features/collections/mappers/collections.mapper.ts rename to src/app/shared/mappers/collections/collections.mapper.ts index e75bcdc9b..2b55caf99 100644 --- a/src/app/features/collections/mappers/collections.mapper.ts +++ b/src/app/shared/mappers/collections/collections.mapper.ts @@ -1,3 +1,8 @@ +import { JsonApiResponseWithPaging } from '@core/models'; +import { + CollectionSubmissionReviewAction, + CollectionSubmissionReviewActionJsonApi, +} from '@osf/features/moderation/models'; import { CollectionContributor, CollectionContributorJsonApi, @@ -8,8 +13,11 @@ import { CollectionSubmission, CollectionSubmissionJsonApi, CollectionSubmissionPayload, -} from '@osf/features/collections/models'; -import { CollectionSubmissionPayloadJsonApi } from '@osf/features/collections/models/collection-submission-payload-json-api.model'; + CollectionSubmissionPayloadJsonApi, + PaginatedData, + ReviewActionPayload, + ReviewActionPayloadJsonApi, +} from '@osf/shared/models'; import { convertToSnakeCase } from '@shared/utils'; export class CollectionsMapper { @@ -78,29 +86,58 @@ export class CollectionsMapper { }; } - static fromGetCollectionSubmissionsResponse(response: CollectionSubmissionJsonApi[]): CollectionSubmission[] { - return response.map((submission) => ({ - id: submission.id, - type: submission.type, - nodeId: submission.embeds.guid.data.id, - nodeUrl: submission.embeds.guid.data.links.html, - title: submission.embeds.guid.data.attributes.title, - description: submission.embeds.guid.data.attributes.description, - category: submission.embeds.guid.data.attributes.category, - dateCreated: submission.embeds.guid.data.attributes.date_created, - dateModified: submission.embeds.guid.data.attributes.date_modified, - public: submission.embeds.guid.data.attributes.public, - reviewsState: submission.attributes.reviews_state, - collectedType: submission.attributes.collected_type, - status: submission.attributes.status, - volume: submission.attributes.volume, - issue: submission.attributes.issue, - programArea: submission.attributes.program_area, - schoolType: submission.attributes.school_type, - studyDesign: submission.attributes.study_design, - dataType: submission.attributes.data_type, - disease: submission.attributes.disease, - gradeLevels: submission.attributes.grade_levels, + static fromGetCollectionSubmissionsResponse( + response: JsonApiResponseWithPaging + ): PaginatedData { + return { + data: response.data.map((submission) => ({ + id: submission.id, + type: submission.type, + nodeId: submission.embeds.guid.data.id, + nodeUrl: submission.embeds.guid.data.links.html, + title: submission.embeds.guid.data.attributes.title, + description: submission.embeds.guid.data.attributes.description, + category: submission.embeds.guid.data.attributes.category, + dateCreated: submission.embeds.guid.data.attributes.date_created, + dateModified: submission.embeds.guid.data.attributes.date_modified, + public: submission.embeds.guid.data.attributes.public, + reviewsState: submission.attributes.reviews_state, + collectedType: submission.attributes.collected_type, + status: submission.attributes.status, + volume: submission.attributes.volume, + issue: submission.attributes.issue, + programArea: submission.attributes.program_area, + schoolType: submission.attributes.school_type, + studyDesign: submission.attributes.study_design, + dataType: submission.attributes.data_type, + disease: submission.attributes.disease, + gradeLevels: submission.attributes.grade_levels, + creator: submission.embeds.creator + ? { + id: submission.embeds.creator.data.id, + fullName: submission.embeds.creator.data.attributes.full_name, + } + : undefined, + })), + totalCount: response.links.meta.total, + }; + } + + static fromGetCollectionSubmissionsActionsResponse( + response: CollectionSubmissionReviewActionJsonApi[] + ): CollectionSubmissionReviewAction[] { + return response.map((action) => ({ + id: action.id, + type: action.type, + dateModified: action.attributes.date_modified, + dateCreated: action.attributes.date_created, + fromState: action.attributes.from_state, + toState: action.attributes.to_state, + trigger: action.attributes.trigger, + comment: action.attributes.comment, + targetId: action.relationships.target.data.id, + targetNodeId: action.relationships.target.data.id.split('-')[0], + createdBy: action.embeds.creator.data.attributes.full_name, })); } @@ -159,4 +196,24 @@ export class CollectionsMapper { }, }; } + + static toReviewActionPayloadJsonApi(payload: ReviewActionPayload): ReviewActionPayloadJsonApi { + return { + data: { + type: 'collection_submission_actions', + attributes: { + trigger: payload.action, + comment: payload.comment, + }, + relationships: { + target: { + data: { + type: 'collection-submissions', + id: payload.targetId, + }, + }, + }, + }, + }; + } } diff --git a/src/app/features/collections/mappers/index.ts b/src/app/shared/mappers/collections/index.ts similarity index 100% rename from src/app/features/collections/mappers/index.ts rename to src/app/shared/mappers/collections/index.ts diff --git a/src/app/shared/mappers/index.ts b/src/app/shared/mappers/index.ts index 2d546d8fd..7d4005eb1 100644 --- a/src/app/shared/mappers/index.ts +++ b/src/app/shared/mappers/index.ts @@ -1,5 +1,6 @@ export * from './addon.mapper'; export * from './citations.mapper'; +export * from './collections'; export * from './components'; export * from './contributors'; export * from './filters'; diff --git a/src/app/features/collections/models/collection-submission-payload-json-api.model.ts b/src/app/shared/models/collections/collection-submission-payload-json-api.model.ts similarity index 100% rename from src/app/features/collections/models/collection-submission-payload-json-api.model.ts rename to src/app/shared/models/collections/collection-submission-payload-json-api.model.ts diff --git a/src/app/features/collections/models/collection-submission-payload.ts b/src/app/shared/models/collections/collection-submission-payload.model.ts similarity index 100% rename from src/app/features/collections/models/collection-submission-payload.ts rename to src/app/shared/models/collections/collection-submission-payload.model.ts diff --git a/src/app/features/collections/models/collections-filters.model.ts b/src/app/shared/models/collections/collections-filters.model.ts similarity index 100% rename from src/app/features/collections/models/collections-filters.model.ts rename to src/app/shared/models/collections/collections-filters.model.ts diff --git a/src/app/features/collections/models/collections-json-api.models.ts b/src/app/shared/models/collections/collections-json-api.models.ts similarity index 93% rename from src/app/features/collections/models/collections-json-api.models.ts rename to src/app/shared/models/collections/collections-json-api.models.ts index 534bad4f1..dff8a6826 100644 --- a/src/app/features/collections/models/collections-json-api.models.ts +++ b/src/app/shared/models/collections/collections-json-api.models.ts @@ -102,6 +102,19 @@ export interface CollectionSubmissionJsonApi { }; }; }; + creator?: { + data: { + attributes: { + full_name: string; + }; + id: string; + }; + }; + }; + links: { + meta: { + total: number; + }; }; } @@ -127,7 +140,7 @@ export interface CollectionDetailsGetResponseJsonApi extends JsonApiResponse 1 ? 's' : ''} ago`; + } else if (seconds < day) { + const hours = Math.floor(seconds / hour); + return `${hours} hour${hours > 1 ? 's' : ''} ago`; + } else if (seconds < month) { + const days = Math.floor(seconds / day); + return `${days} day${days > 1 ? 's' : ''} ago`; + } else if (seconds < year) { + const months = Math.floor(seconds / month); + return `${months} month${months > 1 ? 's' : ''} ago`; + } else { + const years = Math.floor(seconds / year); + return `${years} year${years > 1 ? 's' : ''} ago`; + } + } +} diff --git a/src/app/shared/services/bookmarks.service.ts b/src/app/shared/services/bookmarks.service.ts index 968d96a19..c461e45da 100644 --- a/src/app/shared/services/bookmarks.service.ts +++ b/src/app/shared/services/bookmarks.service.ts @@ -3,7 +3,7 @@ import { map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@core/services'; -import { SparseCollectionsResponseJsonApi } from '@osf/features/collections/models'; +import { SparseCollectionsResponseJsonApi } from '@shared/models'; import { environment } from 'src/environments/environment'; diff --git a/src/app/features/collections/services/collections.service.ts b/src/app/shared/services/collections.service.ts similarity index 64% rename from src/app/features/collections/services/collections.service.ts rename to src/app/shared/services/collections.service.ts index 1f7c2ac2b..0fc59e059 100644 --- a/src/app/features/collections/services/collections.service.ts +++ b/src/app/shared/services/collections.service.ts @@ -5,10 +5,12 @@ import { forkJoin, map, Observable, of, switchMap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { JsonApiResponse, JsonApiResponseWithPaging } from '@core/models'; -import { JsonApiService } from '@osf/core/services'; -import { CollectionsMapper } from '@osf/features/collections/mappers'; -import { SetTotalSubmissions } from '@osf/features/collections/store/collections'; - +import { JsonApiService } from '@core/services'; +import { + CollectionSubmissionReviewAction, + CollectionSubmissionReviewActionJsonApi, +} from '@osf/features/moderation/models'; +import { CollectionsMapper } from '@shared/mappers/collections'; import { CollectionContributor, CollectionDetails, @@ -17,9 +19,13 @@ import { CollectionProviderGetResponseJsonApi, CollectionSubmission, CollectionSubmissionJsonApi, - CollectionSubmissionsPayloadJsonApi, + CollectionSubmissionsSearchPayloadJsonApi, ContributorsResponseJsonApi, -} from '../models'; + PaginatedData, + ReviewActionPayload, +} from '@shared/models'; +import { ReviewActionPayloadJsonApi } from '@shared/models/collections/review-action-payload-json-api.model'; +import { SetTotalSubmissions } from '@shared/stores/collections'; import { environment } from 'src/environments/environment'; @@ -64,7 +70,7 @@ export class CollectionsService { params['sort'] = sortBy; } - const payload: CollectionSubmissionsPayloadJsonApi = { + const payload: CollectionSubmissionsSearchPayloadJsonApi = { data: { attributes: { provider: [providerId], @@ -102,12 +108,61 @@ export class CollectionsService { ); } + fetchCollectionSubmissionsByStatus( + collectionId: string, + status: string, + page = '1', + sortBy: string + ): Observable> { + const params: Record = { + page, + 'filter[reviews_state]': status, + 'page[size]': '10', + embed: 'creator', + sort: sortBy, + }; + + return this.jsonApiService + .get< + JsonApiResponseWithPaging + >(`${environment.apiUrl}/collections/${collectionId}/collection_submissions/`, params) + .pipe( + map((response) => { + return CollectionsMapper.fromGetCollectionSubmissionsResponse(response); + }) + ); + } + + fetchCollectionSubmissionsActions( + projectId: string, + collectionId: string + ): Observable { + const params: Record = { + embed: 'creator', + }; + + return this.jsonApiService + .get< + JsonApiResponse + >(`${environment.apiUrl}/collection_submissions/${projectId}-${collectionId}/actions/?sort=-date_modified`, params) + .pipe(map((response) => CollectionsMapper.fromGetCollectionSubmissionsActionsResponse(response.data))); + } + fetchAllUserCollectionSubmissions(providerId: string, projectIds: string[]): Observable { const pendingSubmissions$ = this.fetchUserCollectionSubmissionsByStatus(providerId, projectIds, 'pending'); const acceptedSubmissions$ = this.fetchUserCollectionSubmissionsByStatus(providerId, projectIds, 'accepted'); return forkJoin([pendingSubmissions$, acceptedSubmissions$]).pipe( - map(([pending, accepted]) => [...pending, ...accepted]) + map(([pending, accepted]) => [...pending.data, ...accepted.data]) + ); + } + + createCollectionSubmissionAction(payload: ReviewActionPayload): Observable { + const params = CollectionsMapper.toReviewActionPayloadJsonApi(payload); + + return this.jsonApiService.post( + `${environment.apiUrl}/collection_submission_actions/`, + params ); } @@ -127,7 +182,7 @@ export class CollectionsService { providerId: string, projectIds: string[], submissionStatus: string - ): Observable { + ): Observable> { const params: Record = { 'filter[reviews_state]': submissionStatus, 'filter[id]': projectIds.join(','), @@ -135,11 +190,11 @@ export class CollectionsService { return this.jsonApiService .get< - JsonApiResponse + JsonApiResponseWithPaging >(`${environment.apiUrl}/collections/${providerId}/collection_submissions/`, params) .pipe( map((response) => { - return CollectionsMapper.fromGetCollectionSubmissionsResponse(response.data); + return CollectionsMapper.fromGetCollectionSubmissionsResponse(response); }) ); } diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index fc5aa6a57..3007f1cc3 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -1,6 +1,7 @@ export * from './addons'; export { BookmarksService } from './bookmarks.service'; export { BrandService } from './brand.service'; +export { CollectionsService } from './collections.service'; export { ContributorsService } from './contributors.service'; export { CustomConfirmationService } from './custom-confirmation.service'; export { FilesService } from './files.service'; diff --git a/src/app/shared/services/node-links.service.ts b/src/app/shared/services/node-links.service.ts index cf834b8d9..4b3a0cdf8 100644 --- a/src/app/shared/services/node-links.service.ts +++ b/src/app/shared/services/node-links.service.ts @@ -71,7 +71,7 @@ export class NodeLinksService { return this.jsonApiService .get< JsonApiResponse - >(`${environment.apiUrl}/nodes/${projectId}/linked_nodes`, params) + >(`${environment.apiUrl}/nodes/${projectId}/linked_nodes/`, params) .pipe( map((response) => { return response.data.map((item) => ComponentsMapper.fromGetComponentResponse(item)); diff --git a/src/app/features/collections/store/collections/collections.actions.ts b/src/app/shared/stores/collections/collections.actions.ts similarity index 97% rename from src/app/features/collections/store/collections/collections.actions.ts rename to src/app/shared/stores/collections/collections.actions.ts index 665b9ccd1..03f289aef 100644 --- a/src/app/features/collections/store/collections/collections.actions.ts +++ b/src/app/shared/stores/collections/collections.actions.ts @@ -1,4 +1,4 @@ -import { CollectionsFilters } from '@osf/features/collections/models'; +import { CollectionsFilters } from '@shared/models'; export class GetCollectionProvider { static readonly type = '[Collections] Get Collection Provider'; diff --git a/src/app/features/collections/store/collections/collections.model.ts b/src/app/shared/stores/collections/collections.model.ts similarity index 78% rename from src/app/features/collections/store/collections/collections.model.ts rename to src/app/shared/stores/collections/collections.model.ts index e0b0b64b4..219e1dbad 100644 --- a/src/app/features/collections/store/collections/collections.model.ts +++ b/src/app/shared/stores/collections/collections.model.ts @@ -1,9 +1,4 @@ -import { - CollectionDetails, - CollectionProvider, - CollectionsFilters, - CollectionSubmission, -} from '@osf/features/collections/models'; +import { CollectionDetails, CollectionProvider, CollectionsFilters, CollectionSubmission } from '@shared/models'; import { AsyncStateModel } from '@shared/models/store'; export interface CollectionsStateModel { diff --git a/src/app/features/collections/store/collections/collections.selectors.ts b/src/app/shared/stores/collections/collections.selectors.ts similarity index 93% rename from src/app/features/collections/store/collections/collections.selectors.ts rename to src/app/shared/stores/collections/collections.selectors.ts index f312e9f6b..047937a55 100644 --- a/src/app/features/collections/store/collections/collections.selectors.ts +++ b/src/app/shared/stores/collections/collections.selectors.ts @@ -1,6 +1,6 @@ import { Selector } from '@ngxs/store'; -import { CollectionsFilters } from '@osf/features/collections/models'; +import { CollectionsFilters } from '@shared/models'; import { CollectionsStateModel } from './collections.model'; import { CollectionsState } from './collections.state'; @@ -37,7 +37,7 @@ export class CollectionsSelectors { } @Selector([CollectionsState]) - static getCollectionSubmissions(state: CollectionsStateModel) { + static getCollectionSubmissionsSearchResult(state: CollectionsStateModel) { return state.collectionSubmissions.data; } diff --git a/src/app/features/collections/store/collections/collections.state.ts b/src/app/shared/stores/collections/collections.state.ts similarity index 99% rename from src/app/features/collections/store/collections/collections.state.ts rename to src/app/shared/stores/collections/collections.state.ts index ba669284f..b58cabad0 100644 --- a/src/app/features/collections/store/collections/collections.state.ts +++ b/src/app/shared/stores/collections/collections.state.ts @@ -4,7 +4,7 @@ import { catchError, tap, throwError } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { CollectionsService } from '@osf/features/collections/services'; +import { CollectionsService } from '@shared/services'; import { ClearCollections, diff --git a/src/app/features/collections/store/collections/index.ts b/src/app/shared/stores/collections/index.ts similarity index 100% rename from src/app/features/collections/store/collections/index.ts rename to src/app/shared/stores/collections/index.ts diff --git a/src/app/shared/stores/index.ts b/src/app/shared/stores/index.ts index 4d6df5181..2350f9cb5 100644 --- a/src/app/shared/stores/index.ts +++ b/src/app/shared/stores/index.ts @@ -1,6 +1,7 @@ export * from './addons'; export * from './bookmarks'; export * from './citations'; +export * from './collections'; export * from './contributors'; export * from './institutions'; export * from './institutions-search'; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 06bdcbcdf..07cca47b7 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1149,6 +1149,7 @@ "inviteModerator": "Invite moderator", "searchModerator": "Search moderator", "selectPermission": "Select permission", + "noSubmissions": "No submissions", "moderatorPermissions": { "administrator": "Administrator", "moderator": "Moderator" @@ -1171,10 +1172,27 @@ "public": "Public", "embargo": "Embargo" }, + "makeDecision": { + "header": "Make decision", + "requestSubmitted": "Request submitted", + "acceptRequest": "Accept request", + "rejectRequest": "Reject request", + "withdrawRequest": "Withdraw Request", + "acceptRequestMessage": "Submission will appear in search results and will be associated with the collection.", + "rejectRequestMessage": "Submission will not appear in search results nor will be associated with the collection.", + "withdrawRequestMessage": "Submission will be withdrawn from the collection and will no longer appear in search results.", + "remarksPlaceholder": "Add remarks to project admins (optional)", + "submitDecision": "Submit decision", + "goBackLink": "< Back to list of submissions", + "acceptSuccess": "Submission has been accepted successfully", + "rejectSuccess": "Submission has been rejected successfully", + "removeSuccess": "Submission has been withdrawn successfully" + }, "submissionReview": { "submitted": "Submitted", "accepted": "Accepted", "rejected": "Rejected", + "withdrawn": "Withdrawn", "by": "by" }, "preprintReviewStatus": { diff --git a/src/assets/styles/overrides/button.scss b/src/assets/styles/overrides/button.scss index a5f230e55..5231609c7 100644 --- a/src/assets/styles/overrides/button.scss +++ b/src/assets/styles/overrides/button.scss @@ -64,3 +64,10 @@ width: 100%; } } + +.link-btn-no-padding { + .p-button { + --p-button-padding-y: 0; + --p-button-padding-x: 0; + } +} diff --git a/src/assets/styles/overrides/message.scss b/src/assets/styles/overrides/message.scss index 36619a575..ddbe2b92d 100644 --- a/src/assets/styles/overrides/message.scss +++ b/src/assets/styles/overrides/message.scss @@ -23,3 +23,9 @@ .p-message-simple { --p-message-text-font-weight: 400; } + +.overview-message { + .p-message { + width: 100%; + } +}