-
+} @else {
+
+}
diff --git a/src/app/features/project/overview/project-overview.component.scss b/src/app/features/project/overview/project-overview.component.scss
index 94523ecd3..10af43d72 100644
--- a/src/app/features/project/overview/project-overview.component.scss
+++ b/src/app/features/project/overview/project-overview.component.scss
@@ -1,60 +1,15 @@
-@use "../../../../assets/styles/variables" as var;
+@use "/assets/styles/variables" as var;
.desktop {
margin-top: 4.5rem;
}
-.wiki,
-.component,
-.linked-projects,
-.activities {
- border: 1px solid var(--grey-2);
- border-radius: 12px;
- color: var(--dark-blue-1);
-
- &-description {
- line-height: 24px;
- }
-}
-
-.component {
- &-title {
- border: 1px solid var(--grey-2);
- border-radius: 12px;
- }
-}
-
-.linked-projects {
- &-project {
- border: 1px solid var(--grey-2);
- border-radius: 12px;
- }
-}
-
-.activities {
- &-activity {
- height: 35px;
- border-bottom: 1px solid var(--grey-2);
- }
-}
-
.left-section {
flex: 3;
}
.right-section {
flex: 1;
- border: 1px solid var(--grey-2);
+ border: 1px solid var.$grey-2;
border-radius: 12px;
}
-
-.subject {
- background: var(--bg-blue-3);
- border-radius: 4px;
- padding: 4px 12px;
- line-height: 24px;
-}
-
-.metadata {
- color: var.$dark-blue-1;
-}
diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts
index bf562e2e5..7071de483 100644
--- a/src/app/features/project/overview/project-overview.component.ts
+++ b/src/app/features/project/overview/project-overview.component.ts
@@ -1,17 +1,94 @@
-import { Button } from 'primeng/button';
+import { createDispatchMap, select } from '@ngxs/store';
-import { NgOptimizedImage } from '@angular/common';
-import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core';
+import { ButtonModule } from 'primeng/button';
+import { DialogService } from 'primeng/dynamicdialog';
+import { TagModule } from 'primeng/tag';
+import { CommonModule } from '@angular/common';
+import { ChangeDetectionStrategy, Component, DestroyRef, HostBinding, inject, OnInit } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { ActivatedRoute } from '@angular/router';
+
+import { ClearCollections, GetBookmarksCollectionId } from '@osf/features/collections/store';
+import { LinkedProjectsComponent } from '@osf/features/project/overview/components/linked-projects/linked-projects.component';
+import { OverviewComponentsComponent } from '@osf/features/project/overview/components/overview-components/overview-components.component';
+import { OverviewMetadataComponent } from '@osf/features/project/overview/components/overview-metadata/overview-metadata.component';
+import { OverviewToolbarComponent } from '@osf/features/project/overview/components/overview-toolbar/overview-toolbar.component';
+import { OverviewWikiComponent } from '@osf/features/project/overview/components/overview-wiki/overview-wiki.component';
+import { RecentActivityComponent } from '@osf/features/project/overview/components/recent-activity/recent-activity.component';
+import { ClearWiki, GetHomeWiki } from '@osf/features/project/wiki/store';
+import { LoadingSpinnerComponent } from '@shared/components';
import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component';
+import {
+ ClearProjectOverview,
+ GetComponents,
+ GetLinkedProjects,
+ GetProjectById,
+} from './store/project-overview.actions';
+import { ProjectOverviewSelectors } from './store/project-overview.selectors';
+
@Component({
selector: 'osf-project-overview',
- imports: [SubHeaderComponent, Button, NgOptimizedImage],
templateUrl: './project-overview.component.html',
- styleUrl: './project-overview.component.scss',
+ styleUrls: ['./project-overview.component.scss'],
+ imports: [
+ CommonModule,
+ ButtonModule,
+ TagModule,
+ SubHeaderComponent,
+ FormsModule,
+ LoadingSpinnerComponent,
+ OverviewWikiComponent,
+ OverviewComponentsComponent,
+ LinkedProjectsComponent,
+ RecentActivityComponent,
+ OverviewMetadataComponent,
+ OverviewToolbarComponent,
+ ],
+ providers: [DialogService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class ProjectOverviewComponent {
- @HostBinding('class') classes = 'flex flex-column w-full h-full';
+export class ProjectOverviewComponent implements OnInit {
+ @HostBinding('class') classes = 'flex flex-1 flex-column w-full h-full';
+
+ private route = inject(ActivatedRoute);
+ private destroyRef = inject(DestroyRef);
+
+ protected actions = createDispatchMap({
+ getProject: GetProjectById,
+ getBookmarksId: GetBookmarksCollectionId,
+ getHomeWiki: GetHomeWiki,
+ getComponents: GetComponents,
+ getLinkedProjects: GetLinkedProjects,
+ clearProjectOverview: ClearProjectOverview,
+ clearWiki: ClearWiki,
+ clearCollections: ClearCollections,
+ });
+
+ protected currentProject = select(ProjectOverviewSelectors.getProject);
+ protected isProjectLoading = select(ProjectOverviewSelectors.getProjectLoading);
+
+ constructor() {
+ this.setupCleanup();
+ }
+
+ ngOnInit(): void {
+ const projectId = this.route.parent?.snapshot.params['id'];
+ if (projectId) {
+ this.actions.getProject(projectId);
+ this.actions.getBookmarksId();
+ this.actions.getHomeWiki(projectId);
+ this.actions.getComponents(projectId);
+ this.actions.getLinkedProjects(projectId);
+ }
+ }
+
+ private setupCleanup(): void {
+ this.destroyRef.onDestroy(() => {
+ this.actions.clearProjectOverview();
+ this.actions.clearWiki();
+ this.actions.clearCollections();
+ });
+ }
}
diff --git a/src/app/features/project/overview/services/index.ts b/src/app/features/project/overview/services/index.ts
new file mode 100644
index 000000000..0b2facadf
--- /dev/null
+++ b/src/app/features/project/overview/services/index.ts
@@ -0,0 +1 @@
+export * from './project-overview.service';
diff --git a/src/app/features/project/overview/services/project-overview.service.ts b/src/app/features/project/overview/services/project-overview.service.ts
new file mode 100644
index 000000000..ab945a914
--- /dev/null
+++ b/src/app/features/project/overview/services/project-overview.service.ts
@@ -0,0 +1,144 @@
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { inject, Injectable } from '@angular/core';
+
+import { JsonApiService } from '@core/services/json-api/json-api.service';
+
+import { environment } from '../../../../../environments/environment';
+import { ProjectOverviewMapper } from '../mappers/project-overview.mapper';
+import {
+ ComponentGetResponse,
+ ComponentOverview,
+ ProjectOverview,
+ ProjectOverviewJsonApiResponse,
+} from '../models/project-overview.models';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ProjectOverviewService {
+ #jsonApiService = inject(JsonApiService);
+
+ getProjectById(projectId: string): Observable
{
+ const params: Record = {
+ 'embed[]': [
+ 'bibliographic_contributors',
+ 'affiliated_institutions',
+ 'identifiers',
+ 'license',
+ 'storage',
+ 'preprints',
+ ],
+ 'fields[institutions]': 'assets,description,name',
+ 'fields[preprints]': 'title,date_created',
+ 'fields[users]': 'family_name,full_name,given_name,middle_name',
+ related_counts: 'forks,view_only_links',
+ };
+
+ return this.#jsonApiService
+ .get(`${environment.apiUrl}/nodes/${projectId}/`, params)
+ .pipe(map((response) => ProjectOverviewMapper.fromGetProjectResponse(response.data)));
+ }
+
+ updateProjectPublicStatus(projectId: string, isPublic: boolean): Observable {
+ const payload = {
+ data: {
+ id: projectId,
+ type: 'nodes',
+ attributes: {
+ public: isPublic,
+ },
+ },
+ };
+
+ return this.#jsonApiService.patch(`${environment.apiUrl}/nodes/${projectId}/`, payload);
+ }
+
+ forkProject(projectId: string): Observable {
+ const payload = {
+ data: {
+ type: 'nodes',
+ },
+ };
+
+ return this.#jsonApiService.post(`${environment.apiUrl}/nodes/${projectId}/forks/`, payload);
+ }
+
+ duplicateProject(projectId: string, title: string): Observable {
+ const payload = {
+ data: {
+ type: 'nodes',
+ attributes: {
+ template_from: projectId,
+ category: 'project',
+ title: 'Templated from ' + title,
+ },
+ },
+ };
+
+ return this.#jsonApiService.post(`${environment.apiUrl}/nodes/`, payload);
+ }
+
+ createComponent(
+ projectId: string,
+ title: string,
+ description: string | null,
+ tags: string[],
+ region: string | null,
+ affiliatedInstitutions: string[],
+ inheritContributors: boolean
+ ): Observable {
+ const payload = {
+ data: {
+ type: 'nodes',
+ attributes: {
+ title,
+ category: 'project',
+ description: description || '',
+ tags,
+ },
+ },
+ };
+
+ const params: Record = {
+ inherit_contributors: inheritContributors,
+ };
+
+ if (region) {
+ params['region'] = region;
+ }
+
+ if (affiliatedInstitutions.length) {
+ params['affiliated_institutions'] = affiliatedInstitutions;
+ }
+
+ return this.#jsonApiService.post(`${environment.apiUrl}/nodes/${projectId}/children/`, payload, params);
+ }
+
+ deleteComponent(componentId: string): Observable {
+ return this.#jsonApiService.delete(`${environment.apiUrl}/nodes/${componentId}/`);
+ }
+
+ getComponents(projectId: string): Observable {
+ const params: Record = {
+ embed: 'bibliographic_contributors',
+ 'fields[users]': 'family_name,full_name,given_name,middle_name',
+ };
+
+ return this.#jsonApiService
+ .get<{ data: ComponentGetResponse[] }>(`${environment.apiUrl}/nodes/${projectId}/children`, params)
+ .pipe(map((response) => response.data.map((item) => ProjectOverviewMapper.fromGetComponentResponse(item))));
+ }
+
+ getLinkedProjects(projectId: string): Observable {
+ const params: Record = {
+ embed: 'bibliographic_contributors',
+ 'fields[users]': 'family_name,full_name,given_name,middle_name',
+ };
+
+ return this.#jsonApiService
+ .get<{ data: ComponentGetResponse[] }>(`${environment.apiUrl}/nodes/${projectId}/linked_nodes`, params)
+ .pipe(map((response) => response.data.map((item) => ProjectOverviewMapper.fromGetComponentResponse(item))));
+ }
+}
diff --git a/src/app/features/project/overview/store/index.ts b/src/app/features/project/overview/store/index.ts
new file mode 100644
index 000000000..36e8524dd
--- /dev/null
+++ b/src/app/features/project/overview/store/index.ts
@@ -0,0 +1,4 @@
+export * from './project-overview.actions';
+export * from './project-overview.model';
+export * from './project-overview.selectors';
+export * from './project-overview.state';
diff --git a/src/app/features/project/overview/store/project-overview.actions.ts b/src/app/features/project/overview/store/project-overview.actions.ts
new file mode 100644
index 000000000..b27a9d663
--- /dev/null
+++ b/src/app/features/project/overview/store/project-overview.actions.ts
@@ -0,0 +1,65 @@
+export class GetProjectById {
+ static readonly type = '[Project Overview] Get Project By Id';
+
+ constructor(public projectId: string) {}
+}
+
+export class UpdateProjectPublicStatus {
+ static readonly type = '[Project Overview] Update Project Public Status';
+
+ constructor(
+ public projectId: string,
+ public isPublic: boolean
+ ) {}
+}
+
+export class ForkProject {
+ static readonly type = '[Project Overview] Fork Project';
+
+ constructor(public projectId: string) {}
+}
+
+export class DuplicateProject {
+ static readonly type = '[Project Overview] Duplicate Project';
+
+ constructor(
+ public projectId: string,
+ public title: string
+ ) {}
+}
+
+export class ClearProjectOverview {
+ static readonly type = '[Project Overview] Clear Project Overview';
+}
+
+export class CreateComponent {
+ static readonly type = '[Project Overview] Create Component';
+
+ constructor(
+ public projectId: string,
+ public title: string,
+ public description: string | null,
+ public tags: string[],
+ public region: string | null,
+ public affiliatedInstitutions: string[],
+ public inheritContributors: boolean
+ ) {}
+}
+
+export class DeleteComponent {
+ static readonly type = '[Project Overview] Delete Component';
+
+ constructor(public componentId: string) {}
+}
+
+export class GetComponents {
+ static readonly type = '[Project Overview] Get Components';
+
+ constructor(public projectId: string) {}
+}
+
+export class GetLinkedProjects {
+ static readonly type = '[Project Overview] Get Linked Projects';
+
+ constructor(public projectId: string) {}
+}
diff --git a/src/app/features/project/overview/store/project-overview.model.ts b/src/app/features/project/overview/store/project-overview.model.ts
new file mode 100644
index 000000000..a22156785
--- /dev/null
+++ b/src/app/features/project/overview/store/project-overview.model.ts
@@ -0,0 +1,8 @@
+import { ComponentOverview, ProjectOverview } from '@osf/features/project/overview/models/project-overview.models';
+import { AsyncStateModel } from '@osf/shared/models/store';
+
+export interface ProjectOverviewStateModel {
+ project: AsyncStateModel;
+ components: AsyncStateModel;
+ linkedProjects: AsyncStateModel;
+}
diff --git a/src/app/features/project/overview/store/project-overview.selectors.ts b/src/app/features/project/overview/store/project-overview.selectors.ts
new file mode 100644
index 000000000..e7844e877
--- /dev/null
+++ b/src/app/features/project/overview/store/project-overview.selectors.ts
@@ -0,0 +1,61 @@
+import { Selector } from '@ngxs/store';
+
+import { ProjectOverviewStateModel } from './project-overview.model';
+import { ProjectOverviewState } from './project-overview.state';
+
+export class ProjectOverviewSelectors {
+ @Selector([ProjectOverviewState])
+ static getProject(state: ProjectOverviewStateModel) {
+ return state.project.data;
+ }
+
+ @Selector([ProjectOverviewState])
+ static getProjectLoading(state: ProjectOverviewStateModel) {
+ return state.project.isLoading;
+ }
+
+ @Selector([ProjectOverviewState])
+ static getComponents(state: ProjectOverviewStateModel) {
+ return state.components.data;
+ }
+
+ @Selector([ProjectOverviewState])
+ static getComponentsLoading(state: ProjectOverviewStateModel) {
+ return state.components.isLoading;
+ }
+
+ @Selector([ProjectOverviewState])
+ static getComponentsSubmitting(state: ProjectOverviewStateModel) {
+ return state.components.isSubmitting;
+ }
+
+ @Selector([ProjectOverviewState])
+ static getComponentsError(state: ProjectOverviewStateModel) {
+ return state.components.error;
+ }
+
+ @Selector([ProjectOverviewState])
+ static getLinkedProjects(state: ProjectOverviewStateModel) {
+ return state.linkedProjects.data;
+ }
+
+ @Selector([ProjectOverviewState])
+ static getLinkedProjectsLoading(state: ProjectOverviewStateModel) {
+ return state.linkedProjects.isLoading;
+ }
+
+ @Selector([ProjectOverviewState])
+ static getForkProjectSubmitting(state: ProjectOverviewStateModel) {
+ return state.project.isSubmitting;
+ }
+
+ @Selector([ProjectOverviewState])
+ static getDuplicateProjectSubmitting(state: ProjectOverviewStateModel) {
+ return state.project.isSubmitting;
+ }
+
+ @Selector([ProjectOverviewState])
+ static getUpdatePublicStatusSubmitting(state: ProjectOverviewStateModel) {
+ return state.project.isSubmitting;
+ }
+}
diff --git a/src/app/features/project/overview/store/project-overview.state.ts b/src/app/features/project/overview/store/project-overview.state.ts
new file mode 100644
index 000000000..33114cfbe
--- /dev/null
+++ b/src/app/features/project/overview/store/project-overview.state.ts
@@ -0,0 +1,280 @@
+import { Action, State, StateContext } from '@ngxs/store';
+
+import { catchError, tap, throwError } from 'rxjs';
+
+import { inject, Injectable } from '@angular/core';
+
+import { ProjectOverviewService } from '@osf/features/project/overview/services/project-overview.service';
+
+import {
+ ClearProjectOverview,
+ CreateComponent,
+ DeleteComponent,
+ DuplicateProject,
+ ForkProject,
+ GetComponents,
+ GetLinkedProjects,
+ GetProjectById,
+ UpdateProjectPublicStatus,
+} from './project-overview.actions';
+import { ProjectOverviewStateModel } from './project-overview.model';
+
+const PROJECT_OVERVIEW_DEFAULTS: ProjectOverviewStateModel = {
+ project: {
+ data: null,
+ isLoading: false,
+ isSubmitting: false,
+ error: null,
+ },
+ components: {
+ data: [],
+ isLoading: false,
+ isSubmitting: false,
+ error: null,
+ },
+ linkedProjects: {
+ data: [],
+ isLoading: false,
+ isSubmitting: false,
+ error: null,
+ },
+};
+
+@State({
+ name: 'projectOverview',
+ defaults: PROJECT_OVERVIEW_DEFAULTS,
+})
+@Injectable()
+export class ProjectOverviewState {
+ projectOverviewService = inject(ProjectOverviewService);
+
+ @Action(GetProjectById)
+ getProjectById(ctx: StateContext, action: GetProjectById) {
+ const state = ctx.getState();
+ ctx.patchState({
+ project: {
+ ...state.project,
+ isLoading: true,
+ },
+ });
+
+ return this.projectOverviewService.getProjectById(action.projectId).pipe(
+ tap((project) => {
+ ctx.patchState({
+ project: {
+ data: project,
+ isLoading: false,
+ error: null,
+ },
+ });
+ }),
+ catchError((error) => this.handleError(ctx, 'project', error))
+ );
+ }
+
+ @Action(ClearProjectOverview)
+ clearProjectOverview(ctx: StateContext) {
+ ctx.patchState(PROJECT_OVERVIEW_DEFAULTS);
+ }
+
+ @Action(UpdateProjectPublicStatus)
+ updateProjectPublicStatus(ctx: StateContext, action: UpdateProjectPublicStatus) {
+ const state = ctx.getState();
+ if (state.project.data) {
+ ctx.patchState({
+ project: {
+ ...state.project,
+ isSubmitting: true,
+ },
+ });
+
+ return this.projectOverviewService.updateProjectPublicStatus(action.projectId, action.isPublic).pipe(
+ tap(() => {
+ ctx.patchState({
+ project: {
+ ...state.project,
+ data: {
+ ...state.project.data!,
+ isPublic: action.isPublic,
+ },
+ isSubmitting: false,
+ },
+ });
+ }),
+ catchError((error) => this.handleError(ctx, 'project', error))
+ );
+ }
+ return;
+ }
+
+ @Action(ForkProject)
+ forkProject(ctx: StateContext, action: ForkProject) {
+ const state = ctx.getState();
+ ctx.patchState({
+ project: {
+ ...state.project,
+ isSubmitting: true,
+ },
+ });
+
+ return this.projectOverviewService.forkProject(action.projectId).pipe(
+ tap(() => {
+ ctx.patchState({
+ project: {
+ ...state.project,
+ data: {
+ ...state.project.data!,
+ forksCount: state.project.data!.forksCount + 1,
+ },
+ isSubmitting: false,
+ },
+ });
+ }),
+ catchError((error) => this.handleError(ctx, 'project', error))
+ );
+ }
+
+ @Action(DuplicateProject)
+ duplicateProject(ctx: StateContext, action: DuplicateProject) {
+ const state = ctx.getState();
+ ctx.patchState({
+ project: {
+ ...state.project,
+ isSubmitting: true,
+ },
+ });
+
+ return this.projectOverviewService.duplicateProject(action.projectId, action.title).pipe(
+ tap(() => {
+ ctx.patchState({
+ project: {
+ ...state.project,
+ isSubmitting: false,
+ },
+ });
+ }),
+ catchError((error) => this.handleError(ctx, 'project', error))
+ );
+ }
+
+ @Action(CreateComponent)
+ createComponent(ctx: StateContext, action: CreateComponent) {
+ const state = ctx.getState();
+ ctx.patchState({
+ components: {
+ ...state.components,
+ isSubmitting: true,
+ },
+ });
+
+ return this.projectOverviewService
+ .createComponent(
+ action.projectId,
+ action.title,
+ action.description,
+ action.tags,
+ action.region,
+ action.affiliatedInstitutions,
+ action.inheritContributors
+ )
+ .pipe(
+ tap(() => {
+ ctx.patchState({
+ components: {
+ ...state.components,
+ isSubmitting: false,
+ },
+ });
+ }),
+ catchError((error) => this.handleError(ctx, 'components', error))
+ );
+ }
+
+ @Action(DeleteComponent)
+ deleteComponent(ctx: StateContext, action: DeleteComponent) {
+ const state = ctx.getState();
+ ctx.patchState({
+ components: {
+ ...state.components,
+ isSubmitting: true,
+ },
+ });
+
+ return this.projectOverviewService.deleteComponent(action.componentId).pipe(
+ tap(() => {
+ ctx.patchState({
+ components: {
+ ...state.components,
+ isSubmitting: false,
+ error: null,
+ },
+ });
+ }),
+ catchError((error) => this.handleError(ctx, 'components', error))
+ );
+ }
+
+ @Action(GetComponents)
+ getProjectComponents(ctx: StateContext, action: GetComponents) {
+ const state = ctx.getState();
+ ctx.patchState({
+ components: {
+ ...state.components,
+ isLoading: true,
+ },
+ });
+
+ return this.projectOverviewService.getComponents(action.projectId).pipe(
+ tap((components) => {
+ ctx.patchState({
+ components: {
+ data: components,
+ isLoading: false,
+ error: null,
+ },
+ });
+ }),
+ catchError((error) => this.handleError(ctx, 'components', error))
+ );
+ }
+
+ @Action(GetLinkedProjects)
+ getLinkedProjects(ctx: StateContext, action: GetLinkedProjects) {
+ const state = ctx.getState();
+ ctx.patchState({
+ linkedProjects: {
+ ...state.linkedProjects,
+ isLoading: true,
+ },
+ });
+
+ return this.projectOverviewService.getLinkedProjects(action.projectId).pipe(
+ tap((linkedProjects) => {
+ ctx.patchState({
+ linkedProjects: {
+ data: linkedProjects,
+ isLoading: false,
+ error: null,
+ },
+ });
+ }),
+ catchError((error) => this.handleError(ctx, 'linkedProjects', error))
+ );
+ }
+
+ private handleError(
+ ctx: StateContext,
+ section: keyof ProjectOverviewStateModel,
+ error: Error
+ ) {
+ ctx.patchState({
+ [section]: {
+ ...ctx.getState()[section],
+ isLoading: false,
+ isSubmitting: false,
+ error: error.message,
+ },
+ });
+ return throwError(() => error);
+ }
+}
diff --git a/src/app/features/project/project.component.html b/src/app/features/project/project.component.html
index f58acf390..07cb3a9d7 100644
--- a/src/app/features/project/project.component.html
+++ b/src/app/features/project/project.component.html
@@ -1,3 +1,3 @@
-
+
diff --git a/src/app/features/project/wiki/mappers/index.ts b/src/app/features/project/wiki/mappers/index.ts
new file mode 100644
index 000000000..4f13b4e6e
--- /dev/null
+++ b/src/app/features/project/wiki/mappers/index.ts
@@ -0,0 +1 @@
+export * from './wiki.mapper';
diff --git a/src/app/features/project/wiki/mappers/wiki.mapper.ts b/src/app/features/project/wiki/mappers/wiki.mapper.ts
new file mode 100644
index 000000000..d552d50fe
--- /dev/null
+++ b/src/app/features/project/wiki/mappers/wiki.mapper.ts
@@ -0,0 +1,12 @@
+import { HomeWiki, HomeWikiGetResponse } from '../models/wiki.model';
+
+export class WikiMapper {
+ static fromGetHomeWikiResponse(response: HomeWikiGetResponse): HomeWiki {
+ return {
+ id: response.id,
+ name: response.attributes.name,
+ contentType: response.attributes.content_type,
+ downloadLink: response.links.download,
+ };
+ }
+}
diff --git a/src/app/features/project/wiki/models/index.ts b/src/app/features/project/wiki/models/index.ts
new file mode 100644
index 000000000..c814841f1
--- /dev/null
+++ b/src/app/features/project/wiki/models/index.ts
@@ -0,0 +1 @@
+export * from './wiki.model';
diff --git a/src/app/features/project/wiki/models/wiki.model.ts b/src/app/features/project/wiki/models/wiki.model.ts
new file mode 100644
index 000000000..27ff0da2e
--- /dev/null
+++ b/src/app/features/project/wiki/models/wiki.model.ts
@@ -0,0 +1,25 @@
+import { JsonApiResponse } from '@core/services/json-api/json-api.entity';
+
+export interface HomeWiki {
+ id: string;
+ name: string;
+ contentType: string;
+ downloadLink: string;
+ content?: string;
+}
+
+export interface HomeWikiGetResponse {
+ id: string;
+ type: string;
+ attributes: {
+ name: string;
+ content_type: string;
+ };
+ links: {
+ download: string;
+ };
+}
+
+export interface HomeWikiJsonApiResponse extends JsonApiResponse
{
+ data: HomeWikiGetResponse[];
+}
diff --git a/src/app/features/project/wiki/services/index.ts b/src/app/features/project/wiki/services/index.ts
new file mode 100644
index 000000000..054b651c3
--- /dev/null
+++ b/src/app/features/project/wiki/services/index.ts
@@ -0,0 +1 @@
+export * from './wiki.service';
diff --git a/src/app/features/project/wiki/services/wiki.service.ts b/src/app/features/project/wiki/services/wiki.service.ts
new file mode 100644
index 000000000..e15fa062f
--- /dev/null
+++ b/src/app/features/project/wiki/services/wiki.service.ts
@@ -0,0 +1,44 @@
+import { Observable, of } from 'rxjs';
+import { map, switchMap } from 'rxjs/operators';
+
+import { HttpClient } from '@angular/common/http';
+import { inject, Injectable } from '@angular/core';
+
+import { JsonApiService } from '@core/services/json-api/json-api.service';
+import { WikiMapper } from '@osf/features/project/wiki/mappers/wiki.mapper';
+
+import { environment } from '../../../../../environments/environment';
+import { HomeWikiJsonApiResponse } from '../models/wiki.model';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class WikiService {
+ readonly #jsonApiService = inject(JsonApiService);
+ readonly #http = inject(HttpClient);
+
+ getHomeWiki(projectId: string): Observable {
+ const params: Record = {
+ 'filter[name]': 'home',
+ };
+ return this.#jsonApiService
+ .get(environment.apiUrl + `/nodes/${projectId}/wikis/`, params)
+ .pipe(
+ map((response) => {
+ const homeWiki = response.data.find((wiki) => wiki.attributes.name === 'home');
+ if (!homeWiki) {
+ return '';
+ }
+ const wiki = WikiMapper.fromGetHomeWikiResponse(homeWiki);
+ return wiki.downloadLink;
+ }),
+ switchMap((downloadLink) => {
+ if (!downloadLink) {
+ return of('');
+ }
+ return this.#http.get(downloadLink, { responseType: 'text' });
+ }),
+ map((content) => (content ? content.replace(/\n/g, ' ') : ''))
+ );
+ }
+}
diff --git a/src/app/features/project/wiki/store/index.ts b/src/app/features/project/wiki/store/index.ts
new file mode 100644
index 000000000..0c93bb710
--- /dev/null
+++ b/src/app/features/project/wiki/store/index.ts
@@ -0,0 +1,4 @@
+export * from './wiki.actions';
+export * from './wiki.model';
+export * from './wiki.selectors';
+export * from './wiki.state';
diff --git a/src/app/features/project/wiki/store/wiki.actions.ts b/src/app/features/project/wiki/store/wiki.actions.ts
new file mode 100644
index 000000000..ef7e10da2
--- /dev/null
+++ b/src/app/features/project/wiki/store/wiki.actions.ts
@@ -0,0 +1,9 @@
+export class GetHomeWiki {
+ static readonly type = '[Wiki] Get Home Wiki';
+
+ constructor(public projectId: string) {}
+}
+
+export class ClearWiki {
+ static readonly type = '[Wiki] Clear Wiki';
+}
diff --git a/src/app/features/project/wiki/store/wiki.model.ts b/src/app/features/project/wiki/store/wiki.model.ts
new file mode 100644
index 000000000..fa2625533
--- /dev/null
+++ b/src/app/features/project/wiki/store/wiki.model.ts
@@ -0,0 +1,5 @@
+import { AsyncStateModel } from '@osf/shared/models/store';
+
+export interface WikiStateModel {
+ homeWikiContent: AsyncStateModel;
+}
diff --git a/src/app/features/project/wiki/store/wiki.selectors.ts b/src/app/features/project/wiki/store/wiki.selectors.ts
new file mode 100644
index 000000000..920aa5045
--- /dev/null
+++ b/src/app/features/project/wiki/store/wiki.selectors.ts
@@ -0,0 +1,17 @@
+import { Selector } from '@ngxs/store';
+
+import { WikiStateModel } from '@osf/features/project/wiki/store/wiki.model';
+
+import { WikiState } from './wiki.state';
+
+export class WikiSelectors {
+ @Selector([WikiState])
+ static getHomeWikiContent(state: WikiStateModel): string {
+ return state.homeWikiContent.data;
+ }
+
+ @Selector([WikiState])
+ static getHomeWikiLoading(state: WikiStateModel): boolean {
+ return state.homeWikiContent.isLoading;
+ }
+}
diff --git a/src/app/features/project/wiki/store/wiki.state.ts b/src/app/features/project/wiki/store/wiki.state.ts
new file mode 100644
index 000000000..90df18f78
--- /dev/null
+++ b/src/app/features/project/wiki/store/wiki.state.ts
@@ -0,0 +1,72 @@
+import { Action, State, StateContext } from '@ngxs/store';
+
+import { catchError, tap, throwError } from 'rxjs';
+
+import { Injectable } from '@angular/core';
+
+import { WikiService } from '../services/wiki.service';
+
+import { ClearWiki, GetHomeWiki } from './wiki.actions';
+import { WikiStateModel } from './wiki.model';
+
+@State({
+ name: 'wiki',
+ defaults: {
+ homeWikiContent: {
+ data: '',
+ isLoading: false,
+ error: null,
+ },
+ },
+})
+@Injectable()
+export class WikiState {
+ constructor(private wikiService: WikiService) {}
+
+ @Action(GetHomeWiki)
+ getHomeWiki(ctx: StateContext, action: GetHomeWiki) {
+ const state = ctx.getState();
+ ctx.patchState({
+ homeWikiContent: {
+ ...state.homeWikiContent,
+ isLoading: true,
+ error: null,
+ },
+ });
+
+ return this.wikiService.getHomeWiki(action.projectId).pipe(
+ tap((content) => {
+ ctx.patchState({
+ homeWikiContent: {
+ data: content,
+ isLoading: false,
+ error: null,
+ },
+ });
+ }),
+ catchError((error) => this.handleError(ctx, error))
+ );
+ }
+
+ @Action(ClearWiki)
+ clearWiki(ctx: StateContext) {
+ ctx.patchState({
+ homeWikiContent: {
+ data: '',
+ isLoading: false,
+ error: null,
+ },
+ });
+ }
+
+ private handleError(ctx: StateContext, error: Error) {
+ ctx.patchState({
+ homeWikiContent: {
+ ...ctx.getState().homeWikiContent,
+ isLoading: false,
+ error: error.message,
+ },
+ });
+ return throwError(() => error);
+ }
+}
diff --git a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts
index a3de613e2..01c47229b 100644
--- a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts
+++ b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.ts
@@ -1,4 +1,4 @@
-import { Store } from '@ngxs/store';
+import { select, Store } from '@ngxs/store';
import { TranslatePipe } from '@ngx-translate/core';
@@ -16,13 +16,13 @@ import { AccountSettingsSelectors } from '@osf/features/settings/account-setting
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AffiliatedInstitutionsComponent {
- readonly #store = inject(Store);
- institutions = this.#store.selectSignal(AccountSettingsSelectors.getUserInstitutions);
- currentUser = this.#store.selectSignal(UserSelectors.getCurrentUser);
+ private readonly store = inject(Store);
+ protected institutions = select(AccountSettingsSelectors.getUserInstitutions);
+ protected currentUser = select(UserSelectors.getCurrentUser);
deleteInstitution(id: string) {
if (this.currentUser()?.id) {
- this.#store.dispatch(new DeleteUserInstitution(id, this.currentUser()!.id));
+ this.store.dispatch(new DeleteUserInstitution(id, this.currentUser()!.id));
}
}
}
diff --git a/src/app/shared/components/loading-spinner/loading-spinner.component.html b/src/app/shared/components/loading-spinner/loading-spinner.component.html
index b957abe20..232fb70c9 100644
--- a/src/app/shared/components/loading-spinner/loading-spinner.component.html
+++ b/src/app/shared/components/loading-spinner/loading-spinner.component.html
@@ -1,6 +1,6 @@
-
+
diff --git a/src/app/shared/components/truncated-text/truncated-text.component.scss b/src/app/shared/components/truncated-text/truncated-text.component.scss
new file mode 100644
index 000000000..a590d92fa
--- /dev/null
+++ b/src/app/shared/components/truncated-text/truncated-text.component.scss
@@ -0,0 +1,24 @@
+@use "assets/styles/variables" as var;
+
+.text-content {
+ max-height: 300px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ line-clamp: var(--line-clamp);
+ line-height: 1.7;
+ -webkit-line-clamp: var(--line-clamp);
+ -webkit-box-orient: vertical;
+
+ &.expanded {
+ display: block;
+ max-height: none;
+ text-overflow: initial;
+ -webkit-line-clamp: initial;
+ line-clamp: initial;
+ }
+}
+
+.read-more-link {
+ cursor: pointer;
+}
diff --git a/src/app/shared/components/truncated-text/truncated-text.component.ts b/src/app/shared/components/truncated-text/truncated-text.component.ts
new file mode 100644
index 000000000..342878692
--- /dev/null
+++ b/src/app/shared/components/truncated-text/truncated-text.component.ts
@@ -0,0 +1,37 @@
+import { TranslateModule } from '@ngx-translate/core';
+
+import { CommonModule } from '@angular/common';
+import { AfterViewInit, Component, ElementRef, input, signal, viewChild } from '@angular/core';
+
+// This component displays text with a "show more/less" functionality when content exceeds the specified number of lines.
+// Use line-clamp CSS property for initial truncation and dynamically show/hide content.
+
+@Component({
+ selector: 'osf-truncated-text',
+ templateUrl: './truncated-text.component.html',
+ styleUrls: ['./truncated-text.component.scss'],
+ imports: [CommonModule, TranslateModule],
+})
+export class TruncatedTextComponent implements AfterViewInit {
+ readonly text = input('');
+ readonly maxVisibleLines = input(3);
+ protected readonly contentElement = viewChild
('textContent');
+ protected isTextExpanded = signal(false);
+ protected hasOverflowingText = signal(false);
+
+ ngAfterViewInit() {
+ this.#checkTextOverflow();
+ }
+
+ #checkTextOverflow(): void {
+ const element = this.contentElement()?.nativeElement;
+ if (!element) return;
+
+ const hasOverflow = element.scrollHeight > element.clientHeight;
+ this.hasOverflowingText.set(hasOverflow);
+ }
+
+ protected toggleTextExpansion(): void {
+ this.isTextExpanded.update((expanded) => !expanded);
+ }
+}
diff --git a/src/app/shared/entities/create-component-form-controls.enum.ts b/src/app/shared/entities/create-component-form-controls.enum.ts
new file mode 100644
index 000000000..a588acfaf
--- /dev/null
+++ b/src/app/shared/entities/create-component-form-controls.enum.ts
@@ -0,0 +1,8 @@
+export enum ComponentFormControls {
+ Title = 'title',
+ Affiliations = 'affiliations',
+ StorageLocation = 'storageLocation',
+ Description = 'description',
+ AddContributors = 'addContributors',
+ AddTags = 'addTags',
+}
diff --git a/src/app/shared/entities/create-component-form.interface.ts b/src/app/shared/entities/create-component-form.interface.ts
new file mode 100644
index 000000000..cbd46a835
--- /dev/null
+++ b/src/app/shared/entities/create-component-form.interface.ts
@@ -0,0 +1,10 @@
+import { FormControl } from '@angular/forms';
+
+export interface ComponentForm {
+ title: FormControl;
+ affiliations: FormControl;
+ storageLocation: FormControl;
+ description: FormControl;
+ addContributors: FormControl;
+ addTags: FormControl;
+}
diff --git a/src/app/shared/utils/scientists.const.ts b/src/app/shared/utils/scientists.const.ts
new file mode 100644
index 000000000..4ecafb7ca
--- /dev/null
+++ b/src/app/shared/utils/scientists.const.ts
@@ -0,0 +1,40 @@
+export const ScientistsNames = [
+ 'Anning',
+ 'Banneker',
+ 'Cannon',
+ 'Carver',
+ 'Chappelle',
+ 'Curie',
+ 'Divine',
+ 'Emeagwali',
+ 'Fahlberg',
+ 'Forssmann',
+ 'Franklin',
+ 'Herschel',
+ 'Hodgkin',
+ 'Hopper',
+ 'Horowitz',
+ 'Jemison',
+ 'Julian',
+ 'Kovalevsky',
+ 'Lamarr',
+ 'Lavoisier',
+ 'Lovelace',
+ 'Massie',
+ 'McClintock',
+ 'Meitner',
+ 'Mitchell',
+ 'Morgan',
+ 'Odum',
+ 'Pasteur',
+ 'Pauling',
+ 'Payne',
+ 'Pearce',
+ 'Pollack',
+ 'Rillieux',
+ 'Sanger',
+ 'Somerville',
+ 'Tesla',
+ 'Tyson',
+ 'Turing',
+];
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index b9ff6f69a..5991a5a29 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -312,6 +312,151 @@
"createLinkDialog": {
"dialogTitle": "Create a new link to share your project"
}
+ },
+ "overview": {
+ "header": {
+ "privateProject": "Private Project",
+ "publicProject": "Public Project"
+ },
+ "wiki": {
+ "title": "Wiki",
+ "noWikiMessage": "Add important information, links, or images here to describe your project. "
+ },
+ "components": {
+ "title": "Components",
+ "addComponentButton": "Add Component",
+ "linkProjectsButton": "Link Projects",
+ "noComponentsMessage": "Add components to organize your project. "
+ },
+ "linkedProjects": {
+ "title": "Linked Projects",
+ "noLinkedProjectsMessage": "Link your project. "
+ },
+ "recentActivity": {
+ "title": "Recent Activity"
+ },
+ "metadata": {
+ "title": "Metadata",
+ "contributors": "Contributors",
+ "description": "Description",
+ "supplements": "Supplements",
+ "supplementsText1": "Has supplemental materials for",
+ "supplementsText2": "on OSF Preprints",
+ "dateCreated": "Date Created",
+ "dateUpdated": "Date Updated",
+ "licence": "License",
+ "publication": "Publication DOI",
+ "subjects": "Subjects",
+ "tags": "Tags",
+ "affiliatedInstitutions": "Affiliated Institutions",
+ "noDescription": "No description",
+ "noLicense": "No License",
+ "noPublicationDoi": "No Publication DOI",
+ "noSubjects": "No subjects",
+ "noTags": "No tags",
+ "noSupplements": "No supplements",
+ "noAffiliatedInstitutions": "No affiliated institutions",
+ "editButton": "Edit"
+ },
+ "dialog": {
+ "makePublic": {
+ "header": "Make Project Public",
+ "confirmButton": "Make Public",
+ "cancelButton": "Cancel",
+ "message": "Please review your projects, components, and add-ons for sensitive or restricted information before making them public. Once they are made public, you should assume they will always be public. You can return them to private later, but search engines (including Google's cache) or others may access files, wiki pages, or analytics before you do. "
+ },
+ "makePrivate": {
+ "header": "Make Project Private",
+ "confirmButton": "Make Private",
+ "cancelButton": "Cancel",
+ "message": "• Public forks and registrations of this project will remain public. • Search engines (including Google's cache) or others may have accessed files, wiki pages, or analytics while this project was public. • The project will automatically be removed from any collections. Any pending requests will be cancelled. "
+ },
+ "toast": {
+ "makePublic": {
+ "success": "Project has been made public successfully"
+ },
+ "makePrivate": {
+ "success": "Project has been made private successfully"
+ },
+ "addComponent": {
+ "success": "Component has been created successfully"
+ },
+ "deleteComponent": {
+ "success": "Component has been deleted successfully"
+ },
+ "fork": {
+ "success": "Project has been forked successfully"
+ },
+ "duplicate": {
+ "success": "Project has been duplicated successfully"
+ },
+ "bookmark": {
+ "add": "Project has been added to bookmarks",
+ "remove": "Project has been removed from bookmarks"
+ }
+ },
+ "addComponent": {
+ "header": "Create New Component",
+ "confirmButton": "Create",
+ "cancelButton": "Cancel",
+ "title": "Title",
+ "description": "Description (Optional)",
+ "storageLocation": "Storage Location",
+ "affiliation": {
+ "title": "Affiliation",
+ "selectAll": "Select All",
+ "removeAll": "Remove All"
+ },
+ "addContributors": "Add Contributors from",
+ "addTags": "Add Tags from",
+ "contributorsMessage": "admins will have read access to this component.",
+ "license": {
+ "title": "License",
+ "description": "This component will inherit the same license as",
+ "learnMore": "Learn More"
+ }
+ },
+ "deleteComponent": {
+ "header": "Are you sure you want to delete this component?",
+ "confirmButton": "Delete",
+ "cancelButton": "Cancel",
+ "message": "It will no longer be available to other contributors on the project.",
+ "confirmation": "Type the following to continue:"
+ },
+ "fork": {
+ "header": "Fork This Project",
+ "confirmButton": "Fork",
+ "cancelButton": "Cancel",
+ "message": "Are you sure you want to fork this project?"
+ },
+ "duplicate": {
+ "header": "Duplicate Template",
+ "confirmButton": "Create",
+ "cancelButton": "Cancel",
+ "message1": "Are you sure you want to create a new project using this project as a template?",
+ "message2": "Any add-ons configured for this project will not be authenticated in the new project."
+ }
+ },
+ "actions": {
+ "manageContributors": "Manage Contributors",
+ "settings": "Settings",
+ "delete": "Delete",
+ "forkProject": "Fork this project",
+ "duplicateProject": "Duplicate this project",
+ "viewDuplication": "View forks",
+ "socials": {
+ "email": "Email",
+ "x": "X",
+ "linkedIn": "LinkedIn",
+ "facebook": "Facebook"
+ }
+ },
+ "tooltips": {
+ "viewOnlyLinks": "View only links",
+ "bookmarks": "Bookmarks",
+ "duplicate": "Duplicate",
+ "share": "Share"
+ }
}
},
"settings": {
@@ -766,6 +911,10 @@
"downloadButton": "Download"
}
},
+ "truncatedText": {
+ "readMore": "Read more",
+ "hide": "Hide"
+ },
"footer": {
"links": {
"centerForOpenScience": "Center for Open Science",
diff --git a/src/assets/icons/dist/icons.css b/src/assets/icons/dist/icons.css
index 9c40a6bbb..a48d6e24a 100644
--- a/src/assets/icons/dist/icons.css
+++ b/src/assets/icons/dist/icons.css
@@ -1,9 +1,9 @@
@font-face {
font-family: 'icons';
src:
- url('./icons.eot?cdc3c964cbe66b7f705386b7b66a51b6#iefix') format('embedded-opentype'),
- url('./icons.woff2?cdc3c964cbe66b7f705386b7b66a51b6') format('woff2'),
- url('./icons.woff?cdc3c964cbe66b7f705386b7b66a51b6') format('woff');
+ url('./icons.eot?2e4364c6b3c036b04fd69f71148f23a1#iefix') format('embedded-opentype'),
+ url('./icons.woff2?2e4364c6b3c036b04fd69f71148f23a1') format('woff2'),
+ url('./icons.woff?2e4364c6b3c036b04fd69f71148f23a1') format('woff');
}
i[class^='osf-icon-']:before,
@@ -39,186 +39,195 @@ i[class*=' osf-icon-']:before {
.osf-icon-supplements:before {
content: '\f107';
}
-.osf-icon-sort-desc:before {
+.osf-icon-sort:before {
content: '\f108';
}
-.osf-icon-sort-asc:before {
+.osf-icon-sort-desc:before {
content: '\f109';
}
-.osf-icon-sort-asc-grey:before {
+.osf-icon-sort-asc:before {
content: '\f10a';
}
-.osf-icon-share:before {
+.osf-icon-sort-asc-grey:before {
content: '\f10b';
}
-.osf-icon-settings:before {
+.osf-icon-share:before {
content: '\f10c';
}
-.osf-icon-search:before {
+.osf-icon-settings:before {
content: '\f10d';
}
-.osf-icon-search-2:before {
+.osf-icon-search:before {
content: '\f10e';
}
-.osf-icon-rejected:before {
+.osf-icon-search-2:before {
content: '\f10f';
}
-.osf-icon-registries:before {
+.osf-icon-rejected:before {
content: '\f110';
}
-.osf-icon-quotes:before {
+.osf-icon-registries:before {
content: '\f111';
}
-.osf-icon-profile:before {
+.osf-icon-quotes:before {
content: '\f112';
}
-.osf-icon-preprints:before {
+.osf-icon-profile:before {
content: '\f113';
}
-.osf-icon-plus:before {
+.osf-icon-preprints:before {
content: '\f114';
}
-.osf-icon-pending:before {
+.osf-icon-plus:before {
content: '\f115';
}
-.osf-icon-pdf:before {
+.osf-icon-pending:before {
content: '\f116';
}
-.osf-icon-papers:before {
+.osf-icon-pdf:before {
content: '\f117';
}
-.osf-icon-padlock:before {
+.osf-icon-papers:before {
content: '\f118';
}
-.osf-icon-padlock-unlock:before {
+.osf-icon-padlock:before {
content: '\f119';
}
-.osf-icon-my-projects:before {
+.osf-icon-padlock-unlock:before {
content: '\f11a';
}
-.osf-icon-minus:before {
+.osf-icon-my-projects:before {
content: '\f11b';
}
-.osf-icon-menu:before {
+.osf-icon-minus:before {
content: '\f11c';
}
-.osf-icon-meetings:before {
+.osf-icon-menu:before {
content: '\f11d';
}
-.osf-icon-materials:before {
+.osf-icon-meetings:before {
content: '\f11e';
}
-.osf-icon-list:before {
+.osf-icon-materials:before {
content: '\f11f';
}
-.osf-icon-link:before {
+.osf-icon-list:before {
content: '\f120';
}
-.osf-icon-last:before {
+.osf-icon-link:before {
content: '\f121';
}
-.osf-icon-institutions:before {
+.osf-icon-last:before {
content: '\f122';
}
-.osf-icon-information:before {
+.osf-icon-institutions:before {
content: '\f123';
}
-.osf-icon-image:before {
+.osf-icon-institution:before {
content: '\f124';
}
-.osf-icon-home:before {
+.osf-icon-information:before {
content: '\f125';
}
-.osf-icon-home-2:before {
+.osf-icon-image:before {
content: '\f126';
}
-.osf-icon-help:before {
+.osf-icon-home:before {
content: '\f127';
}
-.osf-icon-folder:before {
+.osf-icon-home-2:before {
content: '\f128';
}
-.osf-icon-first:before {
+.osf-icon-help:before {
content: '\f129';
}
-.osf-icon-filter:before {
+.osf-icon-folder:before {
content: '\f12a';
}
-.osf-icon-eye-view:before {
+.osf-icon-first:before {
content: '\f12b';
}
-.osf-icon-eye-hidden:before {
+.osf-icon-filter:before {
content: '\f12c';
}
-.osf-icon-email:before {
+.osf-icon-eye-view:before {
content: '\f12d';
}
-.osf-icon-duplicate:before {
+.osf-icon-eye-hidden:before {
content: '\f12e';
}
-.osf-icon-download:before {
+.osf-icon-email:before {
content: '\f12f';
}
-.osf-icon-double-arrow-left:before {
+.osf-icon-duplicate:before {
content: '\f130';
}
-.osf-icon-dots:before {
+.osf-icon-download:before {
content: '\f131';
}
-.osf-icon-donate:before {
+.osf-icon-double-arrow-left:before {
content: '\f132';
}
-.osf-icon-doc:before {
+.osf-icon-dots:before {
content: '\f133';
}
-.osf-icon-diagram:before {
+.osf-icon-donate:before {
content: '\f134';
}
-.osf-icon-data:before {
+.osf-icon-doc:before {
content: '\f135';
}
-.osf-icon-customize:before {
+.osf-icon-diagram:before {
content: '\f136';
}
-.osf-icon-copy:before {
+.osf-icon-data:before {
content: '\f137';
}
-.osf-icon-contact:before {
+.osf-icon-customize:before {
content: '\f138';
}
-.osf-icon-collections:before {
+.osf-icon-cos-shield:before {
content: '\f139';
}
-.osf-icon-code:before {
+.osf-icon-copy:before {
content: '\f13a';
}
-.osf-icon-close:before {
+.osf-icon-contact:before {
content: '\f13b';
}
-.osf-icon-chevron-right:before {
+.osf-icon-collections:before {
content: '\f13c';
}
-.osf-icon-chevron-left:before {
+.osf-icon-code:before {
content: '\f13d';
}
-.osf-icon-calendar-silhouette:before {
+.osf-icon-close:before {
content: '\f13e';
}
-.osf-icon-bookmark:before {
+.osf-icon-chevron-right:before {
content: '\f13f';
}
-.osf-icon-bookmark-fill:before {
+.osf-icon-chevron-left:before {
content: '\f140';
}
-.osf-icon-arrow:before {
+.osf-icon-calendar-silhouette:before {
content: '\f141';
}
-.osf-icon-arrow-left:before {
+.osf-icon-bookmark:before {
content: '\f142';
}
-.osf-icon-arrow-down:before {
+.osf-icon-bookmark-fill:before {
content: '\f143';
}
-.osf-icon-accepted:before {
+.osf-icon-arrow:before {
content: '\f144';
}
+.osf-icon-arrow-left:before {
+ content: '\f145';
+}
+.osf-icon-arrow-down:before {
+ content: '\f146';
+}
+.osf-icon-accepted:before {
+ content: '\f147';
+}
diff --git a/src/assets/icons/dist/icons.eot b/src/assets/icons/dist/icons.eot
index c854344f9..3fd895de3 100644
Binary files a/src/assets/icons/dist/icons.eot and b/src/assets/icons/dist/icons.eot differ
diff --git a/src/assets/icons/dist/icons.html b/src/assets/icons/dist/icons.html
index 0f7b07e64..e287dc063 100644
--- a/src/assets/icons/dist/icons.html
+++ b/src/assets/icons/dist/icons.html
@@ -110,6 +110,14 @@ icons
supplements
+
+
+
+
+
+ sort
+
+
@@ -326,6 +334,14 @@ icons
institutions
+
+
+
+
+
+ institution
+
+
@@ -486,6 +502,14 @@ icons
customize
+
+
+
+
+
+ cos-shield
+
+
diff --git a/src/assets/icons/dist/icons.json b/src/assets/icons/dist/icons.json
index 34e334f6b..acb91a04d 100644
--- a/src/assets/icons/dist/icons.json
+++ b/src/assets/icons/dist/icons.json
@@ -6,65 +6,68 @@
"trash": 61701,
"support": 61702,
"supplements": 61703,
- "sort-desc": 61704,
- "sort-asc": 61705,
- "sort-asc-grey": 61706,
- "share": 61707,
- "settings": 61708,
- "search": 61709,
- "search-2": 61710,
- "rejected": 61711,
- "registries": 61712,
- "quotes": 61713,
- "profile": 61714,
- "preprints": 61715,
- "plus": 61716,
- "pending": 61717,
- "pdf": 61718,
- "papers": 61719,
- "padlock": 61720,
- "padlock-unlock": 61721,
- "my-projects": 61722,
- "minus": 61723,
- "menu": 61724,
- "meetings": 61725,
- "materials": 61726,
- "list": 61727,
- "link": 61728,
- "last": 61729,
- "institutions": 61730,
- "information": 61731,
- "image": 61732,
- "home": 61733,
- "home-2": 61734,
- "help": 61735,
- "folder": 61736,
- "first": 61737,
- "filter": 61738,
- "eye-view": 61739,
- "eye-hidden": 61740,
- "email": 61741,
- "duplicate": 61742,
- "download": 61743,
- "double-arrow-left": 61744,
- "dots": 61745,
- "donate": 61746,
- "doc": 61747,
- "diagram": 61748,
- "data": 61749,
- "customize": 61750,
- "copy": 61751,
- "contact": 61752,
- "collections": 61753,
- "code": 61754,
- "close": 61755,
- "chevron-right": 61756,
- "chevron-left": 61757,
- "calendar-silhouette": 61758,
- "bookmark": 61759,
- "bookmark-fill": 61760,
- "arrow": 61761,
- "arrow-left": 61762,
- "arrow-down": 61763,
- "accepted": 61764
+ "sort": 61704,
+ "sort-desc": 61705,
+ "sort-asc": 61706,
+ "sort-asc-grey": 61707,
+ "share": 61708,
+ "settings": 61709,
+ "search": 61710,
+ "search-2": 61711,
+ "rejected": 61712,
+ "registries": 61713,
+ "quotes": 61714,
+ "profile": 61715,
+ "preprints": 61716,
+ "plus": 61717,
+ "pending": 61718,
+ "pdf": 61719,
+ "papers": 61720,
+ "padlock": 61721,
+ "padlock-unlock": 61722,
+ "my-projects": 61723,
+ "minus": 61724,
+ "menu": 61725,
+ "meetings": 61726,
+ "materials": 61727,
+ "list": 61728,
+ "link": 61729,
+ "last": 61730,
+ "institutions": 61731,
+ "institution": 61732,
+ "information": 61733,
+ "image": 61734,
+ "home": 61735,
+ "home-2": 61736,
+ "help": 61737,
+ "folder": 61738,
+ "first": 61739,
+ "filter": 61740,
+ "eye-view": 61741,
+ "eye-hidden": 61742,
+ "email": 61743,
+ "duplicate": 61744,
+ "download": 61745,
+ "double-arrow-left": 61746,
+ "dots": 61747,
+ "donate": 61748,
+ "doc": 61749,
+ "diagram": 61750,
+ "data": 61751,
+ "customize": 61752,
+ "cos-shield": 61753,
+ "copy": 61754,
+ "contact": 61755,
+ "collections": 61756,
+ "code": 61757,
+ "close": 61758,
+ "chevron-right": 61759,
+ "chevron-left": 61760,
+ "calendar-silhouette": 61761,
+ "bookmark": 61762,
+ "bookmark-fill": 61763,
+ "arrow": 61764,
+ "arrow-left": 61765,
+ "arrow-down": 61766,
+ "accepted": 61767
}
diff --git a/src/assets/icons/dist/icons.ts b/src/assets/icons/dist/icons.ts
index 41e6ca0ae..ada611307 100644
--- a/src/assets/icons/dist/icons.ts
+++ b/src/assets/icons/dist/icons.ts
@@ -6,6 +6,7 @@ export type IconsId =
| 'trash'
| 'support'
| 'supplements'
+ | 'sort'
| 'sort-desc'
| 'sort-asc'
| 'sort-asc-grey'
@@ -33,6 +34,7 @@ export type IconsId =
| 'link'
| 'last'
| 'institutions'
+ | 'institution'
| 'information'
| 'image'
| 'home'
@@ -53,6 +55,7 @@ export type IconsId =
| 'diagram'
| 'data'
| 'customize'
+ | 'cos-shield'
| 'copy'
| 'contact'
| 'collections'
@@ -76,6 +79,7 @@ export type IconsKey =
| 'Trash'
| 'Support'
| 'Supplements'
+ | 'Sort'
| 'SortDesc'
| 'SortAsc'
| 'SortAscGrey'
@@ -103,6 +107,7 @@ export type IconsKey =
| 'Link'
| 'Last'
| 'Institutions'
+ | 'Institution'
| 'Information'
| 'Image'
| 'Home'
@@ -123,6 +128,7 @@ export type IconsKey =
| 'Diagram'
| 'Data'
| 'Customize'
+ | 'CosShield'
| 'Copy'
| 'Contact'
| 'Collections'
@@ -146,6 +152,7 @@ export enum Icons {
Trash = 'trash',
Support = 'support',
Supplements = 'supplements',
+ Sort = 'sort',
SortDesc = 'sort-desc',
SortAsc = 'sort-asc',
SortAscGrey = 'sort-asc-grey',
@@ -173,6 +180,7 @@ export enum Icons {
Link = 'link',
Last = 'last',
Institutions = 'institutions',
+ Institution = 'institution',
Information = 'information',
Image = 'image',
Home = 'home',
@@ -193,6 +201,7 @@ export enum Icons {
Diagram = 'diagram',
Data = 'data',
Customize = 'customize',
+ CosShield = 'cos-shield',
Copy = 'copy',
Contact = 'contact',
Collections = 'collections',
@@ -217,65 +226,68 @@ export const ICONS_CODEPOINTS: Record = {
[Icons.Trash]: '61701',
[Icons.Support]: '61702',
[Icons.Supplements]: '61703',
- [Icons.SortDesc]: '61704',
- [Icons.SortAsc]: '61705',
- [Icons.SortAscGrey]: '61706',
- [Icons.Share]: '61707',
- [Icons.Settings]: '61708',
- [Icons.Search]: '61709',
- [Icons.Search2]: '61710',
- [Icons.Rejected]: '61711',
- [Icons.Registries]: '61712',
- [Icons.Quotes]: '61713',
- [Icons.Profile]: '61714',
- [Icons.Preprints]: '61715',
- [Icons.Plus]: '61716',
- [Icons.Pending]: '61717',
- [Icons.Pdf]: '61718',
- [Icons.Papers]: '61719',
- [Icons.Padlock]: '61720',
- [Icons.PadlockUnlock]: '61721',
- [Icons.MyProjects]: '61722',
- [Icons.Minus]: '61723',
- [Icons.Menu]: '61724',
- [Icons.Meetings]: '61725',
- [Icons.Materials]: '61726',
- [Icons.List]: '61727',
- [Icons.Link]: '61728',
- [Icons.Last]: '61729',
- [Icons.Institutions]: '61730',
- [Icons.Information]: '61731',
- [Icons.Image]: '61732',
- [Icons.Home]: '61733',
- [Icons.Home2]: '61734',
- [Icons.Help]: '61735',
- [Icons.Folder]: '61736',
- [Icons.First]: '61737',
- [Icons.Filter]: '61738',
- [Icons.EyeView]: '61739',
- [Icons.EyeHidden]: '61740',
- [Icons.Email]: '61741',
- [Icons.Duplicate]: '61742',
- [Icons.Download]: '61743',
- [Icons.DoubleArrowLeft]: '61744',
- [Icons.Dots]: '61745',
- [Icons.Donate]: '61746',
- [Icons.Doc]: '61747',
- [Icons.Diagram]: '61748',
- [Icons.Data]: '61749',
- [Icons.Customize]: '61750',
- [Icons.Copy]: '61751',
- [Icons.Contact]: '61752',
- [Icons.Collections]: '61753',
- [Icons.Code]: '61754',
- [Icons.Close]: '61755',
- [Icons.ChevronRight]: '61756',
- [Icons.ChevronLeft]: '61757',
- [Icons.CalendarSilhouette]: '61758',
- [Icons.Bookmark]: '61759',
- [Icons.BookmarkFill]: '61760',
- [Icons.Arrow]: '61761',
- [Icons.ArrowLeft]: '61762',
- [Icons.ArrowDown]: '61763',
- [Icons.Accepted]: '61764',
+ [Icons.Sort]: '61704',
+ [Icons.SortDesc]: '61705',
+ [Icons.SortAsc]: '61706',
+ [Icons.SortAscGrey]: '61707',
+ [Icons.Share]: '61708',
+ [Icons.Settings]: '61709',
+ [Icons.Search]: '61710',
+ [Icons.Search2]: '61711',
+ [Icons.Rejected]: '61712',
+ [Icons.Registries]: '61713',
+ [Icons.Quotes]: '61714',
+ [Icons.Profile]: '61715',
+ [Icons.Preprints]: '61716',
+ [Icons.Plus]: '61717',
+ [Icons.Pending]: '61718',
+ [Icons.Pdf]: '61719',
+ [Icons.Papers]: '61720',
+ [Icons.Padlock]: '61721',
+ [Icons.PadlockUnlock]: '61722',
+ [Icons.MyProjects]: '61723',
+ [Icons.Minus]: '61724',
+ [Icons.Menu]: '61725',
+ [Icons.Meetings]: '61726',
+ [Icons.Materials]: '61727',
+ [Icons.List]: '61728',
+ [Icons.Link]: '61729',
+ [Icons.Last]: '61730',
+ [Icons.Institutions]: '61731',
+ [Icons.Institution]: '61732',
+ [Icons.Information]: '61733',
+ [Icons.Image]: '61734',
+ [Icons.Home]: '61735',
+ [Icons.Home2]: '61736',
+ [Icons.Help]: '61737',
+ [Icons.Folder]: '61738',
+ [Icons.First]: '61739',
+ [Icons.Filter]: '61740',
+ [Icons.EyeView]: '61741',
+ [Icons.EyeHidden]: '61742',
+ [Icons.Email]: '61743',
+ [Icons.Duplicate]: '61744',
+ [Icons.Download]: '61745',
+ [Icons.DoubleArrowLeft]: '61746',
+ [Icons.Dots]: '61747',
+ [Icons.Donate]: '61748',
+ [Icons.Doc]: '61749',
+ [Icons.Diagram]: '61750',
+ [Icons.Data]: '61751',
+ [Icons.Customize]: '61752',
+ [Icons.CosShield]: '61753',
+ [Icons.Copy]: '61754',
+ [Icons.Contact]: '61755',
+ [Icons.Collections]: '61756',
+ [Icons.Code]: '61757',
+ [Icons.Close]: '61758',
+ [Icons.ChevronRight]: '61759',
+ [Icons.ChevronLeft]: '61760',
+ [Icons.CalendarSilhouette]: '61761',
+ [Icons.Bookmark]: '61762',
+ [Icons.BookmarkFill]: '61763',
+ [Icons.Arrow]: '61764',
+ [Icons.ArrowLeft]: '61765',
+ [Icons.ArrowDown]: '61766',
+ [Icons.Accepted]: '61767',
};
diff --git a/src/assets/icons/dist/icons.woff b/src/assets/icons/dist/icons.woff
index f75e80439..cc86215c3 100644
Binary files a/src/assets/icons/dist/icons.woff and b/src/assets/icons/dist/icons.woff differ
diff --git a/src/assets/icons/dist/icons.woff2 b/src/assets/icons/dist/icons.woff2
index 84af7959b..f0452e57f 100644
Binary files a/src/assets/icons/dist/icons.woff2 and b/src/assets/icons/dist/icons.woff2 differ
diff --git a/src/assets/icons/source/cos-shield.svg b/src/assets/icons/source/cos-shield.svg
index 28fde1b59..c12b57c9a 100644
--- a/src/assets/icons/source/cos-shield.svg
+++ b/src/assets/icons/source/cos-shield.svg
@@ -1,9 +1,5 @@
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/src/assets/icons/source/filter.svg b/src/assets/icons/source/filter.svg
index d0e579666..55f40e154 100644
--- a/src/assets/icons/source/filter.svg
+++ b/src/assets/icons/source/filter.svg
@@ -1,4 +1,6 @@
-
-
-
+
+
+
+
+
diff --git a/src/assets/icons/source/institution.svg b/src/assets/icons/source/institution.svg
index 7da93f18d..d8f284ab2 100644
--- a/src/assets/icons/source/institution.svg
+++ b/src/assets/icons/source/institution.svg
@@ -1,20 +1,15 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/icons/source/sort.svg b/src/assets/icons/source/sort.svg
index 31d57840e..a65f1144d 100644
--- a/src/assets/icons/source/sort.svg
+++ b/src/assets/icons/source/sort.svg
@@ -1,11 +1,6 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/src/assets/styles/overrides/button.scss b/src/assets/styles/overrides/button.scss
index 1271cf45b..6d1d6ffa0 100644
--- a/src/assets/styles/overrides/button.scss
+++ b/src/assets/styles/overrides/button.scss
@@ -2,6 +2,7 @@
@use "../mixins" as mix;
.p-button {
+ flex: 1;
border: none;
padding: 0.7rem 1.15rem;
font-size: 1rem;
@@ -101,7 +102,8 @@
.btn-icon-text,
.btn-icon-text-success,
-.form-btn {
+.form-btn,
+.btn-icon {
.p-button {
background-color: transparent;
color: var.$pr-blue-1;
@@ -129,6 +131,22 @@
}
}
+.btn-icon {
+ .p-button {
+ background-color: transparent;
+ color: var.$dark-blue-1;
+ padding: 0.7rem;
+
+ &:hover {
+ color: var.$pr-blue-1;
+ }
+
+ &:disabled {
+ color: var.$grey-1;
+ }
+ }
+}
+
.btn-icon-text-success {
.p-button {
color: var.$green-1;
@@ -235,6 +253,19 @@
}
}
+.overview-btn {
+ width: mix.rem(80px);
+}
+
+.bookmark-btn {
+ .p-button {
+ .p-button-icon {
+ height: mix.rem(21px);
+ width: mix.rem(21px);
+ }
+ }
+}
+
.bg-primary-blue-second.p-button {
background-color: var.$pr-blue-2;
}
diff --git a/src/assets/styles/overrides/menu.scss b/src/assets/styles/overrides/menu.scss
index 2ae6d2fb7..592159e4a 100644
--- a/src/assets/styles/overrides/menu.scss
+++ b/src/assets/styles/overrides/menu.scss
@@ -1,17 +1,15 @@
@use "../variables" as var;
-
-.p-menu-overlay {
- top: 2rem !important;
- left: 0 !important;
-}
+@use "../mixins" as mix;
.p-menu-item-content {
a {
color: var.$dark-blue-2;
}
+
a:hover {
text-decoration: none;
}
+
&:hover {
text-decoration: none;
background: var.$bg-blue-3;
@@ -19,6 +17,6 @@
}
.p-menu {
- min-width: 7rem;
- width: 100%;
+ margin-top: mix.rem(7px);
+ min-width: fit-content;
}
diff --git a/src/assets/styles/overrides/tag.scss b/src/assets/styles/overrides/tag.scss
index 1a71b2be9..f03a55361 100644
--- a/src/assets/styles/overrides/tag.scss
+++ b/src/assets/styles/overrides/tag.scss
@@ -15,3 +15,9 @@
background: var.$grey-1;
color: var.$white;
}
+
+.p-tag-info {
+ color: var.$dark-blue-1;
+ background: var.$bg-blue-3;
+ font-weight: normal;
+}
diff --git a/src/assets/styles/overrides/toggleswitch.scss b/src/assets/styles/overrides/toggleswitch.scss
new file mode 100644
index 000000000..ddea518d6
--- /dev/null
+++ b/src/assets/styles/overrides/toggleswitch.scss
@@ -0,0 +1,19 @@
+@use "assets/styles/variables" as var;
+
+.p-toggleswitch {
+ scale: 1.2;
+
+ .p-toggleswitch-handle {
+ scale: 1.15;
+ }
+}
+
+.p-toggleswitch:not(.p-disabled) .p-toggleswitch-slider,
+.p-toggleswitch:not(.p-disabled):has(.p-toggleswitch-input:hover) .p-toggleswitch-slider {
+ background: var.$grey-1;
+}
+
+.p-toggleswitch:not(.p-disabled).p-toggleswitch-checked .p-toggleswitch-slider,
+.p-toggleswitch:not(.p-disabled):has(.p-toggleswitch-input:hover).p-toggleswitch-checked .p-toggleswitch-slider {
+ background: var.$pr-blue-1;
+}
diff --git a/src/assets/styles/overrides/tooltip.scss b/src/assets/styles/overrides/tooltip.scss
index 0a37ad1d0..18e532846 100644
--- a/src/assets/styles/overrides/tooltip.scss
+++ b/src/assets/styles/overrides/tooltip.scss
@@ -13,3 +13,13 @@
border-right-color: var.$white;
border: 1px solid var.$grey-3;
}
+
+.p-tooltip.p-tooltip-bottom {
+ .p-tooltip-text {
+ min-width: auto;
+ }
+
+ .p-tooltip-arrow {
+ border: none;
+ }
+}
diff --git a/src/assets/styles/styles.scss b/src/assets/styles/styles.scss
index 322830410..1f70a9e46 100644
--- a/src/assets/styles/styles.scss
+++ b/src/assets/styles/styles.scss
@@ -33,5 +33,6 @@
@use "./overrides/menu";
@use "./overrides/spinner";
@use "./overrides/password";
+@use "./overrides/toggleswitch";
@use "./overrides/tooltip";
@use "./overrides/toast";