diff --git a/src/app/features/files/pages/file-detail/file-detail.component.ts b/src/app/features/files/pages/file-detail/file-detail.component.ts index 40ff066d9..d4c60db1a 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.ts @@ -183,8 +183,8 @@ export class FileDetailComponent { this.actions.getFileResourceMetadata(this.resourceId, this.resourceType); this.actions.getFileResourceContributors(this.resourceId, this.resourceType); if (fileId) { - const storageLink = this.file()?.links.download || ''; - this.actions.getFileRevisions(storageLink, fileId); + const storageLink = this.file()?.links.upload || ''; + this.actions.getFileRevisions(storageLink); this.actions.getCedarTemplates(); this.actions.getCedarRecords(fileId, ResourceType.File); } diff --git a/src/app/features/files/store/files.actions.ts b/src/app/features/files/store/files.actions.ts index 96ef709fa..a6fc56f71 100644 --- a/src/app/features/files/store/files.actions.ts +++ b/src/app/features/files/store/files.actions.ts @@ -114,10 +114,7 @@ export class SetFileMetadata { export class GetFileRevisions { static readonly type = '[Files] Get Revisions'; - constructor( - public link: string, - public fileId: string - ) {} + constructor(public link: string) {} } export class UpdateTags { diff --git a/src/app/features/files/store/files.state.ts b/src/app/features/files/store/files.state.ts index d0e977eb2..d9e572374 100644 --- a/src/app/features/files/store/files.state.ts +++ b/src/app/features/files/store/files.state.ts @@ -257,7 +257,7 @@ export class FilesState { const state = ctx.getState(); ctx.patchState({ fileRevisions: { ...state.fileRevisions, isLoading: true, error: null } }); - return this.filesService.getFileRevisions(action.link, action.fileId).pipe( + return this.filesService.getFileRevisions(action.link).pipe( tap({ next: (revisions) => { ctx.patchState({ fileRevisions: { data: revisions, isLoading: false, error: null } }); diff --git a/src/app/features/metadata/pages/add-metadata/add-metadata.component.ts b/src/app/features/metadata/pages/add-metadata/add-metadata.component.ts index 0c9b31a1f..c27330578 100644 --- a/src/app/features/metadata/pages/add-metadata/add-metadata.component.ts +++ b/src/app/features/metadata/pages/add-metadata/add-metadata.component.ts @@ -123,7 +123,11 @@ export class AddMetadataComponent implements OnInit { if (templates?.links?.first && templates?.links?.last && templates.links.first !== templates.links.last) { this.actions.getCedarTemplates(); } else { - this.router.navigate(['..'], { relativeTo: this.activatedRoute }); + if (this.resourceType() === ResourceType.File) { + this.router.navigate([this.resourceId]); + } else { + this.router.navigate(['..'], { relativeTo: this.activatedRoute }); + } } } diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index 94c3d3947..bf93b0e7d 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -4,6 +4,7 @@ import { Routes } from '@angular/router'; import { viewOnlyGuard } from '@osf/core/guards'; import { ResourceType } from '@osf/shared/enums'; +import { LicensesService } from '@osf/shared/services'; import { CitationsState, CollectionsState, @@ -17,6 +18,9 @@ import { ActivityLogsState } from '@osf/shared/stores/activity-logs'; import { AnalyticsState } from '../analytics/store'; import { CollectionsModerationState } from '../moderation/store/collections-moderation'; +import { RegistriesState } from '../registries/store'; +import { LicensesHandlers, ProjectsHandlers, ProvidersHandlers } from '../registries/store/handlers'; +import { FilesHandlers } from '../registries/store/handlers/files.handlers'; import { SettingsState } from './settings/store'; @@ -59,6 +63,14 @@ export const projectRoutes: Routes = [ { path: 'registrations', canActivate: [viewOnlyGuard], + providers: [ + provideStates([RegistriesState]), + ProvidersHandlers, + ProjectsHandlers, + LicensesService, + LicensesHandlers, + FilesHandlers, + ], loadComponent: () => import('../project/registrations/registrations.component').then((mod) => mod.RegistrationsComponent), }, diff --git a/src/app/features/project/registrations/registrations.component.html b/src/app/features/project/registrations/registrations.component.html index 80bf0335a..a9bd9b0ad 100644 --- a/src/app/features/project/registrations/registrations.component.html +++ b/src/app/features/project/registrations/registrations.component.html @@ -15,6 +15,13 @@ @for (registration of registrations(); track registration.id) { } + @if (registrationsTotalCount() > itemsPerPage) { + + } } diff --git a/src/app/features/project/registrations/registrations.component.ts b/src/app/features/project/registrations/registrations.component.ts index 69cf1e5c5..ff5b5c206 100644 --- a/src/app/features/project/registrations/registrations.component.ts +++ b/src/app/features/project/registrations/registrations.component.ts @@ -3,6 +3,7 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; import { DialogService } from 'primeng/dynamicdialog'; +import { PaginatorState } from 'primeng/paginator'; import { map, of } from 'rxjs'; @@ -11,7 +12,12 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { LoadingSpinnerComponent, RegistrationCardComponent, SubHeaderComponent } from '@osf/shared/components'; +import { + CustomPaginatorComponent, + LoadingSpinnerComponent, + RegistrationCardComponent, + SubHeaderComponent, +} from '@osf/shared/components'; import { GetRegistrations, RegistrationsSelectors } from './store'; @@ -19,7 +25,14 @@ import { environment } from 'src/environments/environment'; @Component({ selector: 'osf-registrations', - imports: [RegistrationCardComponent, SubHeaderComponent, FormsModule, TranslatePipe, LoadingSpinnerComponent], + imports: [ + RegistrationCardComponent, + SubHeaderComponent, + FormsModule, + TranslatePipe, + LoadingSpinnerComponent, + CustomPaginatorComponent, + ], templateUrl: './registrations.component.html', styleUrl: './registrations.component.scss', providers: [DialogService], @@ -32,11 +45,15 @@ export class RegistrationsComponent implements OnInit { readonly projectId = toSignal(this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined)); registrations = select(RegistrationsSelectors.getRegistrations); + registrationsTotalCount = select(RegistrationsSelectors.getRegistrationsTotalCount); isRegistrationsLoading = select(RegistrationsSelectors.isRegistrationsLoading); actions = createDispatchMap({ getRegistrations: GetRegistrations }); + itemsPerPage = 10; + first = 0; + ngOnInit(): void { - this.actions.getRegistrations(this.projectId()); + this.actions.getRegistrations(this.projectId(), 1, this.itemsPerPage); } addRegistration(): void { @@ -44,4 +61,9 @@ export class RegistrationsComponent implements OnInit { queryParams: { projectId: this.projectId() }, }); } + + onPageChange(event: PaginatorState): void { + this.actions.getRegistrations(this.projectId(), event.page! + 1, this.itemsPerPage); + this.first = event.first!; + } } diff --git a/src/app/features/project/registrations/services/registrations.service.ts b/src/app/features/project/registrations/services/registrations.service.ts index c222b575d..07ecf3b84 100644 --- a/src/app/features/project/registrations/services/registrations.service.ts +++ b/src/app/features/project/registrations/services/registrations.service.ts @@ -15,10 +15,14 @@ export class RegistrationsService { private readonly jsonApiService = inject(JsonApiService); private readonly apiUrl = `${environment.apiDomainUrl}/v2`; - getRegistrations(projectId: string): Observable> { - const params: Record = { embed: 'contributors' }; + getRegistrations(projectId: string, page: number, pageSize: number): Observable> { + const params = { + page, + 'page[size]': pageSize, + embed: ['bibliographic_contributors', 'registration_schema', 'provider'], + }; - const url = `${this.apiUrl}/nodes/${projectId}/linked_by_registrations/`; + const url = `${this.apiUrl}/nodes/${projectId}/registrations/`; return this.jsonApiService.get>(url, params).pipe( map((response) => { diff --git a/src/app/features/project/registrations/store/registrations.actions.ts b/src/app/features/project/registrations/store/registrations.actions.ts index cec5e1dfd..81a2981ef 100644 --- a/src/app/features/project/registrations/store/registrations.actions.ts +++ b/src/app/features/project/registrations/store/registrations.actions.ts @@ -1,5 +1,9 @@ export class GetRegistrations { static readonly type = '[Registrations] Get Registrations'; - constructor(public projectId: string) {} + constructor( + public projectId: string, + public page = 1, + public pageSize = 10 + ) {} } diff --git a/src/app/features/project/registrations/store/registrations.selectors.ts b/src/app/features/project/registrations/store/registrations.selectors.ts index f958bf568..c72a45b9e 100644 --- a/src/app/features/project/registrations/store/registrations.selectors.ts +++ b/src/app/features/project/registrations/store/registrations.selectors.ts @@ -9,6 +9,11 @@ export class RegistrationsSelectors { return state.registrations.data; } + @Selector([RegistrationsState]) + static getRegistrationsTotalCount(state: RegistrationsStateModel): number { + return state.registrations.totalCount; + } + @Selector([RegistrationsState]) static isRegistrationsLoading(state: RegistrationsStateModel) { return state.registrations.isLoading; diff --git a/src/app/features/project/registrations/store/registrations.state.ts b/src/app/features/project/registrations/store/registrations.state.ts index 0fd503eee..a6cc768f9 100644 --- a/src/app/features/project/registrations/store/registrations.state.ts +++ b/src/app/features/project/registrations/store/registrations.state.ts @@ -27,7 +27,7 @@ export class RegistrationsState { registrations: { ...state.registrations, isLoading: true, error: null }, }); - return this.registrationsService.getRegistrations(action.projectId).pipe( + return this.registrationsService.getRegistrations(action.projectId, action.page, action.pageSize).pipe( tap((registrations) => { ctx.setState({ registrations: { diff --git a/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.html b/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.html index 9379b23c6..f4df946e2 100644 --- a/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.html +++ b/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.html @@ -58,6 +58,7 @@ [disabled]="isRegistrationSubmitting()" /> {{ 'common.links.showExample' | translate }} - - - {{ 'common.links.hideExample' | translate }} - - + + {{ 'common.links.hideExample' | translate }} + {{ q.exampleText }} @@ -207,6 +199,11 @@ {{ 'files.actions.uploadFile' | translate }} class="mr-2" (click)="goBack()" > - + diff --git a/src/app/features/registries/components/custom-step/custom-step.component.ts b/src/app/features/registries/components/custom-step/custom-step.component.ts index fb1467a82..59721646c 100644 --- a/src/app/features/registries/components/custom-step/custom-step.component.ts +++ b/src/app/features/registries/components/custom-step/custom-step.component.ts @@ -152,7 +152,6 @@ export class CustomStepComponent implements OnDestroy { break; default: - console.warn(`Unsupported field type: ${q.fieldType}`); return; } diff --git a/src/app/features/registries/components/metadata/metadata.component.html b/src/app/features/registries/components/metadata/metadata.component.html index c877f627a..a3bb08450 100644 --- a/src/app/features/registries/components/metadata/metadata.component.html +++ b/src/app/features/registries/components/metadata/metadata.component.html @@ -13,6 +13,7 @@ {{ 'registries.metadata.title' | translate }} {{ 'common.labels.description' | translate }} {{ 'registries.metadata.title' | translate }} - + diff --git a/src/app/features/registries/components/metadata/registries-license/registries-license.component.html b/src/app/features/registries/components/metadata/registries-license/registries-license.component.html index 7673e6661..2a0123131 100644 --- a/src/app/features/registries/components/metadata/registries-license/registries-license.component.html +++ b/src/app/features/registries/components/metadata/registries-license/registries-license.component.html @@ -9,6 +9,7 @@ {{ 'shared.license.title' | translate }} + @@ -14,6 +14,7 @@ {{ ('registries.new.steps.title' | translate) + '1' }} {{ ('registries.new.steps.title' | translate) + '1' }} /> {{ ('registries.new.steps.title' | translate) + '2' }} {{ 'registries.new.steps.step2InfoText' | translate }} @@ -53,6 +58,7 @@ {{ ('registries.new.steps.title' | translate) + (fromProject ? {{ 'registries.new.steps.step3' | translate }} {{ ('registries.new.steps.title' | translate) + (fromProject ? (); + constructor() { const userId = this.user()?.id; if (userId) { - this.actions.getProjects(userId); + this.actions.getProjects(userId, ''); } this.actions.getProviderSchemas(this.providerId); effect(() => { @@ -63,6 +70,14 @@ export class NewRegistrationComponent { this.draftForm.get('providerSchema')?.setValue(this.providerSchemas()[0]?.id); } }); + + this.filter$ + .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) + .subscribe((value: string) => { + if (userId) { + this.actions.getProjects(userId, value); + } + }); } onSelectProject(projectId: string) { @@ -71,6 +86,10 @@ export class NewRegistrationComponent { }); } + onProjectFilter(value: string) { + this.filter$.next(value); + } + onSelectProviderSchema(providerSchemaId: string) { this.draftForm.patchValue({ providerSchema: providerSchemaId, diff --git a/src/app/features/registries/components/review/review.component.html b/src/app/features/registries/components/review/review.component.html index 31c3bd704..6e7a84b81 100644 --- a/src/app/features/registries/components/review/review.component.html +++ b/src/app/features/registries/components/review/review.component.html @@ -4,7 +4,7 @@ {{ 'navigation.metadata' | translate }} {{ 'common.labels.title' | translate }} - {{ draftRegistration()?.title }} + {{ draftRegistration()?.title }} @if (!draftRegistration()?.title) { {{ 'common.labels.title' | translate }} {{ 'common.labels.noData' | translate }} @@ -15,7 +15,7 @@ {{ 'common.labels.title' | translate }} {{ 'common.labels.description' | translate }} - {{ draftRegistration()?.description }} + {{ draftRegistration()?.description }} @if (!draftRegistration()?.description) { {{ 'common.labels.noData' | translate }} @@ -44,7 +44,7 @@ {{ 'shared.license.title' | translate }} - {{ license()?.name }} + {{ license()?.name }} {{ license()!.text | interpolate: licenseOptionsRecord() }} @@ -62,7 +62,7 @@ {{ 'shared.license.title' | translate }} {{ 'shared.subjects.title' | translate }} @for (subject of subjects(); track subject.id) { - + } @if (!subjects().length) { @@ -75,7 +75,7 @@ {{ 'shared.subjects.title' | translate }} {{ 'shared.tags.title' | translate }} - + @for (tag of draftRegistration()?.tags; track tag) { } @@ -131,6 +131,7 @@ {{ section.title }} } @for (registration of submittedRegistrations(); track $index) { - + } @if (submittedRegistrationsTotalCount() > itemsPerPage) { this.navigateToJustificationPage())) - .subscribe(); - } - - onContinueUpdateRegistration({ id, unapproved }: { id: string; unapproved: boolean }): void { - this.actions - .getSchemaResponse(id) - .pipe( - tap(() => { - if (unapproved) { - this.navigateToJustificationReview(); - } else { - this.navigateToJustificationPage(); - } - }) - ) - .subscribe(); - } - - private navigateToJustificationPage(): void { - const revisionId = this.schemaResponse()?.id; - this.router.navigate([`/registries/revisions/${revisionId}/justification`]); - } - - private navigateToJustificationReview(): void { - const revisionId = this.schemaResponse()?.id; - this.router.navigate([`/registries/revisions/${revisionId}/review`]); - } } diff --git a/src/app/features/registries/store/handlers/projects.handlers.ts b/src/app/features/registries/store/handlers/projects.handlers.ts index 612749344..659769e57 100644 --- a/src/app/features/registries/store/handlers/projects.handlers.ts +++ b/src/app/features/registries/store/handlers/projects.handlers.ts @@ -11,21 +11,25 @@ import { REGISTRIES_STATE_DEFAULTS, RegistriesStateModel } from '../registries.m export class ProjectsHandlers { projectsService = inject(ProjectsService); - getProjects({ patchState }: StateContext, userId: string) { - // [NM] TODO: move this parameter to projects.service + getProjects(ctx: StateContext, userId: string, search: string) { const params: Record = { 'filter[current_user_permissions]': 'admin', }; - patchState({ + if (search) { + params['filter[title]'] = search; + } + const state = ctx.getState(); + ctx.patchState({ projects: { - ...REGISTRIES_STATE_DEFAULTS.projects, + data: state.projects.data, + error: null, isLoading: true, }, }); return this.projectsService.fetchProjects(userId, params).subscribe({ next: (projects: Project[]) => { - patchState({ + ctx.patchState({ projects: { data: projects, isLoading: false, @@ -34,7 +38,7 @@ export class ProjectsHandlers { }); }, error: (error) => { - patchState({ + ctx.patchState({ projects: { ...REGISTRIES_STATE_DEFAULTS.projects, isLoading: false, error }, }); }, diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts index bf5bf1e7a..79ef8dc4a 100644 --- a/src/app/features/registries/store/registries.actions.ts +++ b/src/app/features/registries/store/registries.actions.ts @@ -18,7 +18,10 @@ export class GetProviderSchemas { export class GetProjects { static readonly type = '[Registries] Get Projects'; - constructor(public userId: string) {} + constructor( + public userId: string, + public search: string + ) {} } export class CreateDraft { diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts index e0d11a646..3a00cc749 100644 --- a/src/app/features/registries/store/registries.state.ts +++ b/src/app/features/registries/store/registries.state.ts @@ -93,8 +93,8 @@ export class RegistriesState { } @Action(GetProjects) - getProjects(ctx: StateContext, { userId }: GetProjects) { - return this.projectsHandler.getProjects(ctx, userId); + getProjects(ctx: StateContext, { userId, search }: GetProjects) { + return this.projectsHandler.getProjects(ctx, userId, search); } @Action(FetchProjectChildren) diff --git a/src/app/shared/components/generic-filter/generic-filter.component.html b/src/app/shared/components/generic-filter/generic-filter.component.html index 9ab2cfbf5..ac27514e4 100644 --- a/src/app/shared/components/generic-filter/generic-filter.component.html +++ b/src/app/shared/components/generic-filter/generic-filter.component.html @@ -18,7 +18,7 @@ filter resetFilterOnHide="false" [virtualScroll]="true" - [virtualScrollItemSize]="35" + [virtualScrollItemSize]="25" [lazy]="true" scrollHeight="200px" [autoOptionFocus]="false" diff --git a/src/app/shared/components/license/license.component.html b/src/app/shared/components/license/license.component.html index c7ecc44a4..ecd763e4d 100644 --- a/src/app/shared/components/license/license.component.html +++ b/src/app/shared/components/license/license.component.html @@ -1,9 +1,8 @@ severity="danger" [label]="'common.buttons.delete' | translate" class="md:ml-auto" - (click)="deleteDraft.emit(registrationData().id)" + (onClick)="deleteDraft.emit(registrationData().id)" > } @else { @if (isApproved) { } @if (isInProgress || isUnapproved) { } diff --git a/src/app/shared/components/registration-card/registration-card.component.ts b/src/app/shared/components/registration-card/registration-card.component.ts index 733cb2197..498bab2e9 100644 --- a/src/app/shared/components/registration-card/registration-card.component.ts +++ b/src/app/shared/components/registration-card/registration-card.component.ts @@ -1,12 +1,17 @@ +import { createDispatchMap, select } from '@ngxs/store'; + import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; +import { tap } from 'rxjs'; + import { DatePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { RouterLink } from '@angular/router'; +import { ChangeDetectionStrategy, Component, inject, input, output } from '@angular/core'; +import { Router, RouterLink } from '@angular/router'; +import { CreateSchemaResponse, FetchAllSchemaResponses, RegistriesSelectors } from '@osf/features/registries/store'; import { RegistrationReviewStates, RegistryStatus, RevisionReviewStates } from '@osf/shared/enums'; import { RegistrationCard } from '@osf/shared/models'; @@ -37,8 +42,14 @@ export class RegistrationCardComponent { readonly isDraft = input(false); readonly registrationData = input.required(); readonly deleteDraft = output(); - readonly updateRegistration = output(); - readonly continueUpdate = output<{ id: string; unapproved: boolean }>(); + + private router = inject(Router); + schemaResponse = select(RegistriesSelectors.getSchemaResponse); + + actions = createDispatchMap({ + getSchemaResponse: FetchAllSchemaResponses, + createSchemaResponse: CreateSchemaResponse, + }); get isAccepted(): boolean { return this.registrationData().reviewsState === RegistrationReviewStates.Accepted; @@ -60,10 +71,36 @@ export class RegistrationCardComponent { return this.registrationData().revisionState === RevisionReviewStates.RevisionInProgress; } - continueUpdateHandler(): void { - this.continueUpdate.emit({ - id: this.registrationData().id, - unapproved: this.registrationData().revisionState === RevisionReviewStates.Unapproved, - }); + updateRegistration(id: string): void { + this.actions + .createSchemaResponse(id) + .pipe(tap(() => this.navigateToJustificationPage())) + .subscribe(); + } + + continueUpdateRegistration(id: string): void { + const unapproved = this.registrationData().revisionState === RevisionReviewStates.Unapproved; + this.actions + .getSchemaResponse(id) + .pipe( + tap(() => { + if (unapproved) { + this.navigateToJustificationReview(); + } else { + this.navigateToJustificationPage(); + } + }) + ) + .subscribe(); + } + + private navigateToJustificationPage(): void { + const revisionId = this.schemaResponse()?.id; + this.router.navigate([`/registries/revisions/${revisionId}/justification`]); + } + + private navigateToJustificationReview(): void { + const revisionId = this.schemaResponse()?.id; + this.router.navigate([`/registries/revisions/${revisionId}/review`]); } } 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 1042335c0..c11b83ec6 100644 --- a/src/app/shared/components/sub-header/sub-header.component.html +++ b/src/app/shared/components/sub-header/sub-header.component.html @@ -8,7 +8,7 @@ @if (isLoading()) { } @else { - + {{ title() }} @if (tooltip()) { { + if (this.text()) { + this.isTextExpanded.set(false); + } + }); + } + ngAfterViewInit() { this.checkTextOverflow(); } diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 8b54eee73..c53acf394 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -248,9 +248,9 @@ export class FilesService { .pipe(map((response) => MapFileCustomMetadata(response))); } - getFileRevisions(link: string, fileId: string): Observable { + getFileRevisions(link: string): Observable { return this.jsonApiService - .get(`${link}/${fileId}?revisions=`) + .get(`${link}?revisions=`) .pipe(map((response) => MapFileRevision(response.data))); }
{{ q.exampleText }}
@@ -14,6 +14,7 @@
{{ 'registries.new.steps.step2InfoText' | translate }}
{{ 'registries.new.steps.step3' | translate }}
{{ draftRegistration()?.title }}
{{ 'common.labels.title' | translate }}
{{ 'common.labels.noData' | translate }}
{{ draftRegistration()?.description }}