From 8b08b1ac6d3a6b92e97653597b92561a09aabacc Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Mon, 19 May 2025 16:30:45 +0300 Subject: [PATCH 1/2] feat(project-settings): added page skeleton" --- src/app/app.routes.ts | 4 + src/app/core/constants/nav-items.constant.ts | 4 + .../metadata/project-metadata.component.html | 2 +- .../accordion-table.component.html | 45 ++++ .../accordion-table.component.spec.ts | 22 ++ .../accordion-table.component.ts | 38 ++++ .../project/settings/components/index.ts | 1 + src/app/features/project/settings/index.ts | 3 + .../features/project/settings/models/index.ts | 1 + .../settings/models/link-table.model.ts | 8 + .../project/settings/settings.component.html | 193 ++++++++++++++++++ .../project/settings/settings.component.scss | 11 + .../settings/settings.component.spec.ts | 22 ++ .../project/settings/settings.component.ts | 124 +++++++++++ src/app/shared/components/index.ts | 1 + .../view-only-table.component.html | 49 +++++ .../view-only-table.component.scss | 16 ++ .../view-only-table.component.spec.ts | 22 ++ .../view-only-table.component.ts | 28 +++ src/app/shared/index.ts | 1 + src/app/shared/utils/remove-nullable.const.ts | 1 + src/assets/i18n/en.json | 35 ++++ src/assets/styles/overrides/button.scss | 10 + src/assets/styles/overrides/dropdown.scss | 9 + src/assets/styles/overrides/table.scss | 24 +++ 25 files changed, 673 insertions(+), 1 deletion(-) create mode 100644 src/app/features/project/settings/components/accordion-table/accordion-table.component.html create mode 100644 src/app/features/project/settings/components/accordion-table/accordion-table.component.spec.ts create mode 100644 src/app/features/project/settings/components/accordion-table/accordion-table.component.ts create mode 100644 src/app/features/project/settings/components/index.ts create mode 100644 src/app/features/project/settings/index.ts create mode 100644 src/app/features/project/settings/models/index.ts create mode 100644 src/app/features/project/settings/models/link-table.model.ts create mode 100644 src/app/features/project/settings/settings.component.html create mode 100644 src/app/features/project/settings/settings.component.scss create mode 100644 src/app/features/project/settings/settings.component.spec.ts create mode 100644 src/app/features/project/settings/settings.component.ts create mode 100644 src/app/shared/components/view-only-table/view-only-table.component.html create mode 100644 src/app/shared/components/view-only-table/view-only-table.component.scss create mode 100644 src/app/shared/components/view-only-table/view-only-table.component.spec.ts create mode 100644 src/app/shared/components/view-only-table/view-only-table.component.ts create mode 100644 src/app/shared/index.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 1e89e5f61..d523901fd 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -104,6 +104,10 @@ export const routes: Routes = [ ), }, { + path: 'settings', + loadComponent: () => + import('./features/project/settings/settings.component').then((mod) => mod.SettingsComponent), + }, path: 'analytics', loadComponent: () => import('@osf/features/project/analytics/analytics.component').then((mod) => mod.AnalyticsComponent), diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index d865e0250..aa391036b 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -90,6 +90,10 @@ export const PROJECT_MENU_ITEMS: MenuItem[] = [ label: 'navigation.project.registrations', routerLink: 'registrations', }, + { + label: 'navigation.project.settings', + routerLink: 'settings', + }, { label: 'navigation.project.analytics', routerLink: 'analytics' }, ], }, 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..84ef91faa --- /dev/null +++ b/src/app/features/project/settings/settings.component.html @@ -0,0 +1,193 @@ +
+ + +
+ + +

{{ '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/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/view-only-table/view-only-table.component.html b/src/app/shared/components/view-only-table/view-only-table.component.html new file mode 100644 index 000000000..b1dfb6f44 --- /dev/null +++ b/src/app/shared/components/view-only-table/view-only-table.component.html @@ -0,0 +1,49 @@ + + + + + {{ '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..c0f57541a --- /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-3; + cursor: pointer; + } +} + +.icon-copy-btn { + right: 1.5rem; + top: 50%; + transform: translateY(-50%); + width: 1.5rem; + cursor: pointer; +} 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..f0e9f9611 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -22,6 +22,7 @@ "metadata": "Metadata", "files": "Files", "registrations": "Registrations", + "settings": "Settings", "analytics": "Analytics" } }, @@ -217,6 +218,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": { 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/table.scss b/src/assets/styles/overrides/table.scss index af8e60712..f4063aa9a 100644 --- a/src/assets/styles/overrides/table.scss +++ b/src/assets/styles/overrides/table.scss @@ -251,3 +251,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; + } + } + } + } +} From db4a274f2150c623bf028f1b3b877edef7b55d19 Mon Sep 17 00:00:00 2001 From: Kyrylo Petrov Date: Tue, 20 May 2025 15:05:51 +0300 Subject: [PATCH 2/2] chore(contributors): ui-ux --- src/app/app.routes.ts | 8 + src/app/core/constants/nav-items.constant.ts | 1 + .../add-contributor-dialog.component.html | 25 +++ .../add-contributor-dialog.component.scss | 0 .../add-contributor-dialog.component.spec.ts | 23 ++ .../add-contributor-dialog.component.ts | 24 ++ .../create-view-link-dialog.component.html | 59 +++++ .../create-view-link-dialog.component.scss | 7 + .../create-view-link-dialog.component.spec.ts | 23 ++ .../create-view-link-dialog.component.ts | 40 ++++ .../contributors/contributors.component.html | 210 ++++++++++++++++++ .../contributors/contributors.component.scss | 51 +++++ .../contributors.component.spec.ts | 23 ++ .../contributors/contributors.component.ts | 184 +++++++++++++++ .../project/settings/settings.component.html | 7 +- .../connect-addon.component.html | 42 ++-- .../my-projects-table.component.html | 2 +- .../view-only-table.component.scss | 4 +- src/assets/i18n/en.json | 37 +++ src/assets/styles/overrides/select.scss | 16 ++ src/assets/styles/overrides/table.scss | 1 - src/assets/styles/overrides/tooltip.scss | 15 ++ src/assets/styles/styles.scss | 7 + 23 files changed, 783 insertions(+), 26 deletions(-) create mode 100644 src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.html create mode 100644 src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.scss create mode 100644 src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.spec.ts create mode 100644 src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.ts create mode 100644 src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html create mode 100644 src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.scss create mode 100644 src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts create mode 100644 src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.ts create mode 100644 src/app/features/project/contributors/contributors.component.html create mode 100644 src/app/features/project/contributors/contributors.component.scss create mode 100644 src/app/features/project/contributors/contributors.component.spec.ts create mode 100644 src/app/features/project/contributors/contributors.component.ts create mode 100644 src/assets/styles/overrides/tooltip.scss diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index d523901fd..57f4a6313 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -108,6 +108,14 @@ export const routes: Routes = [ 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: () => import('@osf/features/project/analytics/analytics.component').then((mod) => mod.AnalyticsComponent), diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index aa391036b..9e0fe3acc 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -94,6 +94,7 @@ export const PROJECT_MENU_ITEMS: MenuItem[] = [ 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/settings/settings.component.html b/src/app/features/project/settings/settings.component.html index 84ef91faa..dada4425b 100644 --- a/src/app/features/project/settings/settings.component.html +++ b/src/app/features/project/settings/settings.component.html @@ -65,12 +65,15 @@

{{ 'myProjects.createProject.storageLocation' | transla

Storage location cannot be changed after project is created.

- +

{{ 'myProjects.settings.viewOnlyLinks' | translate }}

{{ 'myProjects.settings.viewOnlySubtitle' | translate }}

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

+