From 280e3fa5de2ad043e8f50e728df93a061e1ba9f2 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Tue, 12 Aug 2025 15:29:40 +0300 Subject: [PATCH 1/5] feat(project-recent-activity): created activity-logs service --- src/app/shared/mappers/logs.mapper.ts | 1 + .../activity-logs-json-api.model.ts | 157 ++++++++++++++++++ .../activity-logs/activity-logs.model.ts | 0 .../shared/services/activity-logs.service.ts | 26 +++ src/app/shared/services/index.ts | 1 + 5 files changed, 185 insertions(+) create mode 100644 src/app/shared/mappers/logs.mapper.ts create mode 100644 src/app/shared/models/activity-logs/activity-logs-json-api.model.ts create mode 100644 src/app/shared/models/activity-logs/activity-logs.model.ts create mode 100644 src/app/shared/services/activity-logs.service.ts diff --git a/src/app/shared/mappers/logs.mapper.ts b/src/app/shared/mappers/logs.mapper.ts new file mode 100644 index 000000000..152b43821 --- /dev/null +++ b/src/app/shared/mappers/logs.mapper.ts @@ -0,0 +1 @@ +export class LogsMapper {} diff --git a/src/app/shared/models/activity-logs/activity-logs-json-api.model.ts b/src/app/shared/models/activity-logs/activity-logs-json-api.model.ts new file mode 100644 index 000000000..c86ac39b5 --- /dev/null +++ b/src/app/shared/models/activity-logs/activity-logs-json-api.model.ts @@ -0,0 +1,157 @@ +export interface ActivityLogJsonApi { + id: string; + type: string; + attributes: { + action: string; + date: string; + params: { + contributors: unknown[]; + license?: string; + tag?: string; + institution?: { + id: string; + name: string; + }; + params_node: { + id: string; + title: string; + }; + params_project: null; + pointer: PointerJsonApi | null; + preprint_provider: string | null; + }; + }; + embeds?: { + original_node?: { + data: OriginalNodeEmbedsData; + }; + user?: { + data: UserEmbedsData; + }; + linked_node?: { + data: LinkedNodeEmbedsData; + }; + }; +} + +interface PointerJsonApi { + category: string; + id: string; + title: string; + url: string; +} + +interface OriginalNodeEmbedsData { + id: string; + type: string; + attributes: { + title: string; + description: string; + category: string; + custom_citation: string | null; + date_created: string; + date_modified: string; + registration: boolean; + preprint: boolean; + fork: boolean; + collection: boolean; + tags: string[]; + access_requests_enabled: boolean; + node_license: { + copyright_holders: string[]; + year: string | null; + }; + current_user_can_comment: boolean; + current_user_permissions: string[]; + current_user_is_contributor: boolean; + current_user_is_contributor_or_group_member: boolean; + wiki_enabled: boolean; + public: boolean; + subjects: { id: string; text: string }[][]; + }; +} + +interface UserEmbedsData { + id: string; + type: string; + attributes: { + full_name: string; + given_name: string; + middle_names: string; + family_name: string; + suffix: string; + date_registered: string; + active: boolean; + timezone: string; + locale: string; + social: { + ssrn: string; + orcid: string; + github: string; + scholar: string; + twitter: string; + linkedIn: string; + impactStory: string; + baiduScholar: string; + researchGate: string; + researcherId: string; + profileWebsites: string[]; + academiaProfileID: string; + academiaInstitution?: string; + }; + employment: { + title: string; + endYear?: number; + ongoing: boolean; + endMonth?: number; + startYear: number; + department: string; + startMonth: number; + institution: string; + }[]; + education: { + degree: string; + endYear?: number; + ongoing: boolean; + endMonth?: number; + startYear: number; + department: string; + startMonth: number; + institution: string; + }[]; + allow_indexing?: boolean; + can_view_reviews?: boolean; + accepted_terms_of_service?: boolean; + email?: string; + }; +} + +interface LinkedNodeEmbedsData { + id: string; + type: string; + attributes: { + title: string; + description: string; + category: string; + custom_citation: string | null; + date_created: string; + date_modified: string; + registration: boolean; + preprint: boolean; + fork: boolean; + collection: boolean; + tags: string[]; + access_requests_enabled: boolean; + node_license: { + copyright_holders: string[]; + year: string | null; + }; + current_user_can_comment: boolean; + current_user_permissions: string[]; + current_user_is_contributor: boolean; + current_user_is_contributor_or_group_member: boolean; + wiki_enabled: boolean; + public: boolean; + subjects: { id: string; text: string }[][]; + }; +} diff --git a/src/app/shared/models/activity-logs/activity-logs.model.ts b/src/app/shared/models/activity-logs/activity-logs.model.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/services/activity-logs.service.ts b/src/app/shared/services/activity-logs.service.ts new file mode 100644 index 000000000..894ce9476 --- /dev/null +++ b/src/app/shared/services/activity-logs.service.ts @@ -0,0 +1,26 @@ +import { Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiResponseWithPaging } from '@core/models'; +import { JsonApiService } from '@core/services'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class ActivityLogsService { + private jsonApiService = inject(JsonApiService); + + fetchLogs(projectId: string, page = '1', pageSize: string): Observable { + const url = `${environment.apiUrl}/nodes/${projectId}/logs`; + const params: Record = { + 'embed[]': ['original_node', 'user', 'linked_node', 'linked_registration', 'template_node', 'group'], + page, + pageSize, + }; + + return this.jsonApiService.get>(url, params); + } +} diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index 3007f1cc3..faea7f0aa 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -1,3 +1,4 @@ +export { ActivityLogsService } from './activity-logs.service'; export * from './addons'; export { BookmarksService } from './bookmarks.service'; export { BrandService } from './brand.service'; From 3b544604fb86d1bb43d807d4229578f64cf9faa5 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Wed, 13 Aug 2025 12:00:11 +0300 Subject: [PATCH 2/5] feat(project-recent-activity): added get logs method --- .../overview/project-overview.component.ts | 3 + src/app/features/project/project.routes.ts | 11 +++- .../shared/mappers/activity-logs.mapper.ts | 1 + src/app/shared/mappers/logs.mapper.ts | 1 - .../activity-logs-json-api.model.ts | 44 ++------------- .../activity-logs/activity-logs.model.ts | 3 + src/app/shared/models/activity-logs/index.ts | 2 + src/app/shared/models/index.ts | 1 + .../shared/services/activity-logs.service.ts | 14 +++-- .../activity-logs/activity-logs.actions.ts | 13 +++++ .../activity-logs/activity-logs.model.ts | 5 ++ .../activity-logs/activity-logs.selectors.ts | 18 ++++++ .../activity-logs/activity-logs.state.ts | 55 +++++++++++++++++++ src/app/shared/stores/activity-logs/index.ts | 4 ++ 14 files changed, 129 insertions(+), 46 deletions(-) create mode 100644 src/app/shared/mappers/activity-logs.mapper.ts delete mode 100644 src/app/shared/mappers/logs.mapper.ts create mode 100644 src/app/shared/models/activity-logs/index.ts create mode 100644 src/app/shared/stores/activity-logs/activity-logs.actions.ts create mode 100644 src/app/shared/stores/activity-logs/activity-logs.model.ts create mode 100644 src/app/shared/stores/activity-logs/activity-logs.selectors.ts create mode 100644 src/app/shared/stores/activity-logs/activity-logs.state.ts create mode 100644 src/app/shared/stores/activity-logs/index.ts diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index ab91fd96a..fd71e502e 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -41,6 +41,7 @@ import { GetHomeWiki, GetLinkedResources, } from '@shared/stores'; +import { GetActivityLogs } from '@shared/stores/activity-logs'; import { ClearCollections } from '@shared/stores/collections'; import { IS_XSMALL } from '@shared/utils'; @@ -109,6 +110,7 @@ export class ProjectOverviewComponent implements OnInit { getComponents: GetComponents, getLinkedProjects: GetLinkedResources, getNodeLinks: GetAllNodeLinks, + getActivityLogs: GetActivityLogs, setProjectCustomCitation: SetProjectCustomCitation, getCollectionProvider: GetCollectionProvider, getCurrentReviewAction: GetSubmissionsReviewActions, @@ -198,6 +200,7 @@ export class ProjectOverviewComponent implements OnInit { this.actions.getComponents(projectId); this.actions.getNodeLinks(projectId); this.actions.getLinkedProjects(projectId); + this.actions.getActivityLogs(projectId, '1', '5'); } } diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index 29407add9..77b3ba397 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -12,6 +12,7 @@ import { SubjectsState, ViewOnlyLinkState, } from '@osf/shared/stores'; +import { ActivityLogsState } from '@shared/stores/activity-logs'; import { AnalyticsState } from './analytics/store'; import { ProjectFilesState } from './files/store'; @@ -31,7 +32,15 @@ export const projectRoutes: Routes = [ path: 'overview', loadComponent: () => import('../project/overview/project-overview.component').then((mod) => mod.ProjectOverviewComponent), - providers: [provideStates([NodeLinksState, CitationsState, CollectionsState, CollectionsModerationState])], + providers: [ + provideStates([ + NodeLinksState, + CitationsState, + CollectionsState, + CollectionsModerationState, + ActivityLogsState, + ]), + ], }, { path: 'metadata', diff --git a/src/app/shared/mappers/activity-logs.mapper.ts b/src/app/shared/mappers/activity-logs.mapper.ts new file mode 100644 index 000000000..9ce2fc1e4 --- /dev/null +++ b/src/app/shared/mappers/activity-logs.mapper.ts @@ -0,0 +1 @@ +export class ActivityLogsMapper {} diff --git a/src/app/shared/mappers/logs.mapper.ts b/src/app/shared/mappers/logs.mapper.ts deleted file mode 100644 index 152b43821..000000000 --- a/src/app/shared/mappers/logs.mapper.ts +++ /dev/null @@ -1 +0,0 @@ -export class LogsMapper {} diff --git a/src/app/shared/models/activity-logs/activity-logs-json-api.model.ts b/src/app/shared/models/activity-logs/activity-logs-json-api.model.ts index c86ac39b5..eec05e7f9 100644 --- a/src/app/shared/models/activity-logs/activity-logs-json-api.model.ts +++ b/src/app/shared/models/activity-logs/activity-logs-json-api.model.ts @@ -84,45 +84,6 @@ interface UserEmbedsData { active: boolean; timezone: string; locale: string; - social: { - ssrn: string; - orcid: string; - github: string; - scholar: string; - twitter: string; - linkedIn: string; - impactStory: string; - baiduScholar: string; - researchGate: string; - researcherId: string; - profileWebsites: string[]; - academiaProfileID: string; - academiaInstitution?: string; - }; - employment: { - title: string; - endYear?: number; - ongoing: boolean; - endMonth?: number; - startYear: number; - department: string; - startMonth: number; - institution: string; - }[]; - education: { - degree: string; - endYear?: number; - ongoing: boolean; - endMonth?: number; - startYear: number; - department: string; - startMonth: number; - institution: string; - }[]; - allow_indexing?: boolean; - can_view_reviews?: boolean; - accepted_terms_of_service?: boolean; - email?: string; }; } @@ -152,6 +113,9 @@ interface LinkedNodeEmbedsData { current_user_is_contributor_or_group_member: boolean; wiki_enabled: boolean; public: boolean; - subjects: { id: string; text: string }[][]; + subjects: { + id: string; + text: string; + }[][]; }; } diff --git a/src/app/shared/models/activity-logs/activity-logs.model.ts b/src/app/shared/models/activity-logs/activity-logs.model.ts index e69de29bb..ea81dac34 100644 --- a/src/app/shared/models/activity-logs/activity-logs.model.ts +++ b/src/app/shared/models/activity-logs/activity-logs.model.ts @@ -0,0 +1,3 @@ +export interface ActivityLogs { + id: string; +} diff --git a/src/app/shared/models/activity-logs/index.ts b/src/app/shared/models/activity-logs/index.ts new file mode 100644 index 000000000..c84f515ab --- /dev/null +++ b/src/app/shared/models/activity-logs/index.ts @@ -0,0 +1,2 @@ +export * from './activity-logs.model'; +export * from './activity-logs-json-api.model'; diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 154a8a219..4dc829589 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -1,3 +1,4 @@ +export * from './activity-logs'; export * from './addons'; export * from './brand.json-api.model'; export * from './brand.model'; diff --git a/src/app/shared/services/activity-logs.service.ts b/src/app/shared/services/activity-logs.service.ts index 894ce9476..951458c4d 100644 --- a/src/app/shared/services/activity-logs.service.ts +++ b/src/app/shared/services/activity-logs.service.ts @@ -1,9 +1,11 @@ import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; import { JsonApiResponseWithPaging } from '@core/models'; import { JsonApiService } from '@core/services'; +import { ActivityLogJsonApi } from '@shared/models'; import { environment } from 'src/environments/environment'; @@ -13,14 +15,18 @@ import { environment } from 'src/environments/environment'; export class ActivityLogsService { private jsonApiService = inject(JsonApiService); - fetchLogs(projectId: string, page = '1', pageSize: string): Observable { - const url = `${environment.apiUrl}/nodes/${projectId}/logs`; + fetchLogs(projectId: string, page = '1', pageSize: string): Observable { + const url = `${environment.apiUrl}/nodes/${projectId}/logs/`; const params: Record = { 'embed[]': ['original_node', 'user', 'linked_node', 'linked_registration', 'template_node', 'group'], page, - pageSize, + 'page[size]': pageSize, }; - return this.jsonApiService.get>(url, params); + return this.jsonApiService.get>(url, params).pipe( + map((res) => { + return res.data; + }) + ); } } diff --git a/src/app/shared/stores/activity-logs/activity-logs.actions.ts b/src/app/shared/stores/activity-logs/activity-logs.actions.ts new file mode 100644 index 000000000..50d9c2e0b --- /dev/null +++ b/src/app/shared/stores/activity-logs/activity-logs.actions.ts @@ -0,0 +1,13 @@ +export class GetActivityLogs { + static readonly type = '[ActivityLogs] Get Activity Logs'; + + constructor( + public projectId: string, + public page = '1', + public pageSize: string + ) {} +} + +export class ClearActivityLogsStore { + static readonly type = '[ActivityLogs] Clear Store'; +} diff --git a/src/app/shared/stores/activity-logs/activity-logs.model.ts b/src/app/shared/stores/activity-logs/activity-logs.model.ts new file mode 100644 index 000000000..2fa209bdb --- /dev/null +++ b/src/app/shared/stores/activity-logs/activity-logs.model.ts @@ -0,0 +1,5 @@ +import { ActivityLogJsonApi, AsyncStateModel } from '@shared/models'; + +export interface ActivityLogsStateModel { + activityLogs: AsyncStateModel; +} diff --git a/src/app/shared/stores/activity-logs/activity-logs.selectors.ts b/src/app/shared/stores/activity-logs/activity-logs.selectors.ts new file mode 100644 index 000000000..0dbd2bf14 --- /dev/null +++ b/src/app/shared/stores/activity-logs/activity-logs.selectors.ts @@ -0,0 +1,18 @@ +import { Selector } from '@ngxs/store'; + +import { ActivityLogJsonApi } from '@shared/models'; + +import { ActivityLogsStateModel } from './activity-logs.model'; +import { ActivityLogsState } from './activity-logs.state'; + +export class ActivityLogsSelectors { + @Selector([ActivityLogsState]) + static getActivityLogs(state: ActivityLogsStateModel): ActivityLogJsonApi[] { + return state.activityLogs.data; + } + + @Selector([ActivityLogsState]) + static getLoading(state: ActivityLogsStateModel): boolean { + return state.activityLogs.isLoading; + } +} diff --git a/src/app/shared/stores/activity-logs/activity-logs.state.ts b/src/app/shared/stores/activity-logs/activity-logs.state.ts new file mode 100644 index 000000000..7b1f64f89 --- /dev/null +++ b/src/app/shared/stores/activity-logs/activity-logs.state.ts @@ -0,0 +1,55 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { tap } from 'rxjs/operators'; + +import { inject, Injectable } from '@angular/core'; + +import { ActivityLogsService } from '@shared/services'; + +import { ClearActivityLogsStore, GetActivityLogs } from './activity-logs.actions'; +import { ActivityLogsStateModel } from './activity-logs.model'; + +const defaultState: ActivityLogsStateModel = { + activityLogs: { + data: [], + isLoading: false, + error: null, + }, +}; + +@State({ + name: 'activityLogs', + defaults: defaultState, +}) +@Injectable() +export class ActivityLogsState { + private readonly activityLogsService = inject(ActivityLogsService); + + @Action(GetActivityLogs) + getActivityLogs(ctx: StateContext, action: GetActivityLogs) { + ctx.patchState({ + activityLogs: { + data: [], + isLoading: true, + error: null, + }, + }); + + return this.activityLogsService.fetchLogs(action.projectId, action.page, action.pageSize).pipe( + tap((data) => { + ctx.patchState({ + activityLogs: { + data: data, + isLoading: false, + error: null, + }, + }); + }) + ); + } + + @Action(ClearActivityLogsStore) + clearActivityLogsStore(ctx: StateContext) { + ctx.setState(defaultState); + } +} diff --git a/src/app/shared/stores/activity-logs/index.ts b/src/app/shared/stores/activity-logs/index.ts new file mode 100644 index 000000000..399b3f9c8 --- /dev/null +++ b/src/app/shared/stores/activity-logs/index.ts @@ -0,0 +1,4 @@ +export * from './activity-logs.actions'; +export * from './activity-logs.model'; +export * from './activity-logs.selectors'; +export * from './activity-logs.state'; From 9f6deda79f4531de71edc42b0f3a9cf1906d9d05 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Mon, 18 Aug 2025 15:58:43 +0300 Subject: [PATCH 3/5] feat(project-recent-activity): added recent activity logs to the project overview page --- .../collection-moderation.routes.ts | 2 + .../recent-activity.component.html | 58 ++-- .../recent-activity.component.scss | 6 +- .../recent-activity.component.ts | 51 +++- .../overview/project-overview.component.html | 23 +- .../overview/project-overview.component.ts | 7 +- .../resource-metadata.component.html | 2 +- .../shared/mappers/activity-logs.mapper.ts | 157 +++++++++- .../activity-logs-json-api.model.ts | 71 ++++- .../activity-logs/activity-logs.model.ts | 163 ++++++++++- .../activity-log-display.service.ts | 63 ++++ .../activity-log-formatter.service.ts | 268 ++++++++++++++++++ .../activity-log-url-builder.service.ts | 148 ++++++++++ .../activity-logs.service.ts | 14 +- src/app/shared/services/index.ts | 5 +- .../activity-logs/activity-logs.model.ts | 13 +- .../activity-logs/activity-logs.selectors.ts | 11 +- .../activity-logs/activity-logs.state.ts | 20 +- src/assets/i18n/en.json | 155 +++++++++- 19 files changed, 1165 insertions(+), 72 deletions(-) create mode 100644 src/app/shared/services/activity-logs/activity-log-display.service.ts create mode 100644 src/app/shared/services/activity-logs/activity-log-formatter.service.ts create mode 100644 src/app/shared/services/activity-logs/activity-log-url-builder.service.ts rename src/app/shared/services/{ => activity-logs}/activity-logs.service.ts (55%) diff --git a/src/app/features/moderation/collection-moderation.routes.ts b/src/app/features/moderation/collection-moderation.routes.ts index 92ef78df2..9264f9c16 100644 --- a/src/app/features/moderation/collection-moderation.routes.ts +++ b/src/app/features/moderation/collection-moderation.routes.ts @@ -4,6 +4,7 @@ import { Routes } from '@angular/router'; import { CollectionsModerationState } from '@osf/features/moderation/store/collections-moderation'; import { ResourceType } from '@osf/shared/enums'; +import { ActivityLogsState } from '@shared/stores/activity-logs'; import { CollectionsState } from '@shared/stores/collections'; import { ModeratorsState } from './store/moderators'; @@ -16,6 +17,7 @@ export const collectionModerationRoutes: Routes = [ import('@osf/features/moderation/pages/collection-moderation/collection-moderation.component').then( (m) => m.CollectionModerationComponent ), + providers: [provideStates([ActivityLogsState])], children: [ { path: '', diff --git a/src/app/features/project/overview/components/recent-activity/recent-activity.component.html b/src/app/features/project/overview/components/recent-activity/recent-activity.component.html index 1b39472d6..09bacf33f 100644 --- a/src/app/features/project/overview/components/recent-activity/recent-activity.component.html +++ b/src/app/features/project/overview/components/recent-activity/recent-activity.component.html @@ -1,30 +1,36 @@ -
-

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

+
+

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

-
- - - -
- -
- - - -
+ @if (!isLoading()) { + @if (formattedActivityLogs().length) { + @for (activityLog of formattedActivityLogs(); track activityLog.id) { +
+
+ +
+ } + } @else { +
+ {{ 'project.overview.recentActivity.noActivity' | translate }} +
+ } -
-
- Jeremy Wolfe removed tag example from - Project name example + @if (totalCount() > pageSize()) { + + } + } @else { +
+ + + + +
- - -
+ }
diff --git a/src/app/features/project/overview/components/recent-activity/recent-activity.component.scss b/src/app/features/project/overview/components/recent-activity/recent-activity.component.scss index caa0a897f..800d08df0 100644 --- a/src/app/features/project/overview/components/recent-activity/recent-activity.component.scss +++ b/src/app/features/project/overview/components/recent-activity/recent-activity.component.scss @@ -7,9 +7,13 @@ color: var.$dark-blue-1; &-activity { - height: mix.rem(35px); border-bottom: 1px solid var.$grey-2; + + .activity-date { + width: 30%; + } } + &-description { line-height: mix.rem(24px); } diff --git a/src/app/features/project/overview/components/recent-activity/recent-activity.component.ts b/src/app/features/project/overview/components/recent-activity/recent-activity.component.ts index 1cf58fcdd..8a9586d9b 100644 --- a/src/app/features/project/overview/components/recent-activity/recent-activity.component.ts +++ b/src/app/features/project/overview/components/recent-activity/recent-activity.component.ts @@ -1,12 +1,57 @@ +import { createDispatchMap, select } from '@ngxs/store'; + import { TranslatePipe } from '@ngx-translate/core'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { PaginatorState } from 'primeng/paginator'; +import { Skeleton } from 'primeng/skeleton'; + +import { DatePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { CustomPaginatorComponent } from '@shared/components'; +import { ActivityLogDisplayService } from '@shared/services'; +import { ActivityLogsSelectors, GetActivityLogs } from '@shared/stores/activity-logs'; @Component({ selector: 'osf-recent-activity-list', - imports: [TranslatePipe], + imports: [TranslatePipe, Skeleton, DatePipe, CustomPaginatorComponent], templateUrl: './recent-activity.component.html', styleUrl: './recent-activity.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RecentActivityComponent {} +export class RecentActivityComponent { + private readonly activityLogDisplayService = inject(ActivityLogDisplayService); + private readonly route = inject(ActivatedRoute); + + readonly pageSize = input.required(); + protected currentPage = signal(1); + protected activityLogs = select(ActivityLogsSelectors.getActivityLogs); + protected totalCount = select(ActivityLogsSelectors.getActivityLogsTotalCount); + protected isLoading = select(ActivityLogsSelectors.getActivityLogsLoading); + protected firstIndex = computed(() => (this.currentPage() - 1) * this.pageSize()); + + protected actions = createDispatchMap({ + getActivityLogs: GetActivityLogs, + }); + + protected formattedActivityLogs = computed(() => { + const logs = this.activityLogs(); + return logs.map((log) => ({ + ...log, + formattedActivity: this.activityLogDisplayService.getActivityDisplay(log), + })); + }); + + onPageChange(event: PaginatorState) { + if (event.page !== undefined) { + const pageNumber = event.page + 1; + this.currentPage.set(pageNumber); + + const projectId = this.route.snapshot.params['id'] || this.route.parent?.snapshot.params['id']; + if (projectId) { + this.actions.getActivityLogs(projectId, pageNumber.toString(), this.pageSize().toString()); + } + } + } +} diff --git a/src/app/features/project/overview/project-overview.component.html b/src/app/features/project/overview/project-overview.component.html index 70c136e04..a9e0d5866 100644 --- a/src/app/features/project/overview/project-overview.component.html +++ b/src/app/features/project/overview/project-overview.component.html @@ -33,24 +33,35 @@ @if (status && isCollectionsRoute() && collectionProvider()) { @switch (status) { @case (SubmissionReviewStatus.Pending) { - - Pending: Pending entry into {{ collectionProvider()?.name }} + {{ 'project.overview.collectionsModeration.pending' | translate }} + + {{ collectionProvider()?.name }} } @case (SubmissionReviewStatus.Accepted) { - Accepted: Accepted entry into {{ collectionProvider()?.name }} + {{ 'project.overview.collectionsModeration.accepted' | translate }} + + {{ collectionProvider()?.name }} } @case (SubmissionReviewStatus.Rejected) { - Rejected: Rejected entry into {{ collectionProvider()?.name }} + {{ 'project.overview.collectionsModeration.rejected' | translate }} + + {{ collectionProvider()?.name }} } @case (SubmissionReviewStatus.Removed) { - Withdrawn: Entry withdrawn from {{ collectionProvider()?.name }} + {{ 'project.overview.collectionsModeration.removed' | translate }} + + {{ collectionProvider()?.name }} } } @@ -64,7 +75,7 @@ - +
diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index 4311403c6..f4fc8f9ea 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -20,7 +20,7 @@ import { } from '@angular/core'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { SubmissionReviewStatus } from '@osf/features/moderation/enums'; import { IS_XSMALL } from '@osf/shared/helpers'; @@ -84,6 +84,7 @@ import { ResourceMetadataComponent, TranslatePipe, Message, + RouterLink, ], providers: [DialogService], changeDetection: ChangeDetectionStrategy.OnPush, @@ -101,6 +102,8 @@ export class ProjectOverviewComponent implements OnInit { protected submissions = select(CollectionsModerationSelectors.getCollectionSubmissions); protected collectionProvider = select(CollectionsSelectors.getCollectionProvider); protected currentReviewAction = select(CollectionsModerationSelectors.getCurrentReviewAction); + protected readonly activityPageSize = 5; + protected readonly activityDefaultPage = 1; protected actions = createDispatchMap({ getProject: GetProjectById, @@ -197,7 +200,7 @@ export class ProjectOverviewComponent implements OnInit { this.actions.getHomeWiki(ResourceType.Project, projectId); this.actions.getComponents(projectId); this.actions.getLinkedProjects(projectId); - this.actions.getActivityLogs(projectId, '1', '5'); + this.actions.getActivityLogs(projectId, this.activityDefaultPage.toString(), this.activityPageSize.toString()); } } 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 de8675640..db28c153f 100644 --- a/src/app/shared/components/resource-metadata/resource-metadata.component.html +++ b/src/app/shared/components/resource-metadata/resource-metadata.component.html @@ -17,7 +17,7 @@

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

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

-
+
@for (contributor of resource.contributors; track contributor.id) {
{{ contributor.fullName }} diff --git a/src/app/shared/mappers/activity-logs.mapper.ts b/src/app/shared/mappers/activity-logs.mapper.ts index 9ce2fc1e4..d731424b3 100644 --- a/src/app/shared/mappers/activity-logs.mapper.ts +++ b/src/app/shared/mappers/activity-logs.mapper.ts @@ -1 +1,156 @@ -export class ActivityLogsMapper {} +import { ActivityLog, ActivityLogJsonApi, LogContributor, PaginatedData, ResponseJsonApi } from '@shared/models'; +import { LogContributorJsonApi } from '@shared/models/activity-logs/activity-logs-json-api.model'; + +export class ActivityLogsMapper { + static fromActivityLogJsonApi(log: ActivityLogJsonApi): ActivityLog { + return { + id: log.id, + type: log.type, + action: log.attributes.action, + date: log.attributes.date, + params: { + contributors: log.attributes.params.contributors.map((contributor) => this.fromContributorJsonApi(contributor)), + license: log.attributes.params.license, + tag: log.attributes.params.tag, + institution: log.attributes.params.institution, + paramsNode: { + id: log.attributes.params.params_node.id, + title: log.attributes.params.params_node.title, + }, + paramsProject: log.attributes.params.params_project, + pointer: log.attributes.params.pointer + ? { + category: log.attributes.params.pointer.category, + id: log.attributes.params.pointer.id, + title: log.attributes.params.pointer.title, + url: log.attributes.params.pointer.url, + } + : null, + preprintProvider: log.attributes.params.preprint_provider, + // Additional parameters from Ember implementation + addon: log.attributes.params.addon, + anonymousLink: log.attributes.params.anonymous_link, + file: log.attributes.params.file, + wiki: log.attributes.params.wiki, + destination: log.attributes.params.destination, + identifiers: log.attributes.params.identifiers, + kind: log.attributes.params.kind, + oldPage: log.attributes.params.old_page, + page: log.attributes.params.page, + pageId: log.attributes.params.page_id, + path: log.attributes.params.path, + urls: log.attributes.params.urls, + preprint: log.attributes.params.preprint, + source: log.attributes.params.source, + titleNew: log.attributes.params.title_new, + titleOriginal: log.attributes.params.title_original, + updatedFields: log.attributes.params.updated_fields, + value: log.attributes.params.value, + version: log.attributes.params.version, + githubUser: log.attributes.params.github_user, + }, + embeds: log.embeds + ? { + originalNode: log.embeds.original_node?.data + ? { + id: log.embeds.original_node.data.id, + type: log.embeds.original_node.data.type, + title: log.embeds.original_node.data.attributes.title, + description: log.embeds.original_node.data.attributes.description, + category: log.embeds.original_node.data.attributes.category, + customCitation: log.embeds.original_node.data.attributes.custom_citation, + dateCreated: log.embeds.original_node.data.attributes.date_created, + dateModified: log.embeds.original_node.data.attributes.date_modified, + registration: log.embeds.original_node.data.attributes.registration, + preprint: log.embeds.original_node.data.attributes.preprint, + fork: log.embeds.original_node.data.attributes.fork, + collection: log.embeds.original_node.data.attributes.collection, + tags: log.embeds.original_node.data.attributes.tags, + accessRequestsEnabled: log.embeds.original_node.data.attributes.access_requests_enabled, + nodeLicense: log.embeds.original_node.data.attributes.node_license + ? { + copyrightHolders: log.embeds.original_node.data.attributes.node_license.copyright_holders, + year: log.embeds.original_node.data.attributes.node_license.year, + } + : { copyrightHolders: [], year: null }, + currentUserCanComment: log.embeds.original_node.data.attributes.current_user_can_comment, + currentUserPermissions: log.embeds.original_node.data.attributes.current_user_permissions, + currentUserIsContributor: log.embeds.original_node.data.attributes.current_user_is_contributor, + currentUserIsContributorOrGroupMember: + log.embeds.original_node.data.attributes.current_user_is_contributor_or_group_member, + wikiEnabled: log.embeds.original_node.data.attributes.wiki_enabled, + public: log.embeds.original_node.data.attributes.public, + subjects: log.embeds.original_node.data.attributes.subjects, + } + : undefined, + user: log.embeds.user?.data + ? { + id: log.embeds.user.data.id, + type: log.embeds.user.data.type, + fullName: log.embeds.user.data.attributes.full_name, + givenName: log.embeds.user.data.attributes.given_name, + middleNames: log.embeds.user.data.attributes.middle_names, + familyName: log.embeds.user.data.attributes.family_name, + suffix: log.embeds.user.data.attributes.suffix, + dateRegistered: log.embeds.user.data.attributes.date_registered, + active: log.embeds.user.data.attributes.active, + timezone: log.embeds.user.data.attributes.timezone, + locale: log.embeds.user.data.attributes.locale, + } + : undefined, + linkedNode: log.embeds.linked_node?.data + ? { + id: log.embeds.linked_node.data.id, + type: log.embeds.linked_node.data.type, + title: log.embeds.linked_node.data.attributes.title, + description: log.embeds.linked_node.data.attributes.description, + category: log.embeds.linked_node.data.attributes.category, + customCitation: log.embeds.linked_node.data.attributes.custom_citation, + dateCreated: log.embeds.linked_node.data.attributes.date_created, + dateModified: log.embeds.linked_node.data.attributes.date_modified, + registration: log.embeds.linked_node.data.attributes.registration, + preprint: log.embeds.linked_node.data.attributes.preprint, + fork: log.embeds.linked_node.data.attributes.fork, + collection: log.embeds.linked_node.data.attributes.collection, + tags: log.embeds.linked_node.data.attributes.tags, + accessRequestsEnabled: log.embeds.linked_node.data.attributes.access_requests_enabled, + nodeLicense: log.embeds.linked_node.data.attributes.node_license + ? { + copyrightHolders: log.embeds.linked_node.data.attributes.node_license.copyright_holders, + year: log.embeds.linked_node.data.attributes.node_license.year, + } + : { copyrightHolders: [], year: null }, + currentUserCanComment: log.embeds.linked_node.data.attributes.current_user_can_comment, + currentUserPermissions: log.embeds.linked_node.data.attributes.current_user_permissions, + currentUserIsContributor: log.embeds.linked_node.data.attributes.current_user_is_contributor, + currentUserIsContributorOrGroupMember: + log.embeds.linked_node.data.attributes.current_user_is_contributor_or_group_member, + wikiEnabled: log.embeds.linked_node.data.attributes.wiki_enabled, + public: log.embeds.linked_node.data.attributes.public, + subjects: log.embeds.linked_node.data.attributes.subjects, + } + : undefined, + } + : undefined, + }; + } + + static fromGetActivityLogsResponse(logs: ResponseJsonApi): PaginatedData { + return { + data: logs.data.map((log) => this.fromActivityLogJsonApi(log)), + totalCount: logs.meta.total ?? 0, + }; + } + + private static fromContributorJsonApi(contributor: LogContributorJsonApi): LogContributor { + return { + id: contributor.id, + fullName: contributor.full_name, + givenName: contributor.given_name, + middleNames: contributor.middle_names, + familyName: contributor.family_name, + unregisteredName: contributor.unregistered_name, + active: contributor.active, + }; + } +} diff --git a/src/app/shared/models/activity-logs/activity-logs-json-api.model.ts b/src/app/shared/models/activity-logs/activity-logs-json-api.model.ts index eec05e7f9..4554cd5f3 100644 --- a/src/app/shared/models/activity-logs/activity-logs-json-api.model.ts +++ b/src/app/shared/models/activity-logs/activity-logs-json-api.model.ts @@ -5,7 +5,7 @@ export interface ActivityLogJsonApi { action: string; date: string; params: { - contributors: unknown[]; + contributors: LogContributorJsonApi[]; license?: string; tag?: string; institution?: { @@ -18,7 +18,57 @@ export interface ActivityLogJsonApi { }; params_project: null; pointer: PointerJsonApi | null; - preprint_provider: string | null; + preprint_provider?: + | string + | { + url: string; + name: string; + } + | null; + addon?: string; + anonymous_link?: boolean; + file?: { + name: string; + url: string; + }; + wiki?: { + name: string; + url: string; + }; + destination?: { + materialized: string; + addon: string; + url: string; + }; + identifiers?: { + doi?: string; + ark?: string; + }; + kind?: string; + old_page?: string; + page?: string; + page_id?: string; + path?: string; + urls?: { + view: string; + }; + preprint?: string; + source?: { + materialized: string; + addon: string; + }; + title_new?: string; + title_original?: string; + updated_fields?: Record< + string, + { + new: string; + old: string; + } + >; + value?: string; + version?: string; + github_user?: string; }; }; embeds?: { @@ -32,6 +82,9 @@ export interface ActivityLogJsonApi { data: LinkedNodeEmbedsData; }; }; + meta: { + total: number; + }; } interface PointerJsonApi { @@ -60,7 +113,7 @@ interface OriginalNodeEmbedsData { node_license: { copyright_holders: string[]; year: string | null; - }; + } | null; current_user_can_comment: boolean; current_user_permissions: string[]; current_user_is_contributor: boolean; @@ -106,7 +159,7 @@ interface LinkedNodeEmbedsData { node_license: { copyright_holders: string[]; year: string | null; - }; + } | null; current_user_can_comment: boolean; current_user_permissions: string[]; current_user_is_contributor: boolean; @@ -119,3 +172,13 @@ interface LinkedNodeEmbedsData { }[][]; }; } + +export interface LogContributorJsonApi { + id: string; + full_name: string; + given_name: string; + middle_names: string; + family_name: string; + unregistered_name: string | null; + active: boolean; +} diff --git a/src/app/shared/models/activity-logs/activity-logs.model.ts b/src/app/shared/models/activity-logs/activity-logs.model.ts index ea81dac34..34b133518 100644 --- a/src/app/shared/models/activity-logs/activity-logs.model.ts +++ b/src/app/shared/models/activity-logs/activity-logs.model.ts @@ -1,3 +1,164 @@ -export interface ActivityLogs { +export interface ActivityLog { id: string; + type: string; + action: string; + date: string; + params: { + contributors: LogContributor[]; + license?: string; + tag?: string; + institution?: { + id: string; + name: string; + }; + paramsNode: { + id: string; + title: string; + }; + paramsProject: null; + pointer: Pointer | null; + preprintProvider?: + | string + | { + url: string; + name: string; + } + | null; + addon?: string; + anonymousLink?: boolean; + file?: { + name: string; + url: string; + }; + wiki?: { + name: string; + url: string; + }; + destination?: { + materialized: string; + addon: string; + url: string; + }; + identifiers?: { + doi?: string; + ark?: string; + }; + kind?: string; + oldPage?: string; + page?: string; + pageId?: string; + path?: string; + urls?: { + view: string; + }; + preprint?: string; + source?: { + materialized: string; + addon: string; + }; + titleNew?: string; + titleOriginal?: string; + updatedFields?: Record< + string, + { + new: string; + old: string; + } + >; + value?: string; + version?: string; + githubUser?: string; + }; + embeds?: { + originalNode?: OriginalNode; + user?: User; + linkedNode?: LinkedNode; + }; +} + +interface Pointer { + category: string; + id: string; + title: string; + url: string; +} + +interface OriginalNode { + id: string; + type: string; + title: string; + description: string; + category: string; + customCitation: string | null; + dateCreated: string; + dateModified: string; + registration: boolean; + preprint: boolean; + fork: boolean; + collection: boolean; + tags: string[]; + accessRequestsEnabled: boolean; + nodeLicense: { + copyrightHolders: string[]; + year: string | null; + }; + currentUserCanComment: boolean; + currentUserPermissions: string[]; + currentUserIsContributor: boolean; + currentUserIsContributorOrGroupMember: boolean; + wikiEnabled: boolean; + public: boolean; + subjects: { id: string; text: string }[][]; +} + +interface User { + id: string; + type: string; + fullName: string; + givenName: string; + middleNames: string; + familyName: string; + suffix: string; + dateRegistered: string; + active: boolean; + timezone: string; + locale: string; +} + +interface LinkedNode { + id: string; + type: string; + title: string; + description: string; + category: string; + customCitation: string | null; + dateCreated: string; + dateModified: string; + registration: boolean; + preprint: boolean; + fork: boolean; + collection: boolean; + tags: string[]; + accessRequestsEnabled: boolean; + nodeLicense: { + copyrightHolders: string[]; + year: string | null; + }; + currentUserCanComment: boolean; + currentUserPermissions: string[]; + currentUserIsContributor: boolean; + currentUserIsContributorOrGroupMember: boolean; + wikiEnabled: boolean; + public: boolean; + subjects: { id: string; text: string }[][]; +} + +export interface LogContributor { + id: string; + fullName: string; + givenName: string; + middleNames: string; + familyName: string; + unregisteredName: string | null; + active: boolean; } diff --git a/src/app/shared/services/activity-logs/activity-log-display.service.ts b/src/app/shared/services/activity-logs/activity-log-display.service.ts new file mode 100644 index 000000000..2f37c600f --- /dev/null +++ b/src/app/shared/services/activity-logs/activity-log-display.service.ts @@ -0,0 +1,63 @@ +import { TranslateService } from '@ngx-translate/core'; + +import { inject, Injectable } from '@angular/core'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +import { ActivityLog } from '@shared/models'; + +import { ActivityLogFormatterService } from './activity-log-formatter.service'; + +@Injectable({ + providedIn: 'root', +}) +export class ActivityLogDisplayService { + private readonly translateService = inject(TranslateService); + private readonly sanitizer = inject(DomSanitizer); + private readonly formatter = inject(ActivityLogFormatterService); + + getActivityDisplay(log: ActivityLog): SafeHtml { + const translationKey = `activityLog.activities.${log.action}`; + const translationParams = this.buildTranslationParams(log); + + const translation = this.translateService.instant(translationKey, translationParams); + const finalTranslation = translation === translationKey ? this.formatter.buildFallbackMessage(log) : translation; + + const htmlContent = `${finalTranslation}`; + return this.sanitizer.bypassSecurityTrustHtml(htmlContent); + } + + private buildTranslationParams(log: ActivityLog): Record { + return { + addon: log.params.addon, + anonymousLink: this.formatter.buildAnonymous(log), + commentLocation: this.formatter.buildCommentLocation(log), + contributors: this.formatter.buildContributorsList(log), + destination: this.formatter.buildDestination(log), + forkedFrom: this.formatter.buildNode(log), + identifiers: this.formatter.buildIdentifiers(log), + institution: this.formatter.buildInstitution(log), + kind: log.params.kind, + license: log.params.license, + node: this.formatter.buildNode(log), + oldPage: this.formatter.buildOldPage(log), + page: this.formatter.buildPage(log), + path: this.formatter.buildPath(log), + pathType: this.formatter.buildPathType(log), + pointer: this.formatter.buildEmbeddedNode(log), + pointerCategory: this.formatter.getPointerCategory(log), + preprint: this.formatter.buildPreprint(log), + preprintProvider: this.formatter.buildPreprintProvider(log), + preprintWord: this.translateService.instant('activityLog.defaults.preprint'), + preprintWordPlural: this.translateService.instant('activityLog.defaults.preprintPlural'), + source: this.formatter.buildSource(log), + tag: this.formatter.buildTag(log), + template: this.formatter.buildEmbeddedNode(log), + titleNew: this.formatter.buildTitleNew(log), + titleOriginal: this.formatter.buildTitleOriginal(log), + updatedFields: this.formatter.buildUpdatedFields(log), + user: this.formatter.buildUser(log), + value: log.params.value, + version: this.formatter.buildVersion(log), + }; + } +} diff --git a/src/app/shared/services/activity-logs/activity-log-formatter.service.ts b/src/app/shared/services/activity-logs/activity-log-formatter.service.ts new file mode 100644 index 000000000..20c552cef --- /dev/null +++ b/src/app/shared/services/activity-logs/activity-log-formatter.service.ts @@ -0,0 +1,268 @@ +import { TranslateService } from '@ngx-translate/core'; + +import { inject, Injectable } from '@angular/core'; + +import { ActivityLog } from '@shared/models'; + +import { ActivityLogUrlBuilderService } from './activity-log-url-builder.service'; + +@Injectable({ + providedIn: 'root', +}) +export class ActivityLogFormatterService { + private readonly translateService = inject(TranslateService); + private readonly urlBuilder = inject(ActivityLogUrlBuilderService); + + private readonly nodeCategories = [ + 'analysis', + 'communication', + 'data', + 'hypothesis', + 'instrumentation', + 'methods and measures', + 'procedure', + 'project', + 'software', + 'other', + ]; + + buildAnonymous(log: ActivityLog): string { + return log.params.anonymousLink + ? this.translateService.instant('activityLog.defaults.anonymousAn') + : this.translateService.instant('activityLog.defaults.anonymousA'); + } + + buildCommentLocation(log: ActivityLog): string { + const file = log.params.file; + const wiki = log.params.wiki; + + if (file) { + return this.translateService.instant('activityLog.defaults.fileOn', { + file: this.urlBuilder.buildAHrefElement(`/${file.url}`, file.name), + }); + } + + if (wiki) { + return this.translateService.instant('activityLog.defaults.wikiOn', { + wiki: this.urlBuilder.buildAHrefElement(`/${wiki.url}`, wiki.name), + }); + } + + return ''; + } + + buildContributorsList(log: ActivityLog): string { + if (!log.params.contributors || log.params.contributors.length === 0) { + return this.translateService.instant('activityLog.defaults.someUsers'); + } + + const contributors = log.params.contributors; + const maxShown = 3; + const contribList: string[] = []; + const isJustOneMore = contributors.length === maxShown + 1; + + for (let i = 0; i < contributors.length; i++) { + const contributor = contributors[i]; + let separator = ''; + + if (i < contributors.length - 1) { + separator = + i === maxShown - 1 && !isJustOneMore + ? this.translateService.instant('activityLog.defaults.contributorsAnd') + : ', '; + } + + if (i === maxShown && !isJustOneMore) { + const remainingCount = contributors.length - i; + const othersText = this.translateService.instant('activityLog.defaults.contributorsOthers'); + contribList.push(`${remainingCount}${othersText}`); + break; + } + + const displayName = contributor.active + ? `${contributor.fullName}` + : contributor.unregisteredName || contributor.fullName; + + contribList.push(`${displayName}${separator}`); + } + + return contribList.join(' '); + } + + buildDestination(log: ActivityLog): string { + if (!log.params.destination) { + return this.translateService.instant('activityLog.defaults.aNewNameLocation'); + } + + const destination = log.params.destination; + let materialized = destination.materialized; + + if (materialized.endsWith('/')) { + materialized = this.replaceSlash(destination.materialized); + return this.translateService.instant('activityLog.defaults.materialized', { + materialized, + addon: destination.addon, + }); + } else { + return this.translateService.instant('activityLog.defaults.materialized', { + materialized: this.urlBuilder.buildAHrefElement(destination.url, materialized), + addon: destination.addon, + }); + } + } + + buildIdentifiers(log: ActivityLog): string { + if (!log.params.identifiers) { + return ''; + } + + const doi = log.params.identifiers.doi; + const ark = log.params.identifiers.ark; + + if (doi && ark) { + return `doi:${doi} and ark:${ark}`; + } else if (doi) { + return `doi:${doi}`; + } else if (ark) { + return `ark:${ark}`; + } + + return ''; + } + + buildOldPage(log: ActivityLog): string { + return log.params.oldPage ? log.params.oldPage : this.translateService.instant('activityLog.defaults.pageTitle'); + } + + buildPage(log: ActivityLog): string { + if (!log.params.page) { + return this.translateService.instant('activityLog.defaults.pageTitle'); + } + + return this.urlBuilder.buildPageUrl(log, log.params.page); + } + + buildPath(log: ActivityLog): string { + if (!log.params.path) { + return this.translateService.instant('activityLog.defaults.aFile'); + } + + const path = this.replaceSlash(log.params.path); + return this.urlBuilder.buildFileUrl(log, path); + } + + buildPathType(log: ActivityLog): string { + if (!log.params.path) { + return ''; + } + + return log.params.path[0] === '/' + ? this.translateService.instant('activityLog.defaults.folder') + : this.translateService.instant('activityLog.defaults.file'); + } + + buildSource(log: ActivityLog): string { + if (!log.params.source) { + return this.translateService.instant('activityLog.defaults.aNameLocation'); + } + + const source = log.params.source; + const materialized = this.replaceSlash(source.materialized); + + return this.translateService.instant('activityLog.defaults.materialized', { + materialized, + addon: source.addon, + }); + } + + buildTitleNew(log: ActivityLog): string { + const url = this.urlBuilder.buildTitleUrl(log, log.params.titleNew); + return url || this.translateService.instant('activityLog.defaults.aTitle'); + } + + buildTitleOriginal(log: ActivityLog): string { + const url = this.urlBuilder.buildTitleUrl(log, log.params.titleOriginal); + return url || this.translateService.instant('activityLog.defaults.aTitle'); + } + + buildUpdatedFields(log: ActivityLog): string { + if (!log.params.updatedFields) { + return this.translateService.instant('activityLog.defaults.field'); + } + + const updatedFieldsParam = log.params.updatedFields; + const updatedField = Object.keys(updatedFieldsParam)[0]; + + if (updatedField === 'category') { + const newValue = updatedFieldsParam[updatedField].new; + const newText = this.nodeCategories.includes(newValue) + ? this.translateService.instant(`node_categories.${newValue}`) + : this.translateService.instant('activityLog.defaults.uncategorized'); + + return this.translateService.instant('activityLog.defaults.updatedFields', { + old: updatedField, + new: newText, + }); + } + + return this.translateService.instant('activityLog.defaults.updatedFields', { + old: updatedField, + new: updatedFieldsParam[updatedField].new, + }); + } + + buildVersion(log: ActivityLog): string { + return log.params.version || '#'; + } + + getPointerCategory(log: ActivityLog): string { + const linkedNode = log.embeds?.linkedNode; + return linkedNode?.category || ''; + } + + buildUser(log: ActivityLog): string { + const userUrl = this.urlBuilder.buildUserUrl(log); + return userUrl || this.translateService.instant('activityLog.defaults.aUser'); + } + + buildNode(log: ActivityLog): string { + return this.urlBuilder.buildNodeUrl(log); + } + + buildEmbeddedNode(log: ActivityLog): string { + const url = this.urlBuilder.buildEmbeddedUrl(log); + return url || this.translateService.instant('activityLog.defaults.aTitle'); + } + + buildInstitution(log: ActivityLog): string { + return this.urlBuilder.buildInstitutionUrl(log); + } + + buildTag(log: ActivityLog): string { + return this.urlBuilder.buildTagUrl(log); + } + + buildPreprint(log: ActivityLog): string { + const preprintWord = this.translateService.instant('activityLog.defaults.preprint'); + return this.urlBuilder.buildPreprintUrl(log, preprintWord); + } + + buildPreprintProvider(log: ActivityLog): string { + return this.urlBuilder.buildPreprintProviderUrl(log); + } + + buildFallbackMessage(log: ActivityLog): string { + const user = this.buildUser(log); + const node = this.buildNode(log); + const action = log.action.replace(/_/g, ' '); + + if (node) { + return `${user} performed action "${action}" on ${node}`; + } + return `${user} performed action "${action}"`; + } + + private replaceSlash(path: string): string { + return path.replace(/^\/|\/$/g, ''); + } +} diff --git a/src/app/shared/services/activity-logs/activity-log-url-builder.service.ts b/src/app/shared/services/activity-logs/activity-log-url-builder.service.ts new file mode 100644 index 000000000..cc1b70e03 --- /dev/null +++ b/src/app/shared/services/activity-logs/activity-log-url-builder.service.ts @@ -0,0 +1,148 @@ +import { Injectable } from '@angular/core'; + +import { ActivityLog } from '@shared/models'; + +@Injectable({ + providedIn: 'root', +}) +export class ActivityLogUrlBuilderService { + buildAHrefElement(url: string | undefined, value: string): string { + const safeUrl = url || ''; + const relativeUrl = this.toRelativeUrl(safeUrl); + return `${value}`; + } + + buildUserUrl(log: ActivityLog): string { + const user = log.embeds?.user; + const githubUser = log.params.githubUser; + + if (user?.id) { + return this.buildAHrefElement(`/${user.id}`, user.fullName); + } else if (user?.fullName) { + return user.fullName; + } else if (githubUser) { + return githubUser; + } + + return ''; + } + + buildNodeUrl(log: ActivityLog): string { + if (!log.params.paramsNode) { + return ''; + } + + return this.buildAHrefElement(`project/${log.params.paramsNode.id}`, log.params.paramsNode.title); + } + + buildInstitutionUrl(log: ActivityLog): string { + if (!log.params.institution) { + return ''; + } + + return this.buildAHrefElement(`/institutions/${log.params.institution.id}`, log.params.institution.name); + } + + buildTagUrl(log: ActivityLog): string { + if (!log.params.tag) { + return ''; + } + + return this.buildAHrefElement(`/search?search=%22${log.params.tag}%22`, log.params.tag); + } + + buildPreprintUrl(log: ActivityLog, preprintWord: string): string { + if (!log.params.preprint) { + return ''; + } + + return this.buildAHrefElement(`preprints/${log.params.preprint}`, preprintWord); + } + + buildPreprintProviderUrl(log: ActivityLog): string { + if (!log.params.preprintProvider) { + return ''; + } + + if (typeof log.params.preprintProvider === 'string') { + return log.params.preprintProvider; + } + + return this.buildAHrefElement( + `preprints/overview/${log.params.preprintProvider.url}`, + log.params.preprintProvider.name + ); + } + + buildTitleUrl(log: ActivityLog, title: string | undefined): string { + const originalNode = log.embeds?.originalNode; + if (originalNode?.id && title) { + return this.buildAHrefElement(`/${originalNode.id}`, title); + } + return ''; + } + + buildEmbeddedUrl(log: ActivityLog): string { + const linkedNode = log.embeds?.linkedNode; + const originalNode = log.embeds?.originalNode; + + if (linkedNode?.id) { + return this.buildAHrefElement(`project/${linkedNode.id}`, linkedNode.title); + } else if (originalNode?.id) { + return this.buildAHrefElement(`project/${originalNode.id}`, originalNode.title); + } + return ''; + } + + buildFileUrl(log: ActivityLog, path: string): string { + const acceptableLinkedItems = [ + 'osf_storage_file_added', + 'osf_storage_file_updated', + 'file_tag_added', + 'file_tag_removed', + 'github_file_added', + 'github_file_updated', + 'box_file_added', + 'box_file_updated', + 'dropbox_file_added', + 'dropbox_file_updated', + 's3_file_added', + 's3_file_updated', + 'figshare_file_added', + 'checked_in', + 'checked_out', + 'file_metadata_updated', + ]; + + if (acceptableLinkedItems.includes(log.action) && log.params.urls?.view) { + return this.buildAHrefElement(log.params.urls.view, path); + } + + return path; + } + + buildPageUrl(log: ActivityLog, page: string): string { + const acceptableLinkedItems = ['wiki_updated', 'wiki_renamed']; + if (acceptableLinkedItems.includes(log.action) && log.params.pageId) { + return this.buildAHrefElement(`/${log.params.pageId}`, page); + } + + return page; + } + + private toRelativeUrl(url: string): string { + if (!url) return ''; + + try { + const parser = document.createElement('a'); + parser.href = url; + + if (window.location.hostname === parser.hostname) { + return parser.pathname + parser.search + parser.hash; + } + return url; + } catch { + return url; + } + } +} diff --git a/src/app/shared/services/activity-logs.service.ts b/src/app/shared/services/activity-logs/activity-logs.service.ts similarity index 55% rename from src/app/shared/services/activity-logs.service.ts rename to src/app/shared/services/activity-logs/activity-logs.service.ts index 951458c4d..bc215eac1 100644 --- a/src/app/shared/services/activity-logs.service.ts +++ b/src/app/shared/services/activity-logs/activity-logs.service.ts @@ -3,11 +3,11 @@ import { map } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; -import { JsonApiResponseWithPaging } from '@core/models'; -import { JsonApiService } from '@core/services'; -import { ActivityLogJsonApi } from '@shared/models'; +import { ActivityLogsMapper } from '@shared/mappers/activity-logs.mapper'; +import { ActivityLog, ActivityLogJsonApi, PaginatedData, ResponseJsonApi } from '@shared/models'; +import { JsonApiService } from '@shared/services/json-api.service'; -import { environment } from 'src/environments/environment'; +import { environment } from '../../../../environments/environment'; @Injectable({ providedIn: 'root', @@ -15,7 +15,7 @@ import { environment } from 'src/environments/environment'; export class ActivityLogsService { private jsonApiService = inject(JsonApiService); - fetchLogs(projectId: string, page = '1', pageSize: string): Observable { + fetchLogs(projectId: string, page = '1', pageSize: string): Observable> { const url = `${environment.apiUrl}/nodes/${projectId}/logs/`; const params: Record = { 'embed[]': ['original_node', 'user', 'linked_node', 'linked_registration', 'template_node', 'group'], @@ -23,9 +23,9 @@ export class ActivityLogsService { 'page[size]': pageSize, }; - return this.jsonApiService.get>(url, params).pipe( + return this.jsonApiService.get>(url, params).pipe( map((res) => { - return res.data; + return ActivityLogsMapper.fromGetActivityLogsResponse(res); }) ); } diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index 7b24d29ea..04ad4ab1a 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -1,4 +1,7 @@ -export { ActivityLogsService } from './activity-logs.service'; +export { ActivityLogDisplayService } from './activity-logs/activity-log-display.service'; +export { ActivityLogFormatterService } from './activity-logs/activity-log-formatter.service'; +export { ActivityLogUrlBuilderService } from './activity-logs/activity-log-url-builder.service'; +export { ActivityLogsService } from './activity-logs/activity-logs.service'; export * from './addons'; export { BookmarksService } from './bookmarks.service'; export { BrandService } from './brand.service'; diff --git a/src/app/shared/stores/activity-logs/activity-logs.model.ts b/src/app/shared/stores/activity-logs/activity-logs.model.ts index 2fa209bdb..23c480ff0 100644 --- a/src/app/shared/stores/activity-logs/activity-logs.model.ts +++ b/src/app/shared/stores/activity-logs/activity-logs.model.ts @@ -1,5 +1,14 @@ -import { ActivityLogJsonApi, AsyncStateModel } from '@shared/models'; +import { ActivityLog, AsyncStateWithTotalCount } from '@shared/models'; export interface ActivityLogsStateModel { - activityLogs: AsyncStateModel; + activityLogs: AsyncStateWithTotalCount; } + +export const ACTIVITY_LOGS_STATE_DEFAULT: ActivityLogsStateModel = { + activityLogs: { + data: [], + isLoading: false, + error: null, + totalCount: 0, + }, +}; diff --git a/src/app/shared/stores/activity-logs/activity-logs.selectors.ts b/src/app/shared/stores/activity-logs/activity-logs.selectors.ts index 0dbd2bf14..61e746506 100644 --- a/src/app/shared/stores/activity-logs/activity-logs.selectors.ts +++ b/src/app/shared/stores/activity-logs/activity-logs.selectors.ts @@ -1,18 +1,23 @@ import { Selector } from '@ngxs/store'; -import { ActivityLogJsonApi } from '@shared/models'; +import { ActivityLog } from '@shared/models'; import { ActivityLogsStateModel } from './activity-logs.model'; import { ActivityLogsState } from './activity-logs.state'; export class ActivityLogsSelectors { @Selector([ActivityLogsState]) - static getActivityLogs(state: ActivityLogsStateModel): ActivityLogJsonApi[] { + static getActivityLogs(state: ActivityLogsStateModel): ActivityLog[] { return state.activityLogs.data; } @Selector([ActivityLogsState]) - static getLoading(state: ActivityLogsStateModel): boolean { + static getActivityLogsTotalCount(state: ActivityLogsStateModel): number { + return state.activityLogs.totalCount; + } + + @Selector([ActivityLogsState]) + static getActivityLogsLoading(state: ActivityLogsStateModel): boolean { return state.activityLogs.isLoading; } } diff --git a/src/app/shared/stores/activity-logs/activity-logs.state.ts b/src/app/shared/stores/activity-logs/activity-logs.state.ts index 7b1f64f89..0ac91e5c6 100644 --- a/src/app/shared/stores/activity-logs/activity-logs.state.ts +++ b/src/app/shared/stores/activity-logs/activity-logs.state.ts @@ -7,19 +7,11 @@ import { inject, Injectable } from '@angular/core'; import { ActivityLogsService } from '@shared/services'; import { ClearActivityLogsStore, GetActivityLogs } from './activity-logs.actions'; -import { ActivityLogsStateModel } from './activity-logs.model'; - -const defaultState: ActivityLogsStateModel = { - activityLogs: { - data: [], - isLoading: false, - error: null, - }, -}; +import { ACTIVITY_LOGS_STATE_DEFAULT, ActivityLogsStateModel } from './activity-logs.model'; @State({ name: 'activityLogs', - defaults: defaultState, + defaults: ACTIVITY_LOGS_STATE_DEFAULT, }) @Injectable() export class ActivityLogsState { @@ -32,16 +24,18 @@ export class ActivityLogsState { data: [], isLoading: true, error: null, + totalCount: 0, }, }); return this.activityLogsService.fetchLogs(action.projectId, action.page, action.pageSize).pipe( - tap((data) => { + tap((res) => { ctx.patchState({ activityLogs: { - data: data, + data: res.data, isLoading: false, error: null, + totalCount: res.totalCount, }, }); }) @@ -50,6 +44,6 @@ export class ActivityLogsState { @Action(ClearActivityLogsStore) clearActivityLogsStore(ctx: StateContext) { - ctx.setState(defaultState); + ctx.setState(ACTIVITY_LOGS_STATE_DEFAULT); } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 36af20d73..e53e21900 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -566,7 +566,14 @@ "noLinkedProjectsMessage": "Link your project." }, "recentActivity": { - "title": "Recent Activity" + "title": "Recent Activity", + "noActivity": "No recent activity" + }, + "collectionsModeration": { + "pending": "Pending entry into\u00A0", + "accepted": "Included in\u00A0", + "removed": "Removed from\u00A0", + "rejected": "Rejected from\u00A0" }, "metadata": { "title": "Metadata", @@ -2518,5 +2525,151 @@ "noCurrentResource": "No current resource.", "doiValidation": "Please enter a valid DOI in the format: 10.xxxx/xxxxx" } + }, + "activityLog": { + "defaults": { + "preprint": "preprint", + "preprintPlural": "preprints", + "anonymousA": "a", + "anonymousAn": "an", + "someUsers": "some users", + "contributorsAnd": " and ", + "contributorsOthers": " others", + "aNewNameLocation": "a new name/location", + "materialized": "{{materialized}} {{addon}}", + "aTitle": "a title", + "aNameLocation": "a name/location", + "pageTitle": "page title", + "aFile": "a file", + "folder": "folder", + "file": "file", + "aUser": "a user", + "fileOn": "file {{file}}", + "wikiOn": "wiki {{wiki}}", + "field": "field", + "updatedFields": "{{old}} to {{new}}", + "uncategorized": "Uncategorized" + }, + "activities": { + "addon_added": "{{user}} added addon {{addon}} to {{node}}", + "addon_file_copied": "{{user}} copied {{source}} to {{destination}} in {{node}}", + "addon_file_moved": "{{user}} moved {{source}} to {{destination}} in {{node}}", + "addon_file_renamed": "{{user}} renamed {{source}} to {{destination}} in {{node}}", + "addon_removed": "{{user}} removed addon {{addon}} from {{node}}", + "affiliated_institution_added": "{{user}} added {{institution}} affiliation to {{node}}", + "affiliated_institution_removed": "{{user}} removed {{institution}} affiliation from {{node}}", + "article_doi_updated": "{{user}} changed the article_doi of {{node}}", + "category_updated": "{{user}} changed the category of {{node}}", + "checked_in": "{{user}} checked in {{kind}} {{path}} to {{node}}", + "checked_out": "{{user}} checked out {{kind}} {{path}} from {{node}}", + "coi_statement_updated": "{{user}} changed the conflict of interest statement for {{preprint}}.", + "comment_added": "{{user}} added a comment {{commentLocation}} in {{node}}", + "comment_removed": "{{user}} deleted a comment {{commentLocation}} in {{node}}", + "comment_restored": "{{user}} restored a comment {{commentLocation}} in {{node}}", + "comment_updated": "{{user}} updated a comment {{commentLocation}} in {{node}}", + "contributor_added": "{{user}} added {{contributors}} as contributor(s) to {{node}}", + "contributor_removed": "{{user}} removed {{contributors}} as contributor(s) from {{node}}", + "contributors_reordered": "{{user}} reordered contributors for {{node}}", + "created_from": "{{user}} created {{node}} based on {{template}}", + "custom_citation_added": "{{user}} created a custom citation for {{node}}", + "custom_citation_edited": "{{user}} edited a custom citation for {{node}}", + "custom_citation_removed": "{{user}} removed a custom citation from {{node}}", + "data_links_updated": "{{user}} has updated their data links", + "edit_description": "{{user}} edited description of {{node}}", + "edit_title": "{{user}} changed the title from {{titleOriginal}} to {{titleNew}}", + "embargo_approved": "{{user}} approved embargoed registration of {{node}}", + "embargo_approved_no_user": "Embargo of registration of {{node}} approved", + "embargo_cancelled": "{{user}} cancelled embargoed registration of {{node}}", + "embargo_completed": "{{user}} completed embargo of {{node}}", + "embargo_completed_no_user": "Embargo for {{node}} completed", + "embargo_initiated": "{{user}} initiated an embargoed registration of {{node}}", + "embargo_terminated": "Embargo for {{node}} ended", + "external_ids_added": "{{user}} created external identifier(s) {{identifiers}} on {{node}}", + "external_registration_created": "A registration of {{node}} was created on an external registry.", + "external_registration_imported": "A registration of {{node}} was imported to OSF from an external registry.", + "file_added": "{{user}} added file {{path}} to {{node}}", + "file_metadata_updated": "{{user}} updated file metadata for {{path}}", + "file_removed": "{{user}} removed {{pathType}} {{path}} from {{node}}", + "file_restored": "{{user}} restored file {{path}} from {{node}}", + "file_tag_added": "{{user}} added tag {{tag}} to {{path}} in OSF Storage in {{node}}", + "file_tag_removed": "{{user}} removed tag {{tag}} from {{path}} in OSF Storage in {{node}}", + "file_updated": "{{user}} updated file in {{node}}", + "folder_created": "{{user}} created a folder in {{node}}", + "group_added": "{{user}} added {{group}} to {{node}}", + "group_removed": "{{user}} removed {{group}} from {{node}}", + "group_updated": "{{user}} changed {{group}} permissions to {{node}}", + "guid_metadata_updated": "{{user}} updated metadata for {{node}}", + "has_coi_updated": "{{user}} changed the conflict of interest statement availability for {{preprint}}.", + "has_data_links_updated": "{{user}} has updated the has links to data field to {{value}}", + "has_prereg_links_updated": "{{user}} has updated their preregistration data link availability to {{value}}", + "license_changed": "{{user}} updated the license of {{node}} to {{license}}", + "made_contributor_invisible": "{{user}} made bibliographic contributor {{contributors}} a non-bibliographic contributor on {{node}}", + "made_contributor_visible": "{{user}} made non-bibliographic contributor {{contributors}} a bibliographic contributor on {{node}}", + "made_private": "{{user}} made {{node}} private", + "made_public": "{{user}} made {{node}} public", + "made_public_no_user": "{{node}} made public", + "made_wiki_private": "{{user}} made the wiki of {{node}} privately editable", + "made_wiki_public": "{{user}} made the wiki of {{node}} publicly editable", + "migrated_quickfiles": "{{user}} had their QuickFiles migrated into {{node}}", + "node_access_requests_disabled": "{{user}} disabled access requests for {{node}}", + "node_access_requests_enabled": "{{user}} enabled access requests for {{node}}", + "node_created": "{{user}} created {{node}}", + "node_forked": "{{user}} created fork from {{forkedFrom}}", + "node_removed": "{{user}} removed {{node}}", + "osf_storage_file_added": "{{user}} added file {{path}} to OSF Storage in {{node}}", + "osf_storage_file_removed": "{{user}} removed {{pathType}} {{path}} from OSF Storage in {{node}}", + "osf_storage_file_updated": "{{user}} updated file {{path}} in OSF Storage in {{node}}", + "osf_storage_folder_created": "{{user}} created folder {{path}} in OSF Storage in {{node}}", + "permissions_updated": "{{user}} changed permissions for {{node}}", + "pointer_created": "{{user}} created a link to {{pointerCategory}} {{pointer}}", + "pointer_forked": "{{user}} forked a link to {{pointerCategory}} {{pointer}}", + "pointer_removed": "{{user}} removed a link to {{pointerCategory}} {{pointer}}", + "preprint_file_updated": "{{user}} updated the primary file of this {{preprint}} on {{preprintProvider}} {{preprintWordPlural}}", + "preprint_initiated": "{{user}} made {{node}} a {{preprint}} on {{preprintProvider}} {{preprintWordPlural}}", + "preprint_license_updated": "{{user}} updated the license of this {{preprint}} on {{preprintProvider}} {{preprintWordPlural}} {{license}}", + "prereg_links_info_updated": "{{user}} has updated their preregistration links to {{value}}", + "prereg_links_updated": "{{user}} has updated their preregistration data links", + "prereg_registration_initiated": "{{user}} submitted for review to the Preregistration Challenge a registration of {{node}}", + "project_created": "{{user}} created {{node}}", + "project_created_from_draft_reg": "{{node}} was created from a draft registration", + "project_deleted": "{{user}} deleted {{node}}", + "project_registered": "{{user}} registered {{node}}", + "project_registered_no_user": "{{node}} registered", + "registration_approved": "{{user}} approved a registration of {{node}}", + "registration_approved_no_user": "Registration of {{node}} was approved", + "registration_cancelled": "{{user}} cancelled a registration of {{node}}", + "registration_initiated": "{{user}} initiated a registration of {{node}}", + "resource_identifier_added": "{{user}} has added a Resource to Registration {{node}}", + "resource_identifier_removed": "{{user}} has removed a Resource to Registration {{node}}", + "resource_identifier_updated": "{{user}} has updated a Resource to Registration {{node}}", + "retraction_approved": "{{user}} approved a withdrawal of a registration of {{node}}", + "retraction_approved_no_user": "A withdrawal of a registration of {{node}} was approved", + "retraction_cancelled": "{{user}} cancelled withdrawal of a registration of {{node}}", + "retraction_initiated": "{{user}} initiated withdrawal of a registration of {{node}}", + "retraction_initiated_no_user": "A withdrawal of a registration of {{node}} was proposed", + "subjects_updated": "{{user}} updated the subjects on {{node}}", + "tag_added": "{{user}} added tag {{tag}} to {{node}}", + "tag_removed": "{{user}} removed tag {{tag}} from {{node}}", + "updated_fields": "{{user}} changed the {{updatedFields}} for {{node}}", + "view_only_link_added": "{{user}} created {{anonymousLink}} view-only link to {{node}}", + "view_only_link_removed": "{{user}} removed {{anonymousLink}} view-only link to {{node}}", + "why_no_data_updated": "{{user}} has updated their data statement", + "why_no_prereg_updated": "{{user}} has updated their preregistration data availability statement", + "wiki_deleted": "{{user}} deleted wiki page {{page}} of {{node}}", + "wiki_renamed": "{{user}} renamed wiki page {{oldPage}} to {{page}} of {{node}}", + "wiki_updated": "{{user}} updated wiki page {{page}} to version {{version}} of {{node}}" + } + }, + "node_categories": { + "analysis": "Analysis", + "communication": "Communication", + "data": "Data", + "hypothesis": "Hypothesis", + "instrumentation": "Instrumentation", + "methods and measures": "Methods and Measures", + "procedure": "Procedure", + "project": "Project", + "software": "Software", + "other": "Other" } } From 063e28a2db65afc2cc19bc7ad95394e229d19aa3 Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Mon, 18 Aug 2025 16:19:41 +0300 Subject: [PATCH 4/5] feat(project-recent-activity): fixed bookmarks bug --- .../activity-logs/activity-log-url-builder.service.ts | 5 +++-- src/app/shared/services/activity-logs/index.ts | 4 ++++ src/app/shared/services/bookmarks.service.ts | 8 ++++---- src/app/shared/services/index.ts | 5 +---- src/assets/i18n/en.json | 2 +- 5 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 src/app/shared/services/activity-logs/index.ts diff --git a/src/app/shared/services/activity-logs/activity-log-url-builder.service.ts b/src/app/shared/services/activity-logs/activity-log-url-builder.service.ts index cc1b70e03..5148a0acd 100644 --- a/src/app/shared/services/activity-logs/activity-log-url-builder.service.ts +++ b/src/app/shared/services/activity-logs/activity-log-url-builder.service.ts @@ -123,8 +123,9 @@ export class ActivityLogUrlBuilderService { buildPageUrl(log: ActivityLog, page: string): string { const acceptableLinkedItems = ['wiki_updated', 'wiki_renamed']; - if (acceptableLinkedItems.includes(log.action) && log.params.pageId) { - return this.buildAHrefElement(`/${log.params.pageId}`, page); + const projectId = log.embeds?.originalNode?.id; + if (acceptableLinkedItems.includes(log.action) && log.params.pageId && projectId) { + return this.buildAHrefElement(`project/${projectId}/wiki/?wiki=${log.params.pageId}`, page); } return page; diff --git a/src/app/shared/services/activity-logs/index.ts b/src/app/shared/services/activity-logs/index.ts new file mode 100644 index 000000000..af659c147 --- /dev/null +++ b/src/app/shared/services/activity-logs/index.ts @@ -0,0 +1,4 @@ +export { ActivityLogDisplayService } from './activity-log-display.service'; +export { ActivityLogFormatterService } from './activity-log-formatter.service'; +export { ActivityLogUrlBuilderService } from './activity-log-url-builder.service'; +export { ActivityLogsService } from './activity-logs.service'; diff --git a/src/app/shared/services/bookmarks.service.ts b/src/app/shared/services/bookmarks.service.ts index 0c8f20611..6216fa276 100644 --- a/src/app/shared/services/bookmarks.service.ts +++ b/src/app/shared/services/bookmarks.service.ts @@ -33,7 +33,7 @@ export class BookmarksService { const payload = { data: [ { - type: 'linked_nodes', + type: 'nodes', id: projectId, }, ], @@ -47,7 +47,7 @@ export class BookmarksService { const payload = { data: [ { - type: 'linked_nodes', + type: 'nodes', id: projectId, }, ], @@ -61,7 +61,7 @@ export class BookmarksService { const payload = { data: [ { - type: 'linked_registrations', + type: 'registrations', id: registryId, }, ], @@ -75,7 +75,7 @@ export class BookmarksService { const payload = { data: [ { - type: 'linked_registrations', + type: 'registrations', id: registryId, }, ], diff --git a/src/app/shared/services/index.ts b/src/app/shared/services/index.ts index 04ad4ab1a..1d71fa2f4 100644 --- a/src/app/shared/services/index.ts +++ b/src/app/shared/services/index.ts @@ -1,7 +1,4 @@ -export { ActivityLogDisplayService } from './activity-logs/activity-log-display.service'; -export { ActivityLogFormatterService } from './activity-logs/activity-log-formatter.service'; -export { ActivityLogUrlBuilderService } from './activity-logs/activity-log-url-builder.service'; -export { ActivityLogsService } from './activity-logs/activity-logs.service'; +export * from './activity-logs'; export * from './addons'; export { BookmarksService } from './bookmarks.service'; export { BrandService } from './brand.service'; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index e53e21900..df4d8030d 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -2543,7 +2543,7 @@ "aFile": "a file", "folder": "folder", "file": "file", - "aUser": "a user", + "aUser": "A user", "fileOn": "file {{file}}", "wikiOn": "wiki {{wiki}}", "field": "field", From 3bc10d4a05944a2815be03f481bfdbc4ab3af34b Mon Sep 17 00:00:00 2001 From: Roman Nastyuk Date: Tue, 19 Aug 2025 14:46:59 +0300 Subject: [PATCH 5/5] feat(project-recent-activity): fixed comments --- src/app/shared/mappers/activity-logs.mapper.ts | 1 - .../activity-logs/activity-log-formatter.service.ts | 13 ++++++++++--- .../services/activity-logs/activity-logs.service.ts | 10 ++++------ src/assets/i18n/en.json | 8 +++++--- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/app/shared/mappers/activity-logs.mapper.ts b/src/app/shared/mappers/activity-logs.mapper.ts index d731424b3..0194cf38b 100644 --- a/src/app/shared/mappers/activity-logs.mapper.ts +++ b/src/app/shared/mappers/activity-logs.mapper.ts @@ -27,7 +27,6 @@ export class ActivityLogsMapper { } : null, preprintProvider: log.attributes.params.preprint_provider, - // Additional parameters from Ember implementation addon: log.attributes.params.addon, anonymousLink: log.attributes.params.anonymous_link, file: log.attributes.params.file, diff --git a/src/app/shared/services/activity-logs/activity-log-formatter.service.ts b/src/app/shared/services/activity-logs/activity-log-formatter.service.ts index 20c552cef..909e397f9 100644 --- a/src/app/shared/services/activity-logs/activity-log-formatter.service.ts +++ b/src/app/shared/services/activity-logs/activity-log-formatter.service.ts @@ -196,7 +196,7 @@ export class ActivityLogFormatterService { if (updatedField === 'category') { const newValue = updatedFieldsParam[updatedField].new; const newText = this.nodeCategories.includes(newValue) - ? this.translateService.instant(`node_categories.${newValue}`) + ? this.translateService.instant(`nodeCategories.${newValue}`) : this.translateService.instant('activityLog.defaults.uncategorized'); return this.translateService.instant('activityLog.defaults.updatedFields', { @@ -257,9 +257,16 @@ export class ActivityLogFormatterService { const action = log.action.replace(/_/g, ' '); if (node) { - return `${user} performed action "${action}" on ${node}`; + return this.translateService.instant('activityLog.defaults.fallbackWithNode', { + user, + action, + node, + }); } - return `${user} performed action "${action}"`; + return this.translateService.instant('activityLog.defaults.fallbackWithoutNode', { + user, + action, + }); } private replaceSlash(path: string): string { diff --git a/src/app/shared/services/activity-logs/activity-logs.service.ts b/src/app/shared/services/activity-logs/activity-logs.service.ts index bc215eac1..2f7098c7e 100644 --- a/src/app/shared/services/activity-logs/activity-logs.service.ts +++ b/src/app/shared/services/activity-logs/activity-logs.service.ts @@ -7,7 +7,7 @@ import { ActivityLogsMapper } from '@shared/mappers/activity-logs.mapper'; import { ActivityLog, ActivityLogJsonApi, PaginatedData, ResponseJsonApi } from '@shared/models'; import { JsonApiService } from '@shared/services/json-api.service'; -import { environment } from '../../../../environments/environment'; +import { environment } from 'src/environments/environment'; @Injectable({ providedIn: 'root', @@ -23,10 +23,8 @@ export class ActivityLogsService { 'page[size]': pageSize, }; - return this.jsonApiService.get>(url, params).pipe( - map((res) => { - return ActivityLogsMapper.fromGetActivityLogsResponse(res); - }) - ); + return this.jsonApiService + .get>(url, params) + .pipe(map((res) => ActivityLogsMapper.fromGetActivityLogsResponse(res))); } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index df4d8030d..ffedecfff 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -2548,7 +2548,9 @@ "wikiOn": "wiki {{wiki}}", "field": "field", "updatedFields": "{{old}} to {{new}}", - "uncategorized": "Uncategorized" + "uncategorized": "Uncategorized", + "fallbackWithNode": "{{user}} performed action \"{{action}}\" on {{node}}", + "fallbackWithoutNode": "{{user}} performed action \"{{action}}\"" }, "activities": { "addon_added": "{{user}} added addon {{addon}} to {{node}}", @@ -2660,7 +2662,7 @@ "wiki_updated": "{{user}} updated wiki page {{page}} to version {{version}} of {{node}}" } }, - "node_categories": { + "nodeCategories": { "analysis": "Analysis", "communication": "Communication", "data": "Data", @@ -2672,4 +2674,4 @@ "software": "Software", "other": "Other" } -} +} \ No newline at end of file