@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
new file mode 100644
index 000000000..0194cf38b
--- /dev/null
+++ b/src/app/shared/mappers/activity-logs.mapper.ts
@@ -0,0 +1,155 @@
+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,
+ 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
new file mode 100644
index 000000000..4554cd5f3
--- /dev/null
+++ b/src/app/shared/models/activity-logs/activity-logs-json-api.model.ts
@@ -0,0 +1,184 @@
+export interface ActivityLogJsonApi {
+ id: string;
+ type: string;
+ attributes: {
+ action: string;
+ date: string;
+ params: {
+ contributors: LogContributorJsonApi[];
+ license?: string;
+ tag?: string;
+ institution?: {
+ id: string;
+ name: string;
+ };
+ params_node: {
+ id: string;
+ title: string;
+ };
+ params_project: null;
+ pointer: PointerJsonApi | 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?: {
+ original_node?: {
+ data: OriginalNodeEmbedsData;
+ };
+ user?: {
+ data: UserEmbedsData;
+ };
+ linked_node?: {
+ data: LinkedNodeEmbedsData;
+ };
+ };
+ meta: {
+ total: number;
+ };
+}
+
+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;
+ } | 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;
+ };
+}
+
+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;
+ } | 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;
+ }[][];
+ };
+}
+
+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
new file mode 100644
index 000000000..34b133518
--- /dev/null
+++ b/src/app/shared/models/activity-logs/activity-logs.model.ts
@@ -0,0 +1,164 @@
+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/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 804cfabdb..a810336cc 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/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..909e397f9
--- /dev/null
+++ b/src/app/shared/services/activity-logs/activity-log-formatter.service.ts
@@ -0,0 +1,275 @@
+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(`nodeCategories.${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 this.translateService.instant('activityLog.defaults.fallbackWithNode', {
+ user,
+ action,
+ node,
+ });
+ }
+ return this.translateService.instant('activityLog.defaults.fallbackWithoutNode', {
+ user,
+ 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..5148a0acd
--- /dev/null
+++ b/src/app/shared/services/activity-logs/activity-log-url-builder.service.ts
@@ -0,0 +1,149 @@
+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'];
+ 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;
+ }
+
+ 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/activity-logs.service.ts b/src/app/shared/services/activity-logs/activity-logs.service.ts
new file mode 100644
index 000000000..2f7098c7e
--- /dev/null
+++ b/src/app/shared/services/activity-logs/activity-logs.service.ts
@@ -0,0 +1,30 @@
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { inject, Injectable } from '@angular/core';
+
+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';
+
+@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,
+ 'page[size]': pageSize,
+ };
+
+ return this.jsonApiService
+ .get>(url, params)
+ .pipe(map((res) => ActivityLogsMapper.fromGetActivityLogsResponse(res)));
+ }
+}
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 41ca37cad..1d71fa2f4 100644
--- a/src/app/shared/services/index.ts
+++ b/src/app/shared/services/index.ts
@@ -1,3 +1,4 @@
+export * from './activity-logs';
export * from './addons';
export { BookmarksService } from './bookmarks.service';
export { BrandService } from './brand.service';
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..23c480ff0
--- /dev/null
+++ b/src/app/shared/stores/activity-logs/activity-logs.model.ts
@@ -0,0 +1,14 @@
+import { ActivityLog, AsyncStateWithTotalCount } from '@shared/models';
+
+export interface ActivityLogsStateModel {
+ 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
new file mode 100644
index 000000000..61e746506
--- /dev/null
+++ b/src/app/shared/stores/activity-logs/activity-logs.selectors.ts
@@ -0,0 +1,23 @@
+import { Selector } from '@ngxs/store';
+
+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): ActivityLog[] {
+ return state.activityLogs.data;
+ }
+
+ @Selector([ActivityLogsState])
+ 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
new file mode 100644
index 000000000..0ac91e5c6
--- /dev/null
+++ b/src/app/shared/stores/activity-logs/activity-logs.state.ts
@@ -0,0 +1,49 @@
+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 { ACTIVITY_LOGS_STATE_DEFAULT, ActivityLogsStateModel } from './activity-logs.model';
+
+@State({
+ name: 'activityLogs',
+ defaults: ACTIVITY_LOGS_STATE_DEFAULT,
+})
+@Injectable()
+export class ActivityLogsState {
+ private readonly activityLogsService = inject(ActivityLogsService);
+
+ @Action(GetActivityLogs)
+ getActivityLogs(ctx: StateContext, action: GetActivityLogs) {
+ ctx.patchState({
+ activityLogs: {
+ data: [],
+ isLoading: true,
+ error: null,
+ totalCount: 0,
+ },
+ });
+
+ return this.activityLogsService.fetchLogs(action.projectId, action.page, action.pageSize).pipe(
+ tap((res) => {
+ ctx.patchState({
+ activityLogs: {
+ data: res.data,
+ isLoading: false,
+ error: null,
+ totalCount: res.totalCount,
+ },
+ });
+ })
+ );
+ }
+
+ @Action(ClearActivityLogsStore)
+ clearActivityLogsStore(ctx: StateContext) {
+ ctx.setState(ACTIVITY_LOGS_STATE_DEFAULT);
+ }
+}
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';
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 36af20d73..ffedecfff 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,153 @@
"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",
+ "fallbackWithNode": "{{user}} performed action \"{{action}}\" on {{node}}",
+ "fallbackWithoutNode": "{{user}} performed action \"{{action}}\""
+ },
+ "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}}"
+ }
+ },
+ "nodeCategories": {
+ "analysis": "Analysis",
+ "communication": "Communication",
+ "data": "Data",
+ "hypothesis": "Hypothesis",
+ "instrumentation": "Instrumentation",
+ "methods and measures": "Methods and Measures",
+ "procedure": "Procedure",
+ "project": "Project",
+ "software": "Software",
+ "other": "Other"
}
-}
+}
\ No newline at end of file