From 6d743e51a888fc5f08103637e3adcf8d75391897 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Mon, 14 Jul 2025 18:23:55 +0300 Subject: [PATCH 1/6] fix(project-overview-citations-and-collections): changed nullish coalescing operator to logical 'or' --- .../project-metadata-step.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/features/collections/components/add-to-collection/project-metadata-step/project-metadata-step.component.html b/src/app/features/collections/components/add-to-collection/project-metadata-step/project-metadata-step.component.html index 0f571c820..c3b64e3ef 100644 --- a/src/app/features/collections/components/add-to-collection/project-metadata-step/project-metadata-step.component.html +++ b/src/app/features/collections/components/add-to-collection/project-metadata-step/project-metadata-step.component.html @@ -16,13 +16,13 @@

{{ 'collections.addToCollection.projectMetadata' | translate }}

{{ 'collections.addToCollection.form.description' | translate }}

- {{ selectedProject()?.description ?? 'collections.addToCollection.noDescription' | translate }} + {{ selectedProject()?.description || 'collections.addToCollection.noDescription' | translate }}

{{ 'collections.addToCollection.form.license' | translate }}

- {{ projectLicense()?.name ?? 'collections.addToCollection.noLicense' | translate }} + {{ projectLicense()?.name || 'collections.addToCollection.noLicense' | translate }}

From b6560db3b40411dbede9ee550e1c4ca9accad4b7 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Tue, 15 Jul 2025 14:19:10 +0300 Subject: [PATCH 2/6] fix(project-overview-citations-and-collections): created citations service and store --- .../default-citation-titles.const.ts | 7 ++++ src/app/shared/constants/index.ts | 1 + src/app/shared/enums/citation-types.enum.ts | 5 +++ src/app/shared/enums/index.ts | 1 + src/app/shared/mappers/citations.mapper.ts | 16 ++++++++ src/app/shared/mappers/index.ts | 1 + .../default-citation-json-api.model.ts | 9 ++++ .../citations/default-citation.model.ts | 6 +++ src/app/shared/models/citations/index.ts | 2 + src/app/shared/models/index.ts | 1 + src/app/shared/services/citations.service.ts | 41 +++++++++++++++++++ .../stores/citations/citations.actions.ts | 3 ++ .../stores/citations/citations.model.ts | 6 +++ .../stores/citations/citations.selectors.ts | 16 ++++++++ .../stores/citations/citations.state.ts | 39 ++++++++++++++++++ src/app/shared/stores/citations/index.ts | 4 ++ src/app/shared/stores/index.ts | 1 + 17 files changed, 159 insertions(+) create mode 100644 src/app/shared/constants/default-citation-titles.const.ts create mode 100644 src/app/shared/enums/citation-types.enum.ts create mode 100644 src/app/shared/mappers/citations.mapper.ts create mode 100644 src/app/shared/models/citations/default-citation-json-api.model.ts create mode 100644 src/app/shared/models/citations/default-citation.model.ts create mode 100644 src/app/shared/models/citations/index.ts create mode 100644 src/app/shared/services/citations.service.ts create mode 100644 src/app/shared/stores/citations/citations.actions.ts create mode 100644 src/app/shared/stores/citations/citations.model.ts create mode 100644 src/app/shared/stores/citations/citations.selectors.ts create mode 100644 src/app/shared/stores/citations/citations.state.ts create mode 100644 src/app/shared/stores/citations/index.ts diff --git a/src/app/shared/constants/default-citation-titles.const.ts b/src/app/shared/constants/default-citation-titles.const.ts new file mode 100644 index 000000000..0f543dcc7 --- /dev/null +++ b/src/app/shared/constants/default-citation-titles.const.ts @@ -0,0 +1,7 @@ +import { CitationTypes } from '@shared/enums'; + +export const CITATION_TITLES: Record = { + [CitationTypes.APA]: 'APA', + [CitationTypes.MLA]: 'MLA', + [CitationTypes.CHICAGO]: 'Chicago', +}; diff --git a/src/app/shared/constants/index.ts b/src/app/shared/constants/index.ts index 9d3ae69f9..e7bf7be04 100644 --- a/src/app/shared/constants/index.ts +++ b/src/app/shared/constants/index.ts @@ -2,6 +2,7 @@ export * from './addon-terms.const'; export * from './addons-category-options.const'; export * from './addons-tab-options.const'; export * from './contributors'; +export * from './default-citation-titles.const'; export * from './filter-placeholders'; export * from './input-limits.const'; export * from './input-validation-messages.const'; diff --git a/src/app/shared/enums/citation-types.enum.ts b/src/app/shared/enums/citation-types.enum.ts new file mode 100644 index 000000000..f7e32943d --- /dev/null +++ b/src/app/shared/enums/citation-types.enum.ts @@ -0,0 +1,5 @@ +export enum CitationTypes { + APA = 'apa', + MLA = 'modern-language-association', + CHICAGO = 'chicago-author-date', +} diff --git a/src/app/shared/enums/index.ts b/src/app/shared/enums/index.ts index 64dbb2988..0d62c3107 100644 --- a/src/app/shared/enums/index.ts +++ b/src/app/shared/enums/index.ts @@ -3,6 +3,7 @@ export * from './addon-tab.enum'; export * from './addons-category.enum'; export * from './addons-credentials-format.enum'; export * from './breakpoint-queries.enum'; +export * from './citation-types.enum'; export * from './contributors'; export * from './create-component-form-controls.enum'; export * from './create-project-form-controls.enum'; diff --git a/src/app/shared/mappers/citations.mapper.ts b/src/app/shared/mappers/citations.mapper.ts new file mode 100644 index 000000000..283ff34c6 --- /dev/null +++ b/src/app/shared/mappers/citations.mapper.ts @@ -0,0 +1,16 @@ +import { CITATION_TITLES } from '@shared/constants'; +import { CitationTypes } from '@shared/enums'; +import { DefaultCitation, DefaultCitationJsonApi } from '@shared/models'; + +export class CitationsMapper { + static fromGetDefaultResponse(response: DefaultCitationJsonApi): DefaultCitation { + const citationId = response.data.id; + + return { + id: citationId, + type: response.data.type, + citation: response.data.attributes.citation, + title: CITATION_TITLES[citationId as CitationTypes], + }; + } +} diff --git a/src/app/shared/mappers/index.ts b/src/app/shared/mappers/index.ts index 41661151c..b3007c523 100644 --- a/src/app/shared/mappers/index.ts +++ b/src/app/shared/mappers/index.ts @@ -1,4 +1,5 @@ export * from './addon.mapper'; +export * from './citations.mapper'; export * from './contributors'; export * from './filters'; export * from './institutions'; diff --git a/src/app/shared/models/citations/default-citation-json-api.model.ts b/src/app/shared/models/citations/default-citation-json-api.model.ts new file mode 100644 index 000000000..2459fbe01 --- /dev/null +++ b/src/app/shared/models/citations/default-citation-json-api.model.ts @@ -0,0 +1,9 @@ +export interface DefaultCitationJsonApi { + data: { + id: string; + type: string; + attributes: { + citation: string; + }; + }; +} diff --git a/src/app/shared/models/citations/default-citation.model.ts b/src/app/shared/models/citations/default-citation.model.ts new file mode 100644 index 000000000..55725a0f2 --- /dev/null +++ b/src/app/shared/models/citations/default-citation.model.ts @@ -0,0 +1,6 @@ +export interface DefaultCitation { + id: string; + type: string; + title: string; + citation?: string; +} diff --git a/src/app/shared/models/citations/index.ts b/src/app/shared/models/citations/index.ts new file mode 100644 index 000000000..c38362d01 --- /dev/null +++ b/src/app/shared/models/citations/index.ts @@ -0,0 +1,2 @@ +export * from './default-citation.model'; +export * from './default-citation-json-api.model'; diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 9e24c8269..9c894a8ee 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -3,6 +3,7 @@ export * from './brand.json-api.model'; export * from './brand.model'; export * from './can-deactivate.interface'; export * from './charts'; +export * from './citations'; export * from './confirmation-options.model'; export * from './contributors'; export * from './create-component-form.model'; diff --git a/src/app/shared/services/citations.service.ts b/src/app/shared/services/citations.service.ts new file mode 100644 index 000000000..b1d00ba78 --- /dev/null +++ b/src/app/shared/services/citations.service.ts @@ -0,0 +1,41 @@ +import { map, Observable } from 'rxjs'; + +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; + +import { ResourceType } from '@shared/enums'; +import { CitationsMapper } from '@shared/mappers'; +import { DefaultCitation, DefaultCitationJsonApi } from '@shared/models'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class CitationsService { + private readonly http = inject(HttpClient); + + private readonly urlMap = new Map([ + [ResourceType.Project, 'nodes'], + [ResourceType.Registration, 'registrations'], + [ResourceType.Preprint, 'preprints'], + ]); + + getDefaultCitation(resourceType: ResourceType, resourceId: string, citationId: string): Observable { + const baseUrl = this.getBaseUrl(resourceType, resourceId); + return this.http + .get(`${baseUrl}/citation/${citationId}/`) + .pipe(map((response) => CitationsMapper.fromGetDefaultResponse(response))); + } + + private getBaseUrl(resourceType: ResourceType, resourceId: string): string { + const baseUrl = `${environment.apiUrl}`; + const resourcePath = this.urlMap.get(resourceType); + + if (!resourcePath) { + throw new Error(`Unsupported resource type: ${resourceType}`); + } + + return `${baseUrl}/${resourcePath}/${resourceId}/contributors`; + } +} diff --git a/src/app/shared/stores/citations/citations.actions.ts b/src/app/shared/stores/citations/citations.actions.ts new file mode 100644 index 000000000..b7590f419 --- /dev/null +++ b/src/app/shared/stores/citations/citations.actions.ts @@ -0,0 +1,3 @@ +export class GetDefaultCitations { + static readonly type = '[Citations] Get Default Citations'; +} diff --git a/src/app/shared/stores/citations/citations.model.ts b/src/app/shared/stores/citations/citations.model.ts new file mode 100644 index 000000000..6971c0cac --- /dev/null +++ b/src/app/shared/stores/citations/citations.model.ts @@ -0,0 +1,6 @@ +import { DefaultCitation } from '@shared/models'; +import { AsyncStateModel } from '@shared/models/store'; + +export interface CitationsStateModel { + defaultCitations: AsyncStateModel; +} diff --git a/src/app/shared/stores/citations/citations.selectors.ts b/src/app/shared/stores/citations/citations.selectors.ts new file mode 100644 index 000000000..704343058 --- /dev/null +++ b/src/app/shared/stores/citations/citations.selectors.ts @@ -0,0 +1,16 @@ +import { Selector } from '@ngxs/store'; + +import { CitationsStateModel } from './citations.model'; +import { CitationsState } from './citations.state'; + +export class CitationsSelectors { + @Selector([CitationsState]) + static getDefaultCitations(state: CitationsStateModel) { + return state.defaultCitations.data; + } + + @Selector([CitationsState]) + static getDefaultCitationsLoading(state: CitationsStateModel) { + return state.defaultCitations.isLoading; + } +} diff --git a/src/app/shared/stores/citations/citations.state.ts b/src/app/shared/stores/citations/citations.state.ts new file mode 100644 index 000000000..848ae9787 --- /dev/null +++ b/src/app/shared/stores/citations/citations.state.ts @@ -0,0 +1,39 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { inject, Injectable } from '@angular/core'; + +import { CitationsService } from '@shared/services/citations.service'; + +import { GetDefaultCitations } from './citations.actions'; +import { CitationsStateModel } from './citations.model'; + +const CITATIONS_DEFAULTS: CitationsStateModel = { + defaultCitations: { + data: [], + isLoading: false, + isSubmitting: false, + error: null, + }, +}; + +@State({ + name: 'citations', + defaults: CITATIONS_DEFAULTS, +}) +@Injectable() +export class CitationsState { + citationsService = inject(CitationsService); + + @Action(GetDefaultCitations) + getDefaultCitation(ctx: StateContext) { + const state = ctx.getState(); + ctx.patchState({ + defaultCitations: { + ...state.defaultCitations, + isLoading: true, + }, + }); + + // TODO + } +} diff --git a/src/app/shared/stores/citations/index.ts b/src/app/shared/stores/citations/index.ts new file mode 100644 index 000000000..59e249712 --- /dev/null +++ b/src/app/shared/stores/citations/index.ts @@ -0,0 +1,4 @@ +export * from './citations.actions'; +export * from './citations.model'; +export * from './citations.selectors'; +export * from './citations.state'; diff --git a/src/app/shared/stores/index.ts b/src/app/shared/stores/index.ts index c1cd0cb36..4b6925993 100644 --- a/src/app/shared/stores/index.ts +++ b/src/app/shared/stores/index.ts @@ -1,5 +1,6 @@ export * from './addons'; export * from './bookmarks'; +export * from './citations'; export * from './contributors'; export * from './institutions'; export * from './institutions-search'; From e5c74b4b0fe68e933d9564ea58b7407b44ee5ea7 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Tue, 15 Jul 2025 17:13:48 +0300 Subject: [PATCH 3/6] fix(project-overview-citations-and-collections): fixed add-to-collection page submit bug --- ...ollection-confirmation-dialog.component.ts | 8 ++++--- .../add-to-collection.component.ts | 24 +++++++++++++++---- .../overview/store/project-overview.state.ts | 16 ++++++------- .../truncated-text.component.scss | 1 + src/assets/i18n/en.json | 2 +- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.ts b/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.ts index c256076a4..0a43ed039 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.ts +++ b/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.ts @@ -33,14 +33,16 @@ export class AddToCollectionConfirmationDialogComponent { }); protected handleAddToCollectionConfirm(): void { - const project = this.config.data; - if (!project) return; + const payload = this.config.data.payload; + const project = this.config.data.project; + + if (!payload || !project) return; this.isSubmitting.set(true); const updatePublicStatus$ = project.isPublic ? of(null) : this.actions.updateProjectPublicStatus(project.id, true); - const createSubmission$ = this.actions.createCollectionSubmission(project); + const createSubmission$ = this.actions.createCollectionSubmission(payload); forkJoin({ publicStatusUpdate: updatePublicStatus$, 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 d771da099..b7820770a 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 @@ -73,7 +73,7 @@ export class AddToCollectionComponent implements CanDeactivateComponent { protected selectedProject = select(ProjectsSelectors.getSelectedProject); protected currentUser = select(UserSelectors.getCurrentUser); protected providerId = signal(''); - protected isSubmitted = signal(false); + protected allowNavigation = signal(false); protected projectMetadataSaved = signal(false); protected projectContributorsSaved = signal(false); protected collectionMetadataSaved = signal(false); @@ -99,6 +99,7 @@ export class AddToCollectionComponent implements CanDeactivateComponent { handleProjectSelected(): void { this.projectContributorsSaved.set(false); this.projectMetadataSaved.set(false); + this.allowNavigation.set(false); } handleChangeStep(step: number): void { @@ -127,7 +128,6 @@ export class AddToCollectionComponent implements CanDeactivateComponent { collectionMetadata: this.collectionMetadataForm.value || {}, userId: this.currentUser()?.id || '', }; - this.isSubmitted.set(true); const dialogRef = this.dialogService.open(AddToCollectionConfirmationDialogComponent, { width: '500px', @@ -136,12 +136,12 @@ export class AddToCollectionComponent implements CanDeactivateComponent { closeOnEscape: true, modal: true, closable: true, - data: payload, + data: { payload, project: this.selectedProject() }, }); dialogRef.onClose.subscribe((result) => { if (result) { - this.isSubmitted.set(false); + this.allowNavigation.set(true); this.router.navigate(['/my-projects', this.selectedProject()?.id, 'overview']); } }); @@ -162,6 +162,7 @@ export class AddToCollectionComponent implements CanDeactivateComponent { effect(() => { this.destroyRef.onDestroy(() => { this.actions.clearAddToCollectionState(); + this.allowNavigation.set(false); }); }); } @@ -173,6 +174,19 @@ export class AddToCollectionComponent implements CanDeactivateComponent { } canDeactivate(): Observable | boolean { - return this.isSubmitted(); + if (this.allowNavigation()) { + return true; + } + + return !this.hasUnsavedChanges(); + } + + private hasUnsavedChanges(): boolean { + return ( + !!this.selectedProject() || + this.projectMetadataSaved() || + this.projectContributorsSaved() || + this.collectionMetadataSaved() + ); } } diff --git a/src/app/features/project/overview/store/project-overview.state.ts b/src/app/features/project/overview/store/project-overview.state.ts index dfa899118..fcb71f4e1 100644 --- a/src/app/features/project/overview/store/project-overview.state.ts +++ b/src/app/features/project/overview/store/project-overview.state.ts @@ -89,9 +89,10 @@ export class ProjectOverviewState { isSubmitting: true, }, }); - - return this.projectOverviewService.updateProjectPublicStatus(action.projectId, action.isPublic).pipe( - tap(() => { + } + return this.projectOverviewService.updateProjectPublicStatus(action.projectId, action.isPublic).pipe( + tap(() => { + if (state.project.data) { ctx.patchState({ project: { ...state.project, @@ -102,11 +103,10 @@ export class ProjectOverviewState { isSubmitting: false, }, }); - }), - catchError((error) => this.handleError(ctx, 'project', error)) - ); - } - return; + } + }), + catchError((error) => this.handleError(ctx, 'project', error)) + ); } @Action(ForkResource) diff --git a/src/app/shared/components/truncated-text/truncated-text.component.scss b/src/app/shared/components/truncated-text/truncated-text.component.scss index a590d92fa..39ceedd41 100644 --- a/src/app/shared/components/truncated-text/truncated-text.component.scss +++ b/src/app/shared/components/truncated-text/truncated-text.component.scss @@ -9,6 +9,7 @@ line-height: 1.7; -webkit-line-clamp: var(--line-clamp); -webkit-box-orient: vertical; + white-space: pre-line; &.expanded { display: block; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 0cbdc1a69..239258096 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -95,7 +95,7 @@ "message": "Are you sure you want to proceed?" }, "discardChanges": { - "header": "You Might Loose Unsaved Changes", + "header": "You Might Lose Unsaved Changes", "message": "Are you sure you want to proceed?" }, "links": { From d361803e953f39e9a868a56c7ab9a46f95550384 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Thu, 17 Jul 2025 13:14:50 +0300 Subject: [PATCH 4/6] fix(project-overview-citations-and-collections): added resource-citations component --- .../mappers/project-overview.mapper.ts | 1 + .../models/project-overview.models.ts | 2 + src/app/features/project/project.routes.ts | 3 +- .../models/registry-overview.models.ts | 1 + .../resource-citations.component.html | 50 ++++++++ .../resource-citations.component.scss | 0 .../resource-citations.component.spec.ts | 22 ++++ .../resource-citations.component.ts | 113 ++++++++++++++++++ .../resource-metadata.component.html | 2 + .../resource-metadata.component.ts | 3 +- src/app/shared/mappers/citations.mapper.ts | 13 +- .../mappers/resource-overview.mappers.ts | 2 + .../citation-style-json-api.model.ts | 12 ++ .../models/citations/citation-style.model.ts | 8 ++ src/app/shared/models/citations/index.ts | 2 + .../shared/models/resource-overview.model.ts | 1 + src/app/shared/services/citations.service.ts | 26 +++- .../stores/citations/citations.actions.ts | 13 ++ .../stores/citations/citations.model.ts | 3 +- .../stores/citations/citations.selectors.ts | 10 ++ .../stores/citations/citations.state.ts | 59 ++++++++- src/assets/i18n/en.json | 7 +- src/assets/styles/overrides/accordion.scss | 3 +- 23 files changed, 342 insertions(+), 14 deletions(-) create mode 100644 src/app/shared/components/resource-citations/resource-citations.component.html create mode 100644 src/app/shared/components/resource-citations/resource-citations.component.scss create mode 100644 src/app/shared/components/resource-citations/resource-citations.component.spec.ts create mode 100644 src/app/shared/components/resource-citations/resource-citations.component.ts create mode 100644 src/app/shared/models/citations/citation-style-json-api.model.ts create mode 100644 src/app/shared/models/citations/citation-style.model.ts diff --git a/src/app/features/project/overview/mappers/project-overview.mapper.ts b/src/app/features/project/overview/mappers/project-overview.mapper.ts index b840f42b1..ecb4c9e26 100644 --- a/src/app/features/project/overview/mappers/project-overview.mapper.ts +++ b/src/app/features/project/overview/mappers/project-overview.mapper.ts @@ -37,6 +37,7 @@ export class ProjectOverviewMapper { currentUserIsContributor: response.attributes.current_user_is_contributor, currentUserIsContributorOrGroupMember: response.attributes.current_user_is_contributor_or_group_member, wikiEnabled: response.attributes.wiki_enabled, + customCitation: response.attributes.custom_citation, subjects: response.attributes.subjects.map((subjectArray) => subjectArray[0]), contributors: response.embeds.bibliographic_contributors.data.map((contributor) => ({ id: contributor.embeds.users.data.id, diff --git a/src/app/features/project/overview/models/project-overview.models.ts b/src/app/features/project/overview/models/project-overview.models.ts index ce507c7ac..1fc1f215b 100644 --- a/src/app/features/project/overview/models/project-overview.models.ts +++ b/src/app/features/project/overview/models/project-overview.models.ts @@ -101,6 +101,7 @@ export interface ProjectOverview { wikiEnabled: boolean; subjects: ProjectOverviewSubject[]; contributors: ProjectOverviewContributor[]; + customCitation: string | null; region?: { id: string; type: string; @@ -154,6 +155,7 @@ export interface ProjectOverviewGetResponseJsoApi { current_user_is_contributor_or_group_member: boolean; wiki_enabled: boolean; subjects: ProjectOverviewSubject[][]; + custom_citation: string | null; }; embeds: { affiliated_institutions: { diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index 94cbef903..16f856e25 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -3,7 +3,7 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; import { ResourceType } from '@osf/shared/enums'; -import { ContributorsState, SubjectsState, ViewOnlyLinkState } from '@osf/shared/stores'; +import { CitationsState, ContributorsState, SubjectsState, ViewOnlyLinkState } from '@osf/shared/stores'; import { AnalyticsState } from './analytics/store'; import { ProjectFilesState } from './files/store'; @@ -23,6 +23,7 @@ export const projectRoutes: Routes = [ path: 'overview', loadComponent: () => import('../project/overview/project-overview.component').then((mod) => mod.ProjectOverviewComponent), + providers: [provideStates([CitationsState])], }, { path: 'metadata', diff --git a/src/app/features/registry/models/registry-overview.models.ts b/src/app/features/registry/models/registry-overview.models.ts index 91cdc6d53..ee4fd6666 100644 --- a/src/app/features/registry/models/registry-overview.models.ts +++ b/src/app/features/registry/models/registry-overview.models.ts @@ -46,6 +46,7 @@ export interface RegistryOverview { type: string; }; subjects?: RegistrySubject[]; + customCitation: string; hasData: boolean; hasAnalyticCode: boolean; hasMaterials: boolean; diff --git a/src/app/shared/components/resource-citations/resource-citations.component.html b/src/app/shared/components/resource-citations/resource-citations.component.html new file mode 100644 index 000000000..ba0dc281b --- /dev/null +++ b/src/app/shared/components/resource-citations/resource-citations.component.html @@ -0,0 +1,50 @@ +@let resource = currentResource(); + +@if (resource) { + +} diff --git a/src/app/shared/components/resource-citations/resource-citations.component.scss b/src/app/shared/components/resource-citations/resource-citations.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/resource-citations/resource-citations.component.spec.ts b/src/app/shared/components/resource-citations/resource-citations.component.spec.ts new file mode 100644 index 000000000..cd842b506 --- /dev/null +++ b/src/app/shared/components/resource-citations/resource-citations.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ResourceCitationsComponent } from './resource-citations.component'; + +describe('ResourceCitationsComponent', () => { + let component: ResourceCitationsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ResourceCitationsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ResourceCitationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/resource-citations/resource-citations.component.ts b/src/app/shared/components/resource-citations/resource-citations.component.ts new file mode 100644 index 000000000..7e855147e --- /dev/null +++ b/src/app/shared/components/resource-citations/resource-citations.component.ts @@ -0,0 +1,113 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + +import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'primeng/accordion'; +import { Divider } from 'primeng/divider'; +import { Select, SelectChangeEvent, SelectFilterEvent } from 'primeng/select'; + +import { debounceTime, distinctUntilChanged, Subject, takeUntil } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, computed, DestroyRef, effect, inject, input, signal } from '@angular/core'; + +import { ResourceType } from '@shared/enums'; +import { CitationStyle, CustomOption, ResourceOverview } from '@shared/models'; +import { CitationsSelectors, GetCitationStyles, GetDefaultCitations } from '@shared/stores'; + +@Component({ + selector: 'osf-resource-citations', + imports: [Accordion, AccordionPanel, AccordionHeader, TranslatePipe, AccordionContent, Divider, Select], + templateUrl: './resource-citations.component.html', + styleUrl: './resource-citations.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ResourceCitationsComponent { + private readonly destroyRef = inject(DestroyRef); + private readonly translateService = inject(TranslateService); + currentResource = input.required(); + private readonly destroy$ = new Subject(); + private readonly filterSubject = new Subject(); + protected defaultCitations = select(CitationsSelectors.getDefaultCitations); + protected isCitationsLoading = select(CitationsSelectors.getDefaultCitationsLoading); + protected citationStyles = select(CitationsSelectors.getCitationStyles); + protected isCitationStylesLoading = select(CitationsSelectors.getCitationStylesLoading); + + protected citationStylesOptions = signal[]>([]); + protected selectedCitationStyle = signal(null); + protected isEditMode = signal(false); + protected filterMessage = computed(() => { + const isLoading = this.isCitationStylesLoading(); + return isLoading + ? this.translateService.instant('project.overview.metadata.citationLoadingPlaceholder') + : this.translateService.instant('project.overview.metadata.noCitationStylesFound'); + }); + + protected actions = createDispatchMap({ + getDefaultCitations: GetDefaultCitations, + getCitationStyles: GetCitationStyles, + }); + + protected readonly resourceTypeMap = new Map([ + ['nodes', ResourceType.Project], + ['registrations', ResourceType.Registration], + ['preprints', ResourceType.Preprint], + ]); + + constructor() { + this.setupFilterDebounce(); + this.setupDefaultCitationsEffect(); + this.setupCitationStylesEffect(); + this.setupDestroyEffect(); + } + + setupDefaultCitationsEffect(): void { + effect(() => { + const resource = this.currentResource(); + + if (resource) { + const resourceType = this.resourceTypeMap.get(resource.type)!; + this.actions.getDefaultCitations(resourceType, resource.id); + } + }); + } + + handleCitationStyleFilterSearch(event: SelectFilterEvent) { + event.originalEvent.preventDefault(); + this.filterSubject.next(event.filter); + } + + handleCitationStyleChange(event: SelectChangeEvent) { + this.selectedCitationStyle.set(event.value); + // TODO: Implement citation generation with selected style + } + + toggleEditMode() { + this.isEditMode.set(!this.isEditMode()); + } + + private setupFilterDebounce(): void { + this.filterSubject + .pipe(debounceTime(300), distinctUntilChanged(), takeUntil(this.destroy$)) + .subscribe((filterValue) => { + this.actions.getCitationStyles(filterValue); + }); + } + + private setupCitationStylesEffect(): void { + effect(() => { + const styles = this.citationStyles(); + const options = styles.map((style: CitationStyle) => ({ + label: style.title, + value: style, + })); + this.citationStylesOptions.set(options); + }); + } + + private setupDestroyEffect(): void { + this.destroyRef.onDestroy(() => { + this.destroy$.next(); + this.destroy$.complete(); + }); + } +} 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 9a6a93e23..b11f7569d 100644 --- a/src/app/shared/components/resource-metadata/resource-metadata.component.html +++ b/src/app/shared/components/resource-metadata/resource-metadata.component.html @@ -163,5 +163,7 @@

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

{{ 'project.overview.metadata.noTags' | 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 6b02fa95f..d76d14d57 100644 --- a/src/app/shared/components/resource-metadata/resource-metadata.component.ts +++ b/src/app/shared/components/resource-metadata/resource-metadata.component.ts @@ -7,13 +7,14 @@ import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, input } from '@angular/core'; import { RouterLink } from '@angular/router'; +import { ResourceCitationsComponent } from '@shared/components/resource-citations/resource-citations.component'; import { TruncatedTextComponent } from '@shared/components/truncated-text/truncated-text.component'; import { OsfResourceTypes } from '@shared/constants'; import { ResourceOverview } from '@shared/models'; @Component({ selector: 'osf-resource-metadata', - imports: [Button, TranslatePipe, TruncatedTextComponent, RouterLink, Tag, DatePipe], + imports: [Button, TranslatePipe, TruncatedTextComponent, RouterLink, Tag, DatePipe, ResourceCitationsComponent], templateUrl: './resource-metadata.component.html', styleUrl: './resource-metadata.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/shared/mappers/citations.mapper.ts b/src/app/shared/mappers/citations.mapper.ts index 283ff34c6..f01f18620 100644 --- a/src/app/shared/mappers/citations.mapper.ts +++ b/src/app/shared/mappers/citations.mapper.ts @@ -1,6 +1,6 @@ import { CITATION_TITLES } from '@shared/constants'; import { CitationTypes } from '@shared/enums'; -import { DefaultCitation, DefaultCitationJsonApi } from '@shared/models'; +import { CitationStyle, CitationStylesJsonApiResponse, DefaultCitation, DefaultCitationJsonApi } from '@shared/models'; export class CitationsMapper { static fromGetDefaultResponse(response: DefaultCitationJsonApi): DefaultCitation { @@ -13,4 +13,15 @@ export class CitationsMapper { title: CITATION_TITLES[citationId as CitationTypes], }; } + + static fromGetCitationStylesResponse(response: CitationStylesJsonApiResponse): CitationStyle[] { + return response.styles.map((style) => ({ + id: style.id, + title: style.title, + shortTitle: style.short_title, + summary: style.summary, + hasBibliography: style.has_bibliography, + parentStyle: style.parent_style, + })); + } } diff --git a/src/app/shared/mappers/resource-overview.mappers.ts b/src/app/shared/mappers/resource-overview.mappers.ts index 24dc74f26..5daedf955 100644 --- a/src/app/shared/mappers/resource-overview.mappers.ts +++ b/src/app/shared/mappers/resource-overview.mappers.ts @@ -31,6 +31,7 @@ export function MapProjectOverview(project: ProjectOverview): ResourceOverview { wikiEnabled: project.wikiEnabled, subjects: project.subjects?.filter(Boolean) || [], contributors: project.contributors?.filter(Boolean) || [], + customCitation: project.customCitation || null, region: project.region || undefined, affiliatedInstitutions: project.affiliatedInstitutions?.filter(Boolean) || undefined, forksCount: project.forksCount || 0, @@ -73,6 +74,7 @@ export function MapRegistryOverview( region: registry.region || undefined, forksCount: registry.forksCount, subjects: subjects, + customCitation: registry.customCitation || null, affiliatedInstitutions: institutions, associatedProjectId: registry.associatedProjectId, }; diff --git a/src/app/shared/models/citations/citation-style-json-api.model.ts b/src/app/shared/models/citations/citation-style-json-api.model.ts new file mode 100644 index 000000000..901cea5d7 --- /dev/null +++ b/src/app/shared/models/citations/citation-style-json-api.model.ts @@ -0,0 +1,12 @@ +export interface CitationStyleJsonApi { + id: string; + title: string; + short_title: string; + summary: string | null; + has_bibliography: boolean; + parent_style: string; +} + +export interface CitationStylesJsonApiResponse { + styles: CitationStyleJsonApi[]; +} diff --git a/src/app/shared/models/citations/citation-style.model.ts b/src/app/shared/models/citations/citation-style.model.ts new file mode 100644 index 000000000..563bedb4f --- /dev/null +++ b/src/app/shared/models/citations/citation-style.model.ts @@ -0,0 +1,8 @@ +export interface CitationStyle { + id: string; + title: string; + shortTitle: string; + summary: string | null; + hasBibliography: boolean; + parentStyle: string; +} diff --git a/src/app/shared/models/citations/index.ts b/src/app/shared/models/citations/index.ts index c38362d01..c215d4d31 100644 --- a/src/app/shared/models/citations/index.ts +++ b/src/app/shared/models/citations/index.ts @@ -1,2 +1,4 @@ +export * from './citation-style.model'; +export * from './citation-style-json-api.model'; export * from './default-citation.model'; export * from './default-citation-json-api.model'; diff --git a/src/app/shared/models/resource-overview.model.ts b/src/app/shared/models/resource-overview.model.ts index 77caa9968..ad81f1de9 100644 --- a/src/app/shared/models/resource-overview.model.ts +++ b/src/app/shared/models/resource-overview.model.ts @@ -54,6 +54,7 @@ export interface ResourceOverview { wikiEnabled: boolean; subjects: RegistrySubject[]; contributors: ProjectOverviewContributor[]; + customCitation: string | null; region?: { id: string; type: string; diff --git a/src/app/shared/services/citations.service.ts b/src/app/shared/services/citations.service.ts index b1d00ba78..9bef98a35 100644 --- a/src/app/shared/services/citations.service.ts +++ b/src/app/shared/services/citations.service.ts @@ -1,11 +1,12 @@ import { map, Observable } from 'rxjs'; -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; +import { JsonApiService } from '@core/services'; import { ResourceType } from '@shared/enums'; import { CitationsMapper } from '@shared/mappers'; -import { DefaultCitation, DefaultCitationJsonApi } from '@shared/models'; +import { CitationStylesJsonApiResponse, DefaultCitation, DefaultCitationJsonApi } from '@shared/models'; import { environment } from 'src/environments/environment'; @@ -14,6 +15,7 @@ import { environment } from 'src/environments/environment'; }) export class CitationsService { private readonly http = inject(HttpClient); + private readonly jsonApiService = inject(JsonApiService); private readonly urlMap = new Map([ [ResourceType.Project, 'nodes'], @@ -21,13 +23,27 @@ export class CitationsService { [ResourceType.Preprint, 'preprints'], ]); - getDefaultCitation(resourceType: ResourceType, resourceId: string, citationId: string): Observable { + fetchDefaultCitation( + resourceType: ResourceType, + resourceId: string, + citationId: string + ): Observable { const baseUrl = this.getBaseUrl(resourceType, resourceId); return this.http - .get(`${baseUrl}/citation/${citationId}/`) + .get(`${baseUrl}/${citationId}/`) .pipe(map((response) => CitationsMapper.fromGetDefaultResponse(response))); } + fetchCitationStyles(searchQuery?: string) { + const baseUrl = environment.apiUrl; + + const params = new HttpParams().set('filter[title,short_title]', searchQuery || '').set('page[size]', '100'); + + return this.http + .get(`${baseUrl}/citations/styles`, { params }) + .pipe(map((response) => CitationsMapper.fromGetCitationStylesResponse(response))); + } + private getBaseUrl(resourceType: ResourceType, resourceId: string): string { const baseUrl = `${environment.apiUrl}`; const resourcePath = this.urlMap.get(resourceType); @@ -36,6 +52,6 @@ export class CitationsService { throw new Error(`Unsupported resource type: ${resourceType}`); } - return `${baseUrl}/${resourcePath}/${resourceId}/contributors`; + return `${baseUrl}/${resourcePath}/${resourceId}/citation`; } } diff --git a/src/app/shared/stores/citations/citations.actions.ts b/src/app/shared/stores/citations/citations.actions.ts index b7590f419..c11cf0d5f 100644 --- a/src/app/shared/stores/citations/citations.actions.ts +++ b/src/app/shared/stores/citations/citations.actions.ts @@ -1,3 +1,16 @@ +import { ResourceType } from '@shared/enums'; + export class GetDefaultCitations { static readonly type = '[Citations] Get Default Citations'; + + constructor( + public resourceType: ResourceType, + public resourceId: string + ) {} +} + +export class GetCitationStyles { + static readonly type = '[Citations] Get Citation Styles'; + + constructor(public searchQuery?: string) {} } diff --git a/src/app/shared/stores/citations/citations.model.ts b/src/app/shared/stores/citations/citations.model.ts index 6971c0cac..24858b01c 100644 --- a/src/app/shared/stores/citations/citations.model.ts +++ b/src/app/shared/stores/citations/citations.model.ts @@ -1,6 +1,7 @@ -import { DefaultCitation } from '@shared/models'; +import { CitationStyle, DefaultCitation } from '@shared/models'; import { AsyncStateModel } from '@shared/models/store'; export interface CitationsStateModel { defaultCitations: AsyncStateModel; + citationStyles: AsyncStateModel; } diff --git a/src/app/shared/stores/citations/citations.selectors.ts b/src/app/shared/stores/citations/citations.selectors.ts index 704343058..fa0c0774d 100644 --- a/src/app/shared/stores/citations/citations.selectors.ts +++ b/src/app/shared/stores/citations/citations.selectors.ts @@ -13,4 +13,14 @@ export class CitationsSelectors { static getDefaultCitationsLoading(state: CitationsStateModel) { return state.defaultCitations.isLoading; } + + @Selector([CitationsState]) + static getCitationStyles(state: CitationsStateModel) { + return state.citationStyles.data; + } + + @Selector([CitationsState]) + static getCitationStylesLoading(state: CitationsStateModel) { + return state.citationStyles.isLoading; + } } diff --git a/src/app/shared/stores/citations/citations.state.ts b/src/app/shared/stores/citations/citations.state.ts index 848ae9787..985b2f629 100644 --- a/src/app/shared/stores/citations/citations.state.ts +++ b/src/app/shared/stores/citations/citations.state.ts @@ -1,10 +1,14 @@ import { Action, State, StateContext } from '@ngxs/store'; +import { catchError, forkJoin, tap } from 'rxjs'; + import { inject, Injectable } from '@angular/core'; +import { handleSectionError } from '@core/handlers'; +import { CitationTypes } from '@shared/enums'; import { CitationsService } from '@shared/services/citations.service'; -import { GetDefaultCitations } from './citations.actions'; +import { GetCitationStyles, GetDefaultCitations } from './citations.actions'; import { CitationsStateModel } from './citations.model'; const CITATIONS_DEFAULTS: CitationsStateModel = { @@ -14,6 +18,12 @@ const CITATIONS_DEFAULTS: CitationsStateModel = { isSubmitting: false, error: null, }, + citationStyles: { + data: [], + isLoading: false, + isSubmitting: false, + error: null, + }, }; @State({ @@ -25,15 +35,58 @@ export class CitationsState { citationsService = inject(CitationsService); @Action(GetDefaultCitations) - getDefaultCitation(ctx: StateContext) { + getDefaultCitation(ctx: StateContext, action: GetDefaultCitations) { const state = ctx.getState(); ctx.patchState({ defaultCitations: { ...state.defaultCitations, isLoading: true, + error: null, + }, + }); + + const citationRequests = Object.values(CitationTypes).map((citationType) => + this.citationsService.fetchDefaultCitation(action.resourceType, action.resourceId, citationType) + ); + + return forkJoin(citationRequests).pipe( + tap((citations) => { + ctx.patchState({ + defaultCitations: { + data: citations, + isLoading: false, + isSubmitting: false, + error: null, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'defaultCitations', error)) + ); + } + + @Action(GetCitationStyles) + getCitationStyles(ctx: StateContext, action: GetCitationStyles) { + const state = ctx.getState(); + ctx.patchState({ + citationStyles: { + ...state.citationStyles, + isLoading: true, + error: null, }, }); - // TODO + return this.citationsService.fetchCitationStyles(action.searchQuery).pipe( + tap((citationStyles) => { + ctx.patchState({ + citationStyles: { + data: citationStyles, + isLoading: false, + isSubmitting: false, + error: null, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'citationStyles', error)) + ); } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 239258096..8450f8e64 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -564,6 +564,11 @@ "publication": "Publication DOI", "subjects": "Subjects", "tags": "Tags", + "citation": "Citation", + "getMoreCitations": "Get More Citations", + "citationInputPlaceholder": "Select citation style or start typing", + "citationLoadingPlaceholder": "Loading options...", + "noCitationStylesFound": "No results found", "affiliatedInstitutions": "Affiliated Institutions", "noDescription": "No description", "noLicense": "No License", @@ -2120,7 +2125,7 @@ "publicRegistrations": "Public Registrations" }, "institutionUsers": { - "allDepartments": "All departments", + "allDepartments": "All departments", "lastLogin": "Last Login", "lastActive": "Last Active", "accountCreated": "Account Created", diff --git a/src/assets/styles/overrides/accordion.scss b/src/assets/styles/overrides/accordion.scss index da0ce6333..221e87564 100644 --- a/src/assets/styles/overrides/accordion.scss +++ b/src/assets/styles/overrides/accordion.scss @@ -15,7 +15,8 @@ } .resource, -.license { +.license, +.metadata-accordion { .p-accordion { --p-accordion-panel-border-width: 0; From 111fc886b76d9a98a65687aa3c2ee019bc6f93c5 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Fri, 18 Jul 2025 14:01:57 +0300 Subject: [PATCH 5/6] fix(project-overview-citations): added update/delete for custom citations --- .../overview/project-overview.component.html | 5 +- .../overview/project-overview.component.ts | 6 + .../store/project-overview.actions.ts | 6 + .../overview/store/project-overview.state.ts | 15 ++ .../registry-overview.component.html | 5 +- .../registry-overview.component.ts | 6 + .../registry-overview.actions.ts | 6 + .../registry-overview.state.ts | 15 ++ .../resource-citations.component.html | 107 ++++++--- .../resource-citations.component.ts | 225 ++++++++++++++++-- .../resource-metadata.component.html | 5 +- .../resource-metadata.component.ts | 7 +- src/app/shared/mappers/citations.mapper.ts | 51 +++- .../citation-style-json-api.model.ts | 17 +- .../models/citations/citation-style.model.ts | 5 +- .../custom-citation-payload-json-api.model.ts | 7 + .../custom-citation-payload.model.ts | 5 + .../default-citation-json-api.model.ts | 10 +- src/app/shared/models/citations/index.ts | 4 + .../styled-citation-json-api.model.ts | 7 + .../models/citations/styled-citation.model.ts | 5 + src/app/shared/services/citations.service.ts | 67 +++--- .../stores/citations/citations.actions.ts | 20 +- .../stores/citations/citations.model.ts | 4 +- .../stores/citations/citations.selectors.ts | 20 ++ .../stores/citations/citations.state.ts | 67 +++++- src/assets/i18n/en.json | 5 +- 27 files changed, 585 insertions(+), 117 deletions(-) create mode 100644 src/app/shared/models/citations/custom-citation-payload-json-api.model.ts create mode 100644 src/app/shared/models/citations/custom-citation-payload.model.ts create mode 100644 src/app/shared/models/citations/styled-citation-json-api.model.ts create mode 100644 src/app/shared/models/citations/styled-citation.model.ts diff --git a/src/app/features/project/overview/project-overview.component.html b/src/app/features/project/overview/project-overview.component.html index 50413a0de..4175b11b2 100644 --- a/src/app/features/project/overview/project-overview.component.html +++ b/src/app/features/project/overview/project-overview.component.html @@ -18,7 +18,10 @@
- +
} @else { diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index fdfed3824..310770066 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -30,6 +30,7 @@ import { GetLinkedProjects, GetProjectById, ProjectOverviewSelectors, + SetProjectCustomCitation, } from './store'; @Component({ @@ -65,6 +66,7 @@ export class ProjectOverviewComponent implements OnInit { getHomeWiki: GetHomeWiki, getComponents: GetComponents, getLinkedProjects: GetLinkedProjects, + setProjectCustomCitation: SetProjectCustomCitation, clearProjectOverview: ClearProjectOverview, clearWiki: ClearWiki, clearCollections: ClearCollections, @@ -97,6 +99,10 @@ export class ProjectOverviewComponent implements OnInit { this.setupCleanup(); } + onCustomCitationUpdated(citation: string): void { + this.actions.setProjectCustomCitation(citation); + } + ngOnInit(): void { const projectId = this.route.parent?.snapshot.params['id']; if (projectId) { diff --git a/src/app/features/project/overview/store/project-overview.actions.ts b/src/app/features/project/overview/store/project-overview.actions.ts index c7a5fd259..2c5cabe77 100644 --- a/src/app/features/project/overview/store/project-overview.actions.ts +++ b/src/app/features/project/overview/store/project-overview.actions.ts @@ -15,6 +15,12 @@ export class UpdateProjectPublicStatus { ) {} } +export class SetProjectCustomCitation { + static readonly type = '[Project Overview] Set Project Custom Citation'; + + constructor(public citation: string) {} +} + export class ForkResource { static readonly type = '[Project Overview] Fork Resource'; diff --git a/src/app/features/project/overview/store/project-overview.state.ts b/src/app/features/project/overview/store/project-overview.state.ts index fcb71f4e1..0d68ea875 100644 --- a/src/app/features/project/overview/store/project-overview.state.ts +++ b/src/app/features/project/overview/store/project-overview.state.ts @@ -17,6 +17,7 @@ import { GetComponents, GetLinkedProjects, GetProjectById, + SetProjectCustomCitation, UpdateProjectPublicStatus, } from './project-overview.actions'; import { ProjectOverviewStateModel } from './project-overview.model'; @@ -109,6 +110,20 @@ export class ProjectOverviewState { ); } + @Action(SetProjectCustomCitation) + setProjectCustomCitation(ctx: StateContext, action: SetProjectCustomCitation) { + const state = ctx.getState(); + ctx.patchState({ + project: { + ...state.project, + data: { + ...state.project.data!, + customCitation: action.citation, + }, + }, + }); + } + @Action(ForkResource) forkResource(ctx: StateContext, action: ForkResource) { const state = ctx.getState(); diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.html b/src/app/features/registry/pages/registry-overview/registry-overview.component.html index 2096ba64a..2e614bdba 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.html +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.html @@ -62,7 +62,10 @@

{{ block.value }}

- +
diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts index 51ca490db..049753e3f 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts @@ -25,6 +25,7 @@ import { GetRegistryInstitutions, GetRegistrySubjects, RegistryOverviewSelectors, + SetRegistryCustomCitation, } from '../../store/registry-overview'; @Component({ @@ -104,6 +105,7 @@ export class RegistryOverviewComponent { getBookmarksId: GetBookmarksCollectionId, getSubjects: GetRegistrySubjects, getInstitutions: GetRegistryInstitutions, + setCustomCitation: SetRegistryCustomCitation, }); constructor() { @@ -125,4 +127,8 @@ export class RegistryOverviewComponent { openRevision(revisionIndex: number): void { this.selectedRevisionIndex.set(revisionIndex); } + + onCustomCitationUpdated(citation: string): void { + this.actions.setCustomCitation(citation); + } } diff --git a/src/app/features/registry/store/registry-overview/registry-overview.actions.ts b/src/app/features/registry/store/registry-overview/registry-overview.actions.ts index 1fb066991..448637f09 100644 --- a/src/app/features/registry/store/registry-overview/registry-overview.actions.ts +++ b/src/app/features/registry/store/registry-overview/registry-overview.actions.ts @@ -41,3 +41,9 @@ export class MakePublic { constructor(public registryId: string) {} } + +export class SetRegistryCustomCitation { + static readonly type = '[Registry Overview] Set Registry Custom Citation'; + + constructor(public citation: string) {} +} diff --git a/src/app/features/registry/store/registry-overview/registry-overview.state.ts b/src/app/features/registry/store/registry-overview/registry-overview.state.ts index 9f0c47952..8df7c514c 100644 --- a/src/app/features/registry/store/registry-overview/registry-overview.state.ts +++ b/src/app/features/registry/store/registry-overview/registry-overview.state.ts @@ -13,6 +13,7 @@ import { GetRegistrySubjects, GetSchemaBlocks, MakePublic, + SetRegistryCustomCitation, WithdrawRegistration, } from './registry-overview.actions'; import { RegistryOverviewStateModel } from './registry-overview.model'; @@ -211,6 +212,20 @@ export class RegistryOverviewState { ); } + @Action(SetRegistryCustomCitation) + setRegistryCustomCitation(ctx: StateContext, action: SetRegistryCustomCitation) { + const state = ctx.getState(); + ctx.patchState({ + registry: { + ...state.registry, + data: { + ...state.registry.data!, + customCitation: action.citation, + }, + }, + }); + } + private handleError( ctx: StateContext, section: 'registry' | 'subjects' | 'institutions' | 'schemaBlocks', 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 ba0dc281b..4f93f68c4 100644 --- a/src/app/shared/components/resource-citations/resource-citations.component.html +++ b/src/app/shared/components/resource-citations/resource-citations.component.html @@ -2,47 +2,94 @@ @if (resource) { } 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 d76d14d57..533b7af33 100644 --- a/src/app/shared/components/resource-metadata/resource-metadata.component.ts +++ b/src/app/shared/components/resource-metadata/resource-metadata.component.ts @@ -4,7 +4,7 @@ import { Button } from 'primeng/button'; import { Tag } from 'primeng/tag'; import { DatePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; import { RouterLink } from '@angular/router'; import { ResourceCitationsComponent } from '@shared/components/resource-citations/resource-citations.component'; @@ -21,6 +21,11 @@ import { ResourceOverview } from '@shared/models'; }) export class ResourceMetadataComponent { currentResource = input.required(); + customCitationUpdated = output(); protected readonly resourceTypes = OsfResourceTypes; + + onCustomCitationUpdated(citation: string): void { + this.customCitationUpdated.emit(citation); + } } diff --git a/src/app/shared/mappers/citations.mapper.ts b/src/app/shared/mappers/citations.mapper.ts index f01f18620..0a45e6d8a 100644 --- a/src/app/shared/mappers/citations.mapper.ts +++ b/src/app/shared/mappers/citations.mapper.ts @@ -1,27 +1,56 @@ import { CITATION_TITLES } from '@shared/constants'; import { CitationTypes } from '@shared/enums'; -import { CitationStyle, CitationStylesJsonApiResponse, DefaultCitation, DefaultCitationJsonApi } from '@shared/models'; +import { + CitationStyle, + CitationStyleJsonApi, + DefaultCitation, + DefaultCitationJsonApi, + StyledCitation, + StyledCitationJsonApi, +} from '@shared/models'; +import { CustomCitationPayload } from '@shared/models/citations/custom-citation-payload.model'; +import { CustomCitationPayloadJsonApi } from '@shared/models/citations/custom-citation-payload-json-api.model'; export class CitationsMapper { static fromGetDefaultResponse(response: DefaultCitationJsonApi): DefaultCitation { - const citationId = response.data.id; + const citationId = response.id; return { id: citationId, - type: response.data.type, - citation: response.data.attributes.citation, + type: response.type, + citation: response.attributes.citation, title: CITATION_TITLES[citationId as CitationTypes], }; } - static fromGetCitationStylesResponse(response: CitationStylesJsonApiResponse): CitationStyle[] { - return response.styles.map((style) => ({ + static fromGetCitationStylesResponse(response: CitationStyleJsonApi[]): CitationStyle[] { + return response.map((style) => ({ id: style.id, - title: style.title, - shortTitle: style.short_title, - summary: style.summary, - hasBibliography: style.has_bibliography, - parentStyle: style.parent_style, + type: style.type, + title: style.attributes.title, + shortTitle: style.attributes.short_title, + summary: style.attributes.summary, + dateParsed: style.attributes.date_parsed, })); } + + static fromGetStyledCitationResponse(response: StyledCitationJsonApi): StyledCitation { + return { + id: response.id, + type: response.type, + citation: response.attributes.citation, + }; + } + + static toUpdateCustomCitationRequest(payload: CustomCitationPayload): CustomCitationPayloadJsonApi { + return { + data: { + id: payload.id, + type: payload.type, + attributes: { + custom_citation: payload.citationText, + }, + }, + }; + } } diff --git a/src/app/shared/models/citations/citation-style-json-api.model.ts b/src/app/shared/models/citations/citation-style-json-api.model.ts index 901cea5d7..4f31268aa 100644 --- a/src/app/shared/models/citations/citation-style-json-api.model.ts +++ b/src/app/shared/models/citations/citation-style-json-api.model.ts @@ -1,12 +1,11 @@ export interface CitationStyleJsonApi { id: string; - title: string; - short_title: string; - summary: string | null; - has_bibliography: boolean; - parent_style: string; -} - -export interface CitationStylesJsonApiResponse { - styles: CitationStyleJsonApi[]; + type: string; + attributes: { + title: string; + short_title: string | null; + summary: string | null; + date_parsed: string; + }; + links: Record; } diff --git a/src/app/shared/models/citations/citation-style.model.ts b/src/app/shared/models/citations/citation-style.model.ts index 563bedb4f..19879adfa 100644 --- a/src/app/shared/models/citations/citation-style.model.ts +++ b/src/app/shared/models/citations/citation-style.model.ts @@ -1,8 +1,7 @@ export interface CitationStyle { id: string; title: string; - shortTitle: string; + shortTitle: string | null; summary: string | null; - hasBibliography: boolean; - parentStyle: string; + dateParsed: string; } diff --git a/src/app/shared/models/citations/custom-citation-payload-json-api.model.ts b/src/app/shared/models/citations/custom-citation-payload-json-api.model.ts new file mode 100644 index 000000000..34e043fbc --- /dev/null +++ b/src/app/shared/models/citations/custom-citation-payload-json-api.model.ts @@ -0,0 +1,7 @@ +export interface CustomCitationPayloadJsonApi { + data: { + id: string; + type: string; + attributes: { custom_citation: string }; + }; +} diff --git a/src/app/shared/models/citations/custom-citation-payload.model.ts b/src/app/shared/models/citations/custom-citation-payload.model.ts new file mode 100644 index 000000000..862bb2eed --- /dev/null +++ b/src/app/shared/models/citations/custom-citation-payload.model.ts @@ -0,0 +1,5 @@ +export interface CustomCitationPayload { + id: string; + type: string; + citationText: string; +} diff --git a/src/app/shared/models/citations/default-citation-json-api.model.ts b/src/app/shared/models/citations/default-citation-json-api.model.ts index 2459fbe01..104ae90e9 100644 --- a/src/app/shared/models/citations/default-citation-json-api.model.ts +++ b/src/app/shared/models/citations/default-citation-json-api.model.ts @@ -1,9 +1,7 @@ export interface DefaultCitationJsonApi { - data: { - id: string; - type: string; - attributes: { - citation: string; - }; + id: string; + type: string; + attributes: { + citation: string; }; } diff --git a/src/app/shared/models/citations/index.ts b/src/app/shared/models/citations/index.ts index c215d4d31..49b54e04d 100644 --- a/src/app/shared/models/citations/index.ts +++ b/src/app/shared/models/citations/index.ts @@ -1,4 +1,8 @@ export * from './citation-style.model'; export * from './citation-style-json-api.model'; +export * from './custom-citation-payload.model'; +export * from './custom-citation-payload-json-api.model'; export * from './default-citation.model'; export * from './default-citation-json-api.model'; +export * from './styled-citation.model'; +export * from './styled-citation-json-api.model'; diff --git a/src/app/shared/models/citations/styled-citation-json-api.model.ts b/src/app/shared/models/citations/styled-citation-json-api.model.ts new file mode 100644 index 000000000..6b6113311 --- /dev/null +++ b/src/app/shared/models/citations/styled-citation-json-api.model.ts @@ -0,0 +1,7 @@ +export interface StyledCitationJsonApi { + id: string; + type: string; + attributes: { + citation: string; + }; +} diff --git a/src/app/shared/models/citations/styled-citation.model.ts b/src/app/shared/models/citations/styled-citation.model.ts new file mode 100644 index 000000000..9ce85f20a --- /dev/null +++ b/src/app/shared/models/citations/styled-citation.model.ts @@ -0,0 +1,5 @@ +export interface StyledCitation { + id: string; + type: string; + citation: string; +} diff --git a/src/app/shared/services/citations.service.ts b/src/app/shared/services/citations.service.ts index 9bef98a35..74f16cb39 100644 --- a/src/app/shared/services/citations.service.ts +++ b/src/app/shared/services/citations.service.ts @@ -1,12 +1,20 @@ import { map, Observable } from 'rxjs'; -import { HttpClient, HttpParams } from '@angular/common/http'; +import { HttpParams } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; +import { JsonApiResponse } from '@core/models'; import { JsonApiService } from '@core/services'; -import { ResourceType } from '@shared/enums'; import { CitationsMapper } from '@shared/mappers'; -import { CitationStylesJsonApiResponse, DefaultCitation, DefaultCitationJsonApi } from '@shared/models'; +import { + CitationStyle, + CitationStyleJsonApi, + CustomCitationPayload, + DefaultCitation, + DefaultCitationJsonApi, + StyledCitation, + StyledCitationJsonApi, +} from '@shared/models'; import { environment } from 'src/environments/environment'; @@ -14,44 +22,43 @@ import { environment } from 'src/environments/environment'; providedIn: 'root', }) export class CitationsService { - private readonly http = inject(HttpClient); private readonly jsonApiService = inject(JsonApiService); - private readonly urlMap = new Map([ - [ResourceType.Project, 'nodes'], - [ResourceType.Registration, 'registrations'], - [ResourceType.Preprint, 'preprints'], - ]); - - fetchDefaultCitation( - resourceType: ResourceType, - resourceId: string, - citationId: string - ): Observable { - const baseUrl = this.getBaseUrl(resourceType, resourceId); - return this.http - .get(`${baseUrl}/${citationId}/`) - .pipe(map((response) => CitationsMapper.fromGetDefaultResponse(response))); + fetchDefaultCitation(resourceType: string, resourceId: string, citationId: string): Observable { + const baseUrl = this.getBaseCitationUrl(resourceType, resourceId); + return this.jsonApiService + .get>(`${baseUrl}/${citationId}/`) + .pipe(map((response) => CitationsMapper.fromGetDefaultResponse(response.data))); } - fetchCitationStyles(searchQuery?: string) { + fetchCitationStyles(searchQuery?: string): Observable { const baseUrl = environment.apiUrl; const params = new HttpParams().set('filter[title,short_title]', searchQuery || '').set('page[size]', '100'); - return this.http - .get(`${baseUrl}/citations/styles`, { params }) - .pipe(map((response) => CitationsMapper.fromGetCitationStylesResponse(response))); + return this.jsonApiService + .get>(`${baseUrl}/citations/styles`, { params }) + .pipe(map((response) => CitationsMapper.fromGetCitationStylesResponse(response.data))); } - private getBaseUrl(resourceType: ResourceType, resourceId: string): string { - const baseUrl = `${environment.apiUrl}`; - const resourcePath = this.urlMap.get(resourceType); + updateCustomCitation(payload: CustomCitationPayload): Observable { + const baseUrl = environment.apiUrl; + const citationData = CitationsMapper.toUpdateCustomCitationRequest(payload); + + return this.jsonApiService.patch(`${baseUrl}/${payload.type}/${payload.id}/`, citationData); + } - if (!resourcePath) { - throw new Error(`Unsupported resource type: ${resourceType}`); - } + fetchStyledCitation(resourceType: string, resourceId: string, citationStyle: string): Observable { + const baseUrl = this.getBaseCitationUrl(resourceType, resourceId); + + return this.jsonApiService + .get>(`${baseUrl}/${citationStyle}/`) + .pipe(map((response) => CitationsMapper.fromGetStyledCitationResponse(response.data))); + } + + private getBaseCitationUrl(resourceType: string, resourceId: string): string { + const baseUrl = `${environment.apiUrl}`; - return `${baseUrl}/${resourcePath}/${resourceId}/citation`; + return `${baseUrl}/${resourceType}/${resourceId}/citation`; } } diff --git a/src/app/shared/stores/citations/citations.actions.ts b/src/app/shared/stores/citations/citations.actions.ts index c11cf0d5f..bd92a13e3 100644 --- a/src/app/shared/stores/citations/citations.actions.ts +++ b/src/app/shared/stores/citations/citations.actions.ts @@ -1,10 +1,10 @@ -import { ResourceType } from '@shared/enums'; +import { CustomCitationPayload } from '@shared/models'; export class GetDefaultCitations { static readonly type = '[Citations] Get Default Citations'; constructor( - public resourceType: ResourceType, + public resourceType: string, public resourceId: string ) {} } @@ -14,3 +14,19 @@ export class GetCitationStyles { constructor(public searchQuery?: string) {} } + +export class UpdateCustomCitation { + static readonly type = '[Citations] Update Custom Citation'; + + constructor(public payload: CustomCitationPayload) {} +} + +export class GetStyledCitation { + static readonly type = '[Citations] Get Styled Citation'; + + constructor( + public resourceType: string, + public resourceId: string, + public citationStyle: string + ) {} +} diff --git a/src/app/shared/stores/citations/citations.model.ts b/src/app/shared/stores/citations/citations.model.ts index 24858b01c..0f5a91097 100644 --- a/src/app/shared/stores/citations/citations.model.ts +++ b/src/app/shared/stores/citations/citations.model.ts @@ -1,7 +1,9 @@ -import { CitationStyle, DefaultCitation } from '@shared/models'; +import { CitationStyle, DefaultCitation, StyledCitation } from '@shared/models'; import { AsyncStateModel } from '@shared/models/store'; export interface CitationsStateModel { defaultCitations: AsyncStateModel; citationStyles: AsyncStateModel; + styledCitation: AsyncStateModel; + customCitation: AsyncStateModel; } diff --git a/src/app/shared/stores/citations/citations.selectors.ts b/src/app/shared/stores/citations/citations.selectors.ts index fa0c0774d..dda0f1f53 100644 --- a/src/app/shared/stores/citations/citations.selectors.ts +++ b/src/app/shared/stores/citations/citations.selectors.ts @@ -14,6 +14,16 @@ export class CitationsSelectors { return state.defaultCitations.isLoading; } + @Selector([CitationsState]) + static getDefaultCitationsSubmitting(state: CitationsStateModel) { + return state.defaultCitations.isSubmitting; + } + + @Selector([CitationsState]) + static getCustomCitationSubmitting(state: CitationsStateModel) { + return state.customCitation.isSubmitting; + } + @Selector([CitationsState]) static getCitationStyles(state: CitationsStateModel) { return state.citationStyles.data; @@ -23,4 +33,14 @@ export class CitationsSelectors { static getCitationStylesLoading(state: CitationsStateModel) { return state.citationStyles.isLoading; } + + @Selector([CitationsState]) + static getStyledCitation(state: CitationsStateModel) { + return state.styledCitation.data; + } + + @Selector([CitationsState]) + static getStyledCitationLoading(state: CitationsStateModel) { + return state.styledCitation.isLoading; + } } diff --git a/src/app/shared/stores/citations/citations.state.ts b/src/app/shared/stores/citations/citations.state.ts index 985b2f629..a32b4e005 100644 --- a/src/app/shared/stores/citations/citations.state.ts +++ b/src/app/shared/stores/citations/citations.state.ts @@ -1,6 +1,6 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { catchError, forkJoin, tap } from 'rxjs'; +import { catchError, forkJoin, Observable, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; @@ -8,7 +8,7 @@ import { handleSectionError } from '@core/handlers'; import { CitationTypes } from '@shared/enums'; import { CitationsService } from '@shared/services/citations.service'; -import { GetCitationStyles, GetDefaultCitations } from './citations.actions'; +import { GetCitationStyles, GetDefaultCitations, GetStyledCitation, UpdateCustomCitation } from './citations.actions'; import { CitationsStateModel } from './citations.model'; const CITATIONS_DEFAULTS: CitationsStateModel = { @@ -24,6 +24,18 @@ const CITATIONS_DEFAULTS: CitationsStateModel = { isSubmitting: false, error: null, }, + styledCitation: { + data: null, + isLoading: false, + isSubmitting: false, + error: null, + }, + customCitation: { + data: '', + isLoading: false, + isSubmitting: false, + error: null, + }, }; @State({ @@ -89,4 +101,55 @@ export class CitationsState { catchError((error) => handleSectionError(ctx, 'citationStyles', error)) ); } + + @Action(UpdateCustomCitation) + updateCustomCitation(ctx: StateContext, action: UpdateCustomCitation): Observable { + const state = ctx.getState(); + ctx.patchState({ + customCitation: { + ...state.customCitation, + isSubmitting: true, + error: null, + }, + }); + + return this.citationsService.updateCustomCitation(action.payload).pipe( + tap(() => { + ctx.patchState({ + customCitation: { + ...state.customCitation, + data: action.payload.citationText, + isSubmitting: false, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'customCitation', error)) + ); + } + + @Action(GetStyledCitation) + getStyledCitation(ctx: StateContext, action: GetStyledCitation) { + const state = ctx.getState(); + ctx.patchState({ + styledCitation: { + ...state.styledCitation, + isLoading: true, + error: null, + }, + }); + + return this.citationsService.fetchStyledCitation(action.resourceType, action.resourceId, action.citationStyle).pipe( + tap((styledCitation) => { + ctx.patchState({ + styledCitation: { + data: styledCitation, + isLoading: false, + isSubmitting: false, + error: null, + }, + }); + }), + catchError((error) => handleSectionError(ctx, 'styledCitation', error)) + ); + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 155b18b6b..eea51d7d9 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -39,7 +39,8 @@ "submit": "Submit", "view": "View", "review": "Review", - "upload": "Upload" + "upload": "Upload", + "customize": "Customize" }, "search": { "title": "Search", @@ -574,6 +575,8 @@ "getMoreCitations": "Get More Citations", "citationInputPlaceholder": "Select citation style or start typing", "citationLoadingPlaceholder": "Loading options...", + "customCitationPlaceholder": "Enter custom citation", + "citeAs": "Cite as:", "noCitationStylesFound": "No results found", "affiliatedInstitutions": "Affiliated Institutions", "noDescription": "No description", From 80e4788ec761f2418104520ed062e47335dfd71e Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Wed, 30 Jul 2025 19:02:46 +0300 Subject: [PATCH 6/6] feat(project-overview-citations): added citations --- .../overview/project-overview.component.scss | 2 +- .../overview/project-overview.component.ts | 1 - .../mappers/registry-metadata.mapper.ts | 1 + .../resource-citations.component.html | 69 ++++++--- .../resource-citations.component.ts | 142 +++--------------- .../mappers/resource-overview.mappers.ts | 2 +- src/assets/i18n/en.json | 1 + 7 files changed, 70 insertions(+), 148 deletions(-) diff --git a/src/app/features/project/overview/project-overview.component.scss b/src/app/features/project/overview/project-overview.component.scss index fd663b1b5..73572f32d 100644 --- a/src/app/features/project/overview/project-overview.component.scss +++ b/src/app/features/project/overview/project-overview.component.scss @@ -5,7 +5,7 @@ } .right-section { - flex: 1; + width: 23rem; border: 1px solid var.$grey-2; border-radius: 12px; } diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index 3db2d1233..b52f9d4aa 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -27,7 +27,6 @@ import { import { ClearProjectOverview, GetComponents, - GetLinkedResources, GetProjectById, ProjectOverviewSelectors, SetProjectCustomCitation, diff --git a/src/app/features/registry/mappers/registry-metadata.mapper.ts b/src/app/features/registry/mappers/registry-metadata.mapper.ts index d94a07ef1..ca4c1abd9 100644 --- a/src/app/features/registry/mappers/registry-metadata.mapper.ts +++ b/src/app/features/registry/mappers/registry-metadata.mapper.ts @@ -85,6 +85,7 @@ export class RegistryMetadataMapper { doi: (attributes['doi'] as string) || '', isPublic: attributes['public'] as boolean, isFork: attributes['fork'] as boolean, + customCitation: (attributes['custom_citation'] as string) || '', accessRequestsEnabled: attributes['access_requests_enabled'] as boolean, wikiEnabled: attributes['wiki_enabled'] as boolean, currentUserCanComment: attributes['current_user_can_comment'] as boolean, 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 4f93f68c4..aeacd5222 100644 --- a/src/app/shared/components/resource-citations/resource-citations.component.html +++ b/src/app/shared/components/resource-citations/resource-citations.component.html @@ -9,24 +9,30 @@

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

@if (!isEditMode()) { - - @if (isCitationsLoading()) { -
- - - -
+ } @else { @if (resource.customCitation) { -
+

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

{{ resource.customCitation }}

+
+ + +
} @else { @if (defaultCitations().length) {
@@ -49,44 +55,59 @@

{{ citation.title }}

(onFilter)="handleCitationStyleFilterSearch($event)" optionLabel="label" optionValue="value" - [appendTo]="'body'" + appendTo="body" [emptyFilterMessage]="filterMessage()" - [emptyMessage]="filterMessage()" + [emptyMessage]="'project.overview.metadata.citationInputPlaceholder' | translate" (onChange)="handleGetStyledCitation($event)" > {{ selectedOption.label }} - @if (styledCitation()) { -

{{ styledCitation()?.citation }}

+

{{ styledCitation()?.citation }}

} + + } } } @else { -
- - Cancel +
+ Remove + > Save + >
} 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 a1bce4c9d..dc7cabd9b 100644 --- a/src/app/shared/components/resource-citations/resource-citations.component.ts +++ b/src/app/shared/components/resource-citations/resource-citations.component.ts @@ -11,6 +11,7 @@ import { Textarea } from 'primeng/textarea'; import { debounceTime, distinctUntilChanged, Subject, takeUntil } from 'rxjs'; +import { Clipboard } from '@angular/cdk/clipboard'; import { ChangeDetectionStrategy, Component, @@ -24,8 +25,8 @@ import { } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; -import { CitationsMapper } from '@shared/mappers'; import { CitationStyle, CustomOption, ResourceOverview } from '@shared/models'; +import { ToastService } from '@shared/services'; import { CitationsSelectors, GetCitationStyles, @@ -57,6 +58,8 @@ export class ResourceCitationsComponent { private readonly destroyRef = inject(DestroyRef); private readonly translateService = inject(TranslateService); currentResource = input.required(); + private readonly clipboard = inject(Clipboard); + private readonly toastService = inject(ToastService); private readonly destroy$ = new Subject(); private readonly filterSubject = new Subject(); protected customCitation = output(); @@ -66,118 +69,6 @@ export class ResourceCitationsComponent { protected isCitationStylesLoading = select(CitationsSelectors.getCitationStylesLoading); protected isCustomCitationSubmitting = select(CitationsSelectors.getCustomCitationSubmitting); protected styledCitation = select(CitationsSelectors.getStyledCitation); - citationOptions = CitationsMapper.fromGetCitationStylesResponse([ - { - id: 'revista-cubana-de-pediatria', - type: 'citation-styles', - attributes: { - title: 'Revista Cubana de PediatrĂ­a (Spanish)', - date_parsed: '2018-06-14T00:31:10.016240', - short_title: null, - summary: null, - }, - links: {}, - }, - { - id: 'the-astronomical-journal', - type: 'citation-styles', - attributes: { - title: 'The Astronomical Journal', - date_parsed: '2018-06-14T00:31:10.012365', - short_title: null, - summary: null, - }, - links: {}, - }, - { - id: 'vocations-and-learning', - type: 'citation-styles', - attributes: { - title: 'Vocations and Learning', - date_parsed: '2018-06-14T00:31:10.008746', - short_title: 'Vocations and Learning', - summary: null, - }, - links: {}, - }, - { - id: 'chemical-physics-letters', - type: 'citation-styles', - attributes: { - title: 'Chemical Physics Letters', - date_parsed: '2018-06-14T00:31:10.004818', - short_title: null, - summary: null, - }, - links: {}, - }, - { - id: 'expert-review-of-respiratory-medicine', - type: 'citation-styles', - attributes: { - title: 'Expert Review of Respiratory Medicine', - date_parsed: '2018-06-14T00:31:10.001144', - short_title: null, - summary: null, - }, - links: {}, - }, - { - id: 'new-writing', - type: 'citation-styles', - attributes: { - title: 'New Writing', - date_parsed: '2018-06-14T00:31:09.997476', - short_title: null, - summary: null, - }, - links: {}, - }, - { - id: 'ams-review', - type: 'citation-styles', - attributes: { - title: 'AMS Review', - date_parsed: '2018-06-14T00:31:09.993110', - short_title: 'AMS Rev', - summary: null, - }, - links: {}, - }, - { - id: 'acs-catalysis', - type: 'citation-styles', - attributes: { - title: 'ACS Catalysis', - date_parsed: '2018-06-14T00:31:09.985253', - short_title: 'ACS Catal.', - summary: null, - }, - links: {}, - }, - { - id: 'cell-regeneration', - type: 'citation-styles', - attributes: { - title: 'Cell Regeneration', - date_parsed: '2018-06-14T00:31:09.981701', - short_title: null, - summary: null, - }, - links: {}, - }, - { - id: 'evidence-based-communication-assessment-and-intervention', - type: 'citation-styles', - attributes: { - title: 'Evidence-Based Communication Assessment and Intervention', - date_parsed: '2018-06-14T00:31:09.977038', - short_title: null, - summary: null, - }, - links: {}, - }, - ]); protected citationStylesOptions = signal[]>([]); protected isEditMode = signal(false); protected filterMessage = computed(() => { @@ -202,7 +93,7 @@ export class ResourceCitationsComponent { this.setupDestroyEffect(); } - setupDefaultCitationsEffect(): void { + protected setupDefaultCitationsEffect(): void { effect(() => { const resource = this.currentResource(); @@ -213,12 +104,12 @@ export class ResourceCitationsComponent { }); } - handleCitationStyleFilterSearch(event: SelectFilterEvent) { + protected handleCitationStyleFilterSearch(event: SelectFilterEvent) { event.originalEvent.preventDefault(); this.filterSubject.next(event.filter); } - handleGetStyledCitation(event: SelectChangeEvent) { + protected handleGetStyledCitation(event: SelectChangeEvent) { const resource = this.currentResource(); if (resource) { @@ -226,7 +117,7 @@ export class ResourceCitationsComponent { } } - handleUpdateCustomCitation() { + protected handleUpdateCustomCitation(): void { const resource = this.currentResource(); const customCitationText = this.customCitationInput.value?.trim(); @@ -248,7 +139,7 @@ export class ResourceCitationsComponent { } } - handleDeleteCustomCitation() { + protected handleDeleteCustomCitation(): void { const resource = this.currentResource(); if (resource) { @@ -269,10 +160,19 @@ export class ResourceCitationsComponent { } } - toggleEditMode() { + protected toggleEditMode(): void { this.isEditMode.set(!this.isEditMode()); } + protected copyCitation(): void { + const resource = this.currentResource(); + + if (resource?.customCitation) { + this.clipboard.copy(resource.customCitation); + this.toastService.showSuccess('settings.developerApps.messages.copied'); + } + } + private setupFilterDebounce(): void { this.filterSubject .pipe(debounceTime(300), distinctUntilChanged(), takeUntil(this.destroy$)) @@ -283,8 +183,8 @@ export class ResourceCitationsComponent { private setupCitationStylesEffect(): void { effect(() => { - // const styles = this.citationStyles(); - const styles = this.citationOptions; + const styles = this.citationStyles(); + const options = styles.map((style: CitationStyle) => ({ label: style.title, value: style, diff --git a/src/app/shared/mappers/resource-overview.mappers.ts b/src/app/shared/mappers/resource-overview.mappers.ts index 5daedf955..4fa3244a8 100644 --- a/src/app/shared/mappers/resource-overview.mappers.ts +++ b/src/app/shared/mappers/resource-overview.mappers.ts @@ -74,7 +74,7 @@ export function MapRegistryOverview( region: registry.region || undefined, forksCount: registry.forksCount, subjects: subjects, - customCitation: registry.customCitation || null, + customCitation: registry.customCitation, affiliatedInstitutions: institutions, associatedProjectId: registry.associatedProjectId, }; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index ab7c92caf..d1bab0ca4 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -41,6 +41,7 @@ "review": "Review", "upload": "Upload", "customize": "Customize", + "createCustomCitation": "Create Custom Citation", "preview": "Preview", "continueUpdate": "Continue Update" },