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 @@ +
+
+ + +
+ +
+ +
+ +

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 + } + + +
+ @if(item.educationHistory){ + Show education history + } + @else { + No education 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;