From fde9e4dcf6ca35167a04063e72428f2e9d577f03 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Thu, 22 May 2025 17:57:02 +0300 Subject: [PATCH 1/7] feat(settings): integrated api for settings checkbox and other boolean values --- .../core/constants/ngxs-states.constant.ts | 2 + .../contributors/contributors.component.ts | 67 +++++---- .../accordion-table.component.html | 45 ------ .../accordion-table.component.spec.ts | 22 --- .../accordion-table.component.ts | 38 ----- ...ct-detail-setting-accordion.component.html | 33 +++-- ...ject-detail-setting-accordion.component.ts | 10 +- ...ttings-access-requests-card.component.html | 3 +- ...settings-access-requests-card.component.ts | 10 +- .../settings-commenting-card.component.html | 10 +- .../settings-commenting-card.component.ts | 7 +- ...ettings-email-notifications.component.html | 1 + ...ettings-email-notifications.component.scss | 0 ...ings-email-notifications.component.spec.ts | 22 +++ .../settings-email-notifications.component.ts | 10 ++ .../settings-redirect-link.component.html | 80 +++++++++++ .../settings-redirect-link.component.spec.ts | 22 +++ .../settings-redirect-link.component.ts | 59 ++++++++ ...ttings-view-only-links-card.component.html | 2 +- ...settings-view-only-links-card.component.ts | 3 +- .../settings-wiki-card.component.html | 9 +- .../settings-wiki-card.component.ts | 40 +++++- .../project/settings/mappers/index.ts | 0 .../settings/mappers/settings.mapper.ts | 29 ++++ .../features/project/settings/mock-data.ts | 80 +++++------ .../features/project/settings/models/index.ts | 4 + .../settings/models/link-table.model.ts | 7 +- .../project/settings/models/option.model.ts | 4 + .../models/project-settings-response.model.ts | 43 ++++++ .../settings/models/project-settings.model.ts | 14 ++ .../settings/models/right-control.model.ts | 21 ++- .../settings/models/view-only-link.model.ts | 21 +++ .../project/settings/services/index.ts | 1 + .../settings/services/settings.service.ts | 24 ++++ .../project/settings/settings.component.html | 46 +++--- .../project/settings/settings.component.ts | 134 +++++++++++++++--- .../settings/store/empty-project-settings.ts | 14 ++ .../features/project/settings/store/index.ts | 4 + .../settings/store/settings.actions.ts | 5 + .../project/settings/store/settings.model.ts | 6 + .../settings/store/settings.selectors.ts | 11 ++ .../project/settings/store/settings.state.ts | 59 ++++++++ .../view-only-table.component.html | 8 +- .../view-only-table.component.ts | 3 +- src/assets/i18n/en.json | 9 +- 45 files changed, 753 insertions(+), 289 deletions(-) delete mode 100644 src/app/features/project/settings/components/accordion-table/accordion-table.component.html delete mode 100644 src/app/features/project/settings/components/accordion-table/accordion-table.component.spec.ts delete mode 100644 src/app/features/project/settings/components/accordion-table/accordion-table.component.ts create mode 100644 src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.html create mode 100644 src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.scss create mode 100644 src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.spec.ts create mode 100644 src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.ts create mode 100644 src/app/features/project/settings/components/settings-redirect-link/settings-redirect-link.component.html create mode 100644 src/app/features/project/settings/components/settings-redirect-link/settings-redirect-link.component.spec.ts create mode 100644 src/app/features/project/settings/components/settings-redirect-link/settings-redirect-link.component.ts create mode 100644 src/app/features/project/settings/mappers/index.ts create mode 100644 src/app/features/project/settings/mappers/settings.mapper.ts create mode 100644 src/app/features/project/settings/models/option.model.ts create mode 100644 src/app/features/project/settings/models/project-settings-response.model.ts create mode 100644 src/app/features/project/settings/models/project-settings.model.ts create mode 100644 src/app/features/project/settings/models/view-only-link.model.ts create mode 100644 src/app/features/project/settings/services/index.ts create mode 100644 src/app/features/project/settings/services/settings.service.ts create mode 100644 src/app/features/project/settings/store/empty-project-settings.ts create mode 100644 src/app/features/project/settings/store/index.ts create mode 100644 src/app/features/project/settings/store/settings.actions.ts create mode 100644 src/app/features/project/settings/store/settings.model.ts create mode 100644 src/app/features/project/settings/store/settings.selectors.ts create mode 100644 src/app/features/project/settings/store/settings.state.ts diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index 7cc1b0bf2..8909c75c4 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -3,6 +3,7 @@ import { UserState } from '@core/store/user'; import { InstitutionsState } from '@osf/features/institutions/store'; import { MyProjectsState } from '@osf/features/my-projects/store'; import { AnalyticsState } from '@osf/features/project/analytics/store'; +import { SettingsState } from '@osf/features/project/settings/store'; import { AccountSettingsState } from '@osf/features/settings/account-settings/store/account-settings.state'; import { AddonsState } from '@osf/features/settings/addons/store'; import { DeveloperAppsState } from '@osf/features/settings/developer-apps/store'; @@ -20,4 +21,5 @@ export const STATES = [ DeveloperAppsState, AccountSettingsState, AnalyticsState, + SettingsState, ]; diff --git a/src/app/features/project/contributors/contributors.component.ts b/src/app/features/project/contributors/contributors.component.ts index 51e6f4695..e787c4544 100644 --- a/src/app/features/project/contributors/contributors.component.ts +++ b/src/app/features/project/contributors/contributors.component.ts @@ -14,7 +14,6 @@ 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 { CreateViewLinkDialogComponent } from '@osf/features/project/contributors/components/create-view-link-dialog/create-view-link-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'; @@ -107,39 +106,39 @@ export class ContributorsComponent { }, ]; - 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', - }, + tableData = [ + // { + // 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 { 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 deleted file mode 100644 index 918ce5a7a..000000000 --- a/src/app/features/project/settings/components/accordion-table/accordion-table.component.html +++ /dev/null @@ -1,45 +0,0 @@ -
-
- - - - - {{ 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 deleted file mode 100644 index 26df6a5d4..000000000 --- a/src/app/features/project/settings/components/accordion-table/accordion-table.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index a490bf8e3..000000000 --- a/src/app/features/project/settings/components/accordion-table/accordion-table.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Button } from 'primeng/button'; -import { SelectModule } from 'primeng/select'; - -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, SelectModule, 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/project-detail-setting-accordion/project-detail-setting-accordion.component.html b/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.html index c9df99a30..c2b211580 100644 --- a/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.html +++ b/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.html @@ -16,29 +16,28 @@ @if (rightControls().length > 0) {
- @for (control of rightControls(); track control.value) { + @for (control of rightControls(); let index = $index; track control.value) {
@if (control.label) { - {{ control.label }} + {{ control.label | translate }} } - @switch (control.type) { - @case ('dropdown') { - - } - @case ('text') { - - {{ control.value }} - - } - } + + + {{ item.label | translate }} + + + + {{ item.label | translate }} + +
}
diff --git a/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.ts b/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.ts index 163028d3f..d30cd9607 100644 --- a/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.ts +++ b/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.ts @@ -1,23 +1,25 @@ +import { TranslatePipe } from '@ngx-translate/core'; + import { Button } from 'primeng/button'; import { SelectModule } from 'primeng/select'; import { LowerCasePipe, NgClass } from '@angular/common'; -import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RightControl } from '@osf/features/project/settings/models/right-control.model'; @Component({ selector: 'osf-project-detail-setting-accordion', - imports: [NgClass, SelectModule, FormsModule, Button, LowerCasePipe], + imports: [NgClass, SelectModule, FormsModule, Button, LowerCasePipe, TranslatePipe], templateUrl: './project-detail-setting-accordion.component.html', changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, }) export class ProjectDetailSettingAccordionComponent { + emitValueChange = output<{ index: number; value: boolean | string }>(); title = input.required(); - rightControls = input.required(); - expanded = signal(false); toggle() { diff --git a/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.html b/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.html index 7ac5e9139..b7f7b3070 100644 --- a/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.html +++ b/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.html @@ -5,7 +5,8 @@

{{ 'myProjects.settings.accessRequests' | translate }}< diff --git a/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.ts b/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.ts index efc4d9a51..a9dfdf432 100644 --- a/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.ts +++ b/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.ts @@ -3,16 +3,18 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Card } from 'primeng/card'; import { Checkbox } from 'primeng/checkbox'; -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; -import { FormControl } from '@angular/forms'; +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { FormsModule } from '@angular/forms'; @Component({ selector: 'osf-settings-access-requests-card', - imports: [Checkbox, TranslatePipe, Card], + imports: [Checkbox, TranslatePipe, Card, FormsModule], templateUrl: './settings-access-requests-card.component.html', styleUrl: '../../settings.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, }) export class SettingsAccessRequestsCardComponent { - formControl = input.required(); + accessRequestChange = output(); + accessRequest = input.required(); } diff --git a/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.html b/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.html index 9e82b5098..204e5ba50 100644 --- a/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.html +++ b/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.html @@ -6,8 +6,9 @@

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

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

@@ -16,8 +17,9 @@

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

diff --git a/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.ts b/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.ts index f219023a2..4d46cc4a3 100644 --- a/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.ts +++ b/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.ts @@ -3,11 +3,9 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Card } from 'primeng/card'; import { RadioButton } from 'primeng/radiobutton'; -import { ChangeDetectionStrategy, Component, model } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { ShareIndexingEnum } from '@osf/features/settings/account-settings/components/share-indexing/enums/share-indexing.enum'; - @Component({ selector: 'osf-settings-commenting-card', imports: [Card, RadioButton, TranslatePipe, FormsModule], @@ -16,5 +14,6 @@ import { ShareIndexingEnum } from '@osf/features/settings/account-settings/compo changeDetection: ChangeDetectionStrategy.OnPush, }) export class SettingsCommentingCardComponent { - commenting = model(); + anyoneCanComment = input.required(); + readonly anyoneCanCommentChange = output(); } diff --git a/src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.html b/src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.html new file mode 100644 index 000000000..c2658efdb --- /dev/null +++ b/src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.html @@ -0,0 +1 @@ +

settings-email-notifications works!

diff --git a/src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.scss b/src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.spec.ts b/src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.spec.ts new file mode 100644 index 000000000..dc91a8440 --- /dev/null +++ b/src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SettingsEmailNotificationsComponent } from './settings-email-notifications.component'; + +describe('SettingsEmailNotificationsComponent', () => { + let component: SettingsEmailNotificationsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SettingsEmailNotificationsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SettingsEmailNotificationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.ts b/src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.ts new file mode 100644 index 000000000..ee1ce0546 --- /dev/null +++ b/src/app/features/project/settings/components/settings-email-notifications/settings-email-notifications.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'osf-settings-email-notifications', + imports: [], + templateUrl: './settings-email-notifications.component.html', + styleUrl: './settings-email-notifications.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SettingsEmailNotificationsComponent {} diff --git a/src/app/features/project/settings/components/settings-redirect-link/settings-redirect-link.component.html b/src/app/features/project/settings/components/settings-redirect-link/settings-redirect-link.component.html new file mode 100644 index 000000000..806aecef1 --- /dev/null +++ b/src/app/features/project/settings/components/settings-redirect-link/settings-redirect-link.component.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + +

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

+ +
+ + + +
+ + @if (redirectLink()) { +
+
+ + +
+ +
+ + + +
+
+ +
+ +
+ } +
diff --git a/src/app/features/project/settings/components/settings-redirect-link/settings-redirect-link.component.spec.ts b/src/app/features/project/settings/components/settings-redirect-link/settings-redirect-link.component.spec.ts new file mode 100644 index 000000000..73ad22188 --- /dev/null +++ b/src/app/features/project/settings/components/settings-redirect-link/settings-redirect-link.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SettingsRedirectLinkComponent } from './settings-redirect-link.component'; + +describe('SettingsRedirectLinkComponent', () => { + let component: SettingsRedirectLinkComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SettingsRedirectLinkComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SettingsRedirectLinkComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/project/settings/components/settings-redirect-link/settings-redirect-link.component.ts b/src/app/features/project/settings/components/settings-redirect-link/settings-redirect-link.component.ts new file mode 100644 index 000000000..acc9eb667 --- /dev/null +++ b/src/app/features/project/settings/components/settings-redirect-link/settings-redirect-link.component.ts @@ -0,0 +1,59 @@ +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 { TitleCasePipe, UpperCasePipe } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + effect, + input, + model, + output, + signal, + WritableSignal, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'osf-settings-redirect-link', + imports: [Card, Checkbox, TranslatePipe, FormsModule, InputText, TitleCasePipe, UpperCasePipe, Button], + templateUrl: './settings-redirect-link.component.html', + styleUrl: '../../settings.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class SettingsRedirectLinkComponent { + redirectUrlDataChange = output<{ url: string; label: string }>(); + redirectUrlDataInput = input.required<{ url: string; label: string }>(); + redirectLink = model(); + + redirectUrlData: WritableSignal<{ url: string; label: string }> = signal({ url: '', label: '' }); + + constructor() { + effect(() => { + this.redirectUrlData.set(this.redirectUrlDataInput()); + const { url, label } = this.redirectUrlDataInput(); + if (url || label) { + this.redirectLink.set(true); + } + }); + } + + onToggleRedirectLink(checked: boolean): void { + this.redirectLink.set(checked); + if (!checked) { + this.redirectUrlData.set({ url: '', label: '' }); + this.redirectUrlDataChange.emit(this.redirectUrlData()); + } + } + + emitIfChecked(): void { + if (this.redirectLink()) { + this.redirectUrlDataChange.emit(this.redirectUrlData()); + } + } +} diff --git a/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.html b/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.html index bc09d5e89..0e0b8fba3 100644 --- a/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.html +++ b/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.html @@ -3,5 +3,5 @@

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

- + diff --git a/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.ts b/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.ts index 720777bd4..35be36d84 100644 --- a/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.ts +++ b/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.ts @@ -2,7 +2,7 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Card } from 'primeng/card'; -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; import { LinkTableModel } from '@osf/features/project/settings/models'; import { ViewOnlyTableComponent } from '@shared/components'; @@ -15,5 +15,6 @@ import { ViewOnlyTableComponent } from '@shared/components'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class SettingsViewOnlyLinksCardComponent { + deleteTableItem = output(); tableData = input.required(); } diff --git a/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.html b/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.html index e9717272a..327389ed5 100644 --- a/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.html +++ b/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.html @@ -5,7 +5,8 @@

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

@@ -19,7 +20,11 @@

{{ 'myProjects.settings.wikiConfigureTitle' | translate

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

- +
Sub text diff --git a/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.ts b/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.ts index f9baae456..b9c63c93c 100644 --- a/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.ts +++ b/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.ts @@ -3,22 +3,48 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Card } from 'primeng/card'; import { Checkbox } from 'primeng/checkbox'; -import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { FormControl } from '@angular/forms'; +import { ChangeDetectionStrategy, Component, effect, input, output } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { ProjectDetailSettingAccordionComponent } from '@osf/features/project/settings/components'; import { RightControl } from '@osf/features/project/settings/models/right-control.model'; @Component({ selector: 'osf-settings-wiki-card', - imports: [Card, Checkbox, TranslatePipe, ProjectDetailSettingAccordionComponent], + imports: [Card, Checkbox, TranslatePipe, ProjectDetailSettingAccordionComponent, FormsModule], templateUrl: './settings-wiki-card.component.html', styleUrl: '../../settings.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, }) export class SettingsWikiCardComponent { - formControl = input.required(); - rightControls = input.required(); - accessOptions = input.required<{ label: string; value: string }[]>(); - accessChange = output(); + anyoneCanEditWikiEmitValue = output(); + wikiChangeEmit = output(); + + wikiEnabled = input.required(); + anyoneCanEditWiki = input.required(); + + allAccordionData: RightControl[] = []; + constructor() { + effect(() => { + const anyoneCanEditWiki = this.anyoneCanEditWiki(); + this.allAccordionData = [ + { + label: 'myProjects.settings.whoCanEdit', + value: anyoneCanEditWiki, + type: 'dropdown', + options: [ + { label: 'myProjects.settings.contributorsOption', value: true }, + { label: 'myProjects.settings.anyoneOption', value: false }, + ], + }, + ]; + }); + } + + changeEmittedValue(emittedValue: { index: number; value: boolean | string }): void { + if (typeof emittedValue.value === 'boolean') { + this.anyoneCanEditWikiEmitValue.emit(emittedValue.value); + } + } } diff --git a/src/app/features/project/settings/mappers/index.ts b/src/app/features/project/settings/mappers/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/project/settings/mappers/settings.mapper.ts b/src/app/features/project/settings/mappers/settings.mapper.ts new file mode 100644 index 000000000..c1b191663 --- /dev/null +++ b/src/app/features/project/settings/mappers/settings.mapper.ts @@ -0,0 +1,29 @@ +import { ProjectSettingsModel, ProjectSettingsResponseModel } from '@osf/features/project/settings'; + +export class SettingsMapper { + static fromResponse(response: ProjectSettingsResponseModel): ProjectSettingsModel { + return { + attributes: { + accessRequestsEnabled: response.data.attributes.access_requests_enabled, + anyoneCanComment: response.data.attributes.anyone_can_comment, + anyoneCanEditWiki: response.data.attributes.anyone_can_edit_wiki, + redirectLinkEnabled: response.data.attributes.redirect_link_enabled, + redirectLinkLabel: response.data.attributes.redirect_link_label, + redirectLinkUrl: response.data.attributes.redirect_link_url, + wikiEnabled: response.data.attributes.wiki_enabled, + }, + linkTable: response.data.relationships.view_only_links.links.private_links?.length + ? response.data.relationships.view_only_links.links.private_links.map((links) => { + return { + anonymous: links.anonymous, + link: links.name, + createdDate: links.date_created, + createdBy: links.creator, + id: links.id, + nodes: links.nodes, + }; + }) + : [], + } as ProjectSettingsModel; + } +} diff --git a/src/app/features/project/settings/mock-data.ts b/src/app/features/project/settings/mock-data.ts index 8b3ce49d5..3842af222 100644 --- a/src/app/features/project/settings/mock-data.ts +++ b/src/app/features/project/settings/mock-data.ts @@ -1,38 +1,40 @@ +import { LinkTableModel } from '@osf/features/project/settings/models'; + export const mockSettingsData = { tableData: [ - { - 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', - }, - ], + // { + // 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', + // }, + ] as LinkTableModel[], access: 'write', accessOptions: [ { label: 'Contributors (with write access)', value: 'write' }, @@ -52,18 +54,6 @@ export const mockSettingsData = { }, ], rightControls: { - wiki: [ - { - type: 'dropdown', - label: 'Who can edit:', - value: 'write', - options: [ - { label: 'Contributors (with write access)', value: 'write' }, - { label: 'Anyone with link', value: 'public' }, - ], - onChange: (value: string) => console.log('Access changed to', value), - }, - ], notifications: [ { type: 'dropdown', diff --git a/src/app/features/project/settings/models/index.ts b/src/app/features/project/settings/models/index.ts index 4a4b82eda..e21f0b614 100644 --- a/src/app/features/project/settings/models/index.ts +++ b/src/app/features/project/settings/models/index.ts @@ -1 +1,5 @@ export * from './link-table.model'; +export * from './option.model'; +export * from './project-settings.model'; +export * from './project-settings-response.model'; +export * from './view-only-link.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 index 3990216b8..d7bcbed98 100644 --- a/src/app/features/project/settings/models/link-table.model.ts +++ b/src/app/features/project/settings/models/link-table.model.ts @@ -1,8 +1,11 @@ +import { ViewOnlyLinkCreatorModel, ViewOnlyLinkNodeModel } from '@osf/features/project/settings'; + export interface LinkTableModel { - linkName: string; + id: string; sharedComponents: string; createdDate: string | Date; - createdBy: string; + createdBy: ViewOnlyLinkCreatorModel; + nodes: ViewOnlyLinkNodeModel[]; anonymous: boolean; link: string; } diff --git a/src/app/features/project/settings/models/option.model.ts b/src/app/features/project/settings/models/option.model.ts new file mode 100644 index 000000000..ef8647438 --- /dev/null +++ b/src/app/features/project/settings/models/option.model.ts @@ -0,0 +1,4 @@ +export interface OptionModel { + label: string; + value: boolean; +} diff --git a/src/app/features/project/settings/models/project-settings-response.model.ts b/src/app/features/project/settings/models/project-settings-response.model.ts new file mode 100644 index 000000000..f85603c08 --- /dev/null +++ b/src/app/features/project/settings/models/project-settings-response.model.ts @@ -0,0 +1,43 @@ +import { ViewOnlyLinkModel } from '@osf/features/project/settings'; + +export interface ProjectSettingsAttributes { + access_requests_enabled: boolean; + anyone_can_comment: boolean; + anyone_can_edit_wiki: boolean; + wiki_enabled: boolean; + redirect_link_enabled: boolean; + redirect_link_url: string; + redirect_link_label: string; +} + +export interface RelatedLink { + href: string; + meta: Record; +} + +export interface ProjectSettingsRelationships { + view_only_links: { + links: { + private_links: ViewOnlyLinkModel[]; + related: RelatedLink; + }; + }; +} + +export interface ProjectSettingsData { + id: string; + type: 'node-settings'; + attributes: ProjectSettingsAttributes; + relationships: ProjectSettingsRelationships; + links: { + self: string; + iri: string; + }; +} + +export interface ProjectSettingsResponseModel { + data: ProjectSettingsData; + meta: { + version: string; + }; +} diff --git a/src/app/features/project/settings/models/project-settings.model.ts b/src/app/features/project/settings/models/project-settings.model.ts new file mode 100644 index 000000000..15570b3c3 --- /dev/null +++ b/src/app/features/project/settings/models/project-settings.model.ts @@ -0,0 +1,14 @@ +import { LinkTableModel } from '@osf/features/project/settings'; + +export interface ProjectSettingsModel { + attributes: { + accessRequestsEnabled: boolean; + anyoneCanComment: boolean; + anyoneCanEditWiki: boolean; + redirectLinkEnabled: boolean; + redirectLinkLabel: string; + redirectLinkUrl: string; + wikiEnabled: boolean; + }; + linkTable: LinkTableModel[]; +} diff --git a/src/app/features/project/settings/models/right-control.model.ts b/src/app/features/project/settings/models/right-control.model.ts index ab03e6dba..33c0033d4 100644 --- a/src/app/features/project/settings/models/right-control.model.ts +++ b/src/app/features/project/settings/models/right-control.model.ts @@ -1,13 +1,8 @@ -export type RightControl = - | { - type: 'dropdown'; - label?: string; - value: string; - options: { label: string; value: string }[]; - onChange?: (value: string) => void; - } - | { - type: 'text'; - label?: string; - value: string; - }; +import { OptionModel } from '@osf/features/project/settings'; + +export interface RightControl { + type: 'dropdown'; + label?: string; + value: boolean | string; + options: OptionModel[]; +} diff --git a/src/app/features/project/settings/models/view-only-link.model.ts b/src/app/features/project/settings/models/view-only-link.model.ts new file mode 100644 index 000000000..c815030de --- /dev/null +++ b/src/app/features/project/settings/models/view-only-link.model.ts @@ -0,0 +1,21 @@ +export interface ViewOnlyLinkCreatorModel { + fullname: string; + url: string; +} + +export interface ViewOnlyLinkNodeModel { + title: string; + url: string; + scale: string; + category: string; +} + +export interface ViewOnlyLinkModel { + id: string; + date_created: string; + key: string; + name: string; + creator: ViewOnlyLinkCreatorModel; + nodes: ViewOnlyLinkNodeModel[]; + anonymous: boolean; +} diff --git a/src/app/features/project/settings/services/index.ts b/src/app/features/project/settings/services/index.ts new file mode 100644 index 000000000..21c561112 --- /dev/null +++ b/src/app/features/project/settings/services/index.ts @@ -0,0 +1 @@ +export * from './settings.service'; diff --git a/src/app/features/project/settings/services/settings.service.ts b/src/app/features/project/settings/services/settings.service.ts new file mode 100644 index 000000000..0cd3c3f40 --- /dev/null +++ b/src/app/features/project/settings/services/settings.service.ts @@ -0,0 +1,24 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@core/services/json-api/json-api.service'; +import { ProjectSettingsModel, ProjectSettingsResponseModel } from '@osf/features/project/settings'; +import { SettingsMapper } from '@osf/features/project/settings/mappers/settings.mapper'; + +import { environment } from '../../../../../environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class SettingsService { + private readonly baseUrl = environment.apiUrl; + + private readonly jsonApiService = inject(JsonApiService); + + getProjectSettings(nodeId: string): Observable { + return this.jsonApiService + .get(`${this.baseUrl}/nodes/${nodeId}/settings`) + .pipe(map((response) => SettingsMapper.fromResponse(response))); + } +} diff --git a/src/app/features/project/settings/settings.component.html b/src/app/features/project/settings/settings.component.html index b4da870a7..eabda9d52 100644 --- a/src/app/features/project/settings/settings.component.html +++ b/src/app/features/project/settings/settings.component.html @@ -14,18 +14,24 @@ [locationText]="'Storage location cannot be changed after project is created.'" > - + - + + (wikiChangeEmit)="onWikiRequestChange($event)" + (anyoneCanEditWikiEmitValue)="onAnyoneCanEditWikiRequestChange($event)" + [wikiEnabled]="wikiEnabled()" + [anyoneCanEditWiki]="anyoneCanEditWiki()" + /> - +

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

@@ -43,22 +49,10 @@

{{ 'myProjects.settings.emailNotifications' | translate - -

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

- -
- - -
-
+

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

@@ -70,7 +64,9 @@

{{ 'myProjects.settings.projectAffiliation' | translate
  • {{ 'myProjects.settings.publicProjectsToBeDiscoverable' | translate }}
  • {{ 'myProjects.settings.singleSignInToTHeOSF' | translate }}
  • - {{ 'myProjects.settings.faq' | translate }} + {{ + 'myProjects.settings.faq' | translate + }}
  • diff --git a/src/app/features/project/settings/settings.component.ts b/src/app/features/project/settings/settings.component.ts index 7cb884087..f51918b48 100644 --- a/src/app/features/project/settings/settings.component.ts +++ b/src/app/features/project/settings/settings.component.ts @@ -1,15 +1,18 @@ +import { createDispatchMap, select } from '@ngxs/store'; + import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; -import { Checkbox } from 'primeng/checkbox'; import { TabPanels } from 'primeng/tabs'; +import { map, of } from 'rxjs'; + import { NgOptimizedImage } from '@angular/common'; -import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, inject, OnInit, 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 { ActivatedRoute } from '@angular/router'; import { ProjectDetailSettingAccordionComponent, @@ -20,10 +23,11 @@ import { SettingsViewOnlyLinksCardComponent, SettingsWikiCardComponent, } from '@osf/features/project/settings/components'; +import { SettingsRedirectLinkComponent } from '@osf/features/project/settings/components/settings-redirect-link/settings-redirect-link.component'; import { mockSettingsData } from '@osf/features/project/settings/mock-data'; -import { LinkTableModel } from '@osf/features/project/settings/models'; +import { LinkTableModel, ProjectSettingsAttributes } from '@osf/features/project/settings/models'; import { RightControl } from '@osf/features/project/settings/models/right-control.model'; -import { ShareIndexingEnum } from '@osf/features/settings/account-settings/components/share-indexing/enums/share-indexing.enum'; +import { GetProjectSettings, SettingsSelectors } from '@osf/features/project/settings/store'; 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'; @@ -39,9 +43,7 @@ import { IS_WEB } from '@shared/utils/breakpoints.tokens'; ReactiveFormsModule, Card, Button, - Checkbox, ProjectDetailSettingAccordionComponent, - RouterLink, NgOptimizedImage, SettingsProjectFormCardComponent, SettingsStorageLocationCardComponent, @@ -49,16 +51,22 @@ import { IS_WEB } from '@shared/utils/breakpoints.tokens'; SettingsAccessRequestsCardComponent, SettingsWikiCardComponent, SettingsCommentingCardComponent, + SettingsRedirectLinkComponent, ], templateUrl: './settings.component.html', styleUrl: './settings.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, }) -export class SettingsComponent { - protected readonly isDesktop = toSignal(inject(IS_WEB)); +export class SettingsComponent implements OnInit { + private readonly route = inject(ActivatedRoute); + + readonly projectId = toSignal(this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined)); - protected readonly ProjectFormControls = ProjectFormControls; - protected commenting = signal(ShareIndexingEnum.None); + protected settings = select(SettingsSelectors.getSettings); + protected actions = createDispatchMap({ getSettings: GetProjectSettings }); + + protected readonly isDesktop = toSignal(inject(IS_WEB)); projectForm = new FormGroup>({ [ProjectFormControls.Title]: new FormControl('', { @@ -69,9 +77,13 @@ export class SettingsComponent { nonNullable: true, }), }); - accessRequest = new FormControl(false); - wiki = new FormControl(false); - redirectLink = new FormControl(false); + + redirectUrlData = signal<{ url: string; label: string }>({ url: '', label: '' }); + accessRequest = signal(false); + wikiEnabled = signal(false); + anyoneCanEditWiki = signal(false); + redirectLink = signal(false); + anyoneCanComment = signal(false); tableData: LinkTableModel[]; access: string; @@ -80,7 +92,7 @@ export class SettingsComponent { fileSetting: string; dropdownOptions: { label: string; value: string }[]; affiliations: { name: string; canDelete: boolean }[]; - rightControls: { wiki: RightControl[]; notifications: RightControl[] }; + rightControls: { notifications: RightControl[] } = { notifications: [] }; constructor() { [ @@ -91,7 +103,6 @@ export class SettingsComponent { this.fileSetting, this.dropdownOptions, this.affiliations, - this.rightControls, ] = [ mockSettingsData.tableData, mockSettingsData.access, @@ -100,14 +111,65 @@ export class SettingsComponent { mockSettingsData.fileSetting, mockSettingsData.dropdownOptions, mockSettingsData.affiliations, - mockSettingsData.rightControls as { wiki: RightControl[]; notifications: RightControl[] }, ]; + + effect(() => { + const settings = this.settings(); + if (Object.keys(settings).length) { + this.accessRequest.set(settings.attributes.accessRequestsEnabled); + this.wikiEnabled.set(settings.attributes.wikiEnabled); + this.anyoneCanEditWiki.set(settings.attributes.anyoneCanEditWiki); + this.anyoneCanComment.set(settings.attributes.anyoneCanComment); + this.redirectUrlData.set({ + url: settings.attributes.redirectLinkUrl, + label: settings.attributes.redirectLinkLabel, + }); + console.log(settings); + } + }); + } + + ngOnInit(): void { + if (this.projectId()) { + this.actions.getSettings(this.projectId()); + this.setData(); + } + } + + onAccessRequestChange(newValue: boolean): void { + this.accessRequest.set(newValue); + this.syncSettingsChanges('accessRequest', newValue); + } + + onWikiRequestChange(newValue: boolean): void { + this.wikiEnabled.set(newValue); + this.syncSettingsChanges('wikiEnabled', newValue); + } + + onAnyoneCanEditWikiRequestChange(newValue: boolean): void { + this.anyoneCanEditWiki.set(newValue); + this.syncSettingsChanges('anyoneCanEditWiki', newValue); + } + + onAnyoneCanCommentRequestChange(newValue: boolean): void { + this.anyoneCanComment.set(newValue); + this.syncSettingsChanges('anyoneCanComment', newValue); + } + + onRedirectUrlDataRequestChange(data: { url: string; label: string }): void { + this.redirectUrlData.set(data); + this.syncSettingsChanges('redirectUrl', data); + console.log(data); } submitForm(): void { // [VY] TODO: Implement form submission } + deleteLinkItem(item: LinkTableModel): void { + console.log(item); + } + resetForm(): void { this.projectForm.reset(); } @@ -115,4 +177,42 @@ export class SettingsComponent { onAccessChange(event: string): void { console.log(event); } + + private setData(): void { + const settings = this.settings(); + if (settings?.attributes?.accessRequestsEnabled !== undefined) { + this.accessRequest.set(settings.attributes.accessRequestsEnabled); + } + } + + private syncSettingsChanges(changedField: string, value: boolean | { url: string; label: string }): void { + const payload: Partial = {}; + + switch (changedField) { + case 'accessRequest': + payload['access_requests_enabled'] = value as boolean; + break; + case 'wikiEnabled': + payload['wiki_enabled'] = value as boolean; + break; + case 'redirectLink': + payload['redirect_link_enabled'] = value as boolean; + break; + case 'anyoneCanEditWiki': + payload['anyone_can_edit_wiki'] = value as boolean; + break; + case 'anyoneCanComment': + payload['anyone_can_comment'] = value as boolean; + break; + case 'redirectUrl': + if (typeof value === 'object') { + payload['redirect_link_url'] = value.url; + payload['redirect_link_label'] = value.label; + } + break; + } + + console.log('Updated payload', payload); + // TODO: call update API here + } } diff --git a/src/app/features/project/settings/store/empty-project-settings.ts b/src/app/features/project/settings/store/empty-project-settings.ts new file mode 100644 index 000000000..8d38bc81f --- /dev/null +++ b/src/app/features/project/settings/store/empty-project-settings.ts @@ -0,0 +1,14 @@ +import { ProjectSettingsModel } from '@osf/features/project/settings'; + +export const EMPTY_PROJECT_SETTINGS: ProjectSettingsModel = { + attributes: { + wikiEnabled: false, + redirectLinkUrl: '', + redirectLinkLabel: '', + redirectLinkEnabled: false, + anyoneCanEditWiki: false, + anyoneCanComment: false, + accessRequestsEnabled: false, + }, + linkTable: [], +}; diff --git a/src/app/features/project/settings/store/index.ts b/src/app/features/project/settings/store/index.ts new file mode 100644 index 000000000..14eb3b742 --- /dev/null +++ b/src/app/features/project/settings/store/index.ts @@ -0,0 +1,4 @@ +export * from './settings.actions'; +export * from './settings.model'; +export * from './settings.selectors'; +export * from './settings.state'; diff --git a/src/app/features/project/settings/store/settings.actions.ts b/src/app/features/project/settings/store/settings.actions.ts new file mode 100644 index 000000000..a554e10b7 --- /dev/null +++ b/src/app/features/project/settings/store/settings.actions.ts @@ -0,0 +1,5 @@ +export class GetProjectSettings { + static readonly type = 'Get [Settings]'; + + constructor(public projectId: string) {} +} diff --git a/src/app/features/project/settings/store/settings.model.ts b/src/app/features/project/settings/store/settings.model.ts new file mode 100644 index 000000000..8ef499b8a --- /dev/null +++ b/src/app/features/project/settings/store/settings.model.ts @@ -0,0 +1,6 @@ +import { ProjectSettingsModel } from '@osf/features/project/settings'; +import { AsyncStateModel } from '@shared/models/store'; + +export interface SettingsStateModel { + settings: AsyncStateModel; +} diff --git a/src/app/features/project/settings/store/settings.selectors.ts b/src/app/features/project/settings/store/settings.selectors.ts new file mode 100644 index 000000000..08dc0c655 --- /dev/null +++ b/src/app/features/project/settings/store/settings.selectors.ts @@ -0,0 +1,11 @@ +import { Selector } from '@ngxs/store'; + +import { SettingsStateModel } from '@osf/features/project/settings/store/settings.model'; +import { SettingsState } from '@osf/features/project/settings/store/settings.state'; + +export class SettingsSelectors { + @Selector([SettingsState]) + static getSettings(state: SettingsStateModel) { + return state.settings.data; + } +} diff --git a/src/app/features/project/settings/store/settings.state.ts b/src/app/features/project/settings/store/settings.state.ts new file mode 100644 index 000000000..69fd8f928 --- /dev/null +++ b/src/app/features/project/settings/store/settings.state.ts @@ -0,0 +1,59 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { of } from 'rxjs'; +import { catchError, tap } from 'rxjs/operators'; + +import { inject, Injectable } from '@angular/core'; + +import { ProjectSettingsModel } from '@osf/features/project/settings'; +import { SettingsService } from '@osf/features/project/settings/services'; +import { GetProjectSettings } from '@osf/features/project/settings/store/settings.actions'; +import { SettingsStateModel } from '@osf/features/project/settings/store/settings.model'; + +@State({ + name: 'settings', + defaults: { + settings: { + data: {} as ProjectSettingsModel, + isLoading: false, + error: null, + }, + }, +}) +@Injectable() +export class SettingsState { + private readonly settingsService = inject(SettingsService); + + @Action(GetProjectSettings) + getProjectSettings(ctx: StateContext, action: GetProjectSettings) { + ctx.patchState({ + settings: { + ...ctx.getState().settings, + isLoading: true, + error: null, + }, + }); + + return this.settingsService.getProjectSettings(action.projectId).pipe( + tap((settings) => { + ctx.patchState({ + settings: { + data: settings, + isLoading: false, + error: null, + }, + }); + }), + catchError((error) => { + ctx.patchState({ + settings: { + ...ctx.getState().settings, + isLoading: false, + error: error?.message || 'Failed to load project settings', + }, + }); + return of(error); + }) + ); + } +} 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 index 158013e09..8c93d2357 100644 --- 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 @@ -54,7 +54,13 @@ {{ item.createdBy }} {{ item.anonymous }} - + + + 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 index a29c6075a..401ec2113 100644 --- 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 @@ -6,7 +6,7 @@ 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 { ChangeDetectionStrategy, Component, inject, input, output } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { LinkTableModel } from '@osf/features/project/settings/models'; @@ -19,6 +19,7 @@ import { LinkTableModel } from '@osf/features/project/settings/models'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ViewOnlyTableComponent { + deleteLink = output(); tableData = input.required(); readonly #clipboard = inject(Clipboard); diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index b9ff6f69a..13dd1a8c3 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -253,7 +253,14 @@ "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" + "faq": "FAQ", + "contributorsOption": "Contributors (with write access)", + "anyoneOption": "Anyone with link", + "whoCanEdit": "Who can edit:", + "url": "url", + "label": "label", + "redirectUrlPlaceholder": "Send people who visit your OSF project page to this link instead", + "redirectLabelPlaceholder": "Optional" } }, "project": { From fa3655c4842fea8b8a0ebd4d4781c33c4c850800 Mon Sep 17 00:00:00 2001 From: volodyayakubovskyy Date: Tue, 27 May 2025 12:19:14 +0300 Subject: [PATCH 2/7] feat(settings): integrated api for settings checkbox and other boolean values --- src/app/core/interceptors/auth.interceptor.ts | 4 +- .../entities/node-response.model.ts | 107 ++++++++++++++ .../entities/update-node-request.model.ts | 16 ++ .../my-projects/my-projects.service.ts | 36 +++-- .../settings-project-form-card.component.html | 5 +- .../settings-project-form-card.component.ts | 11 +- .../settings-redirect-link.component.html | 25 +--- .../settings/mappers/settings.mapper.ts | 1 + .../settings/models/project-settings.model.ts | 2 + .../settings/services/settings.service.ts | 12 +- .../project/settings/settings.component.html | 7 +- .../project/settings/settings.component.ts | 99 +++++++++---- .../settings/store/settings.actions.ts | 23 ++- .../project/settings/store/settings.model.ts | 2 + .../settings/store/settings.selectors.ts | 5 + .../project/settings/store/settings.state.ts | 139 ++++++++++++++++-- .../notification-subscription.service.ts | 16 +- .../notification-subscription.actions.ts | 6 + .../store/notification-subscription.model.ts | 1 + .../notification-subscription.selectors.ts | 5 + .../store/notification-subscription.state.ts | 25 ++++ src/assets/i18n/en.json | 3 +- 22 files changed, 470 insertions(+), 80 deletions(-) create mode 100644 src/app/features/my-projects/entities/node-response.model.ts create mode 100644 src/app/features/my-projects/entities/update-node-request.model.ts diff --git a/src/app/core/interceptors/auth.interceptor.ts b/src/app/core/interceptors/auth.interceptor.ts index 0e6e44435..543056422 100644 --- a/src/app/core/interceptors/auth.interceptor.ts +++ b/src/app/core/interceptors/auth.interceptor.ts @@ -6,9 +6,9 @@ export const authInterceptor: HttpInterceptorFn = ( req: HttpRequest, next: HttpHandlerFn ): Observable> => { - const authToken = '2rjFZwmdDG4rtKj7hGkEMO6XyHBM2lN7XBbsA1e8OqcFhOWu6Z7fQZiheu9RXtzSeVrgOt'; + const authToken = 'UlO9O9GNKgVzJD7pUeY53jiQTKJ4U2znXVWNvh0KZQruoENuILx0IIYf9LoDz7Duq72EIm'; + // '2rjFZwmdDG4rtKj7hGkEMO6XyHBM2lN7XBbsA1e8OqcFhOWu6Z7fQZiheu9RXtzSeVrgOt'; // OBJoUomBgbUuDgQo5JoaSKNya6XaYcd0ojAX1XOLmWi6J2arQPzByxyEi81fHE60drQUWv - // UlO9O9GNKgVzJD7pUeY53jiQTKJ4U2znXVWNvh0KZQruoENuILx0IIYf9LoDz7Duq72EIm if (authToken) { const authReq = req.clone({ diff --git a/src/app/features/my-projects/entities/node-response.model.ts b/src/app/features/my-projects/entities/node-response.model.ts new file mode 100644 index 000000000..abb41506e --- /dev/null +++ b/src/app/features/my-projects/entities/node-response.model.ts @@ -0,0 +1,107 @@ +export interface NodeAttributes { + title: string; + description: string; + category: string; + custom_citation: string | null; + date_created: string; + date_modified: string; + registration: boolean; + preprint: boolean; + fork: boolean; + collection: boolean; + tags: string[]; + access_requests_enabled: boolean; + node_license: unknown | null; + current_user_can_comment: boolean; + current_user_permissions: string[]; + current_user_is_contributor: boolean; + current_user_is_contributor_or_group_member: boolean; + wiki_enabled: boolean; + public: boolean; + subjects: unknown[]; +} + +export interface RelationshipLinks { + related: { + href: string; + meta: Record; + }; + self?: { + href: string; + meta: Record; + }; +} + +export interface NodeRelationships { + children: { links: RelationshipLinks }; + comments: { links: RelationshipLinks }; + contributors: { links: RelationshipLinks }; + bibliographic_contributors: { links: RelationshipLinks }; + implicit_contributors: { links: RelationshipLinks }; + files: { links: RelationshipLinks }; + settings: { + links: RelationshipLinks; + data: { id: string; type: 'node-setting' }; + }; + wikis: { links: RelationshipLinks }; + forks: { links: RelationshipLinks }; + groups: { links: RelationshipLinks }; + node_links: { links: RelationshipLinks }; + linked_by_nodes: { links: RelationshipLinks }; + linked_by_registrations: { links: RelationshipLinks }; + parent: { + links: RelationshipLinks; + data: { id: string; type: 'nodes' }; + }; + identifiers: { links: RelationshipLinks }; + affiliated_institutions: { links: RelationshipLinks }; + draft_registrations: { links: RelationshipLinks }; + registrations: { links: RelationshipLinks }; + region: { + links: RelationshipLinks; + data: { id: string; type: 'regions' }; + }; + root: { + links: RelationshipLinks; + data: { id: string; type: 'nodes' }; + }; + logs: { links: RelationshipLinks }; + linked_nodes: { links: RelationshipLinks }; + linked_registrations: { links: RelationshipLinks }; + view_only_links: { links: RelationshipLinks }; + citation: { + links: RelationshipLinks; + data: { id: string; type: 'citation' }; + }; + preprints: { links: RelationshipLinks }; + storage: { + links: RelationshipLinks; + data: { id: string; type: 'node-storage' }; + }; + cedar_metadata_records: { links: RelationshipLinks }; + subjects_acceptable: { links: RelationshipLinks }; +} + +export interface NodeLinks { + html: string; + self: string; + iri: string; +} + +export interface NodeData { + id: string; + type: 'nodes'; + attributes: NodeAttributes; + relationships: NodeRelationships; + links: NodeLinks; + lastFetched?: number; +} + +export interface NodeMeta { + version: string; +} + +export interface NodeResponseModel { + data: NodeData; + meta: NodeMeta; +} diff --git a/src/app/features/my-projects/entities/update-node-request.model.ts b/src/app/features/my-projects/entities/update-node-request.model.ts new file mode 100644 index 000000000..db2ee5627 --- /dev/null +++ b/src/app/features/my-projects/entities/update-node-request.model.ts @@ -0,0 +1,16 @@ +export interface UpdateNodeAttributes { + description?: string; + tags?: string[]; + public?: boolean; + title?: string; +} + +export interface UpdateNodeData { + type: 'nodes'; + id: string; + attributes: UpdateNodeAttributes; +} + +export interface UpdateNodeRequestModel { + data: UpdateNodeData; +} diff --git a/src/app/features/my-projects/my-projects.service.ts b/src/app/features/my-projects/my-projects.service.ts index 34bb63b9d..f3ea27199 100644 --- a/src/app/features/my-projects/my-projects.service.ts +++ b/src/app/features/my-projects/my-projects.service.ts @@ -13,6 +13,8 @@ import { } from '@osf/features/my-projects/entities/my-projects.entities'; import { EndpointType } from '@osf/features/my-projects/entities/my-projects.types'; import { MyProjectsSearchFilters } from '@osf/features/my-projects/entities/my-projects-search-filters.models'; +import { NodeResponseModel } from '@osf/features/my-projects/entities/node-response.model'; +import { UpdateNodeRequestModel } from '@osf/features/my-projects/entities/update-node-request.model'; import { MyProjectsMapper } from '@osf/features/my-projects/mappers/my-projects.mapper'; import { SortOrder } from '@shared/utils/sort-order.enum'; @@ -24,13 +26,15 @@ import { CreateProjectPayload } from './entities/create-project.entities'; providedIn: 'root', }) export class MyProjectsService { - #jsonApiService = inject(JsonApiService); - #sortFieldMap: Record = { + private apiUrl = environment.apiUrl; + private sortFieldMap: Record = { title: 'title', dateModified: 'date_modified', }; - #getMyItems( + private readonly jsonApiService = inject(JsonApiService); + + private getMyItems( endpoint: EndpointType, filters?: MyProjectsSearchFilters, pageNumber?: number, @@ -54,8 +58,8 @@ export class MyProjectsService { params['page[size]'] = pageSize; } - if (filters?.sortColumn && this.#sortFieldMap[filters.sortColumn]) { - const apiField = this.#sortFieldMap[filters.sortColumn]; + if (filters?.sortColumn && this.sortFieldMap[filters.sortColumn]) { + const apiField = this.sortFieldMap[filters.sortColumn]; const sortPrefix = filters.sortOrder === SortOrder.Desc ? '-' : ''; params['sort'] = `${sortPrefix}${apiField}`; } else { @@ -66,7 +70,7 @@ export class MyProjectsService { // ? environment.apiUrl + '/' + endpoint // : environment.apiUrl + '/users/me/' + endpoint; - return this.#jsonApiService.get(url, params).pipe( + return this.jsonApiService.get(url, params).pipe( map((response: MyProjectsJsonApiResponse) => ({ data: response.data.map((item: MyProjectsItemGetResponse) => MyProjectsMapper.fromResponse(item)), links: response.links, @@ -79,7 +83,7 @@ export class MyProjectsService { pageNumber?: number, pageSize?: number ): Observable { - return this.#getMyItems('nodes', filters, pageNumber, pageSize); + return this.getMyItems('nodes', filters, pageNumber, pageSize); } getBookmarksCollectionId(): Observable { @@ -87,7 +91,7 @@ export class MyProjectsService { 'fields[collections]': 'title,bookmarks', }; - return this.#jsonApiService.get(environment.apiUrl + '/collections/', params).pipe( + return this.jsonApiService.get(environment.apiUrl + '/collections/', params).pipe( map((response) => { const bookmarksCollection = response.data.find( (collection) => collection.attributes.title === 'Bookmarks' && collection.attributes.bookmarks @@ -102,7 +106,7 @@ export class MyProjectsService { pageNumber?: number, pageSize?: number ): Observable { - return this.#getMyItems('registrations', filters, pageNumber, pageSize); + return this.getMyItems('registrations', filters, pageNumber, pageSize); } getMyPreprints( @@ -110,7 +114,7 @@ export class MyProjectsService { pageNumber?: number, pageSize?: number ): Observable { - return this.#getMyItems('preprints', filters, pageNumber, pageSize); + return this.getMyItems('preprints', filters, pageNumber, pageSize); } getMyBookmarks( @@ -119,7 +123,7 @@ export class MyProjectsService { pageNumber?: number, pageSize?: number ): Observable { - return this.#getMyItems(`collections/${collectionId}/linked_nodes/`, filters, pageNumber, pageSize); + return this.getMyItems(`collections/${collectionId}/linked_nodes/`, filters, pageNumber, pageSize); } createProject( @@ -163,8 +167,16 @@ export class MyProjectsService { 'fields[users]': 'family_name,full_name,given_name,middle_name', }; - return this.#jsonApiService + return this.jsonApiService .post(`${environment.apiUrl}/nodes/`, payload, params) .pipe(map((response) => MyProjectsMapper.fromResponse(response))); } + + getProjectById(projectId: string): Observable { + return this.jsonApiService.get(`${this.apiUrl}/nodes/${projectId}`); + } + + updateProjectById(model: UpdateNodeRequestModel): Observable { + return this.jsonApiService.patch(`${this.apiUrl}/nodes/${model?.data?.id}`, model); + } } diff --git a/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.html b/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.html index c1b323fcc..697b2daf1 100644 --- a/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.html +++ b/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.html @@ -1,7 +1,7 @@

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

    -
    +