diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 1e89e5f61..57f4a6313 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -103,6 +103,18 @@ export const routes: Routes = [
(mod) => mod.RegistrationsComponent
),
},
+ {
+ path: 'settings',
+ loadComponent: () =>
+ import('./features/project/settings/settings.component').then((mod) => mod.SettingsComponent),
+ },
+ {
+ path: 'contributors',
+ loadComponent: () =>
+ import('@osf/features/project/contributors/contributors.component').then(
+ (mod) => mod.ContributorsComponent
+ ),
+ },
{
path: 'analytics',
loadComponent: () =>
diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts
index d865e0250..9e0fe3acc 100644
--- a/src/app/core/constants/nav-items.constant.ts
+++ b/src/app/core/constants/nav-items.constant.ts
@@ -90,6 +90,11 @@ export const PROJECT_MENU_ITEMS: MenuItem[] = [
label: 'navigation.project.registrations',
routerLink: 'registrations',
},
+ {
+ label: 'navigation.project.settings',
+ routerLink: 'settings',
+ },
+ { label: 'navigation.project.contributors', routerLink: 'contributors' },
{ label: 'navigation.project.analytics', routerLink: 'analytics' },
],
},
diff --git a/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.html b/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.html
new file mode 100644
index 000000000..1f4f375f2
--- /dev/null
+++ b/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.html
@@ -0,0 +1,25 @@
+
diff --git a/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.scss b/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.spec.ts b/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.spec.ts
new file mode 100644
index 000000000..888146c98
--- /dev/null
+++ b/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AddContributorDialogComponent } from './add-contributor-dialog.component';
+
+describe('AddContributorDialogComponent', () => {
+ let component: AddContributorDialogComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [AddContributorDialogComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AddContributorDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.ts b/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.ts
new file mode 100644
index 000000000..28206a943
--- /dev/null
+++ b/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.ts
@@ -0,0 +1,24 @@
+import { TranslatePipe } from '@ngx-translate/core';
+
+import { Button } from 'primeng/button';
+import { DynamicDialogRef } from 'primeng/dynamicdialog';
+
+import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
+
+import { SearchInputComponent } from '@osf/shared';
+
+@Component({
+ selector: 'osf-add-contributor-dialog',
+ imports: [Button, TranslatePipe, SearchInputComponent],
+ templateUrl: './add-contributor-dialog.component.html',
+ styleUrl: './add-contributor-dialog.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AddContributorDialogComponent {
+ dialogRef = inject(DynamicDialogRef);
+ protected searchValue = signal('');
+
+ addContributor(): void {
+ this.dialogRef.close();
+ }
+}
diff --git a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html
new file mode 100644
index 000000000..1430fd514
--- /dev/null
+++ b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html
@@ -0,0 +1,59 @@
+
+
+ Link name
+
+
+
+
+
+
+
+
Anonymize contributor list for this link (e.g., for blind peer review).
+ Ensure the wiki pages, files, registration forms and add-ons do not contain identifying information.
+
+
+
+
+
+
Which components would you like to associate with this link?
+ Anyone with the private link can view—but not edit—the components associated with the link.
+
+
+
+
+
Component Name Example
+
+
+
Component Name Example
+
+
+
Component Name Example
+
+
+
+
+
diff --git a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.scss b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.scss
new file mode 100644
index 000000000..0cc299cb5
--- /dev/null
+++ b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.scss
@@ -0,0 +1,7 @@
+@use "assets/styles/variables" as var;
+
+:host {
+ .break-line {
+ border: 1px solid var.$grey-3;
+ }
+}
diff --git a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts
new file mode 100644
index 000000000..e5954f5bc
--- /dev/null
+++ b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CreateViewLinkDialogComponent } from './create-view-link-dialog.component';
+
+describe('CreateViewLinkDialogComponent', () => {
+ let component: CreateViewLinkDialogComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [CreateViewLinkDialogComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(CreateViewLinkDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.ts b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.ts
new file mode 100644
index 000000000..f44a988d6
--- /dev/null
+++ b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.ts
@@ -0,0 +1,40 @@
+import {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core';
+import {Button} from "primeng/button";
+import {SearchInputComponent} from "@osf/shared";
+import {TranslatePipe} from "@ngx-translate/core";
+import {DynamicDialogRef} from 'primeng/dynamicdialog';
+import {InputText} from 'primeng/inputtext';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {Checkbox} from 'primeng/checkbox';
+
+@Component({
+ selector: 'osf-create-view-link-dialog',
+ imports: [
+ Button,
+ SearchInputComponent,
+ TranslatePipe,
+ InputText,
+ ReactiveFormsModule,
+ FormsModule,
+ Checkbox
+ ],
+ templateUrl: './create-view-link-dialog.component.html',
+ styleUrl: './create-view-link-dialog.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class CreateViewLinkDialogComponent {
+ dialogRef = inject(DynamicDialogRef);
+ protected linkName = signal('');
+ anonymous = signal(true);
+ componentExample1 = signal(true);
+ componentExample2 = signal(false);
+ componentExample3 = signal(false);
+
+ addContributor(): void {
+ this.dialogRef.close();
+ }
+
+ onLinkNameChange(value: string): void {
+ this.linkName.set(value);
+ }
+}
diff --git a/src/app/features/project/contributors/contributors.component.html b/src/app/features/project/contributors/contributors.component.html
new file mode 100644
index 000000000..d00fc1371
--- /dev/null
+++ b/src/app/features/project/contributors/contributors.component.html
@@ -0,0 +1,210 @@
+Contributors
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ selectedOption.label | translate }}
+
+
+ {{ item.label | translate }}
+
+
+
+
+
+
+ {{ selectedOption.label | translate }}
+
+
+ {{ item.label | translate }}
+
+
+
+
+
+
+
+
+
+
+ {{'project.contributors.table.headers.name' | translate }}
+
+
+
{{'project.contributors.table.headers.permissions' | translate }}
+
+
+
+
Permission Information
+
+
+
Read
+
+ View project content and comment
+
+
+
+
+
Read + Write
+
+ Read privileges
+ Add and configure components
+ Add and edit content
+
+
+
+
+
Administrator
+
+ Read and write privileges
+ Manage contributor
+ Delete and register project
+ Public private settings
+
+
+
+
+
+
+
+
+
+
{{'project.contributors.table.headers.contributor' | translate }}
+
+
+
+
Bibliographic Contributor Information
+
+ Only bibliographic contributors will be displayed in the Contributors list and in project citations. Non-bibliographic contributors can read and modify the project as normal.
+
+
+
+
+
+
+
{{'project.contributors.table.headers.curator' | translate }}
+
+
+
+
Curator Information
+
+ An administrator designated by your affiliated institution to curate your project
+
+
+
+
+ {{'project.contributors.table.headers.employment' | translate }}
+ {{'project.contributors.table.headers.education' | translate }}
+
+
+
+
+
+
+
+ {{ item.name }}
+
+
+
+
+
+
+ {{ selectedOption.label | translate }}
+
+
+ {{ item.label | translate }}
+
+
+
+
+
+
+
+
+
+
+
+ @if(item.employmentHistory){
+ Show employment history
+ }
+ @else {
+ No employment history
+ }
+
+
+
+
+
+
+
+
+
+
+
{{'project.contributors.viewOnly' | translate}}
+
{{'project.contributors.createLink' | translate}}
+
+
+
+
+
+
+
diff --git a/src/app/features/project/contributors/contributors.component.scss b/src/app/features/project/contributors/contributors.component.scss
new file mode 100644
index 000000000..2d5979f7f
--- /dev/null
+++ b/src/app/features/project/contributors/contributors.component.scss
@@ -0,0 +1,51 @@
+@use "assets/styles/variables" as var;
+@use "assets/styles/mixins" as mix;
+
+:host {
+ .title {
+ padding: 1.5rem 3.4rem 3.4rem 1.7rem;
+ }
+
+ .header {
+ //@include mix.flex-center;
+ text-align: center;
+ }
+
+ .contributors {
+ display: flex;
+ flex-direction: column;
+ row-gap: 1.7rem;
+ padding: 1.7rem;
+ background: var.$white;
+
+ .filters-container {
+ display: flex;
+ gap: 0.8rem;
+
+
+ .search {
+ width: 60%;
+ }
+ .contributors-filter-container {
+ display: flex;
+ gap: 0.8rem;
+ width: 40%;
+ }
+
+ }
+
+ .red-icon {
+ color: var.$red-1;
+ cursor: pointer;
+ }
+
+ .blue-icon {
+ color: var.$pr-blue-1;
+ cursor: pointer;
+ }
+ }
+
+ li {
+ list-style: inside;
+ }
+}
diff --git a/src/app/features/project/contributors/contributors.component.spec.ts b/src/app/features/project/contributors/contributors.component.spec.ts
new file mode 100644
index 000000000..3852329cb
--- /dev/null
+++ b/src/app/features/project/contributors/contributors.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ContributorsComponent } from './contributors.component';
+
+describe('ContributorsComponent', () => {
+ let component: ContributorsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ContributorsComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(ContributorsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/project/contributors/contributors.component.ts b/src/app/features/project/contributors/contributors.component.ts
new file mode 100644
index 000000000..3d1d1700e
--- /dev/null
+++ b/src/app/features/project/contributors/contributors.component.ts
@@ -0,0 +1,184 @@
+import { TranslatePipe, TranslateService } from '@ngx-translate/core';
+
+import { Button } from 'primeng/button';
+import { Checkbox } from 'primeng/checkbox';
+import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
+import { Select } from 'primeng/select';
+import { TableModule, TablePageEvent } from 'primeng/table';
+import { Tooltip } from 'primeng/tooltip';
+
+import { DatePipe } from '@angular/common';
+import { ChangeDetectionStrategy, Component, inject, output, signal } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { FormsModule } from '@angular/forms';
+
+import { MY_PROJECTS_TABLE_PARAMS } from '@core/constants/my-projects-table.constants';
+import { AddContributorDialogComponent } from '@osf/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component';
+import { LinkTableModel } from '@osf/features/project/settings';
+import { ViewOnlyTableComponent } from '@osf/shared';
+import { SearchInputComponent } from '@shared/components/search-input/search-input.component';
+import { SelectOption } from '@shared/entities/select-option.interface';
+import { TableParameters } from '@shared/entities/table-parameters.interface';
+import { IS_WEB, IS_XSMALL } from '@shared/utils/breakpoints.tokens';
+import {
+ CreateViewLinkDialogComponent
+} from "@osf/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component";
+
+@Component({
+ selector: 'osf-contributors',
+ imports: [
+ Button,
+ SearchInputComponent,
+ Select,
+ TranslatePipe,
+ FormsModule,
+ DatePipe,
+ TableModule,
+ ViewOnlyTableComponent,
+ Tooltip,
+ Checkbox,
+ ],
+ templateUrl: './contributors.component.html',
+ styleUrl: './contributors.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [DialogService],
+})
+export class ContributorsComponent {
+ protected searchValue = signal('');
+ readonly #translateService = inject(TranslateService);
+ readonly items = signal([
+ {
+ name: 'John Doe',
+ permissions: 'Administrator',
+ bibliographicContributor: true,
+ curator: true,
+ employmentHistory: 'https://some_link',
+ educationHistory: null,
+ },
+ {
+ name: 'Jeremy Wolfe',
+ permissions: 'Read + Write',
+ bibliographicContributor: false,
+ curator: true,
+ employmentHistory: null,
+ educationHistory: 'https://some_link',
+ },
+ {
+ name: 'John Doe',
+ permissions: 'Administrator',
+ bibliographicContributor: true,
+ curator: true,
+ employmentHistory: 'https://some_link',
+ educationHistory: 'https://some_link',
+ },
+ ]);
+ protected readonly tableParams = signal({
+ ...MY_PROJECTS_TABLE_PARAMS,
+ });
+
+ protected readonly selectedPermission = signal('');
+ protected readonly selectedBibliography = signal('');
+ pageChange = output();
+ protected readonly isWeb = toSignal(inject(IS_WEB));
+ protected readonly isMobile = toSignal(inject(IS_XSMALL));
+
+ dialogRef: DynamicDialogRef | null = null;
+ readonly #dialogService = inject(DialogService);
+
+ protected readonly permissionsOptions: SelectOption[] = [
+ {
+ label: this.#translateService.instant('project.contributors.permissions.administrator'),
+ value: 'Administrator',
+ },
+ {
+ label: this.#translateService.instant('project.contributors.permissions.readAndWrite'),
+ value: 'Read + Write',
+ },
+ {
+ label: this.#translateService.instant('project.contributors.permissions.read'),
+ value: 'Read',
+ },
+ ];
+
+ protected readonly bibliographyOptions: SelectOption[] = [
+ {
+ label: this.#translateService.instant('project.contributors.bibliography.bibliographic'),
+ value: 'Bibliographic',
+ },
+ {
+ label: this.#translateService.instant('project.contributors.bibliography.nonBibliographic'),
+ value: 'Non-Bibliographic',
+ },
+ ];
+
+ tableData: LinkTableModel[] = [
+ {
+ linkName: 'name',
+ sharedComponents: 'Project name',
+ createdDate: new Date(),
+ createdBy: 'Igor',
+ anonymous: false,
+ link: 'www.facebook.com',
+ },
+ {
+ linkName: 'name',
+ sharedComponents: 'Project name',
+ createdDate: new Date(),
+ createdBy: 'Igor',
+ anonymous: false,
+ link: 'www.facebook.com',
+ },
+ {
+ linkName: 'name',
+ sharedComponents: 'Project name',
+ createdDate: new Date(),
+ createdBy: 'Igor',
+ anonymous: false,
+ link: 'www.facebook.com',
+ },
+ {
+ linkName: 'name',
+ sharedComponents: 'Project name',
+ createdDate: new Date(),
+ createdBy: 'Igor',
+ anonymous: false,
+ link: 'www.facebook.com',
+ },
+ ];
+
+ protected onPermissionChange(value: string): void {
+ this.selectedPermission.set(value);
+ }
+
+ protected onBibliographyChange(value: string): void {
+ this.selectedBibliography.set(value);
+ }
+
+ protected onPageChange(event: TablePageEvent): void {
+ this.pageChange.emit(event);
+ }
+
+ protected onItemPermissionChange(event: TablePageEvent): void {}
+
+ addContributor() {
+ this.dialogRef = this.#dialogService.open(AddContributorDialogComponent, {
+ width: '552px',
+ focusOnShow: false,
+ header: this.#translateService.instant('project.contributors.addContributor'),
+ closeOnEscape: true,
+ modal: true,
+ closable: true,
+ });
+ }
+
+ createViewLink() {
+ this.dialogRef = this.#dialogService.open(CreateViewLinkDialogComponent, {
+ width: '448px',
+ focusOnShow: false,
+ header: this.#translateService.instant('project.contributors.createLinkDialog.dialogTitle'),
+ closeOnEscape: true,
+ modal: true,
+ closable: true,
+ });
+ }
+}
diff --git a/src/app/features/project/metadata/project-metadata.component.html b/src/app/features/project/metadata/project-metadata.component.html
index f19d3c730..9e17d0536 100644
--- a/src/app/features/project/metadata/project-metadata.component.html
+++ b/src/app/features/project/metadata/project-metadata.component.html
@@ -29,6 +29,6 @@ {{ meta.title }}
diff --git a/src/app/features/project/settings/components/accordion-table/accordion-table.component.html b/src/app/features/project/settings/components/accordion-table/accordion-table.component.html
new file mode 100644
index 000000000..bfb6d0a56
--- /dev/null
+++ b/src/app/features/project/settings/components/accordion-table/accordion-table.component.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
{{ title() }}
+
+
+ @if (rightControls().length > 0) {
+
+ @for (control of rightControls(); track control) {
+
+ @if (control.label) {
+
+ {{ control.label }}
+
+ }
+
+ @switch (control.type) {
+ @case ('dropdown') {
+
+ }
+ @case ('text') {
+
+ {{ control.value }}
+
+ }
+ }
+
+ }
+
+ }
+
+
+@if (expanded()) {
+
+
+
+}
diff --git a/src/app/features/project/settings/components/accordion-table/accordion-table.component.spec.ts b/src/app/features/project/settings/components/accordion-table/accordion-table.component.spec.ts
new file mode 100644
index 000000000..26df6a5d4
--- /dev/null
+++ b/src/app/features/project/settings/components/accordion-table/accordion-table.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AccordionTableComponent } from './accordion-table.component';
+
+describe('AccordionTableComponent', () => {
+ let component: AccordionTableComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [AccordionTableComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(AccordionTableComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/project/settings/components/accordion-table/accordion-table.component.ts b/src/app/features/project/settings/components/accordion-table/accordion-table.component.ts
new file mode 100644
index 000000000..3654d7a91
--- /dev/null
+++ b/src/app/features/project/settings/components/accordion-table/accordion-table.component.ts
@@ -0,0 +1,38 @@
+import { Button } from 'primeng/button';
+import { DropdownModule } from 'primeng/dropdown';
+
+import { NgClass } from '@angular/common';
+import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+
+type RightControl =
+ | {
+ type: 'dropdown';
+ label?: string;
+ value: string;
+ options: { label: string; value: string }[];
+ onChange?: (value: string) => void;
+ }
+ | {
+ type: 'text';
+ label?: string;
+ value: string;
+ };
+
+@Component({
+ selector: 'osf-accordion-table',
+ imports: [NgClass, DropdownModule, FormsModule, Button],
+ templateUrl: './accordion-table.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AccordionTableComponent {
+ title = input.required();
+
+ rightControls = input.required();
+
+ expanded = signal(false);
+
+ toggle() {
+ this.expanded.set(!this.expanded());
+ }
+}
diff --git a/src/app/features/project/settings/components/index.ts b/src/app/features/project/settings/components/index.ts
new file mode 100644
index 000000000..74042b224
--- /dev/null
+++ b/src/app/features/project/settings/components/index.ts
@@ -0,0 +1 @@
+export * from './accordion-table/accordion-table.component';
diff --git a/src/app/features/project/settings/index.ts b/src/app/features/project/settings/index.ts
new file mode 100644
index 000000000..c27c630b9
--- /dev/null
+++ b/src/app/features/project/settings/index.ts
@@ -0,0 +1,3 @@
+export * from './components';
+export * from './models';
+export * from './settings.component';
diff --git a/src/app/features/project/settings/models/index.ts b/src/app/features/project/settings/models/index.ts
new file mode 100644
index 000000000..4a4b82eda
--- /dev/null
+++ b/src/app/features/project/settings/models/index.ts
@@ -0,0 +1 @@
+export * from './link-table.model';
diff --git a/src/app/features/project/settings/models/link-table.model.ts b/src/app/features/project/settings/models/link-table.model.ts
new file mode 100644
index 000000000..3990216b8
--- /dev/null
+++ b/src/app/features/project/settings/models/link-table.model.ts
@@ -0,0 +1,8 @@
+export interface LinkTableModel {
+ linkName: string;
+ sharedComponents: string;
+ createdDate: string | Date;
+ createdBy: string;
+ anonymous: boolean;
+ link: string;
+}
diff --git a/src/app/features/project/settings/settings.component.html b/src/app/features/project/settings/settings.component.html
new file mode 100644
index 000000000..dada4425b
--- /dev/null
+++ b/src/app/features/project/settings/settings.component.html
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+ {{ 'myProjects.settings.project' | translate }}
+
+
+
+
+
+ {{ 'myProjects.createProject.storageLocation' | translate }}
+
+
+
{{ 'myProjects.createProject.storageLocation' | translate }}:
+
+
United States
+
+
+ Storage location cannot be changed after project is created.
+
+
+
+ {{ 'myProjects.settings.viewOnlyLinks' | translate }}
+
+ {{ 'myProjects.settings.viewOnlySubtitle' | translate }}
+
+
+
+
+
+
+
+
+ {{ 'myProjects.settings.accessRequests' | translate }}
+
+
+
+
+ {{ 'myProjects.settings.accessRequestsText' | translate }}
+
+
+
+
+
+ {{ 'myProjects.settings.wiki' | translate }}
+
+
+
+
+
+ {{ 'myProjects.settings.wikiText' | translate }}
+
+
+
+ {{ 'myProjects.settings.wikiConfigureTitle' | translate }}
+
+ {{ 'myProjects.settings.wikiConfigureText' | translate }}
+
+
+
+
+ Sub text
+
+
+
+
+
+ {{ 'myProjects.settings.commenting' | translate }}
+
+
+
+
+
{{ 'myProjects.settings.contributorsCanPost' | translate }}
+
+
+
+
+
{{ 'myProjects.settings.osfUserCanPost' | translate }}
+
+
+
+
+
+ {{ 'myProjects.settings.emailNotifications' | translate }}
+
+ {{ 'myProjects.settings.emailNotificationsText' | translate }}
+
+
+
+
+ Sub text
+
+
+
+
+
+ {{ 'myProjects.settings.redirectLink' | translate }}
+
+
+
+
+ {{ 'myProjects.settings.redirectLinkText' | translate }}
+
+
+
+
+
+ {{ 'myProjects.settings.projectAffiliation' | translate }}
+
+ {{ 'myProjects.settings.projectsCanBeAffiliated' | translate }}
+
+
+ {{ 'myProjects.settings.institutionalLogos' | translate }}
+ {{ 'myProjects.settings.publicProjectsToBeDiscoverable' | translate }}
+ {{ 'myProjects.settings.singleSignInToTHeOSF' | translate }}
+
+ {{ 'myProjects.settings.faq' | translate }}
+
+
+
+
+
+
diff --git a/src/app/features/project/settings/settings.component.scss b/src/app/features/project/settings/settings.component.scss
new file mode 100644
index 000000000..891d35c2c
--- /dev/null
+++ b/src/app/features/project/settings/settings.component.scss
@@ -0,0 +1,11 @@
+@use "../../../../assets/styles/variables" as var;
+@use "../../../../assets/styles/mixins" as mix;
+
+:host {
+ @include mix.flex-column;
+ flex: 1;
+}
+
+.icon-space {
+ padding: 0.7rem 1.1rem;
+}
diff --git a/src/app/features/project/settings/settings.component.spec.ts b/src/app/features/project/settings/settings.component.spec.ts
new file mode 100644
index 000000000..83f8f7e90
--- /dev/null
+++ b/src/app/features/project/settings/settings.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SettingsComponent } from './settings.component';
+
+describe('SettingsComponent', () => {
+ let component: SettingsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [SettingsComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(SettingsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/features/project/settings/settings.component.ts b/src/app/features/project/settings/settings.component.ts
new file mode 100644
index 000000000..d7071b9c7
--- /dev/null
+++ b/src/app/features/project/settings/settings.component.ts
@@ -0,0 +1,124 @@
+import { TranslatePipe } from '@ngx-translate/core';
+
+import { Button } from 'primeng/button';
+import { Card } from 'primeng/card';
+import { Checkbox } from 'primeng/checkbox';
+import { InputText } from 'primeng/inputtext';
+import { RadioButton } from 'primeng/radiobutton';
+import { TabPanels } from 'primeng/tabs';
+import { Textarea } from 'primeng/textarea';
+
+import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
+import { RouterLink } from '@angular/router';
+
+import { AccordionTableComponent } from '@osf/features/project/settings/components';
+import { LinkTableModel } from '@osf/features/project/settings/models';
+import { ShareIndexingEnum } from '@osf/features/settings/account-settings/components/share-indexing/enums/share-indexing.enum';
+import { ViewOnlyTableComponent } from '@osf/shared';
+import { SubHeaderComponent } from '@shared/components/sub-header/sub-header.component';
+import { ProjectForm } from '@shared/entities/create-project-form.interface';
+import { ProjectFormControls } from '@shared/entities/create-project-form-controls.enum';
+import { IS_WEB } from '@shared/utils/breakpoints.tokens';
+
+@Component({
+ selector: 'osf-settings',
+ imports: [
+ TranslatePipe,
+ SubHeaderComponent,
+ TabPanels,
+ FormsModule,
+ InputText,
+ ReactiveFormsModule,
+ Textarea,
+ Card,
+ Button,
+ ViewOnlyTableComponent,
+ Checkbox,
+ AccordionTableComponent,
+ RadioButton,
+ RouterLink,
+ ],
+ templateUrl: './settings.component.html',
+ styleUrl: './settings.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SettingsComponent {
+ protected readonly isDesktop = toSignal(inject(IS_WEB));
+
+ protected readonly ProjectFormControls = ProjectFormControls;
+ protected commenting = signal(ShareIndexingEnum.None);
+
+ projectForm = new FormGroup>({
+ [ProjectFormControls.Title]: new FormControl('', {
+ nonNullable: true,
+ validators: [Validators.required],
+ }),
+ [ProjectFormControls.Description]: new FormControl('', {
+ nonNullable: true,
+ }),
+ });
+ accessRequest = new FormControl(false);
+ wiki = new FormControl(false);
+ redirectLink = new FormControl(false);
+
+ tableData: LinkTableModel[] = [
+ {
+ linkName: 'name',
+ sharedComponents: 'Project name',
+ createdDate: new Date(),
+ createdBy: 'Igor',
+ anonymous: false,
+ link: 'www.facebook.com',
+ },
+ {
+ linkName: 'name',
+ sharedComponents: 'Project name',
+ createdDate: new Date(),
+ createdBy: 'Igor',
+ anonymous: false,
+ link: 'www.facebook.com',
+ },
+ {
+ linkName: 'name',
+ sharedComponents: 'Project name',
+ createdDate: new Date(),
+ createdBy: 'Igor',
+ anonymous: false,
+ link: 'www.facebook.com',
+ },
+ {
+ linkName: 'name',
+ sharedComponents: 'Project name',
+ createdDate: new Date(),
+ createdBy: 'Igor',
+ anonymous: false,
+ link: 'www.facebook.com',
+ },
+ ];
+ access = 'write';
+ accessOptions = [
+ { label: 'Contributors (with write access)', value: 'write' },
+ { label: 'Anyone with link', value: 'public' },
+ ];
+ commentSetting = 'instantly';
+ fileSetting = 'instantly';
+
+ dropdownOptions = [
+ { label: 'Instantly', value: 'instantly' },
+ { label: 'Daily', value: 'daily' },
+ { label: 'Never', value: 'never' },
+ ];
+ submitForm(): void {
+ // TODO: implement form submission
+ }
+
+ resetForm(): void {
+ this.projectForm.reset();
+ }
+
+ onAccessChange(value: string): void {
+ console.log('Access changed to', value);
+ }
+}
diff --git a/src/app/features/settings/addons/connect-addon/connect-addon.component.html b/src/app/features/settings/addons/connect-addon/connect-addon.component.html
index cbfdb4603..667b70693 100644
--- a/src/app/features/settings/addons/connect-addon/connect-addon.component.html
+++ b/src/app/features/settings/addons/connect-addon/connect-addon.component.html
@@ -10,30 +10,32 @@
{{ 'settings.addons.connectAddon.terms' | translate }}
-
-
-
-
- {{ 'settings.addons.connectAddon.table.function' | translate }}
-
-
- {{ 'settings.addons.connectAddon.table.status' | translate }}
-
-
-
-
-
+
+
+
+
+ {{ 'settings.addons.connectAddon.table.function' | translate }}
+
+
+ {{ 'settings.addons.connectAddon.table.status' | translate }}
+
+
+
+
+
- {{ term.function }}
- {{ term.status }}
-
-
-
+ >
+ {{ term.function }}
+ {{ term.status }}
+
+
+
+
diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts
index 5aeda8a12..fbcdf31e6 100644
--- a/src/app/shared/components/index.ts
+++ b/src/app/shared/components/index.ts
@@ -1,3 +1,4 @@
+export * from './view-only-table/view-only-table.component';
export { AddProjectFormComponent } from './add-project-form/add-project-form.component';
export { BarChartComponent } from './bar-chart/bar-chart.component';
export { LineChartComponent } from './line-chart/line-chart.component';
diff --git a/src/app/shared/components/my-projects-table/my-projects-table.component.html b/src/app/shared/components/my-projects-table/my-projects-table.component.html
index 406b6807e..c91a2778b 100644
--- a/src/app/shared/components/my-projects-table/my-projects-table.component.html
+++ b/src/app/shared/components/my-projects-table/my-projects-table.component.html
@@ -1,4 +1,4 @@
-
+
+
+
+
+ {{ 'myProjects.settings.viewOnlyTable.linkName' | translate }}
+
+
+ {{ 'myProjects.settings.viewOnlyTable.sharedComponents' | translate }}
+
+
+
+ {{ 'myProjects.settings.viewOnlyTable.createdDate' | translate }}
+
+
+
+ {{ 'myProjects.settings.viewOnlyTable.createdBy' | translate }}
+
+
+
+ {{ 'myProjects.settings.viewOnlyTable.anonymous' | translate }}
+
+
+
+
+
+
+
+
+
+ {{ item.linkName }}
+
+
+
+ {{ item.sharedComponents }}
+ {{ item.createdDate | date: 'MMM d, y h:mm a' }}
+ {{ item.createdBy }}
+ {{ item.anonymous }}
+
+
+
+
+
+
diff --git a/src/app/shared/components/view-only-table/view-only-table.component.scss b/src/app/shared/components/view-only-table/view-only-table.component.scss
new file mode 100644
index 000000000..eb73a2095
--- /dev/null
+++ b/src/app/shared/components/view-only-table/view-only-table.component.scss
@@ -0,0 +1,16 @@
+@use "assets/styles/variables" as var;
+
+.delete-icon {
+ &:before {
+ color: var.$red-1;
+ cursor: pointer;
+ }
+}
+
+.icon-copy-btn {
+ right: 1.5rem;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 1.5rem;
+ cursor: pointer;
+}
\ No newline at end of file
diff --git a/src/app/shared/components/view-only-table/view-only-table.component.spec.ts b/src/app/shared/components/view-only-table/view-only-table.component.spec.ts
new file mode 100644
index 000000000..9f039e15c
--- /dev/null
+++ b/src/app/shared/components/view-only-table/view-only-table.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ViewOnlyTableComponent } from './view-only-table.component';
+
+describe('ViewOnlyTableComponent', () => {
+ let component: ViewOnlyTableComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ViewOnlyTableComponent],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(ViewOnlyTableComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/shared/components/view-only-table/view-only-table.component.ts b/src/app/shared/components/view-only-table/view-only-table.component.ts
new file mode 100644
index 000000000..b2e8101ee
--- /dev/null
+++ b/src/app/shared/components/view-only-table/view-only-table.component.ts
@@ -0,0 +1,28 @@
+import { TranslatePipe } from '@ngx-translate/core';
+
+import { Button } from 'primeng/button';
+import { InputText } from 'primeng/inputtext';
+import { TableModule } from 'primeng/table';
+
+import { Clipboard } from '@angular/cdk/clipboard';
+import { DatePipe } from '@angular/common';
+import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+
+import { LinkTableModel } from '@osf/features/project/settings';
+
+@Component({
+ selector: 'osf-view-only-table',
+ imports: [TableModule, TranslatePipe, DatePipe, InputText, ReactiveFormsModule, Button],
+ templateUrl: './view-only-table.component.html',
+ styleUrl: './view-only-table.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ViewOnlyTableComponent {
+ tableData = input.required();
+
+ readonly #clipboard = inject(Clipboard);
+ copy(link: string): void {
+ this.#clipboard.copy(link);
+ }
+}
diff --git a/src/app/shared/index.ts b/src/app/shared/index.ts
new file mode 100644
index 000000000..742094d5a
--- /dev/null
+++ b/src/app/shared/index.ts
@@ -0,0 +1 @@
+export * from './components/index';
diff --git a/src/app/shared/utils/remove-nullable.const.ts b/src/app/shared/utils/remove-nullable.const.ts
index 615efe166..59872b241 100644
--- a/src/app/shared/utils/remove-nullable.const.ts
+++ b/src/app/shared/utils/remove-nullable.const.ts
@@ -1,5 +1,6 @@
export function removeNullable(obj: T): Partial {
return Object.fromEntries(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
Object.entries(obj).filter(([_, value]) => value !== null && value !== undefined)
) as Partial;
}
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 0f5ffaefe..406719273 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -22,6 +22,8 @@
"metadata": "Metadata",
"files": "Files",
"registrations": "Registrations",
+ "settings": "Settings",
+ "contributors": "Contributors",
"analytics": "Analytics"
}
},
@@ -217,6 +219,40 @@
"cancel": "Cancel",
"create": "Create Project"
}
+ },
+ "settings": {
+ "project": "Project",
+ "saveChanges": "Save Changes",
+ "deleteProject": "Delete Project",
+ "viewOnlyLinks": "View-only Links",
+ "viewOnlySubtitle": "Create a link to share this project so those who have the link can view—but not edit—the project.",
+ "viewOnlyTable": {
+ "linkName": "Link Name",
+ "sharedComponents": "Shared Components",
+ "createdDate": "Created date",
+ "createdBy": "Created By",
+ "anonymous": "Anonymous"
+ },
+ "accessRequests": "Access Requests",
+ "accessRequestsText": "Allow users to request access to this project",
+ "wiki": "Wiki",
+ "wikiText": "Enable the wiki In [Dashboard] New and Noteworthy.",
+ "wikiConfigureTitle": "Configure",
+ "wikiConfigureText": "Create a link to share this project so those who have the link can view—but not edit—the project.",
+ "commenting": "Commenting",
+ "contributorsCanPost": "Only contributors can post comments",
+ "osfUserCanPost": "When the project is public, any OSF user can post comments",
+ "emailNotifications": "Email Notifications",
+ "emailNotificationsText": "These notification settings only apply to you. They do NOT affect any other contributor on this project.",
+ "redirectLink": "Redirect Link",
+ "redirectLinkText": "Redirect visitors from your project page to an external webpage",
+ "projectAffiliation": "Project Affiliation / Branding",
+ "projectsCanBeAffiliated": "Projects can be affiliated with institutions that have created OSF for Institutions accounts. This allows:",
+ "institutionalLogos": "institutional logos to be displayed on public projects",
+ "publicProjectsToBeDiscoverable": "public projects to be discoverable on specific institutional landing pages",
+ "singleSignInToTHeOSF": "single sign-in to the OSF with institutional credentials",
+ "faq": "FAQ",
+ "": ""
}
},
"project": {
@@ -239,6 +275,42 @@
"popularPages": "Popular pages",
"visits": "Visits"
}
+ },
+ "contributors": {
+ "addContributor": "Add Contributor",
+ "searchPlaceholder": "Search contributors",
+ "permissionFilter": "Filter by permission",
+ "bibliographyFilter": "Bibliography",
+ "permissions": {
+ "administrator": "Administrator",
+ "readAndWrite": "Read + Write",
+ "read": "Read"
+ },
+ "bibliography": {
+ "bibliographic": "Bibliographic",
+ "nonBibliographic": "Non-Bibliographic"
+ },
+ "table": {
+ "headers": {
+ "name": "Name",
+ "permissions": "Permissions",
+ "contributor": "Bibliographic Contributor",
+ "curator": "Curator",
+ "employment": "Employment history",
+ "education": "Education history"
+ }
+ },
+ "viewOnly": "View-only links",
+ "createLink": "Create a link to share this project so those who have the link can view—but not edit—the project.",
+ "createButton": "Create",
+ "addDialog": {
+ "placeholder": "Search by name or user information",
+ "cancel": "Cancel",
+ "next": "Next"
+ },
+ "createLinkDialog": {
+ "dialogTitle": "Create a new link to share your project"
+ }
}
},
"settings": {
diff --git a/src/assets/styles/overrides/button.scss b/src/assets/styles/overrides/button.scss
index b8de2901f..30a27144f 100644
--- a/src/assets/styles/overrides/button.scss
+++ b/src/assets/styles/overrides/button.scss
@@ -234,3 +234,13 @@
color: var.$grey-1;
}
}
+
+.bg-primary-blue-second {
+ background-color: var.$pr-blue-2;
+}
+
+.button-shadow-none {
+ .p-button {
+ box-shadow: none;
+ }
+}
diff --git a/src/assets/styles/overrides/dropdown.scss b/src/assets/styles/overrides/dropdown.scss
index bbd7a0baf..28b14595c 100644
--- a/src/assets/styles/overrides/dropdown.scss
+++ b/src/assets/styles/overrides/dropdown.scss
@@ -19,4 +19,13 @@
background: var.$bg-blue-2;
}
}
+
+ &.accordion-dropdown {
+ border: none;
+ box-shadow: none;
+ min-width: 170px;
+ width: max-content;
+ max-width: 300px;
+ font-size: 1rem;
+ }
}
diff --git a/src/assets/styles/overrides/select.scss b/src/assets/styles/overrides/select.scss
index 0a6c0ec10..f76e51775 100644
--- a/src/assets/styles/overrides/select.scss
+++ b/src/assets/styles/overrides/select.scss
@@ -81,6 +81,16 @@
}
}
+.select-font-normal {
+ .no-border-dropdown {
+ .p-select {
+ .p-select-label {
+ font-weight: 400;
+ }
+ }
+ }
+}
+
.filter {
.p-select {
.p-select-label {
@@ -116,3 +126,9 @@
}
}
}
+
+.grey-placeholder {
+ .p-select .p-placeholder span {
+ color: var.$grey-1;
+ }
+}
diff --git a/src/assets/styles/overrides/table.scss b/src/assets/styles/overrides/table.scss
index af8e60712..028261d27 100644
--- a/src/assets/styles/overrides/table.scss
+++ b/src/assets/styles/overrides/table.scss
@@ -3,7 +3,6 @@
@layer reset {
p-table {
.p-datatable {
- margin-top: 24px;
border: 1px solid var.$grey-2;
border-radius: 8px;
padding: 24px;
@@ -251,3 +250,27 @@
}
}
}
+
+.view-only-table {
+ .p-datatable {
+ padding: 2px;
+
+ td,
+ th {
+ background-color: transparent;
+ border-bottom: 1px solid var.$grey-2;
+ }
+
+ tr {
+ &:hover {
+ background: transparent;
+ }
+
+ &:last-of-type {
+ td {
+ border-bottom: none;
+ }
+ }
+ }
+ }
+}
diff --git a/src/assets/styles/overrides/tooltip.scss b/src/assets/styles/overrides/tooltip.scss
new file mode 100644
index 000000000..0a37ad1d0
--- /dev/null
+++ b/src/assets/styles/overrides/tooltip.scss
@@ -0,0 +1,15 @@
+@use "assets/styles/variables" as var;
+
+.p-tooltip-text {
+ display: flex !important;
+ background: var.$white;
+ color: var.$dark-blue-1;
+ min-width: 22rem;
+ border: 1px solid var.$grey-3;
+ padding: 1rem;
+}
+
+.p-tooltip-arrow {
+ border-right-color: var.$white;
+ border: 1px solid var.$grey-3;
+}
diff --git a/src/assets/styles/styles.scss b/src/assets/styles/styles.scss
index 175e475ad..86c58ddac 100644
--- a/src/assets/styles/styles.scss
+++ b/src/assets/styles/styles.scss
@@ -31,6 +31,7 @@
@use "./overrides/spinner";
@use "./overrides/password";
@use "./common";
+@use "./overrides/tooltip";
@layer base, primeng, reset;
@@ -76,6 +77,12 @@
list-style: none;
}
+ .inside-list {
+ li {
+ list-style: inside;
+ }
+ }
+
a,
a:visited {
text-decoration: none;