diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index d927a789a..59d7fb9b9 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -6,6 +6,7 @@ import { MeetingsState } from '@osf/features/meetings/store'; import { MyProjectsState } from '@osf/features/my-projects/store'; import { AnalyticsState } from '@osf/features/project/analytics/store'; import { ProjectOverviewState } from '@osf/features/project/overview/store'; +import { SettingsState } from '@osf/features/project/settings/store'; import { WikiState } from '@osf/features/project/wiki/store/wiki.state'; import { AccountSettingsState } from '@osf/features/settings/account-settings/store/account-settings.state'; import { AddonsState } from '@osf/features/settings/addons/store'; @@ -25,6 +26,7 @@ export const STATES = [ DeveloperAppsState, AccountSettingsState, AnalyticsState, + SettingsState, NotificationSubscriptionState, ProjectOverviewState, CollectionsState, diff --git a/src/app/core/interceptors/auth.interceptor.ts b/src/app/core/interceptors/auth.interceptor.ts index b07c44ff7..d6dd8d3ed 100644 --- a/src/app/core/interceptors/auth.interceptor.ts +++ b/src/app/core/interceptors/auth.interceptor.ts @@ -7,8 +7,8 @@ export const authInterceptor: HttpInterceptorFn = ( next: HttpHandlerFn ): Observable> => { const authToken = 'UlO9O9GNKgVzJD7pUeY53jiQTKJ4U2znXVWNvh0KZQruoENuILx0IIYf9LoDz7Duq72EIm'; - // OBJoUomBgbUuDgQo5JoaSKNya6XaYcd0ojAX1XOLmWi6J2arQPzByxyEi81fHE60drQUWv - // UlO9O9GNKgVzJD7pUeY53jiQTKJ4U2znXVWNvh0KZQruoENuILx0IIYf9LoDz7Duq72EIm kyrylo + // '2rjFZwmdDG4rtKj7hGkEMO6XyHBM2lN7XBbsA1e8OqcFhOWu6Z7fQZiheu9RXtzSeVrgOt'; + // 'OBJoUomBgbUuDgQo5JoaSKNya6XaYcd0ojAX1XOLmWi6J2arQPzByxyEi81fHE60drQUWv'; if (authToken) { const authReq = req.clone({ diff --git a/src/app/features/my-projects/services/my-projects.service.ts b/src/app/features/my-projects/services/my-projects.service.ts index e3b3dd940..8d0bc708c 100644 --- a/src/app/features/my-projects/services/my-projects.service.ts +++ b/src/app/features/my-projects/services/my-projects.service.ts @@ -4,7 +4,9 @@ import { map } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; import { JsonApiService } from '@osf/core/services'; +import { SparseCollectionsResponse } from '@osf/features/collections/models'; import { SortOrder } from '@osf/shared/enums'; +import { NodeResponseModel, UpdateNodeRequestModel } from '@shared/models'; import { MyProjectsMapper } from '../mappers'; import { @@ -23,13 +25,15 @@ import { environment } from 'src/environments/environment'; 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, @@ -53,8 +57,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 { @@ -65,7 +69,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, @@ -78,7 +82,22 @@ export class MyProjectsService { pageNumber?: number, pageSize?: number ): Observable { - return this.#getMyItems('nodes', filters, pageNumber, pageSize); + return this.getMyItems('nodes', filters, pageNumber, pageSize); + } + + getBookmarksCollectionId(): Observable { + const params: Record = { + 'fields[collections]': 'title,bookmarks', + }; + + return this.jsonApiService.get(environment.apiUrl + '/collections/', params).pipe( + map((response) => { + const bookmarksCollection = response.data.find( + (collection) => collection.attributes.title === 'Bookmarks' && collection.attributes.bookmarks + ); + return bookmarksCollection?.id ?? ''; + }) + ); } getMyRegistrations( @@ -86,7 +105,7 @@ export class MyProjectsService { pageNumber?: number, pageSize?: number ): Observable { - return this.#getMyItems('registrations', filters, pageNumber, pageSize); + return this.getMyItems('registrations', filters, pageNumber, pageSize); } getMyPreprints( @@ -94,7 +113,7 @@ export class MyProjectsService { pageNumber?: number, pageSize?: number ): Observable { - return this.#getMyItems('preprints', filters, pageNumber, pageSize); + return this.getMyItems('preprints', filters, pageNumber, pageSize); } getMyBookmarks( @@ -103,7 +122,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( @@ -147,8 +166,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/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html index fcfa1ac96..dca09e02d 100644 --- a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html +++ b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.html @@ -7,6 +7,7 @@ type="text" pInputText placeholder="Type link name" + required [ngModel]="linkName()" (ngModelChange)="onLinkNameChange($event)" /> @@ -34,18 +35,19 @@

-
- -

Component Name Example

-
-
- -

Component Name Example

-
-
- -

Component Name Example

-
+ @for (item of config.data.sharedComponents; track item.id) { +
+ + +

{{ item.title }}

+
+ }
diff --git a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.ts b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.ts index 1f50a0349..4651b3d24 100644 --- a/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.ts +++ b/src/app/features/project/contributors/components/create-view-link-dialog/create-view-link-dialog.component.ts @@ -2,12 +2,14 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Checkbox } from 'primeng/checkbox'; -import { DynamicDialogRef } from 'primeng/dynamicdialog'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { InputText } from 'primeng/inputtext'; -import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ViewOnlyLinkNodeModel } from '@osf/features/project/settings/models'; + @Component({ selector: 'osf-create-view-link-dialog', imports: [Button, TranslatePipe, InputText, ReactiveFormsModule, FormsModule, Checkbox], @@ -15,19 +17,65 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; styleUrl: './create-view-link-dialog.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CreateViewLinkDialogComponent { - dialogRef = inject(DynamicDialogRef); +export class CreateViewLinkDialogComponent implements OnInit { + readonly dialogRef = inject(DynamicDialogRef); + protected readonly config = inject(DynamicDialogConfig); + + protected selectedComponents = signal>({}); protected linkName = signal(''); anonymous = signal(true); - componentExample1 = signal(true); - componentExample2 = signal(false); - componentExample3 = signal(false); + readonly projectId = signal(''); + + ngOnInit(): void { + const data = (this.config.data?.['sharedComponents'] as ViewOnlyLinkNodeModel[]) || []; + this.projectId.set(this.config.data?.['projectId']); + const initialState = data.reduce( + (acc, curr) => { + if (curr.id) { + acc[curr.id] = true; + } + return acc; + }, + {} as Record + ); + this.selectedComponents.set(initialState); + } + + isCurrentProject(item: ViewOnlyLinkNodeModel): boolean { + return item.category === 'project' && item.id === this.projectId(); + } addContributor(): void { - this.dialogRef.close(); + if (!this.linkName()) return; + + const components = (this.config.data?.['sharedComponents'] as ViewOnlyLinkNodeModel[]) || []; + const selectedIds = Object.entries(this.selectedComponents()).filter(([component, checked]) => checked); + + const selected = components + .filter((comp: ViewOnlyLinkNodeModel) => + selectedIds.find(([id, checked]: [string, boolean]) => id === comp.id && checked) + ) + .map((comp) => ({ + id: comp.id, + type: 'nodes', + })); + + const data = { + attributes: { + name: this.linkName(), + anonymous: this.anonymous(), + }, + nodes: selected, + }; + + this.dialogRef.close(data); } onLinkNameChange(value: string): void { this.linkName.set(value); } + + onCheckboxToggle(id: string, checked: boolean): void { + this.selectedComponents.update((prev) => ({ ...prev, [id]: checked })); + } } diff --git a/src/app/features/project/contributors/contributors.component.html b/src/app/features/project/contributors/contributors.component.html index 070f7354e..e33bb19eb 100644 --- a/src/app/features/project/contributors/contributors.component.html +++ b/src/app/features/project/contributors/contributors.component.html @@ -204,6 +204,7 @@

{{ 'project.contributors.viewOnly' | translate }}

[label]="'project.contributors.createButton' | translate" (click)="createViewLink()" > - + +
diff --git a/src/app/features/project/contributors/contributors.component.ts b/src/app/features/project/contributors/contributors.component.ts index 1f03080e8..4f27b547b 100644 --- a/src/app/features/project/contributors/contributors.component.ts +++ b/src/app/features/project/contributors/contributors.component.ts @@ -1,5 +1,8 @@ +import { createDispatchMap, select, Store } from '@ngxs/store'; + import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { ConfirmationService } from 'primeng/api'; import { Button } from 'primeng/button'; import { Checkbox } from 'primeng/checkbox'; import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -7,18 +10,29 @@ import { Select } from 'primeng/select'; import { TableModule, TablePageEvent } from 'primeng/table'; import { Tooltip } from 'primeng/tooltip'; -import { ChangeDetectionStrategy, Component, inject, output, signal } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { FormsModule } from '@angular/forms'; - -import { MY_PROJECTS_TABLE_PARAMS } from '@osf/core/constants'; -import { SearchInputComponent, ViewOnlyTableComponent } from '@osf/shared/components'; -import { SelectOption, TableParameters } from '@osf/shared/models'; -import { IS_WEB, IS_XSMALL } from '@osf/shared/utils'; - -import { LinkTableModel } from '../settings/models'; +import { filter, map, of, switchMap } from 'rxjs'; -import { AddContributorDialogComponent, CreateViewLinkDialogComponent } from './components'; +import { ChangeDetectionStrategy, Component, DestroyRef, inject, OnInit, output, signal } from '@angular/core'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; + +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 { ViewOnlyLink, ViewOnlyLinkModel } from '@osf/features/project/settings/models'; +import { + CreateViewOnlyLink, + DeleteViewOnlyLink, + GetProjectDetails, + GetViewOnlyLinksTable, + SettingsSelectors, +} from '@osf/features/project/settings/store'; +import { ViewOnlyTableComponent } from '@shared/components'; +import { SearchInputComponent } from '@shared/components/search-input/search-input.component'; +import { defaultConfirmationConfig } from '@shared/helpers'; +import { SelectOption, TableParameters } from '@shared/models'; +import { IS_WEB, IS_XSMALL } from '@shared/utils/breakpoints.tokens'; @Component({ selector: 'osf-contributors', @@ -38,9 +52,16 @@ import { AddContributorDialogComponent, CreateViewLinkDialogComponent } from './ changeDetection: ChangeDetectionStrategy.OnPush, providers: [DialogService], }) -export class ContributorsComponent { - protected searchValue = signal(''); +export class ContributorsComponent implements OnInit { readonly #translateService = inject(TranslateService); + readonly #dialogService = inject(DialogService); + readonly #route = inject(ActivatedRoute); + readonly #store = inject(Store); + readonly #destroyRef = inject(DestroyRef); + readonly #confirmationService = inject(ConfirmationService); + + protected searchValue = signal(''); + readonly items = signal([ { name: 'John Doe', @@ -67,6 +88,8 @@ export class ContributorsComponent { educationHistory: 'https://some_link', }, ]); + readonly projectId = toSignal(this.#route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined)); + protected readonly tableParams = signal({ ...MY_PROJECTS_TABLE_PARAMS, }); @@ -78,7 +101,9 @@ export class ContributorsComponent { protected readonly isMobile = toSignal(inject(IS_XSMALL)); dialogRef: DynamicDialogRef | null = null; - readonly #dialogService = inject(DialogService); + + protected viewOnlyLinks = select(SettingsSelectors.getViewOnlyLinks); + protected projectDetails = select(SettingsSelectors.getProjectDetails); protected readonly permissionsOptions: SelectOption[] = [ { @@ -106,40 +131,18 @@ 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', - }, - ]; + protected actions = createDispatchMap({ + getViewOnlyLinks: GetViewOnlyLinksTable, + getProjectDetails: GetProjectDetails, + }); + + ngOnInit(): void { + const id = this.projectId(); + if (id) { + this.actions.getViewOnlyLinks(id); + this.actions.getProjectDetails(id); + } + } protected onPermissionChange(value: string): void { this.selectedPermission.set(value); @@ -169,13 +172,47 @@ export class ContributorsComponent { } createViewLink() { + const sharedComponents = [ + { + id: this.projectDetails().id, + title: this.projectDetails().attributes.title, + category: 'project', + }, + ]; this.dialogRef = this.#dialogService.open(CreateViewLinkDialogComponent, { width: '448px', focusOnShow: false, header: this.#translateService.instant('project.contributors.createLinkDialog.dialogTitle'), + data: { sharedComponents, projectId: this.projectId() }, closeOnEscape: true, modal: true, closable: true, }); + + this.dialogRef.onClose + .pipe( + filter((res) => !!res), + switchMap((result) => this.#store.dispatch(new CreateViewOnlyLink(this.projectId(), result as ViewOnlyLink))), + takeUntilDestroyed(this.#destroyRef) + ) + .subscribe(); + } + + deleteLinkItem(link: ViewOnlyLinkModel): void { + this.#confirmationService.confirm({ + ...defaultConfirmationConfig, + message: this.#translateService.instant('myProjects.settings.delete.message'), + header: this.#translateService.instant('myProjects.settings.delete.title', { + name: link.name, + }), + acceptButtonProps: { + ...defaultConfirmationConfig.acceptButtonProps, + severity: 'danger', + label: this.#translateService.instant('settings.developerApps.list.deleteButton'), + }, + accept: () => { + this.#store.dispatch(new DeleteViewOnlyLink(this.projectId(), link.id)).subscribe(); + }, + }); } } 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 2a62c7d78..000000000 --- a/src/app/features/project/settings/components/accordion-table/accordion-table.component.ts +++ /dev/null @@ -1,26 +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'; - -import { RightControl } from '../../models'; - -@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/index.ts b/src/app/features/project/settings/components/index.ts index f53a75cb2..d38eed9dd 100644 --- a/src/app/features/project/settings/components/index.ts +++ b/src/app/features/project/settings/components/index.ts @@ -1,7 +1,9 @@ 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 { 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'; export { SettingsViewOnlyLinksCardComponent } from './settings-view-only-links-card/settings-view-only-links-card.component'; export { SettingsWikiCardComponent } from './settings-wiki-card/settings-wiki-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 c9df99a30..a9a4321cb 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 @@ -14,31 +14,30 @@ {{ title() }} - @if (rightControls().length > 0) { + @if (rightControls()) {
- @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 }} + +
}
@@ -48,8 +47,8 @@ @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 12be4075a..d6e1dea54 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,24 @@ +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 { NgClass } from '@angular/common'; +import { ChangeDetectionStrategy, Component, input, output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { RightControl } from '../../models'; +import { RightControl } from '@osf/features/project/settings/models'; @Component({ selector: 'osf-project-detail-setting-accordion', - imports: [NgClass, SelectModule, FormsModule, Button, LowerCasePipe], + imports: [NgClass, SelectModule, FormsModule, Button, TranslatePipe], templateUrl: './project-detail-setting-accordion.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProjectDetailSettingAccordionComponent { - title = input.required(); - - rightControls = input.required(); - + emitValueChange = output<{ index: number; value: boolean | string }>(); + title = input(); + rightControls = input.required(); 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 new file mode 100644 index 000000000..13d78cfc6 --- /dev/null +++ b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.html @@ -0,0 +1,25 @@ + +

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

+ +

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

+ + +
+ + + {{ subscriptionEvent.Comments | notificationDescription: notifications()?.[0]?.frequency | translate }} + +
+ +
+ + + {{ subscriptionEvent.FileUpdated | notificationDescription: notifications()?.[1]?.frequency | translate }} + +
+
+
diff --git a/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts new file mode 100644 index 000000000..b7d1665f5 --- /dev/null +++ b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProjectSettingNotificationsComponent } from './project-setting-notifications.component'; + +describe('ProjectSettingNotificationsComponent', () => { + let component: ProjectSettingNotificationsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProjectSettingNotificationsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ProjectSettingNotificationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 000000000..bdb3cdc58 --- /dev/null +++ b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.ts @@ -0,0 +1,66 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Card } from 'primeng/card'; + +import { ChangeDetectionStrategy, Component, effect, input, output } from '@angular/core'; + +import { ProjectDetailSettingAccordionComponent } from '@osf/features/project/settings/components'; +import { NotificationDescriptionPipe } from '@osf/features/project/settings/pipes'; +import { NotificationSubscription } from '@osf/features/settings/notifications/models'; +import { SubscriptionEvent, SubscriptionFrequency } from '@shared/enums'; + +import { RightControl } from '../../models'; + +@Component({ + selector: 'osf-project-setting-notifications', + imports: [Card, TranslatePipe, ProjectDetailSettingAccordionComponent, NotificationDescriptionPipe], + templateUrl: './project-setting-notifications.component.html', + styleUrl: '../../settings.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProjectSettingNotificationsComponent { + notificationEmitValue = output<{ event: SubscriptionEvent; frequency: SubscriptionFrequency }>(); + title = input(); + notifications = input(); + + allAccordionData: RightControl[] | undefined = []; + + protected readonly subscriptionEvent = SubscriptionEvent; + protected subscriptionFrequencyOptions = Object.entries(SubscriptionFrequency).map(([key, value]) => ({ + label: key, + value, + })); + + 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; + } + }); + }); + } + + changeEmittedValue(emittedValue: { index: number; value: boolean | string }): void { + if (this.allAccordionData) { + this.notificationEmitValue.emit({ + 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 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..d2a9289fd 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,17 @@ 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, }) 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..1fed0b574 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,9 +6,11 @@

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

+

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

@@ -16,8 +18,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 2315e3322..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/shared/enums'; - @Component({ selector: 'osf-settings-commenting-card', imports: [Card, RadioButton, TranslatePipe, FormsModule], @@ -16,5 +14,6 @@ import { ShareIndexingEnum } from '@osf/shared/enums'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class SettingsCommentingCardComponent { - commenting = model(); + anyoneCanComment = input.required(); + readonly anyoneCanCommentChange = output(); } 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 }}

-
+