diff --git a/src/app/features/preprints/components/stepper/review-step/review-step.component.html b/src/app/features/preprints/components/stepper/review-step/review-step.component.html index 42b38be03..63e7994a6 100644 --- a/src/app/features/preprints/components/stepper/review-step/review-step.component.html +++ b/src/app/features/preprints/components/stepper/review-step/review-step.component.html @@ -28,7 +28,13 @@

}}

- Provider logo + Provider logo

{{ provider()?.name }}

diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index 79d372493..e5124e04f 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -15,7 +15,6 @@ import { import { ActivityLogsState } from '@osf/shared/stores/activity-logs'; import { CollectionsModerationState } from '../moderation/store/collections-moderation'; -import { NotificationSubscriptionState } from '../settings/notifications/store'; import { AnalyticsState } from './analytics/store'; import { SettingsState } from './settings/store'; @@ -63,7 +62,7 @@ export const projectRoutes: Routes = [ { path: 'settings', loadComponent: () => import('../project/settings/settings.component').then((mod) => mod.SettingsComponent), - providers: [provideStates([SettingsState, ViewOnlyLinkState, NotificationSubscriptionState])], + providers: [provideStates([SettingsState, ViewOnlyLinkState])], }, { path: 'contributors', diff --git a/src/app/features/project/settings/components/index.ts b/src/app/features/project/settings/components/index.ts index d38eed9dd..b1d1cc40d 100644 --- a/src/app/features/project/settings/components/index.ts +++ b/src/app/features/project/settings/components/index.ts @@ -1,7 +1,7 @@ export { ProjectDetailSettingAccordionComponent } from './project-detail-setting-accordion/project-detail-setting-accordion.component'; export { ProjectSettingNotificationsComponent } from './project-setting-notifications/project-setting-notifications.component'; export { SettingsAccessRequestsCardComponent } from './settings-access-requests-card/settings-access-requests-card.component'; -export { SettingsCommentingCardComponent } from './settings-commenting-card/settings-commenting-card.component'; +export { SettingsProjectAffiliationComponent } from './settings-project-affiliation/settings-project-affiliation.component'; export { SettingsProjectFormCardComponent } from './settings-project-form-card/settings-project-form-card.component'; export { SettingsRedirectLinkComponent } from './settings-redirect-link/settings-redirect-link.component'; export { SettingsStorageLocationCardComponent } from './settings-storage-location-card/settings-storage-location-card.component'; 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 4a01216a3..aa0da4d82 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 @@ -1,12 +1,12 @@
{{ title() }} @@ -22,20 +22,13 @@ } - - - {{ item.label | translate }} - - - - {{ item.label | translate }} - - + [(selectedValue)]="control.value" + [disabled]="disabledRightControls()" + (changeValue)="emitValueChange.emit({ index: index, value: control.value })" + >
}
@@ -43,11 +36,7 @@ @if (expanded()) { -
+
} 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 1ad7cb38e..0d24079e9 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,16 +1,17 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; -import { SelectModule } from 'primeng/select'; import { ChangeDetectionStrategy, Component, input, output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { SelectComponent } from '@osf/shared/components'; + import { RightControl } from '../../models'; @Component({ selector: 'osf-project-detail-setting-accordion', - imports: [SelectModule, FormsModule, Button, TranslatePipe], + imports: [FormsModule, Button, SelectComponent, TranslatePipe], templateUrl: './project-detail-setting-accordion.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -18,6 +19,8 @@ export class ProjectDetailSettingAccordionComponent { emitValueChange = output<{ index: number; value: boolean | string }>(); title = input(); rightControls = input.required(); + disabledRightControls = input(false); + expanded = signal(false); toggle() { diff --git a/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.html b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.html index e17e76c34..a5ea29bdf 100644 --- a/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.html +++ b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.html @@ -3,23 +3,18 @@

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

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

- -
- - - {{ subscriptionEvent.Comments | notificationDescription: notifications()?.[0]?.frequency | translate }} - -
- -
- - - {{ subscriptionEvent.FileUpdated | notificationDescription: notifications()?.[1]?.frequency | translate }} - -
-
+ @if (notifications().length) { + +
+ + + {{ subscriptionEvent.FileUpdated | notificationDescription: notifications()[0].frequency | translate }} + +
+
+ } diff --git a/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.ts b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.ts index f70feaeba..f915fabbc 100644 --- a/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.ts +++ b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.ts @@ -4,8 +4,8 @@ import { Card } from 'primeng/card'; import { ChangeDetectionStrategy, Component, effect, input, output } from '@angular/core'; -import { NotificationSubscription } from '@osf/features/settings/notifications/models'; import { SubscriptionEvent, SubscriptionFrequency } from '@osf/shared/enums'; +import { NotificationSubscription } from '@osf/shared/models'; import { RightControl } from '../../models'; import { NotificationDescriptionPipe } from '../../pipes'; @@ -19,9 +19,9 @@ import { ProjectDetailSettingAccordionComponent } from '../project-detail-settin changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProjectSettingNotificationsComponent { - notificationEmitValue = output<{ event: SubscriptionEvent; frequency: SubscriptionFrequency }>(); + notifications = input.required(); title = input(); - notifications = input(); + notificationEmitValue = output(); allAccordionData: RightControl[] | undefined = []; @@ -33,24 +33,14 @@ export class ProjectSettingNotificationsComponent { constructor() { effect(() => { - this.allAccordionData = this.notifications()?.map((notification) => { - if (notification.event === SubscriptionEvent.Comments) { - return { - label: 'settings.notifications.notificationPreferences.items.comments', - value: notification.frequency as string, - type: 'dropdown', - options: this.subscriptionFrequencyOptions, - event: notification.event, - } as RightControl; - } else { - return { - label: 'settings.notifications.notificationPreferences.items.files', - value: notification.frequency as string, - type: 'dropdown', - options: this.subscriptionFrequencyOptions, - event: notification.event, - } as RightControl; - } + this.allAccordionData = this.notifications().map((notification) => { + return { + label: 'settings.notifications.notificationPreferences.items.files', + value: notification.frequency as string, + type: 'dropdown', + options: this.subscriptionFrequencyOptions, + event: notification.event, + } as RightControl; }); }); } @@ -58,6 +48,7 @@ export class ProjectSettingNotificationsComponent { changeEmittedValue(emittedValue: { index: number; value: boolean | string }): void { if (this.allAccordionData) { this.notificationEmitValue.emit({ + id: this.notifications()[0].id, event: this.allAccordionData[emittedValue.index].event as SubscriptionEvent, frequency: emittedValue.value as SubscriptionFrequency, }); 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 c41274a91..78c3a9042 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 @@ -9,6 +9,7 @@

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

id="accessRequest" name="ongoing" > + 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 deleted file mode 100644 index 39351e990..000000000 --- a/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.html +++ /dev/null @@ -1,28 +0,0 @@ - -

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

- -
-
- - -

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

-
- -
- - - -

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

-
-
-
diff --git a/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.spec.ts b/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.spec.ts deleted file mode 100644 index 639c49a14..000000000 --- a/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { SettingsCommentingCardComponent } from './settings-commenting-card.component'; - -describe('SettingsCommentingCardComponent', () => { - let component: SettingsCommentingCardComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [SettingsCommentingCardComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(SettingsCommentingCardComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); 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 deleted file mode 100644 index 4d46cc4a3..000000000 --- a/src/app/features/project/settings/components/settings-commenting-card/settings-commenting-card.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { TranslatePipe } from '@ngx-translate/core'; - -import { Card } from 'primeng/card'; -import { RadioButton } from 'primeng/radiobutton'; - -import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { FormsModule } from '@angular/forms'; - -@Component({ - selector: 'osf-settings-commenting-card', - imports: [Card, RadioButton, TranslatePipe, FormsModule], - templateUrl: './settings-commenting-card.component.html', - styleUrl: '../../settings.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SettingsCommentingCardComponent { - anyoneCanComment = input.required(); - readonly anyoneCanCommentChange = output(); -} diff --git a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.html b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.html new file mode 100644 index 000000000..af179c1e5 --- /dev/null +++ b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.html @@ -0,0 +1,40 @@ + +

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

+ +

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

+ +
    +
  1. {{ 'myProjects.settings.institutionalLogos' | translate }}
  2. +
  3. {{ 'myProjects.settings.publicProjectsToBeDiscoverable' | translate }}
  4. +
  5. {{ 'myProjects.settings.singleSignInToTHeOSF' | translate }}
  6. +
  7. + + {{ 'myProjects.settings.faq' | translate }} + +
  8. +
+ + @if (affiliations().length > 0) { +
+ @for (affiliation of affiliations(); track $index) { +
+
+ Affiliation logo + +

{{ affiliation.name }}

+
+ + +
+ } +
+ } +
diff --git a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.scss b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts new file mode 100644 index 000000000..685bcc895 --- /dev/null +++ b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SettingsProjectAffiliationComponent } from './settings-project-affiliation.component'; + +describe('SettingsProjectAffiliationComponent', () => { + let component: SettingsProjectAffiliationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SettingsProjectAffiliationComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SettingsProjectAffiliationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.ts b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.ts new file mode 100644 index 000000000..693cadb10 --- /dev/null +++ b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.ts @@ -0,0 +1,25 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Card } from 'primeng/card'; + +import { NgOptimizedImage } from '@angular/common'; +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; + +import { Institution } from '@osf/shared/models'; + +@Component({ + selector: 'osf-settings-project-affiliation', + imports: [Card, Button, NgOptimizedImage, TranslatePipe], + templateUrl: './settings-project-affiliation.component.html', + styleUrl: './settings-project-affiliation.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SettingsProjectAffiliationComponent { + affiliations = input([]); + removed = output(); + + removeAffiliation(affiliation: Institution) { + this.removed.emit(affiliation); + } +} 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 11332963b..389147fb2 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,25 +1,20 @@

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

-
+
- - +
- +
@@ -34,7 +29,7 @@

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

type="submit" class="w-10rem btn-full-width bg-primary-blue-second" [label]="'myProjects.settings.saveChanges' | translate" - [disabled]="formGroup().invalid" + [disabled]="projectForm.invalid" >
diff --git a/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.ts b/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.ts index cba65b3bf..100387ec1 100644 --- a/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.ts +++ b/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.ts @@ -2,41 +2,72 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; -import { InputText } from 'primeng/inputtext'; import { Textarea } from 'primeng/textarea'; -import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ChangeDetectionStrategy, Component, effect, input, output } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { TextInputComponent } from '@osf/shared/components'; +import { InputLimits } from '@osf/shared/constants'; import { ProjectFormControls } from '@osf/shared/enums'; -import { NodeData } from '@osf/shared/models'; +import { CustomValidators } from '@osf/shared/helpers'; -import { ProjectDetailsModel } from '../../models'; +import { NodeDetailsModel, ProjectDetailsModel } from '../../models'; @Component({ selector: 'osf-settings-project-form-card', - imports: [Button, Card, FormsModule, InputText, Textarea, TranslatePipe, ReactiveFormsModule], + imports: [Button, Card, Textarea, TranslatePipe, ReactiveFormsModule, TextInputComponent], templateUrl: './settings-project-form-card.component.html', styleUrl: '../../settings.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class SettingsProjectFormCardComponent { - projectDetails = input.required(); - formGroup = input.required(); + projectDetails = input.required(); submitForm = output(); deleteProject = output(); - protected readonly ProjectFormControls = ProjectFormControls; + readonly ProjectFormControls = ProjectFormControls; + readonly inputLimits = InputLimits; + + projectForm = new FormGroup({ + [ProjectFormControls.Title]: new FormControl('', CustomValidators.requiredTrimmed()), + [ProjectFormControls.Description]: new FormControl(''), + }); + + constructor() { + this.setupFormEffects(); + } + + private setupFormEffects(): void { + effect(() => { + const details = this.projectDetails(); + + if (details) { + this.projectForm.patchValue( + { + [ProjectFormControls.Title]: details.title, + [ProjectFormControls.Description]: details.description, + }, + { emitEvent: false } + ); + } + }); + } resetForm(): void { - this.formGroup().patchValue({ ...this.projectDetails().attributes }); + const details = this.projectDetails(); + + this.projectForm.patchValue({ + [ProjectFormControls.Title]: details.title, + [ProjectFormControls.Description]: details.description, + }); } submit() { - if (this.formGroup().invalid) { + if (this.projectForm.invalid) { return; } - this.submitForm.emit(this.formGroup().value); + this.submitForm.emit(this.projectForm.value as ProjectDetailsModel); } } 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 index 8c849acbf..e89fe4f2f 100644 --- 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 @@ -1,67 +1,39 @@

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

-
- + +
+ - -
+ +
- @if (redirectLink()) { -
-
- - + - @if (urlInput.invalid && urlInput.touched) { - - {{ 'myProjects.settings.invalidUrl' | translate }} - - } -
- -
- - -
-
-
- -
- } +
+ +
+ } +
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 index 852f3b17e..c208f2c75 100644 --- 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 @@ -3,58 +3,90 @@ 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'; - -import { RedirectUrlDataModel } from '@osf/features/project/settings/models'; + +import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, input, output } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; + +import { TextInputComponent } from '@osf/shared/components'; +import { InputLimits } from '@osf/shared/constants'; +import { CustomValidators } from '@osf/shared/helpers'; + +import { RedirectLinkDataModel, RedirectLinkForm } from '../../models'; @Component({ selector: 'osf-settings-redirect-link', - imports: [Card, Checkbox, TranslatePipe, FormsModule, InputText, TitleCasePipe, UpperCasePipe, Button], + imports: [Card, Checkbox, TranslatePipe, ReactiveFormsModule, TextInputComponent, Button], templateUrl: './settings-redirect-link.component.html', styleUrl: '../../settings.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class SettingsRedirectLinkComponent { - redirectUrlDataChange = output(); - redirectUrlDataInput = input.required(); - redirectLink = model(); + private readonly destroyRef = inject(DestroyRef); + + redirectUrlDataInput = input.required(); + redirectUrlDataChange = output(); - redirectUrlData: WritableSignal = signal({ url: '', label: '' }); + inputLimits = InputLimits; + + redirectForm = new FormGroup({ + isEnabled: new FormControl(false, { + nonNullable: true, + validators: [Validators.required], + }), + url: new FormControl('', [CustomValidators.requiredTrimmed(), CustomValidators.linkValidator()]), + label: new FormControl('', [CustomValidators.requiredTrimmed()]), + }); constructor() { + this.setupFormSubscriptions(); + this.setupInputEffects(); + } + + private setupFormSubscriptions(): void { + this.redirectForm.controls.isEnabled?.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((isEnabled) => { + if (!isEnabled) { + this.redirectForm.get('url')?.setValue(''); + this.redirectForm.get('label')?.setValue(''); + this.emitFormData(); + } + }); + } + + saveRedirectSettings(): void { + if (this.redirectForm.valid) { + this.emitFormData(); + } + } + + private setupInputEffects(): void { effect(() => { - this.redirectUrlData.set(this.redirectUrlDataInput()); - const { url, label } = this.redirectUrlDataInput(); - if (url || label) { - this.redirectLink.set(true); - } + const inputData = this.redirectUrlDataInput(); + this.redirectForm.patchValue( + { + isEnabled: inputData.isEnabled, + url: inputData.url, + label: inputData.label, + }, + { emitEvent: false } + ); + + this.redirectForm.markAsPristine(); }); } - onToggleRedirectLink(checked: boolean): void { - this.redirectLink.set(checked); - if (!checked) { - this.redirectUrlData.set({ url: '', label: '' }); - this.redirectUrlDataChange.emit(this.redirectUrlData()); - } + get hasChanges(): boolean { + return this.redirectForm.dirty; } - emitIfChecked(): void { - if (this.redirectLink()) { - this.redirectUrlDataChange.emit(this.redirectUrlData()); - } + private emitFormData(): void { + const formValue = this.redirectForm.value; + this.redirectUrlDataChange.emit({ + isEnabled: formValue.isEnabled || false, + url: formValue.url || '', + label: formValue.label || '', + }); } } 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 c3ac8b48b..0a70898ab 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 @@ -23,6 +23,7 @@

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

(emitValueChange)="changeEmittedValue($event)" [title]="title()" [rightControls]="allAccordionData" + [disabledRightControls]="!isPublic()" >
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 919c34137..d6203915f 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 @@ -23,6 +23,7 @@ export class SettingsWikiCardComponent { wikiEnabled = input.required(); anyoneCanEditWiki = input.required(); title = input.required(); + isPublic = input(false); allAccordionData: RightControl[] = []; @@ -35,8 +36,8 @@ export class SettingsWikiCardComponent { value: anyoneCanEditWiki, type: 'dropdown', options: [ - { label: 'myProjects.settings.contributorsOption', value: true }, - { label: 'myProjects.settings.anyoneOption', value: false }, + { label: 'myProjects.settings.contributorsOption', value: false }, + { label: 'myProjects.settings.anyoneOption', value: true }, ], }, ]; diff --git a/src/app/features/project/settings/mappers/settings.mapper.ts b/src/app/features/project/settings/mappers/settings.mapper.ts index 396e28617..7ffca3f16 100644 --- a/src/app/features/project/settings/mappers/settings.mapper.ts +++ b/src/app/features/project/settings/mappers/settings.mapper.ts @@ -1,4 +1,12 @@ -import { ProjectSettingsData, ProjectSettingsModel, ProjectSettingsResponseModel } from '../models'; +import { InstitutionsMapper } from '@osf/shared/mappers'; + +import { + NodeDataJsonApi, + NodeDetailsModel, + ProjectSettingsData, + ProjectSettingsModel, + ProjectSettingsResponseModel, +} from '../models'; export class SettingsMapper { static fromResponse( @@ -20,4 +28,19 @@ export class SettingsMapper { }, } as ProjectSettingsModel; } + + static fromNodeResponse(data: NodeDataJsonApi): NodeDetailsModel { + return { + id: data.id, + title: data.attributes.title, + description: data.attributes.description, + isPublic: data.attributes.public, + region: { + id: data.embeds.region.data.id, + name: data.embeds.region.data.attributes.name, + }, + affiliatedInstitutions: InstitutionsMapper.fromInstitutionsResponse(data.embeds.affiliated_institutions), + lastFetched: Date.now(), + }; + } } diff --git a/src/app/features/project/settings/models/index.ts b/src/app/features/project/settings/models/index.ts index 57b6ecf76..282b281d1 100644 --- a/src/app/features/project/settings/models/index.ts +++ b/src/app/features/project/settings/models/index.ts @@ -1,5 +1,8 @@ +export * from './node-details.model'; export * from './project-details.model'; +export * from './project-details-json-api.model'; export * from './project-settings.model'; export * from './project-settings-response.model'; -export * from './redirect-url-data.model'; +export * from './redirect-link-data.model'; +export * from './redirect-link-form.model'; export * from './right-control.model'; diff --git a/src/app/features/project/settings/models/node-details.model.ts b/src/app/features/project/settings/models/node-details.model.ts new file mode 100644 index 000000000..252e05426 --- /dev/null +++ b/src/app/features/project/settings/models/node-details.model.ts @@ -0,0 +1,11 @@ +import { IdName, Institution } from '@osf/shared/models'; + +export interface NodeDetailsModel { + id: string; + title: string; + description: string; + isPublic: boolean; + region: IdName; + affiliatedInstitutions: Institution[]; + lastFetched: number; +} diff --git a/src/app/features/project/settings/models/project-details-json-api.model.ts b/src/app/features/project/settings/models/project-details-json-api.model.ts new file mode 100644 index 000000000..bbfff660f --- /dev/null +++ b/src/app/features/project/settings/models/project-details-json-api.model.ts @@ -0,0 +1,19 @@ +import { InstitutionsJsonApiResponse, NodeData, ResponseDataJsonApi } from '@osf/shared/models'; + +export type NodeResponseJsonApi = ResponseDataJsonApi; + +export interface NodeDataJsonApi extends NodeData { + embeds: NodeEmbedsJsonApi; +} + +interface NodeEmbedsJsonApi { + region: { + data: { + id: string; + attributes: { + name: string; + }; + }; + }; + affiliated_institutions: InstitutionsJsonApiResponse; +} diff --git a/src/app/features/project/settings/models/redirect-link-data.model.ts b/src/app/features/project/settings/models/redirect-link-data.model.ts new file mode 100644 index 000000000..f85e7b63b --- /dev/null +++ b/src/app/features/project/settings/models/redirect-link-data.model.ts @@ -0,0 +1,5 @@ +export interface RedirectLinkDataModel { + isEnabled: boolean; + url: string; + label: string; +} diff --git a/src/app/features/project/settings/models/redirect-link-form.model.ts b/src/app/features/project/settings/models/redirect-link-form.model.ts new file mode 100644 index 000000000..1e9deaa7e --- /dev/null +++ b/src/app/features/project/settings/models/redirect-link-form.model.ts @@ -0,0 +1,7 @@ +import { FormControl } from '@angular/forms'; + +export interface RedirectLinkForm { + isEnabled: FormControl; + url: FormControl; + label: FormControl; +} diff --git a/src/app/features/project/settings/models/redirect-url-data.model.ts b/src/app/features/project/settings/models/redirect-url-data.model.ts deleted file mode 100644 index 10ba37999..000000000 --- a/src/app/features/project/settings/models/redirect-url-data.model.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface RedirectUrlDataModel { - url: string; - label: string; -} diff --git a/src/app/features/project/settings/pipes/notification-description.pipe.ts b/src/app/features/project/settings/pipes/notification-description.pipe.ts index 48e707f84..f57499a6c 100644 --- a/src/app/features/project/settings/pipes/notification-description.pipe.ts +++ b/src/app/features/project/settings/pipes/notification-description.pipe.ts @@ -9,7 +9,6 @@ export class NotificationDescriptionPipe implements PipeTransform { transform(event: SubscriptionEvent, frequency?: SubscriptionFrequency): string { if (!event || !frequency) return ''; - const key = `myProjects.settings.descriptions.${event}.${frequency}`; - return key; + return `myProjects.settings.descriptions.${event}.${frequency}`; } } diff --git a/src/app/features/project/settings/services/settings.service.ts b/src/app/features/project/settings/services/settings.service.ts index 07c040ebb..5a5cb8981 100644 --- a/src/app/features/project/settings/services/settings.service.ts +++ b/src/app/features/project/settings/services/settings.service.ts @@ -2,10 +2,24 @@ import { map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { SubscriptionFrequency } from '@osf/shared/enums'; +import { NotificationSubscriptionMapper } from '@osf/shared/mappers'; +import { + NotificationSubscription, + NotificationSubscriptionGetResponseJsonApi, + ResponseJsonApi, + UpdateNodeRequestModel, +} from '@osf/shared/models'; import { JsonApiService } from '@shared/services'; import { SettingsMapper } from '../mappers'; -import { ProjectSettingsData, ProjectSettingsModel, ProjectSettingsResponseModel } from '../models'; +import { + NodeDetailsModel, + NodeResponseJsonApi, + ProjectSettingsData, + ProjectSettingsModel, + ProjectSettingsResponseModel, +} from '../models'; import { environment } from 'src/environments/environment'; @@ -29,7 +43,55 @@ export class SettingsService { .pipe(map((response) => SettingsMapper.fromResponse(response, model.id))); } + getNotificationSubscriptions(nodeId?: string): Observable { + const params: Record = { + 'filter[id]': `${nodeId}_file_updated`, + }; + + return this.jsonApiService + .get>(`${this.baseUrl}/subscriptions/`, params) + .pipe( + map((responses) => responses.data.map((response) => NotificationSubscriptionMapper.fromGetResponse(response))) + ); + } + + updateSubscription(id: string, frequency: SubscriptionFrequency): Observable { + const request = NotificationSubscriptionMapper.toUpdateRequest(id, frequency, false); + + return this.jsonApiService + .patch(`${this.baseUrl}/subscriptions/${id}/`, request) + .pipe(map((response) => NotificationSubscriptionMapper.fromGetResponse(response))); + } + + getProjectById(projectId: string): Observable { + const params = { + 'embed[]': ['affiliated_institutions', 'region'], + }; + return this.jsonApiService + .get(`${this.baseUrl}/nodes/${projectId}/`, params) + .pipe(map((response) => SettingsMapper.fromNodeResponse(response.data))); + } + + updateProjectById(model: UpdateNodeRequestModel): Observable { + return this.jsonApiService + .patch(`${this.baseUrl}/nodes/${model?.data?.id}/`, model) + .pipe(map((response) => SettingsMapper.fromNodeResponse(response.data))); + } + deleteProject(projectId: string): Observable { return this.jsonApiService.delete(`${this.baseUrl}/nodes/${projectId}/`); } + + deleteInstitution(institutionId: string, projectId: string): Observable { + const data = { + data: [ + { + type: 'nodes', + id: projectId, + }, + ], + }; + + return this.jsonApiService.delete(`${this.baseUrl}/institutions/${institutionId}/relationships/nodes/`, data); + } } diff --git a/src/app/features/project/settings/settings.component.html b/src/app/features/project/settings/settings.component.html index bf82bc5dc..d9ef31199 100644 --- a/src/app/features/project/settings/settings.component.html +++ b/src/app/features/project/settings/settings.component.html @@ -1,87 +1,56 @@
-
- - - - - - - - - - - - - - - - - -

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

- -

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

- -
    -
  1. {{ 'myProjects.settings.institutionalLogos' | translate }}
  2. -
  3. {{ 'myProjects.settings.publicProjectsToBeDiscoverable' | translate }}
  4. -
  5. {{ 'myProjects.settings.singleSignInToTHeOSF' | translate }}
  6. -
  7. - - {{ 'myProjects.settings.faq' | translate }} - -
  8. -
- - @if (affiliations.length > 0) { -
- @for (affiliation of affiliations; track $index) { -
-
- cos-shield - -

{{ affiliation }}

-
- - -
- } -
- } -
-
+ @if (areProjectDetailsLoading() || areNotificationsLoading()) { + + } @else { +
+ + + + + + + + + + + + + + + +
+ }
diff --git a/src/app/features/project/settings/settings.component.ts b/src/app/features/project/settings/settings.component.ts index ce5691c53..c371935b5 100644 --- a/src/app/features/project/settings/settings.component.ts +++ b/src/app/features/project/settings/settings.component.ts @@ -2,46 +2,39 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; -import { Button } from 'primeng/button'; -import { Card } from 'primeng/card'; - import { map, of } from 'rxjs'; -import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, effect, inject, OnInit, signal } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; -import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { - GetNotificationSubscriptionsByNodeId, - NotificationSubscriptionSelectors, - UpdateNotificationSubscriptionForNodeId, -} from '@osf/features/settings/notifications/store'; -import { SubHeaderComponent } from '@osf/shared/components'; -import { ProjectFormControls, ResourceType, SubscriptionEvent, SubscriptionFrequency } from '@osf/shared/enums'; -import { CustomValidators } from '@osf/shared/helpers'; -import { UpdateNodeRequestModel, ViewOnlyLinkModel } from '@osf/shared/models'; +import { LoadingSpinnerComponent, SubHeaderComponent } from '@osf/shared/components'; +import { ResourceType, SubscriptionEvent, SubscriptionFrequency } from '@osf/shared/enums'; +import { Institution, UpdateNodeRequestModel, ViewOnlyLinkModel } from '@osf/shared/models'; import { CustomConfirmationService, LoaderService, ToastService } from '@osf/shared/services'; import { DeleteViewOnlyLink, FetchViewOnlyLinks, ViewOnlyLinkSelectors } from '@osf/shared/stores'; import { ProjectSettingNotificationsComponent, SettingsAccessRequestsCardComponent, - SettingsCommentingCardComponent, + SettingsProjectAffiliationComponent, SettingsProjectFormCardComponent, SettingsRedirectLinkComponent, SettingsStorageLocationCardComponent, SettingsViewOnlyLinksCardComponent, SettingsWikiCardComponent, } from './components'; -import { ProjectDetailsModel, ProjectSettingsAttributes, ProjectSettingsData } from './models'; +import { ProjectDetailsModel, ProjectSettingsAttributes, ProjectSettingsData, RedirectLinkDataModel } from './models'; import { + DeleteInstitution, DeleteProject, GetProjectDetails, + GetProjectNotificationSubscriptions, GetProjectSettings, SettingsSelectors, UpdateProjectDetails, + UpdateProjectNotificationSubscription, UpdateProjectSettings, } from './store'; @@ -52,17 +45,15 @@ import { SubHeaderComponent, FormsModule, ReactiveFormsModule, - Card, - Button, - NgOptimizedImage, SettingsProjectFormCardComponent, SettingsStorageLocationCardComponent, SettingsViewOnlyLinksCardComponent, SettingsAccessRequestsCardComponent, SettingsWikiCardComponent, - SettingsCommentingCardComponent, SettingsRedirectLinkComponent, + SettingsProjectAffiliationComponent, ProjectSettingNotificationsComponent, + LoadingSpinnerComponent, ], templateUrl: './settings.component.html', styleUrl: './settings.component.scss', @@ -77,38 +68,34 @@ export class SettingsComponent implements OnInit { readonly projectId = toSignal(this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined)); - protected settings = select(SettingsSelectors.getSettings); - protected notifications = select(NotificationSubscriptionSelectors.getNotificationSubscriptionsByNodeId); - protected projectDetails = select(SettingsSelectors.getProjectDetails); - protected viewOnlyLinks = select(ViewOnlyLinkSelectors.getViewOnlyLinks); - protected isViewOnlyLinksLoading = select(ViewOnlyLinkSelectors.isViewOnlyLinksLoading); + settings = select(SettingsSelectors.getSettings); + notifications = select(SettingsSelectors.getNotificationSubscriptions); + areNotificationsLoading = select(SettingsSelectors.areNotificationsLoading); + projectDetails = select(SettingsSelectors.getProjectDetails); + areProjectDetailsLoading = select(SettingsSelectors.areProjectDetailsLoading); + viewOnlyLinks = select(ViewOnlyLinkSelectors.getViewOnlyLinks); + isViewOnlyLinksLoading = select(ViewOnlyLinkSelectors.isViewOnlyLinksLoading); - protected actions = createDispatchMap({ + actions = createDispatchMap({ getSettings: GetProjectSettings, - getNotifications: GetNotificationSubscriptionsByNodeId, + getNotifications: GetProjectNotificationSubscriptions, getProjectDetails: GetProjectDetails, getViewOnlyLinks: FetchViewOnlyLinks, updateProjectDetails: UpdateProjectDetails, updateProjectSettings: UpdateProjectSettings, - updateNotificationSubscriptionForNodeId: UpdateNotificationSubscriptionForNodeId, + updateNotificationSubscription: UpdateProjectNotificationSubscription, deleteViewOnlyLink: DeleteViewOnlyLink, deleteProject: DeleteProject, + deleteInstitution: DeleteInstitution, }); - projectForm = new FormGroup({ - [ProjectFormControls.Title]: new FormControl('', CustomValidators.requiredTrimmed()), - [ProjectFormControls.Description]: new FormControl(''), - }); - - redirectUrlData = signal<{ url: string; label: string }>({ url: '', label: '' }); + redirectUrlData = signal({ isEnabled: false, url: '', label: '' }); accessRequest = signal(false); wikiEnabled = signal(false); anyoneCanEditWiki = signal(false); anyoneCanComment = signal(false); title = signal(''); - affiliations = []; - constructor() { this.setupEffects(); } @@ -124,7 +111,7 @@ export class SettingsComponent implements OnInit { } submitForm({ title, description }: ProjectDetailsModel): void { - const current = this.projectDetails().attributes; + const current = this.projectDetails(); if (title === current.title && description === current.description) return; @@ -159,22 +146,17 @@ export class SettingsComponent implements OnInit { this.syncSettingsChanges('anyone_can_edit_wiki', newValue); } - onAnyoneCanCommentRequestChange(newValue: boolean): void { - this.anyoneCanComment.set(newValue); - this.syncSettingsChanges('anyone_can_comment', newValue); - } - - onRedirectUrlDataRequestChange(data: { url: string; label: string }): void { + onRedirectUrlDataRequestChange(data: RedirectLinkDataModel): void { this.redirectUrlData.set(data); this.syncSettingsChanges('redirectUrl', data); } onNotificationRequestChange(data: { event: SubscriptionEvent; frequency: SubscriptionFrequency }): void { - const id = `${'n5str'}_${data.event}`; + const id = `${this.projectId()}_${data.event}`; const frequency = data.frequency; this.loaderService.show(); - this.actions.updateNotificationSubscriptionForNodeId({ id, frequency }).subscribe(() => { + this.actions.updateNotificationSubscription({ id, frequency }).subscribe(() => { this.toastService.showSuccess('myProjects.settings.updateProjectSettingsMessage'); this.loaderService.hide(); }); @@ -197,7 +179,7 @@ export class SettingsComponent implements OnInit { deleteProject(): void { this.customConfirmationService.confirmDelete({ headerKey: 'project.deleteProject.title', - messageParams: { name: this.projectDetails().attributes.title }, + messageParams: { name: this.projectDetails().title }, messageKey: 'project.deleteProject.message', onConfirm: () => { this.loaderService.show(); @@ -210,7 +192,23 @@ export class SettingsComponent implements OnInit { }); } - private syncSettingsChanges(changedField: string, value: boolean | { url: string; label: string }): void { + removeAffiliation(affiliation: Institution): void { + this.customConfirmationService.confirmDelete({ + headerKey: 'project.deleteInstitution.title', + messageParams: { name: affiliation.name }, + messageKey: 'project.deleteInstitution.message', + onConfirm: () => { + this.loaderService.show(); + this.actions.deleteInstitution(affiliation.id, this.projectId()).subscribe(() => { + this.loaderService.hide(); + this.toastService.showSuccess('project.deleteInstitution.success'); + this.actions.getProjectDetails(this.projectId()); + }); + }, + }); + } + + private syncSettingsChanges(changedField: string, value: boolean | RedirectLinkDataModel): void { const payload: Partial = {}; switch (changedField) { @@ -223,9 +221,9 @@ export class SettingsComponent implements OnInit { break; case 'redirectUrl': if (typeof value === 'object') { - payload['redirect_link_enabled'] = true; - payload['redirect_link_url'] = value.url ?? null; - payload['redirect_link_label'] = value.label ?? null; + payload['redirect_link_enabled'] = value.isEnabled; + payload['redirect_link_url'] = value.isEnabled ? value.url : undefined; + payload['redirect_link_label'] = value.isEnabled ? value.label : undefined; } break; } @@ -253,7 +251,9 @@ export class SettingsComponent implements OnInit { this.wikiEnabled.set(settings.attributes.wikiEnabled); this.anyoneCanEditWiki.set(settings.attributes.anyoneCanEditWiki); this.anyoneCanComment.set(settings.attributes.anyoneCanComment); + this.redirectUrlData.set({ + isEnabled: settings.attributes.redirectLinkEnabled, url: settings.attributes.redirectLinkUrl, label: settings.attributes.redirectLinkLabel, }); @@ -262,12 +262,9 @@ export class SettingsComponent implements OnInit { effect(() => { const project = this.projectDetails(); - if (project?.attributes) { - this.projectForm.patchValue({ - [ProjectFormControls.Title]: project.attributes.title, - [ProjectFormControls.Description]: project.attributes.description, - }); - this.title.set(project.attributes.title); + + if (project) { + this.title.set(project.title); } }); } diff --git a/src/app/features/project/settings/store/settings.actions.ts b/src/app/features/project/settings/store/settings.actions.ts index a8749e66c..ae0a30e0b 100644 --- a/src/app/features/project/settings/store/settings.actions.ts +++ b/src/app/features/project/settings/store/settings.actions.ts @@ -1,9 +1,10 @@ +import { SubscriptionFrequency } from '@osf/shared/enums'; import { UpdateNodeRequestModel } from '@shared/models'; import { ProjectSettingsData } from '../models'; export class GetProjectSettings { - static readonly type = '[Settings] Get Project Settings'; + static readonly type = '[Project Settings] Get Project Settings'; constructor(public projectId: string) {} } @@ -15,19 +16,40 @@ export class GetProjectDetails { } export class UpdateProjectSettings { - static readonly type = '[Settings] Update Project Settings'; + static readonly type = '[Project Settings] Update Project Settings'; constructor(public payload: ProjectSettingsData) {} } export class UpdateProjectDetails { - static readonly type = '[Settings] Update Project Details'; + static readonly type = '[Project Settings] Update Project Details'; constructor(public payload: UpdateNodeRequestModel) {} } +export class GetProjectNotificationSubscriptions { + static readonly type = '[Project Settings] Get Project Notification Subscriptions'; + + constructor(public nodeId: string) {} +} + +export class UpdateProjectNotificationSubscription { + static readonly type = '[Project Settings] Update Project Notification Subscription'; + + constructor(public payload: { id: string; frequency: SubscriptionFrequency }) {} +} + export class DeleteProject { - static readonly type = '[Settings] Delete Project'; + static readonly type = '[Project Settings] Delete Project'; constructor(public projectId: string) {} } + +export class DeleteInstitution { + static readonly type = '[Project Settings] Delete Institution'; + + constructor( + public institutionId: string, + 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 index 8137b3961..691010265 100644 --- a/src/app/features/project/settings/store/settings.model.ts +++ b/src/app/features/project/settings/store/settings.model.ts @@ -1,10 +1,11 @@ -import { AsyncStateModel, NodeData } from '@osf/shared/models'; +import { AsyncStateModel, NotificationSubscription } from '@osf/shared/models'; -import { ProjectSettingsModel } from '../models'; +import { NodeDetailsModel, ProjectSettingsModel } from '../models'; export interface SettingsStateModel { settings: AsyncStateModel; - projectDetails: AsyncStateModel; + projectDetails: AsyncStateModel; + notifications: AsyncStateModel; } export const SETTINGS_STATE_DEFAULTS: SettingsStateModel = { @@ -14,8 +15,13 @@ export const SETTINGS_STATE_DEFAULTS: SettingsStateModel = { error: null, }, projectDetails: { - data: {} as NodeData, + data: {} as NodeDetailsModel, isLoading: false, error: null, }, + notifications: { + data: [], + isLoading: false, + error: '', + }, }; diff --git a/src/app/features/project/settings/store/settings.selectors.ts b/src/app/features/project/settings/store/settings.selectors.ts index de6a8ec5e..4a46ce241 100644 --- a/src/app/features/project/settings/store/settings.selectors.ts +++ b/src/app/features/project/settings/store/settings.selectors.ts @@ -1,5 +1,7 @@ import { Selector } from '@ngxs/store'; +import { NotificationSubscription } from '@osf/shared/models'; + import { SettingsStateModel } from './settings.model'; import { SettingsState } from './settings.state'; @@ -13,4 +15,19 @@ export class SettingsSelectors { static getProjectDetails(state: SettingsStateModel) { return state.projectDetails.data; } + + @Selector([SettingsState]) + static areProjectDetailsLoading(state: SettingsStateModel) { + return state.projectDetails.isLoading; + } + + @Selector([SettingsState]) + static getNotificationSubscriptions(state: SettingsStateModel): NotificationSubscription[] { + return state.notifications.data; + } + + @Selector([SettingsState]) + static areNotificationsLoading(state: SettingsStateModel): boolean { + return state.notifications.isLoading; + } } diff --git a/src/app/features/project/settings/store/settings.state.ts b/src/app/features/project/settings/store/settings.state.ts index 218738d60..4e3698001 100644 --- a/src/app/features/project/settings/store/settings.state.ts +++ b/src/app/features/project/settings/store/settings.state.ts @@ -1,21 +1,24 @@ import { Action, State, StateContext } from '@ngxs/store'; +import { patch, updateItem } from '@ngxs/store/operators'; -import { map, of } from 'rxjs'; +import { of } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; import { handleSectionError } from '@osf/shared/helpers'; -import { NodeData } from '@osf/shared/models'; -import { MyResourcesService } from '@osf/shared/services'; +import { NotificationSubscription } from '@osf/shared/models'; import { SettingsService } from '../services'; import { + DeleteInstitution, DeleteProject, GetProjectDetails, + GetProjectNotificationSubscriptions, GetProjectSettings, UpdateProjectDetails, + UpdateProjectNotificationSubscription, UpdateProjectSettings, } from './settings.actions'; import { SETTINGS_STATE_DEFAULTS, SettingsStateModel } from './settings.model'; @@ -27,8 +30,6 @@ import { SETTINGS_STATE_DEFAULTS, SettingsStateModel } from './settings.model'; @Injectable() export class SettingsState { private readonly settingsService = inject(SettingsService); - private readonly myProjectService = inject(MyResourcesService); - private readonly REFRESH_INTERVAL = 5 * 60 * 1000; private shouldRefresh(lastFetched?: number): boolean { @@ -70,23 +71,10 @@ export class SettingsState { @Action(GetProjectDetails) getProjectDetails(ctx: StateContext, action: GetProjectDetails) { const state = ctx.getState(); - const cached = state.projectDetails.data; - const shouldRefresh = this.shouldRefresh(cached.lastFetched); - - if (cached.id === action.projectId && !shouldRefresh) { - return of(cached).pipe( - tap(() => - ctx.patchState({ - projectDetails: { ...state.projectDetails, isLoading: false, error: null }, - }) - ) - ); - } ctx.patchState({ projectDetails: { ...state.projectDetails, isLoading: true, error: null } }); - return this.myProjectService.getProjectById(action.projectId).pipe( - map((response) => response?.data as NodeData), + return this.settingsService.getProjectById(action.projectId).pipe( tap((details) => { const updatedDetails = { ...details, @@ -107,7 +95,7 @@ export class SettingsState { @Action(UpdateProjectDetails) updateProjectDetails(ctx: StateContext, action: UpdateProjectDetails) { - return this.myProjectService.updateProjectById(action.payload).pipe( + return this.settingsService.updateProjectById(action.payload).pipe( tap((updatedProject) => { ctx.patchState({ projectDetails: { @@ -152,8 +140,54 @@ export class SettingsState { ); } + @Action(GetProjectNotificationSubscriptions) + getNotificationSubscriptionsByNodeId( + ctx: StateContext, + action: GetProjectNotificationSubscriptions + ) { + return this.settingsService.getNotificationSubscriptions(action.nodeId).pipe( + tap((notificationSubscriptions) => { + ctx.setState( + patch({ + notifications: patch({ + data: notificationSubscriptions, + isLoading: false, + }), + }) + ); + }), + catchError((error) => handleSectionError(ctx, 'notifications', error)) + ); + } + + @Action(UpdateProjectNotificationSubscription) + updateNotificationSubscriptionForNodeId( + ctx: StateContext, + action: UpdateProjectNotificationSubscription + ) { + return this.settingsService.updateSubscription(action.payload.id, action.payload.frequency).pipe( + tap((updatedSubscription) => { + ctx.setState( + patch({ + notifications: patch({ + data: updateItem((app) => app.id === action.payload.id, updatedSubscription), + error: null, + isLoading: false, + }), + }) + ); + }), + catchError((error) => handleSectionError(ctx, 'notifications', error)) + ); + } + @Action(DeleteProject) deleteProject(ctx: StateContext, action: DeleteProject) { return this.settingsService.deleteProject(action.projectId); } + + @Action(DeleteInstitution) + deleteInstitution(ctx: StateContext, action: DeleteInstitution) { + return this.settingsService.deleteInstitution(action.institutionId, action.projectId); + } } diff --git a/src/app/features/settings/notifications/mappers/index.ts b/src/app/features/settings/notifications/mappers/index.ts deleted file mode 100644 index a10b45bdd..000000000 --- a/src/app/features/settings/notifications/mappers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './notification-subscription.mapper'; diff --git a/src/app/features/settings/notifications/models/index.ts b/src/app/features/settings/notifications/models/index.ts index a62c932ec..f306b2228 100644 --- a/src/app/features/settings/notifications/models/index.ts +++ b/src/app/features/settings/notifications/models/index.ts @@ -1,4 +1,2 @@ -export * from './notification-subscription.models'; -export * from './notification-subscription-json-api.models'; export * from './notifications-form.model'; export * from './subscription-event.model'; diff --git a/src/app/features/settings/notifications/services/notification-subscription.service.ts b/src/app/features/settings/notifications/services/notification-subscription.service.ts index b92219ea0..576e1a775 100644 --- a/src/app/features/settings/notifications/services/notification-subscription.service.ts +++ b/src/app/features/settings/notifications/services/notification-subscription.service.ts @@ -2,12 +2,14 @@ import { map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { JsonApiResponse } from '@osf/shared/models'; +import { SubscriptionFrequency } from '@osf/shared/enums'; +import { NotificationSubscriptionMapper } from '@osf/shared/mappers'; +import { + JsonApiResponse, + NotificationSubscription, + NotificationSubscriptionGetResponseJsonApi, +} from '@osf/shared/models'; import { JsonApiService } from '@osf/shared/services'; -import { SubscriptionFrequency } from '@shared/enums'; - -import { NotificationSubscriptionMapper } from '../mappers'; -import { NotificationSubscription, NotificationSubscriptionGetResponseJsonApi } from '../models'; import { environment } from 'src/environments/environment'; @@ -18,18 +20,10 @@ export class NotificationSubscriptionService { private readonly jsonApiService = inject(JsonApiService); private readonly baseUrl = `${environment.apiUrl}/subscriptions/`; - getAllGlobalNotificationSubscriptions(nodeId?: string): Observable { - let params: Record; - if (nodeId) { - params = { - 'filter[id]': `${nodeId}_file_updated,${nodeId}_comments`, - }; - } else { - params = { - 'filter[event_name]': - 'global_reviews,global_comments,global_comment_replies,global_file_updated,global_mentions', - }; - } + getAllGlobalNotificationSubscriptions(): Observable { + const params: Record = { + 'filter[event_name]': 'global_reviews,global_comments,global_comment_replies,global_file_updated,global_mentions', + }; return this.jsonApiService .get>(this.baseUrl, params) @@ -38,15 +32,11 @@ export class NotificationSubscriptionService { ); } - updateSubscription( - id: string, - frequency: SubscriptionFrequency, - isNodeSubscription?: boolean - ): Observable { - const request = NotificationSubscriptionMapper.toUpdateRequest(id, frequency, isNodeSubscription); + updateSubscription(id: string, frequency: SubscriptionFrequency): Observable { + const request = NotificationSubscriptionMapper.toUpdateRequest(id, frequency); return this.jsonApiService - .patch(`${this.baseUrl}/${id}/`, request) + .patch(`${this.baseUrl}${id}/`, request) .pipe(map((response) => NotificationSubscriptionMapper.fromGetResponse(response))); } } diff --git a/src/app/features/settings/notifications/store/notification-subscription.actions.ts b/src/app/features/settings/notifications/store/notification-subscription.actions.ts index 22380163a..dd987411f 100644 --- a/src/app/features/settings/notifications/store/notification-subscription.actions.ts +++ b/src/app/features/settings/notifications/store/notification-subscription.actions.ts @@ -4,20 +4,8 @@ export class GetAllGlobalNotificationSubscriptions { static readonly type = '[Notification Subscriptions] Get All Global'; } -export class GetNotificationSubscriptionsByNodeId { - static readonly type = '[Notification Subscriptions] Get By Node Id'; - - constructor(public nodeId: string) {} -} - export class UpdateNotificationSubscription { static readonly type = '[Notification Subscriptions] Update'; constructor(public payload: { id: string; frequency: SubscriptionFrequency }) {} } - -export class UpdateNotificationSubscriptionForNodeId { - static readonly type = '[Notification Subscriptions] Update For Node'; - - constructor(public payload: { id: string; frequency: SubscriptionFrequency }) {} -} diff --git a/src/app/features/settings/notifications/store/notification-subscription.model.ts b/src/app/features/settings/notifications/store/notification-subscription.model.ts index 129edd72f..b76894743 100644 --- a/src/app/features/settings/notifications/store/notification-subscription.model.ts +++ b/src/app/features/settings/notifications/store/notification-subscription.model.ts @@ -1,10 +1,7 @@ -import { AsyncStateModel } from '@osf/shared/models'; - -import { NotificationSubscription } from '../models'; +import { AsyncStateModel, NotificationSubscription } from '@osf/shared/models'; export interface NotificationSubscriptionStateModel { notificationSubscriptions: AsyncStateModel; - notificationSubscriptionsByNodeId: AsyncStateModel; } export const NOTIFICATION_SUBSCRIPTION_STATE_DEFAULTS: NotificationSubscriptionStateModel = { @@ -13,9 +10,4 @@ export const NOTIFICATION_SUBSCRIPTION_STATE_DEFAULTS: NotificationSubscriptionS isLoading: false, error: '', }, - notificationSubscriptionsByNodeId: { - data: [], - isLoading: false, - error: '', - }, }; diff --git a/src/app/features/settings/notifications/store/notification-subscription.selectors.ts b/src/app/features/settings/notifications/store/notification-subscription.selectors.ts index 629d72796..caf6c19e5 100644 --- a/src/app/features/settings/notifications/store/notification-subscription.selectors.ts +++ b/src/app/features/settings/notifications/store/notification-subscription.selectors.ts @@ -1,6 +1,6 @@ import { Selector } from '@ngxs/store'; -import { NotificationSubscription } from '../models'; +import { NotificationSubscription } from '@osf/shared/models'; import { NotificationSubscriptionStateModel } from './notification-subscription.model'; import { NotificationSubscriptionState } from './notification-subscription.state'; @@ -11,11 +11,6 @@ export class NotificationSubscriptionSelectors { return state.notificationSubscriptions.data; } - @Selector([NotificationSubscriptionState]) - static getNotificationSubscriptionsByNodeId(state: NotificationSubscriptionStateModel): NotificationSubscription[] { - return state.notificationSubscriptionsByNodeId.data; - } - @Selector([NotificationSubscriptionState]) static isLoading(state: NotificationSubscriptionStateModel): boolean { return state.notificationSubscriptions.isLoading; diff --git a/src/app/features/settings/notifications/store/notification-subscription.state.ts b/src/app/features/settings/notifications/store/notification-subscription.state.ts index 41deb5474..daad47f5c 100644 --- a/src/app/features/settings/notifications/store/notification-subscription.state.ts +++ b/src/app/features/settings/notifications/store/notification-subscription.state.ts @@ -6,15 +6,13 @@ import { catchError, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { handleSectionError } from '@osf/shared/helpers'; +import { NotificationSubscription } from '@osf/shared/models'; -import { NotificationSubscription } from '../models'; import { NotificationSubscriptionService } from '../services'; import { GetAllGlobalNotificationSubscriptions, - GetNotificationSubscriptionsByNodeId, UpdateNotificationSubscription, - UpdateNotificationSubscriptionForNodeId, } from './notification-subscription.actions'; import { NOTIFICATION_SUBSCRIPTION_STATE_DEFAULTS, @@ -48,26 +46,6 @@ export class NotificationSubscriptionState { ); } - @Action(GetNotificationSubscriptionsByNodeId) - getNotificationSubscriptionsByNodeId( - ctx: StateContext, - action: GetNotificationSubscriptionsByNodeId - ) { - return this.notificationSubscriptionService.getAllGlobalNotificationSubscriptions(action.nodeId).pipe( - tap((notificationSubscriptions) => { - ctx.setState( - patch({ - notificationSubscriptionsByNodeId: patch({ - data: notificationSubscriptions, - isLoading: false, - }), - }) - ); - }), - catchError((error) => handleSectionError(ctx, 'notificationSubscriptionsByNodeId', error)) - ); - } - @Action(UpdateNotificationSubscription) updateNotificationSubscription( ctx: StateContext, @@ -88,27 +66,4 @@ export class NotificationSubscriptionState { catchError((error) => handleSectionError(ctx, 'notificationSubscriptions', error)) ); } - - @Action(UpdateNotificationSubscriptionForNodeId) - updateNotificationSubscriptionForNodeId( - ctx: StateContext, - action: UpdateNotificationSubscription - ) { - return this.notificationSubscriptionService - .updateSubscription(action.payload.id, action.payload.frequency, true) - .pipe( - tap((updatedSubscription) => { - ctx.setState( - patch({ - notificationSubscriptionsByNodeId: patch({ - data: updateItem((app) => app.id === action.payload.id, updatedSubscription), - error: null, - isLoading: false, - }), - }) - ); - }), - catchError((error) => handleSectionError(ctx, 'notificationSubscriptionsByNodeId', error)) - ); - } } diff --git a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.ts b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.ts index 32111c9e1..cf2a5d7d1 100644 --- a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.ts +++ b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.ts @@ -45,13 +45,13 @@ export class TokenAddEditFormComponent implements OnInit { isEditMode = input(false); initialValues = input(null); - inputLimits = InputLimits.fullName; + readonly inputLimits = InputLimits.fullName; - protected readonly tokenId = toSignal(this.route.params.pipe(map((params) => params['id']))); - protected readonly dialogRef = inject(DynamicDialogRef); - protected readonly TokenFormControls = TokenFormControls; - protected readonly tokenScopes = select(TokensSelectors.getScopes); - protected readonly isLoading = select(TokensSelectors.isTokensLoading); + readonly tokenId = toSignal(this.route.params.pipe(map((params) => params['id']))); + readonly dialogRef = inject(DynamicDialogRef); + readonly TokenFormControls = TokenFormControls; + readonly tokenScopes = select(TokensSelectors.getScopes); + readonly isLoading = select(TokensSelectors.isTokensLoading); readonly tokenForm: TokenForm = new FormGroup({ [TokenFormControls.TokenName]: new FormControl('', { @@ -98,14 +98,14 @@ export class TokenAddEditFormComponent implements OnInit { const tokens = this.store.selectSignal(TokensSelectors.getTokens); const newToken = tokens()[0]; this.dialogRef.close(); - this.showTokenCreatedDialog(newToken.name, newToken.tokenId); + this.showTokenCreatedDialog(newToken.name, newToken.id); }, }); } else { this.actions.updateToken(this.tokenId(), tokenName, scopes).subscribe({ complete: () => { - this.toastService.showSuccess('settings.tokens.toastMessage.successEdit'); this.router.navigate(['settings/tokens']); + this.toastService.showSuccess('settings.tokens.toastMessage.successEdit'); }, }); } diff --git a/src/app/features/settings/tokens/components/token-created-dialog/token-created-dialog.component.ts b/src/app/features/settings/tokens/components/token-created-dialog/token-created-dialog.component.ts index 954116dae..e377692b3 100644 --- a/src/app/features/settings/tokens/components/token-created-dialog/token-created-dialog.component.ts +++ b/src/app/features/settings/tokens/components/token-created-dialog/token-created-dialog.component.ts @@ -26,8 +26,8 @@ import { CopyButtonComponent } from '@shared/components'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class TokenCreatedDialogComponent { - protected readonly dialogRef = inject(DynamicDialogRef); - protected readonly config = inject(DynamicDialogConfig); + readonly dialogRef = inject(DynamicDialogRef); + readonly config = inject(DynamicDialogConfig); readonly tokenInput = viewChild>('tokenInput'); readonly tokenName = input(this.config.data?.tokenName ?? ''); diff --git a/src/app/features/settings/tokens/mappers/token.mapper.ts b/src/app/features/settings/tokens/mappers/token.mapper.ts index b3f0ae3ff..92451a3cf 100644 --- a/src/app/features/settings/tokens/mappers/token.mapper.ts +++ b/src/app/features/settings/tokens/mappers/token.mapper.ts @@ -1,4 +1,4 @@ -import { TokenCreateRequestJsonApi, TokenCreateResponseJsonApi, TokenGetResponseJsonApi, TokenModel } from '../models'; +import { TokenCreateRequestJsonApi, TokenGetResponseJsonApi, TokenModel } from '../models'; export class TokenMapper { static toRequest(name: string, scopes: string[]): TokenCreateRequestJsonApi { @@ -13,23 +13,11 @@ export class TokenMapper { }; } - static fromCreateResponse(response: TokenCreateResponseJsonApi): TokenModel { - return { - id: response.id, - name: response.attributes.name, - tokenId: response.attributes.token_id, - scopes: response.attributes.scopes.split(' '), - ownerId: response.attributes.owner, - }; - } - static fromGetResponse(response: TokenGetResponseJsonApi): TokenModel { return { id: response.id, name: response.attributes.name, - tokenId: response.id, - scopes: response.attributes.scopes.split(' '), - ownerId: response.attributes.owner, + scopes: response.embeds.scopes.data.map((item) => item.id), }; } } diff --git a/src/app/features/settings/tokens/models/add-edit-token.model.ts b/src/app/features/settings/tokens/models/add-edit-token.model.ts deleted file mode 100644 index f7cfdac4e..000000000 --- a/src/app/features/settings/tokens/models/add-edit-token.model.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface AddEditTokenModel { - name: string; - scopes: string[]; -} diff --git a/src/app/features/settings/tokens/models/scope-json-api.model.ts b/src/app/features/settings/tokens/models/scope-json-api.model.ts index 443b1a22f..8933a7ce3 100644 --- a/src/app/features/settings/tokens/models/scope-json-api.model.ts +++ b/src/app/features/settings/tokens/models/scope-json-api.model.ts @@ -4,7 +4,4 @@ export interface ScopeJsonApi { attributes: { description: string; }; - links: { - self: string; - }; } diff --git a/src/app/features/settings/tokens/models/token-json-api.model.ts b/src/app/features/settings/tokens/models/token-json-api.model.ts index 657116c8b..66bc0ac31 100644 --- a/src/app/features/settings/tokens/models/token-json-api.model.ts +++ b/src/app/features/settings/tokens/models/token-json-api.model.ts @@ -8,23 +8,22 @@ export interface TokenCreateRequestJsonApi { }; } -export interface TokenCreateResponseJsonApi { +export interface TokenGetResponseJsonApi { id: string; - type: 'tokens'; - attributes: { - name: string; - token_id: string; - scopes: string; - owner: string; + attributes: TokenAttributesJsonApi; + embeds: TokenEmbedsJsonApi; +} + +interface TokenAttributesJsonApi { + name: string; +} + +interface TokenEmbedsJsonApi { + scopes: { + data: TokenEmbedsDataItemJsonApi[]; }; } -export interface TokenGetResponseJsonApi { +interface TokenEmbedsDataItemJsonApi { id: string; - type: 'tokens'; - attributes: { - name: string; - scopes: string; - owner: string; - }; } diff --git a/src/app/features/settings/tokens/models/token.model.ts b/src/app/features/settings/tokens/models/token.model.ts index f4b5a3748..735b29bcd 100644 --- a/src/app/features/settings/tokens/models/token.model.ts +++ b/src/app/features/settings/tokens/models/token.model.ts @@ -1,7 +1,5 @@ export interface TokenModel { id: string; name: string; - tokenId: string; scopes: string[]; - ownerId: string; } diff --git a/src/app/features/settings/tokens/services/tokens.service.ts b/src/app/features/settings/tokens/services/tokens.service.ts index c29cb06f4..2735c5bad 100644 --- a/src/app/features/settings/tokens/services/tokens.service.ts +++ b/src/app/features/settings/tokens/services/tokens.service.ts @@ -7,7 +7,7 @@ import { JsonApiResponse } from '@osf/shared/models'; import { JsonApiService } from '@osf/shared/services'; import { ScopeMapper, TokenMapper } from '../mappers'; -import { ScopeJsonApi, ScopeModel, TokenCreateResponseJsonApi, TokenGetResponseJsonApi, TokenModel } from '../models'; +import { ScopeJsonApi, ScopeModel, TokenGetResponseJsonApi, TokenModel } from '../models'; import { environment } from 'src/environments/environment'; @@ -39,16 +39,16 @@ export class TokensService { const request = TokenMapper.toRequest(name, scopes); return this.jsonApiService - .post>(environment.apiUrl + '/tokens/', request) - .pipe(map((response) => TokenMapper.fromCreateResponse(response.data))); + .post>(environment.apiUrl + '/tokens/', request) + .pipe(map((response) => TokenMapper.fromGetResponse(response.data))); } updateToken(tokenId: string, name: string, scopes: string[]): Observable { const request = TokenMapper.toRequest(name, scopes); return this.jsonApiService - .patch(`${environment.apiUrl}/tokens/${tokenId}/`, request) - .pipe(map((response) => TokenMapper.fromCreateResponse(response))); + .patch(`${environment.apiUrl}/tokens/${tokenId}/`, request) + .pipe(map((response) => TokenMapper.fromGetResponse(response))); } deleteToken(tokenId: string): Observable { diff --git a/src/app/features/settings/tokens/store/tokens.models.ts b/src/app/features/settings/tokens/store/tokens.models.ts index 3f5a6a8b5..fcbd2c285 100644 --- a/src/app/features/settings/tokens/store/tokens.models.ts +++ b/src/app/features/settings/tokens/store/tokens.models.ts @@ -6,3 +6,16 @@ export interface TokensStateModel { scopes: AsyncStateModel; tokens: AsyncStateModel; } + +export const TOKENS_STATE_DEFAULTS: TokensStateModel = { + scopes: { + data: [], + isLoading: false, + error: null, + }, + tokens: { + data: [], + isLoading: false, + error: null, + }, +}; diff --git a/src/app/features/settings/tokens/store/tokens.state.ts b/src/app/features/settings/tokens/store/tokens.state.ts index a955fcb41..2230aab44 100644 --- a/src/app/features/settings/tokens/store/tokens.state.ts +++ b/src/app/features/settings/tokens/store/tokens.state.ts @@ -1,33 +1,24 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { catchError, of, tap, throwError } from 'rxjs'; +import { catchError, of, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { handleSectionError } from '@osf/shared/helpers'; + import { TokenModel } from '../models'; import { TokensService } from '../services'; import { CreateToken, DeleteToken, GetScopes, GetTokenById, GetTokens, UpdateToken } from './tokens.actions'; -import { TokensStateModel } from './tokens.models'; +import { TOKENS_STATE_DEFAULTS, TokensStateModel } from './tokens.models'; @State({ name: 'tokens', - defaults: { - scopes: { - data: [], - isLoading: false, - error: null, - }, - tokens: { - data: [], - isLoading: false, - error: null, - }, - }, + defaults: TOKENS_STATE_DEFAULTS, }) @Injectable() export class TokensState { - tokensService = inject(TokensService); + private readonly tokensService = inject(TokensService); @Action(GetScopes) getScopes(ctx: StateContext) { @@ -39,7 +30,7 @@ export class TokensState { tap((scopes) => { ctx.patchState({ scopes: { data: scopes, isLoading: false, error: null } }); }), - catchError((error) => this.handleError(ctx, 'scopes', error)) + catchError((error) => handleSectionError(ctx, 'scopes', error)) ); } @@ -51,7 +42,7 @@ export class TokensState { tap((tokens) => { ctx.patchState({ tokens: { data: tokens, isLoading: false, error: null } }); }), - catchError((error) => this.handleError(ctx, 'tokens', error)) + catchError((error) => handleSectionError(ctx, 'tokens', error)) ); } @@ -71,7 +62,22 @@ export class TokensState { const updatedTokens = [...state.tokens.data, token]; ctx.patchState({ tokens: { data: updatedTokens, isLoading: false, error: null } }); }), - catchError((error) => this.handleError(ctx, 'tokens', error)) + catchError((error) => handleSectionError(ctx, 'tokens', error)) + ); + } + + @Action(CreateToken) + createToken(ctx: StateContext, action: CreateToken) { + const state = ctx.getState(); + ctx.patchState({ tokens: { ...state.tokens, isLoading: true, error: null } }); + + return this.tokensService.createToken(action.name, action.scopes).pipe( + tap((newToken) => { + const state = ctx.getState(); + const updatedTokens = [newToken, ...state.tokens.data]; + ctx.patchState({ tokens: { data: updatedTokens, isLoading: false, error: null } }); + }), + catchError((error) => handleSectionError(ctx, 'tokens', error)) ); } @@ -88,7 +94,7 @@ export class TokensState { ); ctx.patchState({ tokens: { data: updatedTokens, isLoading: false, error: null } }); }), - catchError((error) => this.handleError(ctx, 'tokens', error)) + catchError((error) => handleSectionError(ctx, 'tokens', error)) ); } @@ -103,36 +109,7 @@ export class TokensState { const updatedTokens = state.tokens.data.filter((token: TokenModel) => token.id !== action.tokenId); ctx.patchState({ tokens: { data: updatedTokens, isLoading: false, error: null } }); }), - catchError((error) => this.handleError(ctx, 'tokens', error)) + catchError((error) => handleSectionError(ctx, 'tokens', error)) ); } - - @Action(CreateToken) - createToken(ctx: StateContext, action: CreateToken) { - const state = ctx.getState(); - ctx.patchState({ tokens: { ...state.tokens, isLoading: true, error: null } }); - - return this.tokensService.createToken(action.name, action.scopes).pipe( - tap((newToken) => { - const state = ctx.getState(); - const updatedTokens = [newToken, ...state.tokens.data]; - ctx.patchState({ tokens: { data: updatedTokens, isLoading: false, error: null } }); - - return newToken; - }), - catchError((error) => this.handleError(ctx, 'tokens', error)) - ); - } - - private handleError(ctx: StateContext, key: keyof TokensStateModel, error: Error) { - const state = ctx.getState(); - ctx.patchState({ - [key]: { - ...state[key], - isLoading: false, - error: error.message, - }, - }); - return throwError(() => error); - } } diff --git a/src/app/features/settings/tokens/tokens.component.ts b/src/app/features/settings/tokens/tokens.component.ts index f2057e31d..194bfd636 100644 --- a/src/app/features/settings/tokens/tokens.component.ts +++ b/src/app/features/settings/tokens/tokens.component.ts @@ -30,11 +30,10 @@ export class TokensComponent implements OnInit { private readonly translateService = inject(TranslateService); private readonly actions = createDispatchMap({ getScopes: GetScopes }); - protected readonly isSmall = toSignal(inject(IS_SMALL)); - protected readonly isBaseRoute = toSignal( - this.router.events.pipe(map(() => this.router.url === '/settings/tokens')), - { initialValue: this.router.url === '/settings/tokens' } - ); + readonly isSmall = toSignal(inject(IS_SMALL)); + readonly isBaseRoute = toSignal(this.router.events.pipe(map(() => this.router.url === '/settings/tokens')), { + initialValue: this.router.url === '/settings/tokens', + }); ngOnInit() { this.actions.getScopes(); diff --git a/src/app/shared/enums/subscriptions/index.ts b/src/app/shared/enums/subscriptions/index.ts index c76c1b642..64dbd8be9 100644 --- a/src/app/shared/enums/subscriptions/index.ts +++ b/src/app/shared/enums/subscriptions/index.ts @@ -1,3 +1,3 @@ -export * from '@shared/enums/subscriptions/subscription-event.enum'; -export * from '@shared/enums/subscriptions/subscription-frequency.enum'; -export * from '@shared/enums/subscriptions/subscription-type.enum'; +export * from './subscription-event.enum'; +export * from './subscription-frequency.enum'; +export * from './subscription-type.enum'; diff --git a/src/app/shared/mappers/index.ts b/src/app/shared/mappers/index.ts index 893f2d106..8f9023fad 100644 --- a/src/app/shared/mappers/index.ts +++ b/src/app/shared/mappers/index.ts @@ -8,6 +8,7 @@ export * from './files/files.mapper'; export * from './filters'; export * from './institutions'; export * from './licenses.mapper'; +export * from './notification-subscription.mapper'; export * from './registry'; export * from './resource-card'; export * from './resource-overview.mappers'; diff --git a/src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts b/src/app/shared/mappers/notification-subscription.mapper.ts similarity index 89% rename from src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts rename to src/app/shared/mappers/notification-subscription.mapper.ts index 387603e99..bd0bcc6c9 100644 --- a/src/app/features/settings/notifications/mappers/notification-subscription.mapper.ts +++ b/src/app/shared/mappers/notification-subscription.mapper.ts @@ -1,5 +1,4 @@ -import { SubscriptionEvent, SubscriptionFrequency, SubscriptionType } from '@shared/enums'; - +import { SubscriptionEvent, SubscriptionFrequency, SubscriptionType } from '../enums'; import { NotificationSubscription, NotificationSubscriptionGetResponseJsonApi, @@ -26,7 +25,7 @@ export class NotificationSubscriptionMapper { return { data: { - type: isNodeSubscription ? SubscriptionType.Node : SubscriptionType.Global, + type: SubscriptionType.Global, attributes: baseAttributes, ...(isNodeSubscription ? {} : { id }), }, diff --git a/src/app/shared/models/id-name.model.ts b/src/app/shared/models/common/id-name.model.ts similarity index 100% rename from src/app/shared/models/id-name.model.ts rename to src/app/shared/models/common/id-name.model.ts diff --git a/src/app/shared/models/common/id-type.model.ts b/src/app/shared/models/common/id-type.model.ts new file mode 100644 index 000000000..c01fe4dc5 --- /dev/null +++ b/src/app/shared/models/common/id-type.model.ts @@ -0,0 +1,4 @@ +export interface IdTypeModel { + id: string; + type: string; +} diff --git a/src/app/shared/models/common/index.ts b/src/app/shared/models/common/index.ts index 5595a8933..3a7369809 100644 --- a/src/app/shared/models/common/index.ts +++ b/src/app/shared/models/common/index.ts @@ -1 +1,3 @@ +export * from './id-name.model'; +export * from './id-type.model'; export * from './json-api.model'; diff --git a/src/app/shared/models/common/json-api.model.ts b/src/app/shared/models/common/json-api.model.ts index f1a7ff950..6a6b8fe01 100644 --- a/src/app/shared/models/common/json-api.model.ts +++ b/src/app/shared/models/common/json-api.model.ts @@ -13,6 +13,11 @@ export interface ResponseJsonApi { meta: MetaJsonApi; } +export interface ResponseDataJsonApi { + data: Data; + meta: MetaJsonApi; +} + export interface ApiData { id: string; attributes: Attributes; @@ -25,7 +30,7 @@ export interface ApiData { export interface MetaJsonApi { total: number; per_page: number; - version?: string; + version: string; } export interface PaginationLinksJsonApi { diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 74b2fb24e..dfa9a7433 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -17,7 +17,6 @@ export * from './filter-labels.model'; export * from './filters'; export * from './google-drive-folder.model'; export * from './guid-response-json-api.model'; -export * from './id-name.model'; export * from './institutions'; export * from './language-code.model'; export * from './license'; @@ -29,6 +28,7 @@ export * from './metadata-field.model'; export * from './my-resources'; export * from './nodes/create-project-form.model'; export * from './nodes/nodes-json-api.model'; +export * from './notifications'; export * from './paginated-data.model'; export * from './pagination-links.model'; export * from './profile-settings-update.model'; diff --git a/src/app/shared/models/nodes/nodes-json-api.model.ts b/src/app/shared/models/nodes/nodes-json-api.model.ts index e760308f5..7a87a5b8c 100644 --- a/src/app/shared/models/nodes/nodes-json-api.model.ts +++ b/src/app/shared/models/nodes/nodes-json-api.model.ts @@ -1,4 +1,48 @@ -export interface NodeAttributes { +export interface NodeData { + id: string; + type: 'nodes'; + attributes: NodeAttributes; + relationships: NodeRelationships; + links: NodeLinks; + lastFetched?: number; +} + +export interface NodeResponseModel { + data: NodeData; + meta: NodeMeta; +} + +export interface UpdateNodeRequestModel { + data: UpdateNodeData; +} + +export interface CreateProjectPayloadJsoApi { + data: { + type: 'nodes'; + attributes: { + title: string; + description?: string; + category: 'project'; + template_from?: string; + }; + relationships: { + region: { + data: { + type: 'regions'; + id: string; + }; + }; + affiliated_institutions?: { + data: { + type: 'institutions'; + id: string; + }[]; + }; + }; + }; +} + +interface NodeAttributes { title: string; description: string; category: string; @@ -21,7 +65,7 @@ export interface NodeAttributes { subjects: unknown[]; } -export interface RelationshipLinks { +interface RelationshipLinks { related: { href: string; meta: Record; @@ -32,7 +76,7 @@ export interface RelationshipLinks { }; } -export interface NodeRelationships { +interface NodeRelationships { children: { links: RelationshipLinks }; comments: { links: RelationshipLinks }; contributors: { links: RelationshipLinks }; @@ -82,69 +126,25 @@ export interface NodeRelationships { subjects_acceptable: { links: RelationshipLinks }; } -export interface NodeLinks { +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 { +interface NodeMeta { version: string; } -export interface NodeResponseModel { - data: NodeData; - meta: NodeMeta; -} - -export interface UpdateNodeAttributes { +interface UpdateNodeAttributes { description?: string; tags?: string[]; public?: boolean; title?: string; } -export interface UpdateNodeData { +interface UpdateNodeData { type: 'nodes'; id: string; attributes: UpdateNodeAttributes; } - -export interface UpdateNodeRequestModel { - data: UpdateNodeData; -} - -export interface CreateProjectPayloadJsoApi { - data: { - type: 'nodes'; - attributes: { - title: string; - description?: string; - category: 'project'; - template_from?: string; - }; - relationships: { - region: { - data: { - type: 'regions'; - id: string; - }; - }; - affiliated_institutions?: { - data: { - type: 'institutions'; - id: string; - }[]; - }; - }; - }; -} diff --git a/src/app/shared/models/notifications/index.ts b/src/app/shared/models/notifications/index.ts new file mode 100644 index 000000000..91dcdba7c --- /dev/null +++ b/src/app/shared/models/notifications/index.ts @@ -0,0 +1,2 @@ +export * from './notification-subscription.model'; +export * from './notification-subscription-json-api.model'; diff --git a/src/app/features/settings/notifications/models/notification-subscription-json-api.models.ts b/src/app/shared/models/notifications/notification-subscription-json-api.model.ts similarity index 87% rename from src/app/features/settings/notifications/models/notification-subscription-json-api.models.ts rename to src/app/shared/models/notifications/notification-subscription-json-api.model.ts index 5d7714c85..a3afd5695 100644 --- a/src/app/features/settings/notifications/models/notification-subscription-json-api.models.ts +++ b/src/app/shared/models/notifications/notification-subscription-json-api.model.ts @@ -1,4 +1,4 @@ -import { SubscriptionFrequency } from '@shared/enums'; +import { SubscriptionFrequency } from '@osf/shared/enums'; export interface NotificationSubscriptionGetResponseJsonApi { id: string; diff --git a/src/app/features/settings/notifications/models/notification-subscription.models.ts b/src/app/shared/models/notifications/notification-subscription.model.ts similarity index 61% rename from src/app/features/settings/notifications/models/notification-subscription.models.ts rename to src/app/shared/models/notifications/notification-subscription.model.ts index 9ad8d3701..7cbefa654 100644 --- a/src/app/features/settings/notifications/models/notification-subscription.models.ts +++ b/src/app/shared/models/notifications/notification-subscription.model.ts @@ -1,4 +1,4 @@ -import { SubscriptionEvent, SubscriptionFrequency } from '@shared/enums'; +import { SubscriptionEvent, SubscriptionFrequency } from '@osf/shared/enums'; export interface NotificationSubscription { id: string; diff --git a/src/app/shared/services/my-resources.service.ts b/src/app/shared/services/my-resources.service.ts index c63d1f178..d7cb7b931 100644 --- a/src/app/shared/services/my-resources.service.ts +++ b/src/app/shared/services/my-resources.service.ts @@ -14,9 +14,6 @@ import { MyResourcesItemResponseJsonApi, MyResourcesResponseJsonApi, MyResourcesSearchFilters, - NodeData, - NodeResponseModel, - UpdateNodeRequestModel, } from '@shared/models'; import { JsonApiService } from '@shared/services'; @@ -211,12 +208,4 @@ export class MyResourcesService { .post>(`${environment.apiUrl}/nodes/`, payload, params) .pipe(map((response) => MyResourcesMapper.fromResponse(response.data))); } - - 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/assets/i18n/en.json b/src/assets/i18n/en.json index f8b1f82b4..4c408ceea 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -415,10 +415,10 @@ "contributorsOption": "Contributors (with write access)", "anyoneOption": "Anyone with link", "whoCanEdit": "Who can edit:", - "url": "url", - "label": "label", + "url": "URL", + "label": "Label", + "storageLocationMessage": "Storage location cannot be changed after project is created.", "redirectUrlPlaceholder": "Send people who visit your OSF project page to this link instead", - "redirectLabelPlaceholder": "Optional", "invalidUrl": "Please enter a valid URL, such as: https://example.com", "disabledForWiki": "This feature is disabled for wikis of private projects.", "enabledForWiki": "This feature is enabled for wikis of private projects.", @@ -918,6 +918,11 @@ "title": "Delete project", "message": "Are you sure you want to delete {{name}} project?", "success": "Project has been successfully deleted." + }, + "deleteInstitution": { + "title": "Delete institution", + "message": "Are you sure you want to delete {{name}} from this project?", + "success": "Institution has been successfully deleted." } }, "files": {