From 33c4acf5c08daee5a25b806152461140309ee15e Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 11 Jun 2025 13:55:56 +0300 Subject: [PATCH 1/7] feat(199): added custom select --- src/app/shared/components/index.ts | 1 + .../components/select/select.component.html | 9 ++++++++ .../components/select/select.component.scss | 0 .../select/select.component.spec.ts | 22 +++++++++++++++++++ .../components/select/select.component.ts | 20 +++++++++++++++++ 5 files changed, 52 insertions(+) create mode 100644 src/app/shared/components/select/select.component.html create mode 100644 src/app/shared/components/select/select.component.scss create mode 100644 src/app/shared/components/select/select.component.spec.ts create mode 100644 src/app/shared/components/select/select.component.ts diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index 9b3862021..a899494f7 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -12,6 +12,7 @@ export { MyProjectsTableComponent } from './my-projects-table/my-projects-table. export { PasswordInputHintComponent } from './password-input-hint/password-input-hint.component'; export { PieChartComponent } from './pie-chart/pie-chart.component'; export { SearchInputComponent } from './search-input/search-input.component'; +export { SelectComponent } from './select/select.component'; export { SubHeaderComponent } from './sub-header/sub-header.component'; export { TextInputComponent } from './text-input/text-input.component'; export { ToastComponent } from './toast/toast.component'; diff --git a/src/app/shared/components/select/select.component.html b/src/app/shared/components/select/select.component.html new file mode 100644 index 000000000..b78ec9218 --- /dev/null +++ b/src/app/shared/components/select/select.component.html @@ -0,0 +1,9 @@ + + + {{ selectedOption.label | translate }} + + + + {{ item.label | translate }} + + diff --git a/src/app/shared/components/select/select.component.scss b/src/app/shared/components/select/select.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/select/select.component.spec.ts b/src/app/shared/components/select/select.component.spec.ts new file mode 100644 index 000000000..afac4f93c --- /dev/null +++ b/src/app/shared/components/select/select.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectComponent } from './select.component'; + +describe('SelectComponent', () => { + let component: SelectComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SelectComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SelectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/select/select.component.ts b/src/app/shared/components/select/select.component.ts new file mode 100644 index 000000000..dbc4345a8 --- /dev/null +++ b/src/app/shared/components/select/select.component.ts @@ -0,0 +1,20 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Select } from 'primeng/select'; + +import { ChangeDetectionStrategy, Component, input, model } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { SelectOption } from '@osf/shared/models'; + +@Component({ + selector: 'osf-select', + imports: [FormsModule, Select, TranslatePipe], + templateUrl: './select.component.html', + styleUrl: './select.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SelectComponent { + options = input.required(); + selectedValue = model.required(); +} From 72a4c622466a4053c52b4bbeafe516b865ba35cc Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 11 Jun 2025 14:21:56 +0300 Subject: [PATCH 2/7] feat(199): updated custom select --- .../contributors-list.component.html | 18 ++++-------------- .../contributors-list.component.ts | 4 ++-- .../components/select/select.component.html | 9 ++++++++- .../components/select/select.component.ts | 1 + 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/app/features/project/contributors/components/contributors-list/contributors-list.component.html b/src/app/features/project/contributors/components/contributors-list/contributors-list.component.html index 43b8921ad..5389de277 100644 --- a/src/app/features/project/contributors/components/contributors-list/contributors-list.component.html +++ b/src/app/features/project/contributors/components/contributors-list/contributors-list.component.html @@ -99,21 +99,11 @@

{{ 'project.contributors.curatorInfo.heading' | translate }}

- - -

{{ selectedOption.label | translate }}

-
- - -

{{ item.label | translate }}

-
-
+ [placeholder]="'project.contributors.permissionFilter'" + [(selectedValue)]="contributor.permission" + >
diff --git a/src/app/features/project/contributors/components/contributors-list/contributors-list.component.ts b/src/app/features/project/contributors/components/contributors-list/contributors-list.component.ts index 5615b4925..ddeb56729 100644 --- a/src/app/features/project/contributors/components/contributors-list/contributors-list.component.ts +++ b/src/app/features/project/contributors/components/contributors-list/contributors-list.component.ts @@ -2,7 +2,6 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Checkbox } from 'primeng/checkbox'; -import { Select } from 'primeng/select'; import { Skeleton } from 'primeng/skeleton'; import { TableModule } from 'primeng/table'; import { Tooltip } from 'primeng/tooltip'; @@ -11,6 +10,7 @@ import { ChangeDetectionStrategy, Component, input, output, signal } from '@angu import { FormsModule } from '@angular/forms'; import { MY_PROJECTS_TABLE_PARAMS } from '@osf/core/constants'; +import { SelectComponent } from '@osf/shared/components'; import { SelectOption, TableParameters } from '@osf/shared/models'; import { PERMISSION_OPTIONS } from '../../constants'; @@ -18,7 +18,7 @@ import { ContributorModel } from '../../models'; @Component({ selector: 'osf-contributors-list', - imports: [Select, TranslatePipe, FormsModule, TableModule, Tooltip, Checkbox, Skeleton, Button], + imports: [TranslatePipe, FormsModule, TableModule, Tooltip, Checkbox, Skeleton, Button, SelectComponent], templateUrl: './contributors-list.component.html', styleUrl: './contributors-list.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/shared/components/select/select.component.html b/src/app/shared/components/select/select.component.html index b78ec9218..b0f93c25e 100644 --- a/src/app/shared/components/select/select.component.html +++ b/src/app/shared/components/select/select.component.html @@ -1,4 +1,11 @@ - + {{ selectedOption.label | translate }} diff --git a/src/app/shared/components/select/select.component.ts b/src/app/shared/components/select/select.component.ts index dbc4345a8..70c54115e 100644 --- a/src/app/shared/components/select/select.component.ts +++ b/src/app/shared/components/select/select.component.ts @@ -17,4 +17,5 @@ import { SelectOption } from '@osf/shared/models'; export class SelectComponent { options = input.required(); selectedValue = model.required(); + placeholder = input(''); } From 768b3b2578fa455aa8b137c0eebfdaa3100ef83e Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 11 Jun 2025 15:05:38 +0300 Subject: [PATCH 3/7] feat(199): added moderation component and list --- src/app/core/models/json-api.model.ts | 19 +- .../collections/collections.routes.ts | 15 +- ...lection-moderation-settings.component.html | 6 + ...lection-moderation-settings.component.scss | 0 ...tion-moderation-settings.component.spec.ts | 22 ++ ...ollection-moderation-settings.component.ts | 13 + .../collection-moderators-list.component.html | 129 +++++++++ .../collection-moderators-list.component.scss | 13 + ...llection-moderators-list.component.spec.ts | 22 ++ .../collection-moderators-list.component.ts | 49 ++++ .../collection-moderators.component.html | 24 ++ .../collection-moderators.component.scss | 0 .../collection-moderators.component.spec.ts | 22 ++ .../collection-moderators.component.ts | 113 ++++++++ .../features/moderation/components/index.ts | 3 + .../collection-moderation-tabs.const.ts | 9 + .../features/moderation/constants/index.ts | 1 + .../enums/collection-moderation-tab.enum.ts | 5 + src/app/features/moderation/enums/index.ts | 2 + .../enums/moderator-permission.enum.ts | 4 + src/app/features/moderation/mappers/index.ts | 1 + .../moderation/mappers/moderation.mapper.ts | 14 + src/app/features/moderation/models/index.ts | 2 + .../models/moderator-json-api.model.ts | 255 ++++++++++++++++++ .../moderation/models/moderator.model.ts | 12 + .../collection-moderation.component.html | 30 +++ .../collection-moderation.component.scss | 3 + .../collection-moderation.component.spec.ts | 22 ++ .../collection-moderation.component.ts | 45 ++++ src/app/features/moderation/pages/index.ts | 1 + src/app/features/moderation/services/index.ts | 1 + .../moderation/services/moderation.service.ts | 24 ++ src/assets/i18n/en.json | 19 ++ 33 files changed, 895 insertions(+), 5 deletions(-) create mode 100644 src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.html create mode 100644 src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.scss create mode 100644 src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.spec.ts create mode 100644 src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.ts create mode 100644 src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html create mode 100644 src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.scss create mode 100644 src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.spec.ts create mode 100644 src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.ts create mode 100644 src/app/features/moderation/components/collection-moderators/collection-moderators.component.html create mode 100644 src/app/features/moderation/components/collection-moderators/collection-moderators.component.scss create mode 100644 src/app/features/moderation/components/collection-moderators/collection-moderators.component.spec.ts create mode 100644 src/app/features/moderation/components/collection-moderators/collection-moderators.component.ts create mode 100644 src/app/features/moderation/components/index.ts create mode 100644 src/app/features/moderation/constants/collection-moderation-tabs.const.ts create mode 100644 src/app/features/moderation/constants/index.ts create mode 100644 src/app/features/moderation/enums/collection-moderation-tab.enum.ts create mode 100644 src/app/features/moderation/enums/index.ts create mode 100644 src/app/features/moderation/enums/moderator-permission.enum.ts create mode 100644 src/app/features/moderation/mappers/index.ts create mode 100644 src/app/features/moderation/mappers/moderation.mapper.ts create mode 100644 src/app/features/moderation/models/index.ts create mode 100644 src/app/features/moderation/models/moderator-json-api.model.ts create mode 100644 src/app/features/moderation/models/moderator.model.ts create mode 100644 src/app/features/moderation/pages/collection-moderation/collection-moderation.component.html create mode 100644 src/app/features/moderation/pages/collection-moderation/collection-moderation.component.scss create mode 100644 src/app/features/moderation/pages/collection-moderation/collection-moderation.component.spec.ts create mode 100644 src/app/features/moderation/pages/collection-moderation/collection-moderation.component.ts create mode 100644 src/app/features/moderation/pages/index.ts create mode 100644 src/app/features/moderation/services/index.ts create mode 100644 src/app/features/moderation/services/moderation.service.ts diff --git a/src/app/core/models/json-api.model.ts b/src/app/core/models/json-api.model.ts index 83a81623b..8f52461e3 100644 --- a/src/app/core/models/json-api.model.ts +++ b/src/app/core/models/json-api.model.ts @@ -5,10 +5,7 @@ export interface JsonApiResponse { export interface JsonApiResponseWithPaging extends JsonApiResponse { links: { - meta: { - total: number; - per_page: number; - }; + meta: MetaJsonApi; }; } @@ -20,3 +17,17 @@ export interface ApiData { relationships: Relationships; links: Links; } + +export interface MetaJsonApi { + total: number; + per_page: number; + version?: string; +} + +export interface PaginationLinksJsonApi { + self?: string; + first?: string | null; + last?: string | null; + prev?: string | null; + next?: string | null; +} diff --git a/src/app/features/collections/collections.routes.ts b/src/app/features/collections/collections.routes.ts index 2c7681352..3dc1c1f63 100644 --- a/src/app/features/collections/collections.routes.ts +++ b/src/app/features/collections/collections.routes.ts @@ -5,6 +5,19 @@ import { CollectionsComponent } from '@osf/features/collections/collections.comp export const collectionsRoutes: Routes = [ { path: '', - component: CollectionsComponent, + children: [ + { + path: '', + pathMatch: 'full', + component: CollectionsComponent, + }, + { + path: 'moderation', + loadComponent: () => + import('@osf/features/moderation/pages/collection-moderation/collection-moderation.component').then( + (m) => m.CollectionModerationComponent + ), + }, + ], }, ]; diff --git a/src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.html b/src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.html new file mode 100644 index 000000000..07e77467a --- /dev/null +++ b/src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.html @@ -0,0 +1,6 @@ +

+ {{ 'moderation.settingsMessage' | translate }} + + {{ 'moderation.userSettings' | translate }} + +

diff --git a/src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.scss b/src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.spec.ts b/src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.spec.ts new file mode 100644 index 000000000..4d76e2c1b --- /dev/null +++ b/src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CollectionModerationSettingsComponent } from './collection-moderation-settings.component'; + +describe('CollectionModerationSettingsComponent', () => { + let component: CollectionModerationSettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CollectionModerationSettingsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CollectionModerationSettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.ts b/src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.ts new file mode 100644 index 000000000..24c76ee3e --- /dev/null +++ b/src/app/features/moderation/components/collection-moderation-settings/collection-moderation-settings.component.ts @@ -0,0 +1,13 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'osf-collection-moderation-settings', + imports: [TranslatePipe, RouterLink], + templateUrl: './collection-moderation-settings.component.html', + styleUrl: './collection-moderation-settings.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CollectionModerationSettingsComponent {} diff --git a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html new file mode 100644 index 000000000..47246c50f --- /dev/null +++ b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html @@ -0,0 +1,129 @@ + + + + {{ 'project.contributors.table.headers.name' | translate }} + +
+ {{ 'project.contributors.table.headers.permissions' | translate }} + + +
+

{{ 'project.contributors.permissionInfo.title' | translate }}

+ +
+

{{ 'project.contributors.permissions.read' | translate }}

+
    +
  • {{ 'project.contributors.permissionInfo.viewProjectContent' | translate }}
  • +
+
+ +
+

{{ 'project.contributors.permissions.readAndWrite' | translate }}

+
    +
  • {{ 'project.contributors.permissionInfo.read' | translate }}
  • +
  • {{ 'project.contributors.permissionInfo.addComponents' | translate }}
  • +
  • {{ 'project.contributors.permissionInfo.editContent' | translate }}
  • +
+
+ +
+

{{ 'project.contributors.permissions.administrator' | translate }}

+
    +
  • {{ 'project.contributors.permissionInfo.readWrite' | translate }}
  • +
  • {{ 'project.contributors.permissionInfo.manageContributors' | translate }}
  • +
  • {{ 'project.contributors.permissionInfo.deleteRegister' | translate }}
  • +
  • {{ 'project.contributors.permissionInfo.publicPrivate' | translate }}
  • +
+
+
+
+
+ + + {{ 'project.contributors.table.headers.employment' | translate }} + {{ 'project.contributors.table.headers.education' | translate }} + + +
+ + + @if (item.id) { + + +

+ {{ item.fullName }} +

+ + +
+ +
+ + + @if (item.employment) { + + {{ 'project.contributors.employment.show' | translate }} + + } @else { + {{ 'project.contributors.employment.none' | translate }} + } + + +
+ @if (item.education) { + + {{ 'project.contributors.education.show' | translate }} + + } @else { + {{ 'project.contributors.education.none' | translate }} + } +
+ + + + + + + } @else { + + + + + + } +
+ + + + {{ 'project.contributors.table.emptyMessage' | translate }} + + +
diff --git a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.scss b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.scss new file mode 100644 index 000000000..4816093ca --- /dev/null +++ b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.scss @@ -0,0 +1,13 @@ +@use "assets/styles/variables" as var; +@use "assets/styles/mixins" as mix; + +.blue-icon { + margin-top: mix.rem(2px); + cursor: pointer; +} + +.inside-list { + li { + list-style: inside; + } +} diff --git a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.spec.ts b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.spec.ts new file mode 100644 index 000000000..ec58a2a34 --- /dev/null +++ b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CollectionModeratorsListComponent } from './collection-moderators-list.component'; + +describe('CollectionModeratorsListComponent', () => { + let component: CollectionModeratorsListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CollectionModeratorsListComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CollectionModeratorsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.ts b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.ts new file mode 100644 index 000000000..524d4ff1a --- /dev/null +++ b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.ts @@ -0,0 +1,49 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Skeleton } from 'primeng/skeleton'; +import { TableModule } from 'primeng/table'; +import { Tooltip } from 'primeng/tooltip'; + +import { ChangeDetectionStrategy, Component, input, output, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { MY_PROJECTS_TABLE_PARAMS } from '@osf/core/constants'; +import { PERMISSION_OPTIONS } from '@osf/features/project/contributors/constants'; +import { SelectComponent } from '@osf/shared/components'; +import { SelectOption, TableParameters } from '@osf/shared/models'; + +import { Moderator } from '../../models'; + +@Component({ + selector: 'osf-collection-moderators-list', + imports: [TranslatePipe, FormsModule, TableModule, Tooltip, Skeleton, Button, SelectComponent], + templateUrl: './collection-moderators-list.component.html', + styleUrl: './collection-moderators-list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CollectionModeratorsListComponent { + items = input([]); + isLoading = input(false); + + remove = output(); + showEducationHistory = output(); + showEmploymentHistory = output(); + + protected readonly tableParams = signal({ ...MY_PROJECTS_TABLE_PARAMS }); + protected readonly permissionsOptions: SelectOption[] = PERMISSION_OPTIONS; + + skeletonData: Moderator[] = Array.from({ length: 3 }, () => ({}) as Moderator); + + protected removeContributor(item: Moderator) { + this.remove.emit(item); + } + + protected openEducationHistory(item: Moderator) { + this.showEducationHistory.emit(item); + } + + protected openEmploymentHistory(item: Moderator) { + this.showEmploymentHistory.emit(item); + } +} diff --git a/src/app/features/moderation/components/collection-moderators/collection-moderators.component.html b/src/app/features/moderation/components/collection-moderators/collection-moderators.component.html new file mode 100644 index 000000000..f1d510270 --- /dev/null +++ b/src/app/features/moderation/components/collection-moderators/collection-moderators.component.html @@ -0,0 +1,24 @@ +
+
+ +
+ +
+ +
+
+ +
+ +
diff --git a/src/app/features/moderation/components/collection-moderators/collection-moderators.component.scss b/src/app/features/moderation/components/collection-moderators/collection-moderators.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/moderation/components/collection-moderators/collection-moderators.component.spec.ts b/src/app/features/moderation/components/collection-moderators/collection-moderators.component.spec.ts new file mode 100644 index 000000000..860d5f4e2 --- /dev/null +++ b/src/app/features/moderation/components/collection-moderators/collection-moderators.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CollectionModeratorsComponent } from './collection-moderators.component'; + +describe('CollectionModeratorsComponent', () => { + let component: CollectionModeratorsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CollectionModeratorsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CollectionModeratorsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/collection-moderators/collection-moderators.component.ts b/src/app/features/moderation/components/collection-moderators/collection-moderators.component.ts new file mode 100644 index 000000000..4438c8aa9 --- /dev/null +++ b/src/app/features/moderation/components/collection-moderators/collection-moderators.component.ts @@ -0,0 +1,113 @@ +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + +import { ConfirmationService } from 'primeng/api'; +import { Button } from 'primeng/button'; +import { DialogService } from 'primeng/dynamicdialog'; + +import { debounceTime, distinctUntilChanged, skip } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl } from '@angular/forms'; + +import { + ContributorEducationHistoryComponent, + ContributorEmploymentHistoryComponent, +} from '@osf/features/project/contributors/components'; +import { SearchInputComponent } from '@osf/shared/components'; +import { ToastService } from '@osf/shared/services'; +import { defaultConfirmationConfig } from '@osf/shared/utils'; + +import { Moderator } from '../../models'; +import { CollectionModeratorsListComponent } from '../collection-moderators-list/collection-moderators-list.component'; + +@Component({ + selector: 'osf-collection-moderators', + imports: [CollectionModeratorsListComponent, SearchInputComponent, Button, TranslatePipe], + templateUrl: './collection-moderators.component.html', + styleUrl: './collection-moderators.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [DialogService], +}) +export class CollectionModeratorsComponent implements OnInit { + protected searchControl = new FormControl(''); + + private readonly destroyRef = inject(DestroyRef); + private readonly translateService = inject(TranslateService); + private readonly confirmationService = inject(ConfirmationService); + private readonly dialogService = inject(DialogService); + private readonly toastService = inject(ToastService); + + moderators = signal([]); + initialContributors = signal([]); + isModeratorsLoading = signal(false); + + constructor() { + effect(() => { + this.moderators.set(JSON.parse(JSON.stringify(this.initialContributors()))); + + if (this.isModeratorsLoading()) { + this.searchControl.disable(); + } else { + this.searchControl.enable(); + } + }); + } + + ngOnInit(): void { + this.setSearchSubscription(); + } + + addModerator() { + console.log('Add moderator'); + } + + removeModerator(moderator: Moderator) { + this.confirmationService.confirm({ + ...defaultConfirmationConfig, + header: this.translateService.instant('moderation.removeDialog.title'), + message: this.translateService.instant('moderation.removeDialog.message', { + name: moderator.fullName, + }), + acceptButtonProps: { + ...defaultConfirmationConfig.acceptButtonProps, + severity: 'danger', + label: this.translateService.instant('common.buttons.remove'), + }, + accept: () => { + console.log('Remove moderator', moderator); + this.toastService.showSuccess('project.contributors.removeDialog.successMessage', { name: moderator.fullName }); + }, + }); + } + + openEmploymentHistory(contributor: Moderator) { + this.dialogService.open(ContributorEmploymentHistoryComponent, { + width: '552px', + data: contributor.employment, + focusOnShow: false, + header: this.translateService.instant('project.contributors.table.headers.employment'), + closeOnEscape: true, + modal: true, + closable: true, + }); + } + + openEducationHistory(contributor: Moderator) { + this.dialogService.open(ContributorEducationHistoryComponent, { + width: '552px', + data: contributor.education, + focusOnShow: false, + header: this.translateService.instant('project.contributors.table.headers.education'), + closeOnEscape: true, + modal: true, + closable: true, + }); + } + + private setSearchSubscription() { + this.searchControl.valueChanges + .pipe(skip(1), debounceTime(500), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) + .subscribe((res) => console.log(res)); + } +} diff --git a/src/app/features/moderation/components/index.ts b/src/app/features/moderation/components/index.ts new file mode 100644 index 000000000..4a4c8a15d --- /dev/null +++ b/src/app/features/moderation/components/index.ts @@ -0,0 +1,3 @@ +export { CollectionModerationSettingsComponent } from './collection-moderation-settings/collection-moderation-settings.component'; +export { CollectionModeratorsComponent } from './collection-moderators/collection-moderators.component'; +export { CollectionModeratorsListComponent } from './collection-moderators-list/collection-moderators-list.component'; diff --git a/src/app/features/moderation/constants/collection-moderation-tabs.const.ts b/src/app/features/moderation/constants/collection-moderation-tabs.const.ts new file mode 100644 index 000000000..cb37b1a64 --- /dev/null +++ b/src/app/features/moderation/constants/collection-moderation-tabs.const.ts @@ -0,0 +1,9 @@ +import { TabOption } from '@osf/shared/models'; + +import { CollectionModerationTab } from '../enums'; + +export const COLLECTION_MODERATION_TABS: TabOption[] = [ + { label: 'moderation.allItems', value: CollectionModerationTab.AllItems }, + { label: 'moderation.moderators', value: CollectionModerationTab.Moderators }, + { label: 'moderation.settings', value: CollectionModerationTab.Settings }, +]; diff --git a/src/app/features/moderation/constants/index.ts b/src/app/features/moderation/constants/index.ts new file mode 100644 index 000000000..b8135444f --- /dev/null +++ b/src/app/features/moderation/constants/index.ts @@ -0,0 +1 @@ +export * from './collection-moderation-tabs.const'; diff --git a/src/app/features/moderation/enums/collection-moderation-tab.enum.ts b/src/app/features/moderation/enums/collection-moderation-tab.enum.ts new file mode 100644 index 000000000..609112aa4 --- /dev/null +++ b/src/app/features/moderation/enums/collection-moderation-tab.enum.ts @@ -0,0 +1,5 @@ +export enum CollectionModerationTab { + AllItems = 1, + Moderators, + Settings, +} diff --git a/src/app/features/moderation/enums/index.ts b/src/app/features/moderation/enums/index.ts new file mode 100644 index 000000000..47d2f091a --- /dev/null +++ b/src/app/features/moderation/enums/index.ts @@ -0,0 +1,2 @@ +export * from './collection-moderation-tab.enum'; +export * from './moderator-permission.enum'; diff --git a/src/app/features/moderation/enums/moderator-permission.enum.ts b/src/app/features/moderation/enums/moderator-permission.enum.ts new file mode 100644 index 000000000..8c1f035b7 --- /dev/null +++ b/src/app/features/moderation/enums/moderator-permission.enum.ts @@ -0,0 +1,4 @@ +export enum ModeratorPermission { + Admin = 'admin', + Moderator = 'moderator', +} diff --git a/src/app/features/moderation/mappers/index.ts b/src/app/features/moderation/mappers/index.ts new file mode 100644 index 000000000..9fdb5c4b8 --- /dev/null +++ b/src/app/features/moderation/mappers/index.ts @@ -0,0 +1 @@ +export * from './moderation.mapper'; diff --git a/src/app/features/moderation/mappers/moderation.mapper.ts b/src/app/features/moderation/mappers/moderation.mapper.ts new file mode 100644 index 000000000..3585d01cb --- /dev/null +++ b/src/app/features/moderation/mappers/moderation.mapper.ts @@ -0,0 +1,14 @@ +import { Moderator, ModeratorDataJsonApi } from '../models'; + +export class ModerationMapper { + static fromModeratorResponse(response: ModeratorDataJsonApi): Moderator { + return { + id: response.id, + userId: response.embeds.user.id, + fullName: response.attributes.full_name, + permission: response.attributes.permission_group, + employment: response.embeds.user.attributes.employment || [], + education: response.embeds.user.attributes.education || [], + }; + } +} diff --git a/src/app/features/moderation/models/index.ts b/src/app/features/moderation/models/index.ts new file mode 100644 index 000000000..c1fe9d67d --- /dev/null +++ b/src/app/features/moderation/models/index.ts @@ -0,0 +1,2 @@ +export * from './moderator.model'; +export * from './moderator-json-api.model'; diff --git a/src/app/features/moderation/models/moderator-json-api.model.ts b/src/app/features/moderation/models/moderator-json-api.model.ts new file mode 100644 index 000000000..57f998d48 --- /dev/null +++ b/src/app/features/moderation/models/moderator-json-api.model.ts @@ -0,0 +1,255 @@ +import { ApiData, MetaJsonApi, PaginationLinksJsonApi, UserGetResponse } from '@osf/core/models'; + +export interface ModeratorResponseJsonApi { + data: ModeratorDataJsonApi[]; + meta: MetaJsonApi; + links: PaginationLinksJsonApi; +} + +export type ModeratorDataJsonApi = ApiData; + +interface ModeratorAttributesJsonApi { + full_name: string; + permission_group: 'moderator' | 'admin'; +} + +interface ModeratorEmbedsJsonApi { + user: UserGetResponse; +} + +// { +// "data": [ +// { +// "id": "nmwt5", +// "type": "moderators", +// "attributes": { +// "full_name": "Blaine Butler", +// "permission_group": "admin" +// }, +// "relationships": { +// "user": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/users/nmwt5/", +// "meta": {} +// } +// }, +// "data": { +// "id": "nmwt5", +// "type": "users" +// } +// }, +// "provider": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/providers/collections/colmod/", +// "meta": {} +// } +// }, +// "data": { +// "id": "colmod", +// "type": "collection-providers" +// } +// } +// }, +// "embeds": { +// "user": { +// "data": { +// "id": "nmwt5", +// "type": "users", +// "attributes": { +// "full_name": "Blaine Butler", +// "given_name": "Blaine", +// "middle_names": "", +// "family_name": "Butler", +// "suffix": "", +// "date_registered": "2022-11-03T19:23:28.110924Z", +// "active": false, +// "timezone": "Etc/UTC", +// "locale": "en_US", +// "social": {}, +// "employment": [], +// "education": [] +// }, +// "relationships": { +// "nodes": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/users/nmwt5/nodes/", +// "meta": {} +// } +// } +// }, +// "groups": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/users/nmwt5/groups/", +// "meta": {} +// } +// } +// }, +// "registrations": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/users/nmwt5/registrations/", +// "meta": {} +// } +// } +// }, +// "institutions": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/users/nmwt5/institutions/", +// "meta": {} +// }, +// "self": { +// "href": "https://api.staging.osf.io/v2/users/nmwt5/relationships/institutions/", +// "meta": {} +// } +// } +// }, +// "preprints": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/users/nmwt5/preprints/", +// "meta": {} +// } +// } +// } +// }, +// "links": { +// "html": "https://staging.osf.io/nmwt5/", +// "profile_image": "https://secure.gravatar.com/avatar/4a1f62c6580a151e5c1c0aec72b7fc2a?d=identicon", +// "self": "https://api.staging.osf.io/v2/users/nmwt5/" +// } +// } +// } +// }, +// "links": { +// "self": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/nmwt5/" +// } +// }, +// { +// "id": "m8ku3", +// "type": "moderators", +// "attributes": { +// "full_name": "DC Test - AMC", +// "permission_group": "admin" +// }, +// "relationships": { +// "user": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/users/m8ku3/", +// "meta": {} +// } +// }, +// "data": { +// "id": "m8ku3", +// "type": "users" +// } +// }, +// "provider": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/providers/collections/colmod/", +// "meta": {} +// } +// }, +// "data": { +// "id": "colmod", +// "type": "collection-providers" +// } +// } +// }, +// "embeds": { +// "user": { +// "data": { +// "id": "m8ku3", +// "type": "users", +// "attributes": { +// "full_name": "DC Test - AMC", +// "given_name": "DC", +// "middle_names": "Test -", +// "family_name": "AMC", +// "suffix": "", +// "date_registered": "2022-09-14T11:28:08.681787Z", +// "active": true, +// "timezone": "Etc/UTC", +// "locale": "en_US", +// "social": {}, +// "employment": [], +// "education": [] +// }, +// "relationships": { +// "nodes": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/users/m8ku3/nodes/", +// "meta": {} +// } +// } +// }, +// "groups": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/users/m8ku3/groups/", +// "meta": {} +// } +// } +// }, +// "registrations": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/users/m8ku3/registrations/", +// "meta": {} +// } +// } +// }, +// "institutions": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/users/m8ku3/institutions/", +// "meta": {} +// }, +// "self": { +// "href": "https://api.staging.osf.io/v2/users/m8ku3/relationships/institutions/", +// "meta": {} +// } +// } +// }, +// "preprints": { +// "links": { +// "related": { +// "href": "https://api.staging.osf.io/v2/users/m8ku3/preprints/", +// "meta": {} +// } +// } +// } +// }, +// "links": { +// "html": "https://staging.osf.io/m8ku3/", +// "profile_image": "https://secure.gravatar.com/avatar/ce38ca4e4a1361446468960716f57b5e?d=identicon", +// "self": "https://api.staging.osf.io/v2/users/m8ku3/" +// } +// } +// } +// }, +// "links": { +// "self": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/m8ku3/" +// } +// } +// ], +// "meta": { +// "total": 8, +// "per_page": 2, +// "version": "2.20" +// }, +// "links": { +// "self": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/?page%5Bsize%5D=2", +// "first": null, +// "last": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/?page=4&page%5Bsize%5D=2", +// "prev": null, +// "next": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/?page=2&page%5Bsize%5D=2" +// } +// } diff --git a/src/app/features/moderation/models/moderator.model.ts b/src/app/features/moderation/models/moderator.model.ts new file mode 100644 index 000000000..42ef0e502 --- /dev/null +++ b/src/app/features/moderation/models/moderator.model.ts @@ -0,0 +1,12 @@ +import { Education, Employment } from '@osf/shared/models'; + +import { ModeratorPermission } from '../enums'; + +export interface Moderator { + id: string; + userId: string; + fullName: string; + permission: ModeratorPermission; + employment: Employment[]; + education: Education[]; +} diff --git a/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.html b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.html new file mode 100644 index 000000000..86805253b --- /dev/null +++ b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.html @@ -0,0 +1,30 @@ +
+ + +
+ + @if (isMedium()) { + + @for (item of tabOptions; track $index) { + {{ item.label | translate }} + } + + } + + + @if (!isMedium()) { + + } + + + + + + + + + + + +
+
diff --git a/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.scss b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.scss new file mode 100644 index 000000000..069a5eb7c --- /dev/null +++ b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.scss @@ -0,0 +1,3 @@ +:host { + flex: 1 1 0%; +} diff --git a/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.spec.ts b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.spec.ts new file mode 100644 index 000000000..b8a38d5f5 --- /dev/null +++ b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CollectionModerationComponent } from './collection-moderation.component'; + +describe('CollectionModerationComponent', () => { + let component: CollectionModerationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CollectionModerationComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CollectionModerationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.ts b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.ts new file mode 100644 index 000000000..8c4ad1a3f --- /dev/null +++ b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.ts @@ -0,0 +1,45 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Tab, TabList, TabPanel, TabPanels, Tabs } from 'primeng/tabs'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; + +import { SelectComponent, SubHeaderComponent } from '@osf/shared/components'; +import { IS_MEDIUM } from '@osf/shared/utils'; + +import { CollectionModerationSettingsComponent, CollectionModeratorsComponent } from '../../components'; +import { COLLECTION_MODERATION_TABS } from '../../constants'; +import { CollectionModerationTab } from '../../enums'; + +@Component({ + selector: 'osf-collection-moderation', + imports: [ + SubHeaderComponent, + TabList, + Tabs, + Tab, + TabPanel, + TabPanels, + TranslatePipe, + FormsModule, + SelectComponent, + CollectionModerationSettingsComponent, + CollectionModeratorsComponent, + ], + templateUrl: './collection-moderation.component.html', + styleUrl: './collection-moderation.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CollectionModerationComponent { + readonly tabOptions = COLLECTION_MODERATION_TABS; + readonly tabs = CollectionModerationTab; + readonly isMedium = toSignal(inject(IS_MEDIUM)); + + selectedTab = this.tabs.AllItems; + + onTabChange(index: number): void { + this.selectedTab = index; + } +} diff --git a/src/app/features/moderation/pages/index.ts b/src/app/features/moderation/pages/index.ts new file mode 100644 index 000000000..b2898d7fe --- /dev/null +++ b/src/app/features/moderation/pages/index.ts @@ -0,0 +1 @@ +export { CollectionModerationComponent } from './collection-moderation/collection-moderation.component'; diff --git a/src/app/features/moderation/services/index.ts b/src/app/features/moderation/services/index.ts new file mode 100644 index 000000000..63cc0e7c4 --- /dev/null +++ b/src/app/features/moderation/services/index.ts @@ -0,0 +1 @@ +export { ModerationService } from './moderation.service'; diff --git a/src/app/features/moderation/services/moderation.service.ts b/src/app/features/moderation/services/moderation.service.ts new file mode 100644 index 000000000..4b61b638e --- /dev/null +++ b/src/app/features/moderation/services/moderation.service.ts @@ -0,0 +1,24 @@ +import { map, Observable } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { JsonApiService } from '@osf/core/services'; + +import { ModerationMapper } from '../mappers'; +import { Moderator, ModeratorResponseJsonApi } from '../models'; + +import { environment } from 'src/environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class ModerationService { + private readonly baseUrl = environment.apiUrl; + private readonly jsonApiService = inject(JsonApiService); + + getCollectionModerators(providerId: string): Observable { + return this.jsonApiService + .get(`${this.baseUrl}/providers/collections/${providerId}/moderators/`) + .pipe(map((response) => response.data.map((moderator) => ModerationMapper.fromModeratorResponse(moderator)))); + } +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 72faf4dba..b4a021222 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -33,6 +33,7 @@ "myProjects": "My Projects", "preprints": "Preprints", "collections": "Collections", + "moderation": "Moderation", "donate": "Donate", "profileSettings": "Profile Settings", "accountSettings": "Account Settings", @@ -773,6 +774,24 @@ "dateModified": "Date Modified:" } }, + "moderation": { + "allItems": "All Items", + "moderators": "Moderators", + "settings": "Settings", + "settingsMessage": "To configure your notification preferences visit your", + "userSettings": "User settings", + "addModerator": "Add moderator", + "removeDialog": { + "title": "Remove moderator", + "message": "Are you sure you want to remove {{name}} moderator?" + }, + "toastMessages": { + "addSuccessMessage": "Moderator {{name}} successfully added.", + "multipleAddSuccessMessage": "Moderators successfully added.", + "multipleUpdateSuccessMessage": "Moderators successfully updated.", + "deleteSuccessMessage": "Moderator {{name}} successfully removed." + } + }, "settings": { "developerApps": { "header": { From d0d13c2888748c1ab1c138f4d1354b71dbd425f6 Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 11 Jun 2025 16:35:31 +0300 Subject: [PATCH 4/7] feat(199): add moderation state and mock moderators --- .../collections/collections.routes.ts | 5 + .../collection-moderators-list.component.html | 5 +- .../collection-moderators.component.ts | 26 +- .../moderation/mappers/moderation.mapper.ts | 9 +- .../models/moderator-json-api.model.ts | 241 +----------------- .../moderation/services/moderation.service.ts | 10 +- src/app/features/moderation/store/index.ts | 4 + .../moderation/store/moderation.actions.ts | 42 +++ .../moderation/store/moderation.model.ts | 11 + .../moderation/store/moderation.selectors.ts | 25 ++ .../moderation/store/moderation.state.ts | 64 +++++ .../contributors-list.component.html | 5 +- .../components/select/select.component.html | 3 +- .../components/select/select.component.ts | 1 + src/assets/collection-moderators.json | 236 +++++++++++++++++ 15 files changed, 434 insertions(+), 253 deletions(-) create mode 100644 src/app/features/moderation/store/index.ts create mode 100644 src/app/features/moderation/store/moderation.actions.ts create mode 100644 src/app/features/moderation/store/moderation.model.ts create mode 100644 src/app/features/moderation/store/moderation.selectors.ts create mode 100644 src/app/features/moderation/store/moderation.state.ts create mode 100644 src/assets/collection-moderators.json diff --git a/src/app/features/collections/collections.routes.ts b/src/app/features/collections/collections.routes.ts index 3dc1c1f63..5b3337107 100644 --- a/src/app/features/collections/collections.routes.ts +++ b/src/app/features/collections/collections.routes.ts @@ -1,7 +1,11 @@ +import { provideStates } from '@ngxs/store'; + import { Routes } from '@angular/router'; import { CollectionsComponent } from '@osf/features/collections/collections.component'; +import { ModerationState } from '../moderation/store'; + export const collectionsRoutes: Routes = [ { path: '', @@ -17,6 +21,7 @@ export const collectionsRoutes: Routes = [ import('@osf/features/moderation/pages/collection-moderation/collection-moderation.component').then( (m) => m.CollectionModerationComponent ), + providers: [provideStates([ModerationState])], }, ], }, diff --git a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html index 47246c50f..4e6dc8769 100644 --- a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html +++ b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html @@ -73,12 +73,13 @@

{{ 'project.contributors.permissionInfo.title' | translate }}

- @if (item.employment) { + @if (item.employment?.length) { {{ 'project.contributors.permissionInfo.title' | translate }}
- @if (item.education) { + @if (item.education?.length) { { - this.moderators.set(JSON.parse(JSON.stringify(this.initialContributors()))); + this.moderators.set(JSON.parse(JSON.stringify(this.initialModerators()))); if (this.isModeratorsLoading()) { this.searchControl.disable(); @@ -56,6 +74,8 @@ export class CollectionModeratorsComponent implements OnInit { ngOnInit(): void { this.setSearchSubscription(); + + this.actions.loadModerators(''); } addModerator() { diff --git a/src/app/features/moderation/mappers/moderation.mapper.ts b/src/app/features/moderation/mappers/moderation.mapper.ts index 3585d01cb..45641a8d7 100644 --- a/src/app/features/moderation/mappers/moderation.mapper.ts +++ b/src/app/features/moderation/mappers/moderation.mapper.ts @@ -1,14 +1,15 @@ +import { ModeratorPermission } from '../enums'; import { Moderator, ModeratorDataJsonApi } from '../models'; export class ModerationMapper { static fromModeratorResponse(response: ModeratorDataJsonApi): Moderator { return { id: response.id, - userId: response.embeds.user.id, + userId: response.embeds.user.data.id, fullName: response.attributes.full_name, - permission: response.attributes.permission_group, - employment: response.embeds.user.attributes.employment || [], - education: response.embeds.user.attributes.education || [], + permission: response.attributes.permission_group as ModeratorPermission, + employment: response.embeds.user.data.attributes.employment || [], + education: response.embeds.user.data.attributes.education || [], }; } } diff --git a/src/app/features/moderation/models/moderator-json-api.model.ts b/src/app/features/moderation/models/moderator-json-api.model.ts index 57f998d48..16114cb67 100644 --- a/src/app/features/moderation/models/moderator-json-api.model.ts +++ b/src/app/features/moderation/models/moderator-json-api.model.ts @@ -14,242 +14,7 @@ interface ModeratorAttributesJsonApi { } interface ModeratorEmbedsJsonApi { - user: UserGetResponse; + user: { + data: UserGetResponse; + }; } - -// { -// "data": [ -// { -// "id": "nmwt5", -// "type": "moderators", -// "attributes": { -// "full_name": "Blaine Butler", -// "permission_group": "admin" -// }, -// "relationships": { -// "user": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/users/nmwt5/", -// "meta": {} -// } -// }, -// "data": { -// "id": "nmwt5", -// "type": "users" -// } -// }, -// "provider": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/providers/collections/colmod/", -// "meta": {} -// } -// }, -// "data": { -// "id": "colmod", -// "type": "collection-providers" -// } -// } -// }, -// "embeds": { -// "user": { -// "data": { -// "id": "nmwt5", -// "type": "users", -// "attributes": { -// "full_name": "Blaine Butler", -// "given_name": "Blaine", -// "middle_names": "", -// "family_name": "Butler", -// "suffix": "", -// "date_registered": "2022-11-03T19:23:28.110924Z", -// "active": false, -// "timezone": "Etc/UTC", -// "locale": "en_US", -// "social": {}, -// "employment": [], -// "education": [] -// }, -// "relationships": { -// "nodes": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/users/nmwt5/nodes/", -// "meta": {} -// } -// } -// }, -// "groups": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/users/nmwt5/groups/", -// "meta": {} -// } -// } -// }, -// "registrations": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/users/nmwt5/registrations/", -// "meta": {} -// } -// } -// }, -// "institutions": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/users/nmwt5/institutions/", -// "meta": {} -// }, -// "self": { -// "href": "https://api.staging.osf.io/v2/users/nmwt5/relationships/institutions/", -// "meta": {} -// } -// } -// }, -// "preprints": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/users/nmwt5/preprints/", -// "meta": {} -// } -// } -// } -// }, -// "links": { -// "html": "https://staging.osf.io/nmwt5/", -// "profile_image": "https://secure.gravatar.com/avatar/4a1f62c6580a151e5c1c0aec72b7fc2a?d=identicon", -// "self": "https://api.staging.osf.io/v2/users/nmwt5/" -// } -// } -// } -// }, -// "links": { -// "self": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/nmwt5/" -// } -// }, -// { -// "id": "m8ku3", -// "type": "moderators", -// "attributes": { -// "full_name": "DC Test - AMC", -// "permission_group": "admin" -// }, -// "relationships": { -// "user": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/users/m8ku3/", -// "meta": {} -// } -// }, -// "data": { -// "id": "m8ku3", -// "type": "users" -// } -// }, -// "provider": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/providers/collections/colmod/", -// "meta": {} -// } -// }, -// "data": { -// "id": "colmod", -// "type": "collection-providers" -// } -// } -// }, -// "embeds": { -// "user": { -// "data": { -// "id": "m8ku3", -// "type": "users", -// "attributes": { -// "full_name": "DC Test - AMC", -// "given_name": "DC", -// "middle_names": "Test -", -// "family_name": "AMC", -// "suffix": "", -// "date_registered": "2022-09-14T11:28:08.681787Z", -// "active": true, -// "timezone": "Etc/UTC", -// "locale": "en_US", -// "social": {}, -// "employment": [], -// "education": [] -// }, -// "relationships": { -// "nodes": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/users/m8ku3/nodes/", -// "meta": {} -// } -// } -// }, -// "groups": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/users/m8ku3/groups/", -// "meta": {} -// } -// } -// }, -// "registrations": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/users/m8ku3/registrations/", -// "meta": {} -// } -// } -// }, -// "institutions": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/users/m8ku3/institutions/", -// "meta": {} -// }, -// "self": { -// "href": "https://api.staging.osf.io/v2/users/m8ku3/relationships/institutions/", -// "meta": {} -// } -// } -// }, -// "preprints": { -// "links": { -// "related": { -// "href": "https://api.staging.osf.io/v2/users/m8ku3/preprints/", -// "meta": {} -// } -// } -// } -// }, -// "links": { -// "html": "https://staging.osf.io/m8ku3/", -// "profile_image": "https://secure.gravatar.com/avatar/ce38ca4e4a1361446468960716f57b5e?d=identicon", -// "self": "https://api.staging.osf.io/v2/users/m8ku3/" -// } -// } -// } -// }, -// "links": { -// "self": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/m8ku3/" -// } -// } -// ], -// "meta": { -// "total": 8, -// "per_page": 2, -// "version": "2.20" -// }, -// "links": { -// "self": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/?page%5Bsize%5D=2", -// "first": null, -// "last": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/?page=4&page%5Bsize%5D=2", -// "prev": null, -// "next": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/?page=2&page%5Bsize%5D=2" -// } -// } diff --git a/src/app/features/moderation/services/moderation.service.ts b/src/app/features/moderation/services/moderation.service.ts index 4b61b638e..f5c73fe5d 100644 --- a/src/app/features/moderation/services/moderation.service.ts +++ b/src/app/features/moderation/services/moderation.service.ts @@ -14,11 +14,15 @@ import { environment } from 'src/environments/environment'; }) export class ModerationService { private readonly baseUrl = environment.apiUrl; + private readonly tesModeratorsUrl = 'assets/collection-moderators.json'; private readonly jsonApiService = inject(JsonApiService); getCollectionModerators(providerId: string): Observable { - return this.jsonApiService - .get(`${this.baseUrl}/providers/collections/${providerId}/moderators/`) - .pipe(map((response) => response.data.map((moderator) => ModerationMapper.fromModeratorResponse(moderator)))); + return ( + this.jsonApiService + // .get(`${this.baseUrl}/providers/collections/${providerId}/moderators/`) + .get(this.tesModeratorsUrl) + .pipe(map((response) => response.data.map((moderator) => ModerationMapper.fromModeratorResponse(moderator)))) + ); } } diff --git a/src/app/features/moderation/store/index.ts b/src/app/features/moderation/store/index.ts new file mode 100644 index 000000000..7a6bd71e5 --- /dev/null +++ b/src/app/features/moderation/store/index.ts @@ -0,0 +1,4 @@ +export * from './moderation.actions'; +export * from './moderation.model'; +export * from './moderation.selectors'; +export * from './moderation.state'; diff --git a/src/app/features/moderation/store/moderation.actions.ts b/src/app/features/moderation/store/moderation.actions.ts new file mode 100644 index 000000000..969a39cb3 --- /dev/null +++ b/src/app/features/moderation/store/moderation.actions.ts @@ -0,0 +1,42 @@ +import { Moderator } from '../models'; + +const ACTION_SCOPE = '[Moderation]'; + +export class LoadCollectionModerators { + static readonly type = `${ACTION_SCOPE} Load Collection Moderators`; + + constructor(public providerId: string) {} +} + +export class AddCollectionModerator { + static readonly type = `${ACTION_SCOPE} Add Collection Moderator`; + + constructor( + public projectId: string, + public moderator: Moderator + ) {} +} + +export class UpdateCollectionModerator { + static readonly type = `${ACTION_SCOPE} Update Collection Moderator`; + + constructor( + public projectId: string, + public moderator: Moderator + ) {} +} + +export class DeleteCollectionModerator { + static readonly type = `${ACTION_SCOPE} Delete Collection Moderator`; + + constructor( + public projectId: string, + public moderatorId: string + ) {} +} + +export class UpdateCollectionSearchValue { + static readonly type = `${ACTION_SCOPE} Update Collection Search Value`; + + constructor(public searchValue: string | null) {} +} diff --git a/src/app/features/moderation/store/moderation.model.ts b/src/app/features/moderation/store/moderation.model.ts new file mode 100644 index 000000000..12a04f80b --- /dev/null +++ b/src/app/features/moderation/store/moderation.model.ts @@ -0,0 +1,11 @@ +import { AsyncStateModel } from '@osf/shared/models'; + +import { Moderator } from '../models'; + +interface ModerationDataStateModel extends AsyncStateModel { + searchValue: string | null; +} + +export interface ModerationStateModel { + collectionModerators: ModerationDataStateModel; +} diff --git a/src/app/features/moderation/store/moderation.selectors.ts b/src/app/features/moderation/store/moderation.selectors.ts new file mode 100644 index 000000000..b18a6e147 --- /dev/null +++ b/src/app/features/moderation/store/moderation.selectors.ts @@ -0,0 +1,25 @@ +import { Selector } from '@ngxs/store'; + +import { ModerationStateModel } from './moderation.model'; +import { ModerationState } from './moderation.state'; + +export class ModerationSelectors { + @Selector([ModerationState]) + static getCollectionModerators(state: ModerationStateModel) { + return state.collectionModerators.data.filter((moderator) => { + return state.collectionModerators.searchValue + ? moderator.fullName.toLowerCase().includes(state.collectionModerators.searchValue.toLowerCase()) + : true; + }); + } + + @Selector([ModerationState]) + static isModeratorsLoading(state: ModerationStateModel) { + return state.collectionModerators.isLoading || false; + } + + @Selector([ModerationState]) + static isModeratorsError(state: ModerationStateModel) { + return !!state.collectionModerators.error?.length; + } +} diff --git a/src/app/features/moderation/store/moderation.state.ts b/src/app/features/moderation/store/moderation.state.ts new file mode 100644 index 000000000..44c2b04ac --- /dev/null +++ b/src/app/features/moderation/store/moderation.state.ts @@ -0,0 +1,64 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { catchError, tap, throwError } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { ModerationService } from '../services'; + +import { LoadCollectionModerators } from './moderation.actions'; +import { ModerationStateModel } from './moderation.model'; + +@State({ + name: 'moderation', + defaults: { + collectionModerators: { + data: [], + isLoading: false, + error: null, + searchValue: null, + }, + }, +}) +@Injectable() +export class ModerationState { + private readonly moderationService = inject(ModerationService); + + @Action(LoadCollectionModerators) + loadCollectionModerators(ctx: StateContext, action: LoadCollectionModerators) { + const state = ctx.getState(); + + ctx.patchState({ + collectionModerators: { + ...state.collectionModerators, + isLoading: true, + }, + }); + + return this.moderationService.getCollectionModerators(action.providerId).pipe( + tap((moderators) => { + const state = ctx.getState(); + + ctx.patchState({ + collectionModerators: { + ...state.collectionModerators, + data: moderators, + isLoading: false, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'collectionModerators', error)) + ); + } + + private handleError(ctx: StateContext, section: keyof ModerationStateModel, error: Error) { + ctx.patchState({ + [section]: { + ...ctx.getState()[section], + isLoading: false, + error: error.message, + }, + }); + return throwError(() => error); + } +} diff --git a/src/app/features/project/contributors/components/contributors-list/contributors-list.component.html b/src/app/features/project/contributors/components/contributors-list/contributors-list.component.html index 5389de277..60fedef2d 100644 --- a/src/app/features/project/contributors/components/contributors-list/contributors-list.component.html +++ b/src/app/features/project/contributors/components/contributors-list/contributors-list.component.html @@ -102,6 +102,7 @@

{{ 'project.contributors.curatorInfo.heading' | translate }}

@@ -117,7 +118,7 @@

{{ 'project.contributors.curatorInfo.heading' | translate }}

- @if (contributor.employment) { + @if (contributor.employment?.length) {
{{ 'project.contributors.curatorInfo.heading' | translate }}
- @if (contributor.education) { + @if (contributor.education?.length) { diff --git a/src/app/shared/components/select/select.component.ts b/src/app/shared/components/select/select.component.ts index 70c54115e..4698ceee5 100644 --- a/src/app/shared/components/select/select.component.ts +++ b/src/app/shared/components/select/select.component.ts @@ -18,4 +18,5 @@ export class SelectComponent { options = input.required(); selectedValue = model.required(); placeholder = input(''); + appendTo = input(null); } diff --git a/src/assets/collection-moderators.json b/src/assets/collection-moderators.json new file mode 100644 index 000000000..48eb3f5a8 --- /dev/null +++ b/src/assets/collection-moderators.json @@ -0,0 +1,236 @@ +{ + "data": [ + { + "id": "nmwt5", + "type": "moderators", + "attributes": { + "full_name": "Blaine Butler", + "permission_group": "admin" + }, + "relationships": { + "user": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/users/nmwt5/", + "meta": {} + } + }, + "data": { + "id": "nmwt5", + "type": "users" + } + }, + "provider": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/providers/collections/colmod/", + "meta": {} + } + }, + "data": { + "id": "colmod", + "type": "collection-providers" + } + } + }, + "embeds": { + "user": { + "data": { + "id": "nmwt5", + "type": "users", + "attributes": { + "full_name": "Blaine Butler", + "given_name": "Blaine", + "middle_names": "", + "family_name": "Butler", + "suffix": "", + "date_registered": "2022-11-03T19:23:28.110924Z", + "active": false, + "timezone": "Etc/UTC", + "locale": "en_US", + "social": {}, + "employment": [], + "education": [] + }, + "relationships": { + "nodes": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/users/nmwt5/nodes/", + "meta": {} + } + } + }, + "groups": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/users/nmwt5/groups/", + "meta": {} + } + } + }, + "registrations": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/users/nmwt5/registrations/", + "meta": {} + } + } + }, + "institutions": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/users/nmwt5/institutions/", + "meta": {} + }, + "self": { + "href": "https://api.staging.osf.io/v2/users/nmwt5/relationships/institutions/", + "meta": {} + } + } + }, + "preprints": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/users/nmwt5/preprints/", + "meta": {} + } + } + } + }, + "links": { + "html": "https://staging.osf.io/nmwt5/", + "profile_image": "https://secure.gravatar.com/avatar/4a1f62c6580a151e5c1c0aec72b7fc2a?d=identicon", + "self": "https://api.staging.osf.io/v2/users/nmwt5/" + } + } + } + }, + "links": { + "self": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/nmwt5/" + } + }, + { + "id": "m8ku3", + "type": "moderators", + "attributes": { + "full_name": "DC Test - AMC", + "permission_group": "admin" + }, + "relationships": { + "user": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/users/m8ku3/", + "meta": {} + } + }, + "data": { + "id": "m8ku3", + "type": "users" + } + }, + "provider": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/providers/collections/colmod/", + "meta": {} + } + }, + "data": { + "id": "colmod", + "type": "collection-providers" + } + } + }, + "embeds": { + "user": { + "data": { + "id": "m8ku3", + "type": "users", + "attributes": { + "full_name": "DC Test - AMC", + "given_name": "DC", + "middle_names": "Test -", + "family_name": "AMC", + "suffix": "", + "date_registered": "2022-09-14T11:28:08.681787Z", + "active": true, + "timezone": "Etc/UTC", + "locale": "en_US", + "social": {}, + "employment": [], + "education": [] + }, + "relationships": { + "nodes": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/users/m8ku3/nodes/", + "meta": {} + } + } + }, + "groups": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/users/m8ku3/groups/", + "meta": {} + } + } + }, + "registrations": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/users/m8ku3/registrations/", + "meta": {} + } + } + }, + "institutions": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/users/m8ku3/institutions/", + "meta": {} + }, + "self": { + "href": "https://api.staging.osf.io/v2/users/m8ku3/relationships/institutions/", + "meta": {} + } + } + }, + "preprints": { + "links": { + "related": { + "href": "https://api.staging.osf.io/v2/users/m8ku3/preprints/", + "meta": {} + } + } + } + }, + "links": { + "html": "https://staging.osf.io/m8ku3/", + "profile_image": "https://secure.gravatar.com/avatar/ce38ca4e4a1361446468960716f57b5e?d=identicon", + "self": "https://api.staging.osf.io/v2/users/m8ku3/" + } + } + } + }, + "links": { + "self": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/m8ku3/" + } + } + ], + "meta": { + "total": 8, + "per_page": 2, + "version": "2.20" + }, + "links": { + "self": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/?page%5Bsize%5D=2", + "first": null, + "last": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/?page=4&page%5Bsize%5D=2", + "prev": null, + "next": "https://api.staging.osf.io/v2/providers/collections/colmod/moderators/?page=2&page%5Bsize%5D=2" + } +} From aac4d99b1f5073de9a94cdd15d607b168c42229d Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 12 Jun 2025 17:23:30 +0300 Subject: [PATCH 5/7] feat(199): added all items list --- ...tion-moderation-submissions.component.html | 30 ++++++++ ...tion-moderation-submissions.component.scss | 15 ++++ ...n-moderation-submissions.component.spec.ts | 22 ++++++ ...ection-moderation-submissions.component.ts | 39 ++++++++++ .../collection-moderators-list.component.html | 2 +- .../features/moderation/components/index.ts | 3 + .../submission-item.component.html | 16 ++++ .../submission-item.component.scss | 15 ++++ .../submission-item.component.spec.ts | 22 ++++++ .../submission-item.component.ts | 23 ++++++ .../submissions-list.component.html | 7 ++ .../submissions-list.component.scss | 11 +++ .../submissions-list.component.spec.ts | 22 ++++++ .../submissions-list.component.ts | 15 ++++ .../moderation/components/test-data.ts | 37 +++++++++ .../features/moderation/constants/index.ts | 1 + .../moderation/constants/submission.const.ts | 43 +++++++++++ src/app/features/moderation/enums/index.ts | 1 + .../enums/submission-review-status.enum.ts | 6 ++ src/app/features/moderation/models/index.ts | 1 + .../moderation/models/submission.model.ts | 7 ++ .../collection-moderation.component.html | 7 +- .../collection-moderation.component.ts | 7 +- .../my-projects-table.component.html | 2 +- .../components/select/select.component.html | 3 +- .../components/select/select.component.ts | 7 +- src/app/shared/constants/index.ts | 1 + .../shared/constants/sort-options.const.ts | 21 ++++++ src/app/shared/enums/index.ts | 1 + src/app/shared/enums/sort-type.enum.ts | 6 ++ src/assets/i18n/en.json | 75 +++---------------- .../styles/overrides/button-toggle.scss | 25 +++++++ src/assets/styles/overrides/select.scss | 2 +- src/assets/styles/styles.scss | 1 + 34 files changed, 424 insertions(+), 72 deletions(-) create mode 100644 src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.html create mode 100644 src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.scss create mode 100644 src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts create mode 100644 src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.ts create mode 100644 src/app/features/moderation/components/submission-item/submission-item.component.html create mode 100644 src/app/features/moderation/components/submission-item/submission-item.component.scss create mode 100644 src/app/features/moderation/components/submission-item/submission-item.component.spec.ts create mode 100644 src/app/features/moderation/components/submission-item/submission-item.component.ts create mode 100644 src/app/features/moderation/components/submissions-list/submissions-list.component.html create mode 100644 src/app/features/moderation/components/submissions-list/submissions-list.component.scss create mode 100644 src/app/features/moderation/components/submissions-list/submissions-list.component.spec.ts create mode 100644 src/app/features/moderation/components/submissions-list/submissions-list.component.ts create mode 100644 src/app/features/moderation/components/test-data.ts create mode 100644 src/app/features/moderation/constants/submission.const.ts create mode 100644 src/app/features/moderation/enums/submission-review-status.enum.ts create mode 100644 src/app/features/moderation/models/submission.model.ts create mode 100644 src/app/shared/constants/sort-options.const.ts create mode 100644 src/app/shared/enums/sort-type.enum.ts create mode 100644 src/assets/styles/overrides/button-toggle.scss diff --git a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.html b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.html new file mode 100644 index 000000000..85b4740fa --- /dev/null +++ b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.html @@ -0,0 +1,30 @@ +
+
+ + + +

{{ totalCount }}

+

{{ item.label | translate }}

+
+
+
+ +
+ +
+
+ + diff --git a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.scss b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.scss new file mode 100644 index 000000000..baf8accf8 --- /dev/null +++ b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.scss @@ -0,0 +1,15 @@ +.pending { + color: var(--yellow-1); +} + +.accepted { + color: var(--green-1); +} + +.rejected { + color: var(--red-1); +} + +.withdrawn { + color: var(--dark-blue-1); +} diff --git a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts new file mode 100644 index 000000000..4439b4b98 --- /dev/null +++ b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CollectionModerationSubmissionsComponent } from './collection-moderation-submissions.component'; + +describe('CollectionModerationSubmissionsComponent', () => { + let component: CollectionModerationSubmissionsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CollectionModerationSubmissionsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CollectionModerationSubmissionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.ts b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.ts new file mode 100644 index 000000000..fa86117c3 --- /dev/null +++ b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.ts @@ -0,0 +1,39 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { SelectButton } from 'primeng/selectbutton'; + +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { Primitive } from '@osf/core/helpers'; +import { IconComponent, SelectComponent } from '@osf/shared/components'; +import { ALL_SORT_OPTIONS } from '@osf/shared/constants'; + +import { SUBMISSION_REVIEW_OPTIONS } from '../../constants'; +import { SubmissionReviewStatus } from '../../enums'; +import { SubmissionsListComponent } from '../submissions-list/submissions-list.component'; + +@Component({ + selector: 'osf-collection-moderation-submissions', + imports: [SelectButton, TranslatePipe, FormsModule, SelectComponent, SubmissionsListComponent, IconComponent], + templateUrl: './collection-moderation-submissions.component.html', + styleUrl: './collection-moderation-submissions.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CollectionModerationSubmissionsComponent { + readonly submissionReviewOptions = SUBMISSION_REVIEW_OPTIONS; + + sortOptions = ALL_SORT_OPTIONS; + selectedSortOption = signal(null); + selectedReviewOption = this.submissionReviewOptions[0].value; + + totalCount = 5; + + changeReviewStatus(value: SubmissionReviewStatus) { + console.log(value); + } + + changeSort(value: Primitive) { + console.log(value); + } +} diff --git a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html index 4e6dc8769..a2093fcd3 100644 --- a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html +++ b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html @@ -124,7 +124,7 @@

{{ 'project.contributors.permissionInfo.title' | translate }}

- {{ 'project.contributors.table.emptyMessage' | translate }} + {{ 'common.search.noResultsFound' | translate }} diff --git a/src/app/features/moderation/components/index.ts b/src/app/features/moderation/components/index.ts index 4a4c8a15d..2a1b15400 100644 --- a/src/app/features/moderation/components/index.ts +++ b/src/app/features/moderation/components/index.ts @@ -1,3 +1,6 @@ export { CollectionModerationSettingsComponent } from './collection-moderation-settings/collection-moderation-settings.component'; +export { CollectionModerationSubmissionsComponent } from './collection-moderation-submissions/collection-moderation-submissions.component'; export { CollectionModeratorsComponent } from './collection-moderators/collection-moderators.component'; export { CollectionModeratorsListComponent } from './collection-moderators-list/collection-moderators-list.component'; +export { SubmissionItemComponent } from './submission-item/submission-item.component'; +export { SubmissionsListComponent } from './submissions-list/submissions-list.component'; diff --git a/src/app/features/moderation/components/submission-item/submission-item.component.html b/src/app/features/moderation/components/submission-item/submission-item.component.html new file mode 100644 index 000000000..3007cb633 --- /dev/null +++ b/src/app/features/moderation/components/submission-item/submission-item.component.html @@ -0,0 +1,16 @@ +
+
+ + {{ submission().name }} +
+ +

+ {{ 'moderation.submissionReview.submitted' | translate }} + {{ submission().dateSubmitted }} + {{ 'moderation.submissionReview.by' | translate }} + {{ submission().submittedBy }} +

+
diff --git a/src/app/features/moderation/components/submission-item/submission-item.component.scss b/src/app/features/moderation/components/submission-item/submission-item.component.scss new file mode 100644 index 000000000..baf8accf8 --- /dev/null +++ b/src/app/features/moderation/components/submission-item/submission-item.component.scss @@ -0,0 +1,15 @@ +.pending { + color: var(--yellow-1); +} + +.accepted { + color: var(--green-1); +} + +.rejected { + color: var(--red-1); +} + +.withdrawn { + color: var(--dark-blue-1); +} diff --git a/src/app/features/moderation/components/submission-item/submission-item.component.spec.ts b/src/app/features/moderation/components/submission-item/submission-item.component.spec.ts new file mode 100644 index 000000000..870d57d0d --- /dev/null +++ b/src/app/features/moderation/components/submission-item/submission-item.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubmissionItemComponent } from './submission-item.component'; + +describe('SubmissionItemComponent', () => { + let component: SubmissionItemComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SubmissionItemComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SubmissionItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/submission-item/submission-item.component.ts b/src/app/features/moderation/components/submission-item/submission-item.component.ts new file mode 100644 index 000000000..0d134acea --- /dev/null +++ b/src/app/features/moderation/components/submission-item/submission-item.component.ts @@ -0,0 +1,23 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { IconComponent } from '@osf/shared/components'; + +import { ReviewStatusIcon } from '../../constants'; +import { SubmissionReviewStatus } from '../../enums'; +import { Submission } from '../../models'; + +@Component({ + selector: 'osf-submission-item', + imports: [TranslatePipe, IconComponent], + templateUrl: './submission-item.component.html', + styleUrl: './submission-item.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SubmissionItemComponent { + submission = input.required(); + + readonly reviewStatusIcon = ReviewStatusIcon; + readonly selectedIcon = SubmissionReviewStatus.Pending; +} diff --git a/src/app/features/moderation/components/submissions-list/submissions-list.component.html b/src/app/features/moderation/components/submissions-list/submissions-list.component.html new file mode 100644 index 000000000..d20471122 --- /dev/null +++ b/src/app/features/moderation/components/submissions-list/submissions-list.component.html @@ -0,0 +1,7 @@ +
+ @for (item of pendingSubmissions; track $index) { +
+ +
+ } +
diff --git a/src/app/features/moderation/components/submissions-list/submissions-list.component.scss b/src/app/features/moderation/components/submissions-list/submissions-list.component.scss new file mode 100644 index 000000000..a51b1236f --- /dev/null +++ b/src/app/features/moderation/components/submissions-list/submissions-list.component.scss @@ -0,0 +1,11 @@ +@use "assets/styles/variables" as var; +@use "assets/styles/mixins" as mix; + +.submission-container { + border: 1px solid var(--grey-2); + border-radius: mix.rem(8px); +} + +.submission-item:not(:last-child) { + border-bottom: 1px solid var(--grey-2); +} diff --git a/src/app/features/moderation/components/submissions-list/submissions-list.component.spec.ts b/src/app/features/moderation/components/submissions-list/submissions-list.component.spec.ts new file mode 100644 index 000000000..6b65de3a6 --- /dev/null +++ b/src/app/features/moderation/components/submissions-list/submissions-list.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubmissionsListComponent } from './submissions-list.component'; + +describe('SubmissionsListComponent', () => { + let component: SubmissionsListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SubmissionsListComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SubmissionsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/submissions-list/submissions-list.component.ts b/src/app/features/moderation/components/submissions-list/submissions-list.component.ts new file mode 100644 index 000000000..562a838cb --- /dev/null +++ b/src/app/features/moderation/components/submissions-list/submissions-list.component.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +import { SubmissionItemComponent } from '../submission-item/submission-item.component'; +import { pendingReviews } from '../test-data'; + +@Component({ + selector: 'osf-submissions-list', + imports: [SubmissionItemComponent], + templateUrl: './submissions-list.component.html', + styleUrl: './submissions-list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SubmissionsListComponent { + pendingSubmissions = pendingReviews; +} diff --git a/src/app/features/moderation/components/test-data.ts b/src/app/features/moderation/components/test-data.ts new file mode 100644 index 000000000..8fa34e0d7 --- /dev/null +++ b/src/app/features/moderation/components/test-data.ts @@ -0,0 +1,37 @@ +export const pendingReviews = [ + { + id: 'p1', + name: 'Pending Review 1', + reviewStatus: 'pending', + dateSubmitted: '10 mins ago', + submittedBy: 'Alice Green', + }, + { + id: 'p2', + name: 'Pending Review 2', + reviewStatus: 'pending', + dateSubmitted: '33 mins ago', + submittedBy: 'Ben Carter', + }, + { + id: 'p3', + name: 'Pending Review 3', + reviewStatus: 'pending', + dateSubmitted: '2 hours ago', + submittedBy: 'Cara Kim', + }, + { + id: 'p4', + name: 'Pending Review 4', + reviewStatus: 'pending', + dateSubmitted: '1 day ago', + submittedBy: 'David Lin', + }, + { + id: 'p5', + name: 'Pending Review 5', + reviewStatus: 'pending', + dateSubmitted: '1 week ago', + submittedBy: 'Ella Nguyen', + }, +]; diff --git a/src/app/features/moderation/constants/index.ts b/src/app/features/moderation/constants/index.ts index b8135444f..d0cf06657 100644 --- a/src/app/features/moderation/constants/index.ts +++ b/src/app/features/moderation/constants/index.ts @@ -1 +1,2 @@ export * from './collection-moderation-tabs.const'; +export * from './submission.const'; diff --git a/src/app/features/moderation/constants/submission.const.ts b/src/app/features/moderation/constants/submission.const.ts new file mode 100644 index 000000000..57a086a65 --- /dev/null +++ b/src/app/features/moderation/constants/submission.const.ts @@ -0,0 +1,43 @@ +import { SubmissionReviewStatus } from '../enums'; + +export const SUBMISSION_REVIEW_OPTIONS = [ + { + value: SubmissionReviewStatus.Pending, + icon: 'fas fa-hourglass', + label: 'moderation.submissionReviewStatus.pending', + }, + { + value: SubmissionReviewStatus.Accepted, + icon: 'fas fa-circle-check', + label: 'moderation.submissionReviewStatus.accepted', + }, + { + value: SubmissionReviewStatus.Rejected, + icon: 'fas fa-circle-xmark', + label: 'moderation.submissionReviewStatus.rejected', + }, + { + value: SubmissionReviewStatus.Withdrawn, + icon: 'fas fa-circle-minus', + label: 'moderation.submissionReviewStatus.withdrawn', + }, +]; + +export const ReviewStatusIcon: Record = { + [SubmissionReviewStatus.Pending]: { + value: SubmissionReviewStatus.Pending, + icon: 'fas fa-hourglass', + }, + [SubmissionReviewStatus.Accepted]: { + value: SubmissionReviewStatus.Accepted, + icon: 'fas fa-circle-check', + }, + [SubmissionReviewStatus.Rejected]: { + value: SubmissionReviewStatus.Rejected, + icon: 'fas fa-circle-xmark', + }, + [SubmissionReviewStatus.Withdrawn]: { + value: SubmissionReviewStatus.Withdrawn, + icon: 'fas fa-circle-minus', + }, +}; diff --git a/src/app/features/moderation/enums/index.ts b/src/app/features/moderation/enums/index.ts index 47d2f091a..750874b0f 100644 --- a/src/app/features/moderation/enums/index.ts +++ b/src/app/features/moderation/enums/index.ts @@ -1,2 +1,3 @@ export * from './collection-moderation-tab.enum'; export * from './moderator-permission.enum'; +export * from './submission-review-status.enum'; diff --git a/src/app/features/moderation/enums/submission-review-status.enum.ts b/src/app/features/moderation/enums/submission-review-status.enum.ts new file mode 100644 index 000000000..e0f9faad3 --- /dev/null +++ b/src/app/features/moderation/enums/submission-review-status.enum.ts @@ -0,0 +1,6 @@ +export enum SubmissionReviewStatus { + Pending = 'pending', + Accepted = 'accepted', + Rejected = 'rejected', + Withdrawn = 'withdrawn', +} diff --git a/src/app/features/moderation/models/index.ts b/src/app/features/moderation/models/index.ts index c1fe9d67d..18c8d7254 100644 --- a/src/app/features/moderation/models/index.ts +++ b/src/app/features/moderation/models/index.ts @@ -1,2 +1,3 @@ export * from './moderator.model'; export * from './moderator-json-api.model'; +export * from './submission.model'; diff --git a/src/app/features/moderation/models/submission.model.ts b/src/app/features/moderation/models/submission.model.ts new file mode 100644 index 000000000..c3e06aadf --- /dev/null +++ b/src/app/features/moderation/models/submission.model.ts @@ -0,0 +1,7 @@ +export interface Submission { + id: string; + name: string; + reviewStatus: string; + dateSubmitted: string; + submittedBy: string; +} diff --git a/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.html b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.html index 86805253b..999ddfc4e 100644 --- a/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.html +++ b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.html @@ -13,10 +13,13 @@ @if (!isMedium()) { - + } - + + + + diff --git a/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.ts b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.ts index 8c4ad1a3f..29a65612b 100644 --- a/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.ts +++ b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.ts @@ -9,7 +9,11 @@ import { FormsModule } from '@angular/forms'; import { SelectComponent, SubHeaderComponent } from '@osf/shared/components'; import { IS_MEDIUM } from '@osf/shared/utils'; -import { CollectionModerationSettingsComponent, CollectionModeratorsComponent } from '../../components'; +import { + CollectionModerationSettingsComponent, + CollectionModerationSubmissionsComponent, + CollectionModeratorsComponent, +} from '../../components'; import { COLLECTION_MODERATION_TABS } from '../../constants'; import { CollectionModerationTab } from '../../enums'; @@ -27,6 +31,7 @@ import { CollectionModerationTab } from '../../enums'; SelectComponent, CollectionModerationSettingsComponent, CollectionModeratorsComponent, + CollectionModerationSubmissionsComponent, ], templateUrl: './collection-moderation.component.html', styleUrl: './collection-moderation.component.scss', diff --git a/src/app/shared/components/my-projects-table/my-projects-table.component.html b/src/app/shared/components/my-projects-table/my-projects-table.component.html index d1a10600c..0cbb0b763 100644 --- a/src/app/shared/components/my-projects-table/my-projects-table.component.html +++ b/src/app/shared/components/my-projects-table/my-projects-table.component.html @@ -77,7 +77,7 @@ - {{ 'project.contributors.table.emptyMessage' | translate }} + {{ 'common.search.noResultsFound' | translate }} diff --git a/src/app/shared/components/select/select.component.html b/src/app/shared/components/select/select.component.html index a2bfdbaa6..17a71494c 100644 --- a/src/app/shared/components/select/select.component.html +++ b/src/app/shared/components/select/select.component.html @@ -1,11 +1,12 @@ {{ selectedOption.label | translate }} diff --git a/src/app/shared/components/select/select.component.ts b/src/app/shared/components/select/select.component.ts index 4698ceee5..6b658c9a4 100644 --- a/src/app/shared/components/select/select.component.ts +++ b/src/app/shared/components/select/select.component.ts @@ -2,9 +2,10 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Select } from 'primeng/select'; -import { ChangeDetectionStrategy, Component, input, model } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input, model, output } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { Primitive } from '@osf/core/helpers'; import { SelectOption } from '@osf/shared/models'; @Component({ @@ -16,7 +17,9 @@ import { SelectOption } from '@osf/shared/models'; }) export class SelectComponent { options = input.required(); - selectedValue = model.required(); + selectedValue = model.required(); placeholder = input(''); appendTo = input(null); + changeValue = output(); + fullWidth = input(false); } diff --git a/src/app/shared/constants/index.ts b/src/app/shared/constants/index.ts index 9c28141dd..9ac25857b 100644 --- a/src/app/shared/constants/index.ts +++ b/src/app/shared/constants/index.ts @@ -2,3 +2,4 @@ export * from './input-limits.const'; export * from './input-validation-messages.const'; export * from './remove-nullable.const'; export * from './scientists.const'; +export * from './sort-options.const'; diff --git a/src/app/shared/constants/sort-options.const.ts b/src/app/shared/constants/sort-options.const.ts new file mode 100644 index 000000000..2ec4981a6 --- /dev/null +++ b/src/app/shared/constants/sort-options.const.ts @@ -0,0 +1,21 @@ +import { SortType } from '../enums'; +import { SelectOption } from '../models'; + +export const ALL_SORT_OPTIONS: SelectOption[] = [ + { + value: SortType.NameAZ, + label: 'project.files.sort.nameAZ', + }, + { + value: SortType.NameZA, + label: 'project.files.sort.nameZA', + }, + { + value: SortType.LastModifiedOldest, + label: 'project.files.sort.lastModifiedOldest', + }, + { + value: SortType.LastModifiedNewest, + label: 'project.files.sort.lastModifiedNewest', + }, +]; diff --git a/src/app/shared/enums/index.ts b/src/app/shared/enums/index.ts index 5b89b32f6..765fc7353 100644 --- a/src/app/shared/enums/index.ts +++ b/src/app/shared/enums/index.ts @@ -6,4 +6,5 @@ export * from './resource-tab.enum'; export * from './resource-type.enum'; export * from './share-indexing.enum'; export * from './sort-order.enum'; +export * from './sort-type.enum'; export * from './subscriptions'; diff --git a/src/app/shared/enums/sort-type.enum.ts b/src/app/shared/enums/sort-type.enum.ts new file mode 100644 index 000000000..e45dcd466 --- /dev/null +++ b/src/app/shared/enums/sort-type.enum.ts @@ -0,0 +1,6 @@ +export enum SortType { + NameAZ = 'name', + NameZA = '-name', + LastModifiedOldest = 'date_modified', + LastModifiedNewest = '-date_modified', +} diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index b4a021222..38b6f0c85 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -565,69 +565,6 @@ "openResources": "Open Resources" } }, - "files": { - "title": "Files", - "storageLocation": "OSF Storage", - "searchPlaceholder": "Search your projects", - "sort": { - "placeholder": "Sort", - "nameAZ": "Name: A-Z", - "nameZA": "Name: Z-A", - "lastModifiedOldest": "Last modified: oldest to newest", - "lastModifiedNewest": "Last modified: newest to oldest" - }, - "actions": { - "downloadAsZip": "Download As Zip", - "createFolder": "Create Folder", - "uploadFile": "Upload File" - }, - "dialogs": { - "uploadFile": { - "title": "Upload file" - }, - "createFolder": { - "title": "Create folder", - "folderName": "New folder name", - "folderNamePlaceholder": "Please enter a folder name" - }, - "renameFile": { - "title": "Rename file", - "renameLabel": "Please rename the file" - }, - "moveFile": "Cannot move to the same folder" - }, - "emptyState": "This folder is empty", - "detail": { - "backToList": "Back to list of files", - "fileMetadata": { - "title": "File Metadata", - "edit": "Edit", - "fields": { - "title": "Title", - "description": "Description", - "resourceType": "Resource Type", - "resourceLanguage": "Resource Language" - } - }, - "projectMetadata": { - "title": "Project Metadata", - "edit": "Edit", - "fields": { - "funder": "Funder", - "awardTitle": "Award title", - "awardNumber": "Award number", - "awardUri": "Award URI", - "title": "Title", - "description": "Description", - "resourceType": "Resource type", - "resourceLanguage": "Resource language", - "dateCreated": "Date created", - "dateModified": "Date modified", - "contributors": "Contributors" - } - } - } - }, "files": { "title": "Files", "storageLocation": "OSF Storage", @@ -790,6 +727,18 @@ "multipleAddSuccessMessage": "Moderators successfully added.", "multipleUpdateSuccessMessage": "Moderators successfully updated.", "deleteSuccessMessage": "Moderator {{name}} successfully removed." + }, + "submissionReviewStatus": { + "pending": "Pending", + "accepted": "Accepted", + "rejected": "Rejected", + "withdrawn": "Withdrawn" + }, + "submissionReview": { + "submitted": "Submitted", + "accepted": "Accepted", + "rejected": "Rejected", + "by": "by" } }, "settings": { diff --git a/src/assets/styles/overrides/button-toggle.scss b/src/assets/styles/overrides/button-toggle.scss new file mode 100644 index 000000000..db9126b9d --- /dev/null +++ b/src/assets/styles/overrides/button-toggle.scss @@ -0,0 +1,25 @@ +@use "assets/styles/mixins" as mix; + +.review-state-select { + --p-togglebutton-color: var(--dark-blue-1); + --p-togglebutton-background: transparent; + --p-togglebutton-border-color: transparent; + --p-togglebutton-checked-background: transparent; + --p-togglebutton-checked-border-color: transparent; + --p-togglebutton-content-checked-shadow: 0 0 4px 0 var(--grey-outline); + --p-togglebutton-content-padding: 0.625rem 0.75rem; + --p-togglebutton-hover-background: transparent; + + .p-togglebutton { + font-size: mix.rem(14px); + opacity: 0.6; + + &:hover { + opacity: 1; + } + + &.p-togglebutton-checked { + opacity: 1; + } + } +} diff --git a/src/assets/styles/overrides/select.scss b/src/assets/styles/overrides/select.scss index b09629282..a712f2a37 100644 --- a/src/assets/styles/overrides/select.scss +++ b/src/assets/styles/overrides/select.scss @@ -6,7 +6,7 @@ border: 1px solid var(--grey-2); border-radius: mix.rem(8px); outline: none; - font-size: mix.rem(16px); + font-size: mix.rem(14px); color: var.$dark-blue-1; .p-placeholder { diff --git a/src/assets/styles/styles.scss b/src/assets/styles/styles.scss index 57faab2c4..9a55318c2 100644 --- a/src/assets/styles/styles.scss +++ b/src/assets/styles/styles.scss @@ -7,6 +7,7 @@ @use "base"; @use "./overrides/button"; +@use "./overrides/button-toggle"; @use "./overrides/input"; @use "./overrides/checkbox"; @use "./overrides/divider"; From 27ada3b85fc5b8ffdefad50b830c80541055ee8d Mon Sep 17 00:00:00 2001 From: nsemets Date: Fri, 13 Jun 2025 13:35:58 +0300 Subject: [PATCH 6/7] feat(199): updated moderators list --- .../components/topnav/topnav.component.scss | 2 +- .../add-moderator-dialog.component.html | 62 ++++++++ .../add-moderator-dialog.component.scss | 17 ++ .../add-moderator-dialog.component.spec.ts | 22 +++ .../add-moderator-dialog.component.ts | 95 +++++++++++ .../collection-moderators-list.component.html | 35 +--- .../collection-moderators-list.component.scss | 13 -- .../collection-moderators-list.component.ts | 32 ++-- .../collection-moderators.component.html | 5 +- .../collection-moderators.component.ts | 99 +++++++++--- .../features/moderation/components/index.ts | 2 + .../invite-moderator-dialog.component.html | 74 +++++++++ .../invite-moderator-dialog.component.scss} | 0 .../invite-moderator-dialog.component.spec.ts | 22 +++ .../invite-moderator-dialog.component.ts | 74 +++++++++ .../collection-moderation-tabs.const.ts | 9 +- .../enums/add-moderator-type.enum.ts | 4 + src/app/features/moderation/enums/index.ts | 1 + .../moderation/mappers/moderation.mapper.ts | 23 ++- src/app/features/moderation/models/index.ts | 3 + .../models/invite-moderator-form.model.ts | 9 ++ .../moderation/models/moderator-add.model.ts | 8 + .../models/moderator-dialog-add.model.ts | 8 + .../moderation/models/moderator.model.ts | 2 +- .../collection-moderation.component.scss | 1 + .../moderation/services/moderation.service.ts | 31 +++- .../moderation/store/moderation.actions.ts | 26 ++- .../moderation/store/moderation.model.ts | 9 +- .../moderation/store/moderation.selectors.ts | 22 +++ .../moderation/store/moderation.state.ts | 150 ++++++++++++++++-- .../add-contributor-dialog.component.html | 4 +- ...gistered-contributor-dialog.component.html | 1 - ...ibutor-education-history.component.spec.ts | 22 --- ...butor-employment-history.component.spec.ts | 22 --- .../project/contributors/components/index.ts | 2 - .../contributors/contributors.component.ts | 13 +- .../education-history-dialog.component.html} | 0 .../education-history-dialog.component.scss} | 0 ...education-history-dialog.component.spec.ts | 22 +++ .../education-history-dialog.component.ts} | 8 +- .../employment-history-dialog.component.html} | 0 .../employment-history-dialog.component.scss | 0 ...mployment-history-dialog.component.spec.ts | 22 +++ .../employment-history-dialog.component.ts} | 8 +- .../full-screen-loader.component.scss | 2 +- src/app/shared/components/index.ts | 2 + src/assets/i18n/en.json | 13 +- src/assets/styles/overrides/select.scss | 6 +- 48 files changed, 826 insertions(+), 181 deletions(-) create mode 100644 src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.html create mode 100644 src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.scss create mode 100644 src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.spec.ts create mode 100644 src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.ts create mode 100644 src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.html rename src/app/features/{project/contributors/components/contributor-education-history/contributor-education-history.component.scss => moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.scss} (100%) create mode 100644 src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.spec.ts create mode 100644 src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.ts create mode 100644 src/app/features/moderation/enums/add-moderator-type.enum.ts create mode 100644 src/app/features/moderation/models/invite-moderator-form.model.ts create mode 100644 src/app/features/moderation/models/moderator-add.model.ts create mode 100644 src/app/features/moderation/models/moderator-dialog-add.model.ts delete mode 100644 src/app/features/project/contributors/components/contributor-education-history/contributor-education-history.component.spec.ts delete mode 100644 src/app/features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.spec.ts rename src/app/{features/project/contributors/components/contributor-education-history/contributor-education-history.component.html => shared/components/education-history-dialog/education-history-dialog.component.html} (100%) rename src/app/{features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.scss => shared/components/education-history-dialog/education-history-dialog.component.scss} (100%) create mode 100644 src/app/shared/components/education-history-dialog/education-history-dialog.component.spec.ts rename src/app/{features/project/contributors/components/contributor-education-history/contributor-education-history.component.ts => shared/components/education-history-dialog/education-history-dialog.component.ts} (77%) rename src/app/{features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.html => shared/components/employment-history-dialog/employment-history-dialog.component.html} (100%) create mode 100644 src/app/shared/components/employment-history-dialog/employment-history-dialog.component.scss create mode 100644 src/app/shared/components/employment-history-dialog/employment-history-dialog.component.spec.ts rename src/app/{features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.ts => shared/components/employment-history-dialog/employment-history-dialog.component.ts} (77%) diff --git a/src/app/core/components/topnav/topnav.component.scss b/src/app/core/components/topnav/topnav.component.scss index b18b8a565..2deb687b0 100644 --- a/src/app/core/components/topnav/topnav.component.scss +++ b/src/app/core/components/topnav/topnav.component.scss @@ -2,7 +2,7 @@ @use "assets/styles/mixins" as mix; :host { - z-index: 1300; + z-index: 1103; .nav-container { background: var.$dark-blue-1; diff --git a/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.html b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.html new file mode 100644 index 000000000..95951aaac --- /dev/null +++ b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.html @@ -0,0 +1,62 @@ +
+ + +
+ @if (isLoading()) { + + } @else { + @for (item of users(); track $index) { +
+ + +
+ } + + @if (!totalUsersCount() && !isInitialState()) { +
{{ 'common.search.noResultsFound' | translate }}
+ } + } +
+ + @if (totalUsersCount() > rows()) { + + } + +
+ +
+ +
+ + + + + +
+
diff --git a/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.scss b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.scss new file mode 100644 index 000000000..b4205d981 --- /dev/null +++ b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.scss @@ -0,0 +1,17 @@ +@use "assets/styles/variables" as var; +@use "assets/styles/mixins" as mix; + +.label { + color: var.$dark-blue-1; + margin: 0; + cursor: pointer; +} + +.users-list { + height: 30vh; + overflow: auto; +} + +.border-divider { + border-bottom: 1px solid var.$grey-2; +} diff --git a/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.spec.ts b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.spec.ts new file mode 100644 index 000000000..9f800d05e --- /dev/null +++ b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddModeratorDialogComponent } from './add-moderator-dialog.component'; + +describe('AddModeratorDialogComponent', () => { + let component: AddModeratorDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AddModeratorDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AddModeratorDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.ts b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.ts new file mode 100644 index 000000000..2f8b25351 --- /dev/null +++ b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.ts @@ -0,0 +1,95 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Checkbox } from 'primeng/checkbox'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { PaginatorState } from 'primeng/paginator'; + +import { debounceTime, distinctUntilChanged, filter, switchMap } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, DestroyRef, inject, OnDestroy, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl, FormsModule } from '@angular/forms'; + +import { CustomPaginatorComponent, LoadingSpinnerComponent, SearchInputComponent } from '@osf/shared/components'; + +import { AddModeratorType } from '../../enums'; +import { ModeratorAddModel, ModeratorDialogAddModel } from '../../models'; +import { ClearUsers, ModerationSelectors, SearchUsers } from '../../store'; + +@Component({ + selector: 'osf-add-moderator-dialog', + imports: [ + Button, + Checkbox, + FormsModule, + TranslatePipe, + SearchInputComponent, + LoadingSpinnerComponent, + CustomPaginatorComponent, + ], + templateUrl: './add-moderator-dialog.component.html', + styleUrl: './add-moderator-dialog.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AddModeratorDialogComponent implements OnInit, OnDestroy { + protected dialogRef = inject(DynamicDialogRef); + private readonly destroyRef = inject(DestroyRef); + readonly config = inject(DynamicDialogConfig); + + protected users = select(ModerationSelectors.getUsers); + protected isLoading = select(ModerationSelectors.isUsersLoading); + protected totalUsersCount = select(ModerationSelectors.getUsersTotalCount); + protected isInitialState = signal(true); + + protected currentPage = signal(1); + protected first = signal(0); + protected rows = signal(10); + + protected selectedUsers = signal([]); + protected searchControl = new FormControl(''); + + protected actions = createDispatchMap({ searchUsers: SearchUsers, clearUsers: ClearUsers }); + + ngOnInit(): void { + this.setSearchSubscription(); + this.selectedUsers.set([]); + } + + ngOnDestroy(): void { + this.actions.clearUsers(); + } + + addModerator(): void { + const dialogData: ModeratorDialogAddModel = { data: this.selectedUsers(), type: AddModeratorType.Search }; + this.dialogRef.close(dialogData); + } + + inviteModerator() { + const dialogData: ModeratorDialogAddModel = { data: [], type: AddModeratorType.Invite }; + this.dialogRef.close(dialogData); + } + + pageChanged(event: PaginatorState) { + this.currentPage.set(event.page ? this.currentPage() + 1 : 1); + this.first.set(event.first ?? 0); + this.actions.searchUsers(this.searchControl.value, this.currentPage()); + } + + private setSearchSubscription() { + this.searchControl.valueChanges + .pipe( + filter((searchTerm) => !!searchTerm && searchTerm.trim().length > 0), + debounceTime(500), + distinctUntilChanged(), + switchMap((searchTerm) => this.actions.searchUsers(searchTerm, this.currentPage())), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => { + this.isInitialState.set(false); + this.selectedUsers.set([]); + }); + } +} diff --git a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html index a2093fcd3..02cb0065f 100644 --- a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html +++ b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.html @@ -19,38 +19,6 @@
{{ 'project.contributors.table.headers.permissions' | translate }} - - -
-

{{ 'project.contributors.permissionInfo.title' | translate }}

- -
-

{{ 'project.contributors.permissions.read' | translate }}

-
    -
  • {{ 'project.contributors.permissionInfo.viewProjectContent' | translate }}
  • -
-
- -
-

{{ 'project.contributors.permissions.readAndWrite' | translate }}

-
    -
  • {{ 'project.contributors.permissionInfo.read' | translate }}
  • -
  • {{ 'project.contributors.permissionInfo.addComponents' | translate }}
  • -
  • {{ 'project.contributors.permissionInfo.editContent' | translate }}
  • -
-
- -
-

{{ 'project.contributors.permissions.administrator' | translate }}

-
    -
  • {{ 'project.contributors.permissionInfo.readWrite' | translate }}
  • -
  • {{ 'project.contributors.permissionInfo.manageContributors' | translate }}
  • -
  • {{ 'project.contributors.permissionInfo.deleteRegister' | translate }}
  • -
  • {{ 'project.contributors.permissionInfo.publicPrivate' | translate }}
  • -
-
-
-
@@ -75,6 +43,7 @@

{{ 'project.contributors.permissionInfo.title' | translate }}

[placeholder]="'project.contributors.permissionFilter'" [appendTo]="'body'" [(selectedValue)]="item.permission" + (changeValue)="updatePermission(item)" >
@@ -109,7 +78,7 @@

{{ 'project.contributors.permissionInfo.title' | translate }}

- + diff --git a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.scss b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.scss index 4816093ca..e69de29bb 100644 --- a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.scss +++ b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.scss @@ -1,13 +0,0 @@ -@use "assets/styles/variables" as var; -@use "assets/styles/mixins" as mix; - -.blue-icon { - margin-top: mix.rem(2px); - cursor: pointer; -} - -.inside-list { - li { - list-style: inside; - } -} diff --git a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.ts b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.ts index 524d4ff1a..374960557 100644 --- a/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.ts +++ b/src/app/features/moderation/components/collection-moderators-list/collection-moderators-list.component.ts @@ -3,47 +3,51 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Skeleton } from 'primeng/skeleton'; import { TableModule } from 'primeng/table'; -import { Tooltip } from 'primeng/tooltip'; import { ChangeDetectionStrategy, Component, input, output, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MY_PROJECTS_TABLE_PARAMS } from '@osf/core/constants'; -import { PERMISSION_OPTIONS } from '@osf/features/project/contributors/constants'; import { SelectComponent } from '@osf/shared/components'; -import { SelectOption, TableParameters } from '@osf/shared/models'; +import { TableParameters } from '@osf/shared/models'; -import { Moderator } from '../../models'; +import { MODERATION_PERMISSIONS } from '../../constants'; +import { ModeratorModel } from '../../models'; @Component({ selector: 'osf-collection-moderators-list', - imports: [TranslatePipe, FormsModule, TableModule, Tooltip, Skeleton, Button, SelectComponent], + imports: [TranslatePipe, FormsModule, TableModule, Skeleton, Button, SelectComponent], templateUrl: './collection-moderators-list.component.html', styleUrl: './collection-moderators-list.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class CollectionModeratorsListComponent { - items = input([]); + items = input([]); isLoading = input(false); - remove = output(); - showEducationHistory = output(); - showEmploymentHistory = output(); + update = output(); + remove = output(); + showEducationHistory = output(); + showEmploymentHistory = output(); protected readonly tableParams = signal({ ...MY_PROJECTS_TABLE_PARAMS }); - protected readonly permissionsOptions: SelectOption[] = PERMISSION_OPTIONS; + protected readonly permissionsOptions = MODERATION_PERMISSIONS; - skeletonData: Moderator[] = Array.from({ length: 3 }, () => ({}) as Moderator); + skeletonData: ModeratorModel[] = Array.from({ length: 3 }, () => ({}) as ModeratorModel); - protected removeContributor(item: Moderator) { + protected updatePermission(item: ModeratorModel) { + this.update.emit(item); + } + + protected removeModerator(item: ModeratorModel) { this.remove.emit(item); } - protected openEducationHistory(item: Moderator) { + protected openEducationHistory(item: ModeratorModel) { this.showEducationHistory.emit(item); } - protected openEmploymentHistory(item: Moderator) { + protected openEmploymentHistory(item: ModeratorModel) { this.showEmploymentHistory.emit(item); } } diff --git a/src/app/features/moderation/components/collection-moderators/collection-moderators.component.html b/src/app/features/moderation/components/collection-moderators/collection-moderators.component.html index f1d510270..49b861fe1 100644 --- a/src/app/features/moderation/components/collection-moderators/collection-moderators.component.html +++ b/src/app/features/moderation/components/collection-moderators/collection-moderators.component.html @@ -1,6 +1,6 @@
- +
@@ -8,7 +8,7 @@ class="w-full" styleClass="w-full" [label]="'moderation.addModerator' | translate" - (click)="addModerator()" + (click)="openAddModeratorDialog()" >
@@ -17,6 +17,7 @@ x.userId); + + this.dialogService + .open(AddModeratorDialogComponent, { + width: '448px', + data: addedModeratorsIds, + focusOnShow: false, + header: this.translateService.instant('moderation.addModerator'), + closeOnEscape: true, + modal: true, + closable: true, + }) + .onClose.pipe( + filter((res: ModeratorDialogAddModel) => !!res), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe((res: ModeratorDialogAddModel) => { + if (res.type === AddModeratorType.Invite) { + this.openInviteModeratorDialog(); + } else { + // [NS] TODO: Implement logic + this.toastService.showSuccess('moderation.toastMessages.multipleAddSuccessMessage'); + } + }); } - addModerator() { - console.log('Add moderator'); + openInviteModeratorDialog() { + this.dialogService + .open(InviteModeratorDialogComponent, { + width: '448px', + focusOnShow: false, + header: this.translateService.instant('moderation.inviteModerator'), + closeOnEscape: true, + modal: true, + closable: true, + }) + .onClose.pipe( + filter((res: ModeratorDialogAddModel) => !!res), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe((res: ModeratorDialogAddModel) => { + if (res.type === AddModeratorType.Search) { + this.openAddModeratorDialog(); + } else { + // [NS] TODO: Implement logic + this.toastService.showSuccess('moderation.toastMessages.addSuccessMessage', { + name: res.data[0].fullName, + }); + } + }); + } + + updateModerator(item: ModeratorModel) { + // this.loaderService.show(); + + this.toastService.showSuccess('moderation.toastMessages.updateSuccessMessage', { + name: item.fullName, + }); } - removeModerator(moderator: Moderator) { + removeModerator(moderator: ModeratorModel) { this.confirmationService.confirm({ ...defaultConfirmationConfig, header: this.translateService.instant('moderation.removeDialog.title'), @@ -95,16 +153,15 @@ export class CollectionModeratorsComponent implements OnInit { label: this.translateService.instant('common.buttons.remove'), }, accept: () => { - console.log('Remove moderator', moderator); - this.toastService.showSuccess('project.contributors.removeDialog.successMessage', { name: moderator.fullName }); + this.toastService.showSuccess('moderation.toastMessages.deleteSuccessMessage', { name: moderator.fullName }); }, }); } - openEmploymentHistory(contributor: Moderator) { - this.dialogService.open(ContributorEmploymentHistoryComponent, { + openEmploymentHistory(moderator: ModeratorModel) { + this.dialogService.open(EmploymentHistoryComponent, { width: '552px', - data: contributor.employment, + data: moderator.employment, focusOnShow: false, header: this.translateService.instant('project.contributors.table.headers.employment'), closeOnEscape: true, @@ -113,10 +170,10 @@ export class CollectionModeratorsComponent implements OnInit { }); } - openEducationHistory(contributor: Moderator) { - this.dialogService.open(ContributorEducationHistoryComponent, { + openEducationHistory(moderator: ModeratorModel) { + this.dialogService.open(EducationHistoryComponent, { width: '552px', - data: contributor.education, + data: moderator.education, focusOnShow: false, header: this.translateService.instant('project.contributors.table.headers.education'), closeOnEscape: true, @@ -128,6 +185,6 @@ export class CollectionModeratorsComponent implements OnInit { private setSearchSubscription() { this.searchControl.valueChanges .pipe(skip(1), debounceTime(500), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) - .subscribe((res) => console.log(res)); + .subscribe((res) => this.actions.updateSearchValue(res ?? null)); } } diff --git a/src/app/features/moderation/components/index.ts b/src/app/features/moderation/components/index.ts index 2a1b15400..e9f6d7ed8 100644 --- a/src/app/features/moderation/components/index.ts +++ b/src/app/features/moderation/components/index.ts @@ -1,6 +1,8 @@ +export { AddModeratorDialogComponent } from './add-moderator-dialog/add-moderator-dialog.component'; export { CollectionModerationSettingsComponent } from './collection-moderation-settings/collection-moderation-settings.component'; export { CollectionModerationSubmissionsComponent } from './collection-moderation-submissions/collection-moderation-submissions.component'; export { CollectionModeratorsComponent } from './collection-moderators/collection-moderators.component'; export { CollectionModeratorsListComponent } from './collection-moderators-list/collection-moderators-list.component'; +export { InviteModeratorDialogComponent } from './invite-moderator-dialog/invite-moderator-dialog.component'; export { SubmissionItemComponent } from './submission-item/submission-item.component'; export { SubmissionsListComponent } from './submissions-list/submissions-list.component'; diff --git a/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.html b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.html new file mode 100644 index 000000000..21d3f9221 --- /dev/null +++ b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.html @@ -0,0 +1,74 @@ +
+
+ + + + + + +
+ + + + + {{ selectedOption.label | translate }} + + + + {{ item.label | translate }} + + +
+
+ +
+ + +
+ +
+ + + + + +
+
diff --git a/src/app/features/project/contributors/components/contributor-education-history/contributor-education-history.component.scss b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.scss similarity index 100% rename from src/app/features/project/contributors/components/contributor-education-history/contributor-education-history.component.scss rename to src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.scss diff --git a/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.spec.ts b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.spec.ts new file mode 100644 index 000000000..56e882487 --- /dev/null +++ b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InviteModeratorDialogComponent } from './invite-moderator-dialog.component'; + +describe('InviteModeratorDialogComponent', () => { + let component: InviteModeratorDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InviteModeratorDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(InviteModeratorDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.ts b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.ts new file mode 100644 index 000000000..4f8d1dd39 --- /dev/null +++ b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.ts @@ -0,0 +1,74 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { DynamicDialogRef } from 'primeng/dynamicdialog'; +import { Select } from 'primeng/select'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +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/utils'; + +import { MODERATION_PERMISSIONS } from '../../constants'; +import { AddModeratorType, ModeratorPermission } from '../../enums'; +import { InviteModeratorForm, ModeratorAddModel, ModeratorDialogAddModel } from '../../models'; + +@Component({ + selector: 'osf-invite-moderator-dialog', + imports: [Button, ReactiveFormsModule, TranslatePipe, TextInputComponent, Select], + templateUrl: './invite-moderator-dialog.component.html', + styleUrl: './invite-moderator-dialog.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class InviteModeratorDialogComponent { + protected dialogRef = inject(DynamicDialogRef); + protected moderatorForm!: FormGroup; + protected inputLimits = InputLimits; + protected readonly permissionsOptions = MODERATION_PERMISSIONS; + + constructor() { + this.initForm(); + } + + initForm() { + this.moderatorForm = new FormGroup({ + fullName: new FormControl('', { + nonNullable: true, + validators: [CustomValidators.requiredTrimmed(), Validators.maxLength(InputLimits.fullName.maxLength)], + }), + email: new FormControl('', { + nonNullable: true, + validators: [ + CustomValidators.requiredTrimmed(), + CustomValidators.emailValidator(), + Validators.maxLength(InputLimits.email.maxLength), + ], + }), + permission: new FormControl(ModeratorPermission.Moderator, { + nonNullable: true, + }), + }); + } + + searchModerator() { + const data: ModeratorDialogAddModel = { data: [], type: AddModeratorType.Search }; + this.dialogRef.close(data); + } + + submit(): void { + if (this.moderatorForm.invalid) { + return; + } + + const formData = this.moderatorForm.value; + const moderatorData: ModeratorAddModel = { + fullName: formData.fullName, + email: formData.email, + permission: formData.permission, + }; + const data: ModeratorDialogAddModel = { data: [moderatorData], type: AddModeratorType.Invite }; + this.dialogRef.close(data); + } +} diff --git a/src/app/features/moderation/constants/collection-moderation-tabs.const.ts b/src/app/features/moderation/constants/collection-moderation-tabs.const.ts index cb37b1a64..a4975f447 100644 --- a/src/app/features/moderation/constants/collection-moderation-tabs.const.ts +++ b/src/app/features/moderation/constants/collection-moderation-tabs.const.ts @@ -1,9 +1,14 @@ -import { TabOption } from '@osf/shared/models'; +import { SelectOption, TabOption } from '@osf/shared/models'; -import { CollectionModerationTab } from '../enums'; +import { CollectionModerationTab, ModeratorPermission } from '../enums'; export const COLLECTION_MODERATION_TABS: TabOption[] = [ { label: 'moderation.allItems', value: CollectionModerationTab.AllItems }, { label: 'moderation.moderators', value: CollectionModerationTab.Moderators }, { label: 'moderation.settings', value: CollectionModerationTab.Settings }, ]; + +export const MODERATION_PERMISSIONS: SelectOption[] = [ + { label: 'moderation.moderatorPermissions.administrator', value: ModeratorPermission.Admin }, + { label: 'moderation.moderatorPermissions.moderator', value: ModeratorPermission.Moderator }, +]; diff --git a/src/app/features/moderation/enums/add-moderator-type.enum.ts b/src/app/features/moderation/enums/add-moderator-type.enum.ts new file mode 100644 index 000000000..1145b73bb --- /dev/null +++ b/src/app/features/moderation/enums/add-moderator-type.enum.ts @@ -0,0 +1,4 @@ +export enum AddModeratorType { + Search = 1, + Invite, +} diff --git a/src/app/features/moderation/enums/index.ts b/src/app/features/moderation/enums/index.ts index 750874b0f..575ea124a 100644 --- a/src/app/features/moderation/enums/index.ts +++ b/src/app/features/moderation/enums/index.ts @@ -1,3 +1,4 @@ +export * from './add-moderator-type.enum'; export * from './collection-moderation-tab.enum'; export * from './moderator-permission.enum'; export * from './submission-review-status.enum'; diff --git a/src/app/features/moderation/mappers/moderation.mapper.ts b/src/app/features/moderation/mappers/moderation.mapper.ts index 45641a8d7..0eff0b4d6 100644 --- a/src/app/features/moderation/mappers/moderation.mapper.ts +++ b/src/app/features/moderation/mappers/moderation.mapper.ts @@ -1,8 +1,11 @@ +import { JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; +import { PaginatedData } from '@osf/shared/models'; + import { ModeratorPermission } from '../enums'; -import { Moderator, ModeratorDataJsonApi } from '../models'; +import { ModeratorAddModel, ModeratorDataJsonApi, ModeratorModel } from '../models'; export class ModerationMapper { - static fromModeratorResponse(response: ModeratorDataJsonApi): Moderator { + static fromModeratorResponse(response: ModeratorDataJsonApi): ModeratorModel { return { id: response.id, userId: response.embeds.user.data.id, @@ -12,4 +15,20 @@ export class ModerationMapper { education: response.embeds.user.data.attributes.education || [], }; } + + static fromUsersWithPaginationGetResponse( + response: JsonApiResponseWithPaging + ): PaginatedData { + return { + data: response.data.map( + (user) => + ({ + id: user.id, + fullName: user.attributes.full_name, + permission: ModeratorPermission.Moderator, + }) as ModeratorAddModel + ), + totalCount: response.links.meta.total, + }; + } } diff --git a/src/app/features/moderation/models/index.ts b/src/app/features/moderation/models/index.ts index 18c8d7254..38d8d0d1c 100644 --- a/src/app/features/moderation/models/index.ts +++ b/src/app/features/moderation/models/index.ts @@ -1,3 +1,6 @@ +export * from './invite-moderator-form.model'; export * from './moderator.model'; +export * from './moderator-add.model'; +export * from './moderator-dialog-add.model'; export * from './moderator-json-api.model'; export * from './submission.model'; diff --git a/src/app/features/moderation/models/invite-moderator-form.model.ts b/src/app/features/moderation/models/invite-moderator-form.model.ts new file mode 100644 index 000000000..747968f60 --- /dev/null +++ b/src/app/features/moderation/models/invite-moderator-form.model.ts @@ -0,0 +1,9 @@ +import { FormControl } from '@angular/forms'; + +import { ModeratorPermission } from '../enums'; + +export interface InviteModeratorForm { + fullName: FormControl; + email: FormControl; + permission: FormControl; +} diff --git a/src/app/features/moderation/models/moderator-add.model.ts b/src/app/features/moderation/models/moderator-add.model.ts new file mode 100644 index 000000000..bb3ce5582 --- /dev/null +++ b/src/app/features/moderation/models/moderator-add.model.ts @@ -0,0 +1,8 @@ +import { ModeratorPermission } from '../enums'; + +export interface ModeratorAddModel { + id?: string; + permission?: ModeratorPermission; + fullName?: string; + email?: string; +} diff --git a/src/app/features/moderation/models/moderator-dialog-add.model.ts b/src/app/features/moderation/models/moderator-dialog-add.model.ts new file mode 100644 index 000000000..7ab13ee22 --- /dev/null +++ b/src/app/features/moderation/models/moderator-dialog-add.model.ts @@ -0,0 +1,8 @@ +import { AddModeratorType } from '../enums'; + +import { ModeratorAddModel } from './moderator-add.model'; + +export interface ModeratorDialogAddModel { + data: ModeratorAddModel[]; + type: AddModeratorType; +} diff --git a/src/app/features/moderation/models/moderator.model.ts b/src/app/features/moderation/models/moderator.model.ts index 42ef0e502..7170e6dd0 100644 --- a/src/app/features/moderation/models/moderator.model.ts +++ b/src/app/features/moderation/models/moderator.model.ts @@ -2,7 +2,7 @@ import { Education, Employment } from '@osf/shared/models'; import { ModeratorPermission } from '../enums'; -export interface Moderator { +export interface ModeratorModel { id: string; userId: string; fullName: string; diff --git a/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.scss b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.scss index 069a5eb7c..3010ca6d5 100644 --- a/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.scss +++ b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.scss @@ -1,3 +1,4 @@ :host { + display: flex; flex: 1 1 0%; } diff --git a/src/app/features/moderation/services/moderation.service.ts b/src/app/features/moderation/services/moderation.service.ts index f5c73fe5d..1ba57fd09 100644 --- a/src/app/features/moderation/services/moderation.service.ts +++ b/src/app/features/moderation/services/moderation.service.ts @@ -1,11 +1,13 @@ -import { map, Observable } from 'rxjs'; +import { map, Observable, of } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { JsonApiResponseWithPaging, UserGetResponse } from '@osf/core/models'; import { JsonApiService } from '@osf/core/services'; +import { PaginatedData } from '@osf/shared/models'; import { ModerationMapper } from '../mappers'; -import { Moderator, ModeratorResponseJsonApi } from '../models'; +import { ModeratorAddModel, ModeratorModel, ModeratorResponseJsonApi } from '../models'; import { environment } from 'src/environments/environment'; @@ -13,11 +15,10 @@ import { environment } from 'src/environments/environment'; providedIn: 'root', }) export class ModerationService { - private readonly baseUrl = environment.apiUrl; private readonly tesModeratorsUrl = 'assets/collection-moderators.json'; private readonly jsonApiService = inject(JsonApiService); - getCollectionModerators(providerId: string): Observable { + getCollectionModerators(providerId: string): Observable { return ( this.jsonApiService // .get(`${this.baseUrl}/providers/collections/${providerId}/moderators/`) @@ -25,4 +26,26 @@ export class ModerationService { .pipe(map((response) => response.data.map((moderator) => ModerationMapper.fromModeratorResponse(moderator)))) ); } + + addCollectionModerator(providerId: string, data: ModeratorAddModel): Observable { + return of({} as ModeratorModel); + } + + updateCollectionModerator(providerId: string, data: ModeratorAddModel): Observable { + return of({} as ModeratorModel); + } + + deleteCollectionModerator(providerId: string, userId: string): Observable { + const baseUrl = ``; + + return this.jsonApiService.delete(baseUrl); + } + + searchUsers(value: string, page = 1): Observable> { + const baseUrl = `${environment.apiUrl}/users/?filter[full_name]=${value}&page=${page}`; + + return this.jsonApiService + .get>(baseUrl) + .pipe(map((response) => ModerationMapper.fromUsersWithPaginationGetResponse(response))); + } } diff --git a/src/app/features/moderation/store/moderation.actions.ts b/src/app/features/moderation/store/moderation.actions.ts index 969a39cb3..047a1a843 100644 --- a/src/app/features/moderation/store/moderation.actions.ts +++ b/src/app/features/moderation/store/moderation.actions.ts @@ -1,19 +1,19 @@ -import { Moderator } from '../models'; +import { ModeratorModel } from '../models'; const ACTION_SCOPE = '[Moderation]'; export class LoadCollectionModerators { static readonly type = `${ACTION_SCOPE} Load Collection Moderators`; - constructor(public providerId: string) {} + constructor(public collectionId: string) {} } export class AddCollectionModerator { static readonly type = `${ACTION_SCOPE} Add Collection Moderator`; constructor( - public projectId: string, - public moderator: Moderator + public collectionId: string, + public moderator: ModeratorModel ) {} } @@ -21,8 +21,8 @@ export class UpdateCollectionModerator { static readonly type = `${ACTION_SCOPE} Update Collection Moderator`; constructor( - public projectId: string, - public moderator: Moderator + public collectionId: string, + public moderator: ModeratorModel ) {} } @@ -30,7 +30,7 @@ export class DeleteCollectionModerator { static readonly type = `${ACTION_SCOPE} Delete Collection Moderator`; constructor( - public projectId: string, + public collectionId: string, public moderatorId: string ) {} } @@ -40,3 +40,15 @@ export class UpdateCollectionSearchValue { constructor(public searchValue: string | null) {} } + +export class SearchUsers { + static readonly type = `${ACTION_SCOPE} Search Users`; + constructor( + public searchValue: string | null, + public page: number + ) {} +} + +export class ClearUsers { + static readonly type = `${ACTION_SCOPE} Clear Users`; +} diff --git a/src/app/features/moderation/store/moderation.model.ts b/src/app/features/moderation/store/moderation.model.ts index 12a04f80b..a2ef142d6 100644 --- a/src/app/features/moderation/store/moderation.model.ts +++ b/src/app/features/moderation/store/moderation.model.ts @@ -1,11 +1,16 @@ import { AsyncStateModel } from '@osf/shared/models'; -import { Moderator } from '../models'; +import { ModeratorAddModel, ModeratorModel } from '../models'; -interface ModerationDataStateModel extends AsyncStateModel { +interface ModerationDataStateModel extends AsyncStateModel { searchValue: string | null; } +interface UsersDataStateModel extends AsyncStateModel { + totalCount: number; +} + export interface ModerationStateModel { collectionModerators: ModerationDataStateModel; + users: UsersDataStateModel; } diff --git a/src/app/features/moderation/store/moderation.selectors.ts b/src/app/features/moderation/store/moderation.selectors.ts index b18a6e147..ee4646596 100644 --- a/src/app/features/moderation/store/moderation.selectors.ts +++ b/src/app/features/moderation/store/moderation.selectors.ts @@ -1,5 +1,7 @@ import { Selector } from '@ngxs/store'; +import { ModeratorAddModel } from '../models'; + import { ModerationStateModel } from './moderation.model'; import { ModerationState } from './moderation.state'; @@ -22,4 +24,24 @@ export class ModerationSelectors { static isModeratorsError(state: ModerationStateModel) { return !!state.collectionModerators.error?.length; } + + @Selector([ModerationState]) + static getUsers(state: ModerationStateModel): ModeratorAddModel[] { + return state.users.data; + } + + @Selector([ModerationState]) + static isUsersLoading(state: ModerationStateModel): boolean { + return state.users.isLoading; + } + + @Selector([ModerationState]) + static getUsersError(state: ModerationStateModel): string | null { + return state.users.error; + } + + @Selector([ModerationState]) + static getUsersTotalCount(state: ModerationStateModel): number { + return state.users.totalCount; + } } diff --git a/src/app/features/moderation/store/moderation.state.ts b/src/app/features/moderation/store/moderation.state.ts index 44c2b04ac..e51a2357f 100644 --- a/src/app/features/moderation/store/moderation.state.ts +++ b/src/app/features/moderation/store/moderation.state.ts @@ -1,12 +1,21 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { catchError, tap, throwError } from 'rxjs'; +import { catchError, of, tap, throwError } from 'rxjs'; import { inject, Injectable } from '@angular/core'; +import { ModeratorModel } from '../models'; import { ModerationService } from '../services'; -import { LoadCollectionModerators } from './moderation.actions'; +import { + AddCollectionModerator, + ClearUsers, + DeleteCollectionModerator, + LoadCollectionModerators, + SearchUsers, + UpdateCollectionModerator, + UpdateCollectionSearchValue, +} from './moderation.actions'; import { ModerationStateModel } from './moderation.model'; @State({ @@ -18,6 +27,12 @@ import { ModerationStateModel } from './moderation.model'; error: null, searchValue: null, }, + users: { + data: [], + isLoading: false, + error: null, + totalCount: 0, + }, }, }) @Injectable() @@ -29,16 +44,11 @@ export class ModerationState { const state = ctx.getState(); ctx.patchState({ - collectionModerators: { - ...state.collectionModerators, - isLoading: true, - }, + collectionModerators: { ...state.collectionModerators, isLoading: true, error: null }, }); - return this.moderationService.getCollectionModerators(action.providerId).pipe( - tap((moderators) => { - const state = ctx.getState(); - + return this.moderationService.getCollectionModerators(action.collectionId).pipe( + tap((moderators: ModeratorModel[]) => { ctx.patchState({ collectionModerators: { ...state.collectionModerators, @@ -51,10 +61,124 @@ export class ModerationState { ); } - private handleError(ctx: StateContext, section: keyof ModerationStateModel, error: Error) { + @Action(UpdateCollectionSearchValue) + updateSearchValue(ctx: StateContext, action: UpdateCollectionSearchValue) { + ctx.patchState({ + collectionModerators: { ...ctx.getState().collectionModerators, searchValue: action.searchValue }, + }); + } + + @Action(AddCollectionModerator) + addCollectionModerator(ctx: StateContext, action: AddCollectionModerator) { + const state = ctx.getState(); + + ctx.patchState({ + collectionModerators: { ...state.collectionModerators, isLoading: true, error: null }, + }); + + return this.moderationService.addCollectionModerator(action.collectionId, action.moderator).pipe( + tap((moderator) => { + const currentState = ctx.getState(); + + ctx.patchState({ + collectionModerators: { + ...currentState.collectionModerators, + data: [...currentState.collectionModerators.data, moderator], + isLoading: false, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'collectionModerators', error)) + ); + } + + @Action(UpdateCollectionModerator) + updateCollectionModerator(ctx: StateContext, action: UpdateCollectionModerator) { + const state = ctx.getState(); + + ctx.patchState({ + collectionModerators: { ...state.collectionModerators, isLoading: true, error: null }, + }); + + return this.moderationService.updateCollectionModerator(action.collectionId, action.moderator).pipe( + tap((updatedModerator) => { + const currentState = ctx.getState(); + + ctx.patchState({ + collectionModerators: { + ...currentState.collectionModerators, + data: currentState.collectionModerators.data.map((moderator) => + moderator.id === updatedModerator.id ? updatedModerator : moderator + ), + isLoading: false, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'collectionModerators', error)) + ); + } + + @Action(DeleteCollectionModerator) + deleteCollectionModerator(ctx: StateContext, action: DeleteCollectionModerator) { + const state = ctx.getState(); + + ctx.patchState({ + collectionModerators: { ...state.collectionModerators, isLoading: true, error: null }, + }); + + return this.moderationService.deleteCollectionModerator(action.collectionId, action.moderatorId).pipe( + tap(() => { + ctx.patchState({ + collectionModerators: { + ...state.collectionModerators, + data: state.collectionModerators.data.filter((moderator) => moderator.userId !== action.moderatorId), + isLoading: false, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'collectionModerators', error)) + ); + } + + @Action(SearchUsers) + searchUsers(ctx: StateContext, action: SearchUsers) { + const state = ctx.getState(); + + ctx.patchState({ + users: { ...state.users, isLoading: true, error: null }, + }); + + const addedModeratorsIds = state.collectionModerators.data.map((moderator) => moderator.userId); + + if (!action.searchValue) { + return of([]); + } + + return this.moderationService.searchUsers(action.searchValue, action.page).pipe( + tap((users) => { + ctx.patchState({ + users: { + data: users.data.filter((user) => !addedModeratorsIds.includes(user.id!)), + isLoading: false, + error: '', + totalCount: users.totalCount, + }, + }); + }), + catchError((error) => this.handleError(ctx, 'users', error)) + ); + } + + @Action(ClearUsers) + clearUsers(ctx: StateContext) { + ctx.patchState({ users: { data: [], isLoading: false, error: null, totalCount: 0 } }); + } + + private handleError(ctx: StateContext, key: keyof ModerationStateModel, error: Error) { + const state = ctx.getState(); ctx.patchState({ - [section]: { - ...ctx.getState()[section], + [key]: { + ...state[key], isLoading: false, error: error.message, }, diff --git a/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.html b/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.html index d785acb3d..381808a33 100644 --- a/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.html +++ b/src/app/features/project/contributors/components/add-contributor-dialog/add-contributor-dialog.component.html @@ -58,7 +58,7 @@ styleClass="w-full" (click)="dialogRef.close()" severity="info" - [label]="'project.contributors.addDialog.cancel' | translate" + [label]="'common.buttons.cancel' | translate" >
@@ -66,7 +66,7 @@ class="w-full" styleClass="w-full" (click)="addContributor()" - [label]="'project.contributors.addDialog.next' | translate" + [label]="'common.buttons.next' | translate" [disabled]="!selectedUsers().length" > diff --git a/src/app/features/project/contributors/components/add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component.html b/src/app/features/project/contributors/components/add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component.html index 9e4a5074f..0b00297cd 100644 --- a/src/app/features/project/contributors/components/add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component.html +++ b/src/app/features/project/contributors/components/add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component.html @@ -38,7 +38,6 @@ class="w-full" styleClass="w-full" (click)="dialogRef.close()" - [text]="true" severity="info" [label]="'common.buttons.cancel' | translate" > diff --git a/src/app/features/project/contributors/components/contributor-education-history/contributor-education-history.component.spec.ts b/src/app/features/project/contributors/components/contributor-education-history/contributor-education-history.component.spec.ts deleted file mode 100644 index 36e2cbdf8..000000000 --- a/src/app/features/project/contributors/components/contributor-education-history/contributor-education-history.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ContributorEducationHistoryComponent } from './contributor-education-history.component'; - -describe('ContributorEducationHistoryComponent', () => { - let component: ContributorEducationHistoryComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ContributorEducationHistoryComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(ContributorEducationHistoryComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.spec.ts b/src/app/features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.spec.ts deleted file mode 100644 index ecafca8ea..000000000 --- a/src/app/features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ContributorEmploymentHistoryComponent } from './contributor-employment-history.component'; - -describe('ContributorEmploymentHistoryComponent', () => { - let component: ContributorEmploymentHistoryComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ContributorEmploymentHistoryComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(ContributorEmploymentHistoryComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/features/project/contributors/components/index.ts b/src/app/features/project/contributors/components/index.ts index 02ba41359..6714f9e81 100644 --- a/src/app/features/project/contributors/components/index.ts +++ b/src/app/features/project/contributors/components/index.ts @@ -1,6 +1,4 @@ export { AddContributorDialogComponent } from './add-contributor-dialog/add-contributor-dialog.component'; export { AddUnregisteredContributorDialogComponent } from './add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component'; -export { ContributorEducationHistoryComponent } from './contributor-education-history/contributor-education-history.component'; -export { ContributorEmploymentHistoryComponent } from './contributor-employment-history/contributor-employment-history.component'; export { ContributorsListComponent } from './contributors-list/contributors-list.component'; export { CreateViewLinkDialogComponent } from './create-view-link-dialog/create-view-link-dialog.component'; diff --git a/src/app/features/project/contributors/contributors.component.ts b/src/app/features/project/contributors/contributors.component.ts index 5b2ca5d1b..c633dc0f6 100644 --- a/src/app/features/project/contributors/contributors.component.ts +++ b/src/app/features/project/contributors/contributors.component.ts @@ -15,7 +15,12 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; -import { SearchInputComponent, ViewOnlyTableComponent } from '@osf/shared/components'; +import { + EducationHistoryDialogComponent, + EmploymentHistoryDialogComponent, + SearchInputComponent, + ViewOnlyTableComponent, +} from '@osf/shared/components'; import { SelectOption } from '@osf/shared/models'; import { ToastService } from '@osf/shared/services'; import { defaultConfirmationConfig, findChangedItems } from '@osf/shared/utils'; @@ -32,8 +37,6 @@ import { import { AddContributorDialogComponent, AddUnregisteredContributorDialogComponent, - ContributorEducationHistoryComponent, - ContributorEmploymentHistoryComponent, ContributorsListComponent, CreateViewLinkDialogComponent, } from './components'; @@ -166,7 +169,7 @@ export class ContributorsComponent implements OnInit { } openEmploymentHistory(contributor: ContributorModel) { - this.dialogService.open(ContributorEmploymentHistoryComponent, { + this.dialogService.open(EmploymentHistoryDialogComponent, { width: '552px', data: contributor.employment, focusOnShow: false, @@ -178,7 +181,7 @@ export class ContributorsComponent implements OnInit { } openEducationHistory(contributor: ContributorModel) { - this.dialogService.open(ContributorEducationHistoryComponent, { + this.dialogService.open(EducationHistoryDialogComponent, { width: '552px', data: contributor.education, focusOnShow: false, diff --git a/src/app/features/project/contributors/components/contributor-education-history/contributor-education-history.component.html b/src/app/shared/components/education-history-dialog/education-history-dialog.component.html similarity index 100% rename from src/app/features/project/contributors/components/contributor-education-history/contributor-education-history.component.html rename to src/app/shared/components/education-history-dialog/education-history-dialog.component.html diff --git a/src/app/features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.scss b/src/app/shared/components/education-history-dialog/education-history-dialog.component.scss similarity index 100% rename from src/app/features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.scss rename to src/app/shared/components/education-history-dialog/education-history-dialog.component.scss diff --git a/src/app/shared/components/education-history-dialog/education-history-dialog.component.spec.ts b/src/app/shared/components/education-history-dialog/education-history-dialog.component.spec.ts new file mode 100644 index 000000000..acc651a68 --- /dev/null +++ b/src/app/shared/components/education-history-dialog/education-history-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EducationHistoryDialogComponent } from './education-history-dialog.component'; + +describe('EducationHistoryDialogComponent', () => { + let component: EducationHistoryDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EducationHistoryDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(EducationHistoryDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/project/contributors/components/contributor-education-history/contributor-education-history.component.ts b/src/app/shared/components/education-history-dialog/education-history-dialog.component.ts similarity index 77% rename from src/app/features/project/contributors/components/contributor-education-history/contributor-education-history.component.ts rename to src/app/shared/components/education-history-dialog/education-history-dialog.component.ts index 4f92fd1c6..26870d2dc 100644 --- a/src/app/features/project/contributors/components/contributor-education-history/contributor-education-history.component.ts +++ b/src/app/shared/components/education-history-dialog/education-history-dialog.component.ts @@ -9,13 +9,13 @@ import { EducationHistoryComponent } from '@osf/shared/components'; import { Education } from '@osf/shared/models'; @Component({ - selector: 'osf-contributor-education-history', + selector: 'osf-education-history-dialog', imports: [Button, TranslatePipe, EducationHistoryComponent], - templateUrl: './contributor-education-history.component.html', - styleUrl: './contributor-education-history.component.scss', + templateUrl: './education-history-dialog.component.html', + styleUrl: './education-history-dialog.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ContributorEducationHistoryComponent { +export class EducationHistoryDialogComponent { protected dialogRef = inject(DynamicDialogRef); private readonly config = inject(DynamicDialogConfig); protected readonly educationHistory = signal([]); diff --git a/src/app/features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.html b/src/app/shared/components/employment-history-dialog/employment-history-dialog.component.html similarity index 100% rename from src/app/features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.html rename to src/app/shared/components/employment-history-dialog/employment-history-dialog.component.html diff --git a/src/app/shared/components/employment-history-dialog/employment-history-dialog.component.scss b/src/app/shared/components/employment-history-dialog/employment-history-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/employment-history-dialog/employment-history-dialog.component.spec.ts b/src/app/shared/components/employment-history-dialog/employment-history-dialog.component.spec.ts new file mode 100644 index 000000000..cb9e1920e --- /dev/null +++ b/src/app/shared/components/employment-history-dialog/employment-history-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EmploymentHistoryDialogComponent } from './employment-history-dialog.component'; + +describe('EmploymentHistoryDialogComponent', () => { + let component: EmploymentHistoryDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EmploymentHistoryDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(EmploymentHistoryDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.ts b/src/app/shared/components/employment-history-dialog/employment-history-dialog.component.ts similarity index 77% rename from src/app/features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.ts rename to src/app/shared/components/employment-history-dialog/employment-history-dialog.component.ts index cd132a423..6c924d32a 100644 --- a/src/app/features/project/contributors/components/contributor-employment-history/contributor-employment-history.component.ts +++ b/src/app/shared/components/employment-history-dialog/employment-history-dialog.component.ts @@ -9,13 +9,13 @@ import { EmploymentHistoryComponent } from '@osf/shared/components'; import { Employment } from '@osf/shared/models'; @Component({ - selector: 'osf-contributor-employment-history', + selector: 'osf-employment-history-dialog', imports: [Button, TranslatePipe, EmploymentHistoryComponent], - templateUrl: './contributor-employment-history.component.html', - styleUrl: './contributor-employment-history.component.scss', + templateUrl: './employment-history-dialog.component.html', + styleUrl: './employment-history-dialog.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ContributorEmploymentHistoryComponent { +export class EmploymentHistoryDialogComponent { private readonly config = inject(DynamicDialogConfig); protected dialogRef = inject(DynamicDialogRef); protected readonly employmentHistory = signal([]); diff --git a/src/app/shared/components/full-screen-loader/full-screen-loader.component.scss b/src/app/shared/components/full-screen-loader/full-screen-loader.component.scss index 6bb6d941f..d69ed1a29 100644 --- a/src/app/shared/components/full-screen-loader/full-screen-loader.component.scss +++ b/src/app/shared/components/full-screen-loader/full-screen-loader.component.scss @@ -5,5 +5,5 @@ background-color: var(--white-60); width: 100%; height: 100%; - z-index: 2000; + z-index: 3000; } diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index a899494f7..877fd7d5a 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -3,7 +3,9 @@ export { BarChartComponent } from './bar-chart/bar-chart.component'; export { CopyButtonComponent } from './copy-button/copy-button.component'; export { CustomPaginatorComponent } from './custom-paginator/custom-paginator.component'; export { EducationHistoryComponent } from './education-history/education-history.component'; +export { EducationHistoryDialogComponent } from './education-history-dialog/education-history-dialog.component'; export { EmploymentHistoryComponent } from './employment-history/employment-history.component'; +export { EmploymentHistoryDialogComponent } from './employment-history-dialog/employment-history-dialog.component'; export { FullScreenLoaderComponent } from './full-screen-loader/full-screen-loader.component'; export { IconComponent } from './icon/icon.component'; export { LineChartComponent } from './line-chart/line-chart.component'; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 38b6f0c85..bc0a861ab 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -10,9 +10,11 @@ "download": "Download", "copy": "Copy", "move": "Move", - "rename": "Rename" + "rename": "Rename", + "next": "Next" }, "search": { + "title": "Search", "noResultsFound": "No results found." }, "labels": { @@ -718,6 +720,13 @@ "settingsMessage": "To configure your notification preferences visit your", "userSettings": "User settings", "addModerator": "Add moderator", + "inviteModerator": "Invite moderator", + "searchModerator": "Search moderator", + "selectPermission": "Select permission", + "moderatorPermissions": { + "administrator": "Administrator", + "moderator": "Moderator" + }, "removeDialog": { "title": "Remove moderator", "message": "Are you sure you want to remove {{name}} moderator?" @@ -725,7 +734,7 @@ "toastMessages": { "addSuccessMessage": "Moderator {{name}} successfully added.", "multipleAddSuccessMessage": "Moderators successfully added.", - "multipleUpdateSuccessMessage": "Moderators successfully updated.", + "updateSuccessMessage": "Moderator {{name}} updated.", "deleteSuccessMessage": "Moderator {{name}} successfully removed." }, "submissionReviewStatus": { diff --git a/src/assets/styles/overrides/select.scss b/src/assets/styles/overrides/select.scss index a712f2a37..c294057d3 100644 --- a/src/assets/styles/overrides/select.scss +++ b/src/assets/styles/overrides/select.scss @@ -6,7 +6,7 @@ border: 1px solid var(--grey-2); border-radius: mix.rem(8px); outline: none; - font-size: mix.rem(14px); + font-size: mix.rem(16px); color: var.$dark-blue-1; .p-placeholder { @@ -21,6 +21,10 @@ max-width: 300px; font-size: 1rem; } + + &:not(:disabled):hover { + border-color: var.$pr-blue-1; + } } .p-select-label { From a36e91314256fbe5b074b8bc71d56b5ceb6a84c4 Mon Sep 17 00:00:00 2001 From: nsemets Date: Fri, 13 Jun 2025 14:07:02 +0300 Subject: [PATCH 7/7] feat(199): added form select --- .../invite-moderator-dialog.component.html | 22 ++++------------ .../invite-moderator-dialog.component.ts | 5 ++-- .../form-select/form-select.component.html | 24 +++++++++++++++++ .../form-select/form-select.component.scss | 0 .../form-select/form-select.component.spec.ts | 22 ++++++++++++++++ .../form-select/form-select.component.ts | 26 +++++++++++++++++++ src/app/shared/components/index.ts | 1 + 7 files changed, 80 insertions(+), 20 deletions(-) create mode 100644 src/app/shared/components/form-select/form-select.component.html create mode 100644 src/app/shared/components/form-select/form-select.component.scss create mode 100644 src/app/shared/components/form-select/form-select.component.spec.ts create mode 100644 src/app/shared/components/form-select/form-select.component.ts diff --git a/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.html b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.html index 21d3f9221..4700677f6 100644 --- a/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.html +++ b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.html @@ -21,24 +21,12 @@
- - - - - {{ selectedOption.label | translate }} - - - - {{ item.label | translate }} - - + [label]="'moderation.selectPermission'" + [fullWidth]="true" + >
diff --git a/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.ts b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.ts index 4f8d1dd39..c86243bfd 100644 --- a/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.ts +++ b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.ts @@ -2,12 +2,11 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; -import { Select } from 'primeng/select'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { TextInputComponent } from '@osf/shared/components'; +import { FormSelectComponent, TextInputComponent } from '@osf/shared/components'; import { InputLimits } from '@osf/shared/constants'; import { CustomValidators } from '@osf/shared/utils'; @@ -17,7 +16,7 @@ import { InviteModeratorForm, ModeratorAddModel, ModeratorDialogAddModel } from @Component({ selector: 'osf-invite-moderator-dialog', - imports: [Button, ReactiveFormsModule, TranslatePipe, TextInputComponent, Select], + imports: [Button, ReactiveFormsModule, TranslatePipe, TextInputComponent, FormSelectComponent], templateUrl: './invite-moderator-dialog.component.html', styleUrl: './invite-moderator-dialog.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/shared/components/form-select/form-select.component.html b/src/app/shared/components/form-select/form-select.component.html new file mode 100644 index 000000000..ea6de93cb --- /dev/null +++ b/src/app/shared/components/form-select/form-select.component.html @@ -0,0 +1,24 @@ +
+ @if (label().length) { + + } + + + + {{ selectedOption.label | translate }} + + + + {{ item.label | translate }} + + +
diff --git a/src/app/shared/components/form-select/form-select.component.scss b/src/app/shared/components/form-select/form-select.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/form-select/form-select.component.spec.ts b/src/app/shared/components/form-select/form-select.component.spec.ts new file mode 100644 index 000000000..0498da8db --- /dev/null +++ b/src/app/shared/components/form-select/form-select.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FormSelectComponent } from './form-select.component'; + +describe('FormSelectComponent', () => { + let component: FormSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FormSelectComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FormSelectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/form-select/form-select.component.ts b/src/app/shared/components/form-select/form-select.component.ts new file mode 100644 index 000000000..4fa0daefb --- /dev/null +++ b/src/app/shared/components/form-select/form-select.component.ts @@ -0,0 +1,26 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Select } from 'primeng/select'; + +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; + +import { SelectOption } from '@osf/shared/models'; + +@Component({ + selector: 'osf-form-select', + imports: [ReactiveFormsModule, Select, TranslatePipe], + templateUrl: './form-select.component.html', + styleUrl: './form-select.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FormSelectComponent { + control = input.required(); + options = input.required(); + label = input(''); + placeholder = input(''); + appendTo = input(null); + fullWidth = input(false); + + selectId = `select-${Math.random().toString(36).substring(2, 15)}`; +} diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index 07066c904..9e50cb85d 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -6,6 +6,7 @@ export { EducationHistoryComponent } from './education-history/education-history export { EducationHistoryDialogComponent } from './education-history-dialog/education-history-dialog.component'; export { EmploymentHistoryComponent } from './employment-history/employment-history.component'; export { EmploymentHistoryDialogComponent } from './employment-history-dialog/employment-history-dialog.component'; +export { FormSelectComponent } from './form-select/form-select.component'; export { FullScreenLoaderComponent } from './full-screen-loader/full-screen-loader.component'; export { IconComponent } from './icon/icon.component'; export { LineChartComponent } from './line-chart/line-chart.component';