Skip to content

Commit ee478e4

Browse files
authored
Merge pull request #109 from CenterForOpenScience/feat/199-moderation-page
Feat/199 moderation page
2 parents 4671318 + a36e913 commit ee478e4

File tree

102 files changed

+2330
-152
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+2330
-152
lines changed

src/app/core/components/topnav/topnav.component.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
@use "assets/styles/mixins" as mix;
33

44
:host {
5-
z-index: 1300;
5+
z-index: 1103;
66

77
.nav-container {
88
background: var.$dark-blue-1;

src/app/core/models/json-api.model.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ export interface JsonApiResponse<Data, Included> {
55

66
export interface JsonApiResponseWithPaging<Data, Included> extends JsonApiResponse<Data, Included> {
77
links: {
8-
meta: {
9-
total: number;
10-
per_page: number;
11-
};
8+
meta: MetaJsonApi;
129
};
1310
}
1411

@@ -20,3 +17,17 @@ export interface ApiData<Attributes, Embeds, Relationships, Links> {
2017
relationships: Relationships;
2118
links: Links;
2219
}
20+
21+
export interface MetaJsonApi {
22+
total: number;
23+
per_page: number;
24+
version?: string;
25+
}
26+
27+
export interface PaginationLinksJsonApi {
28+
self?: string;
29+
first?: string | null;
30+
last?: string | null;
31+
prev?: string | null;
32+
next?: string | null;
33+
}
Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
1+
import { provideStates } from '@ngxs/store';
2+
13
import { Routes } from '@angular/router';
24

35
import { CollectionsComponent } from '@osf/features/collections/collections.component';
46

7+
import { ModerationState } from '../moderation/store';
8+
59
export const collectionsRoutes: Routes = [
610
{
711
path: '',
8-
component: CollectionsComponent,
12+
children: [
13+
{
14+
path: '',
15+
pathMatch: 'full',
16+
component: CollectionsComponent,
17+
},
18+
{
19+
path: 'moderation',
20+
loadComponent: () =>
21+
import('@osf/features/moderation/pages/collection-moderation/collection-moderation.component').then(
22+
(m) => m.CollectionModerationComponent
23+
),
24+
providers: [provideStates([ModerationState])],
25+
},
26+
],
927
},
1028
];
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<div class="flex flex-column">
2+
<osf-search-input
3+
[control]="searchControl"
4+
[placeholder]="'project.contributors.addDialog.placeholder' | translate"
5+
/>
6+
7+
<div class="flex flex-column gap-3" [class.users-list]="!isInitialState()" [class.mt-4]="!isInitialState()">
8+
@if (isLoading()) {
9+
<osf-loading-spinner class="mt-3"></osf-loading-spinner>
10+
} @else {
11+
@for (item of users(); track $index) {
12+
<div class="border-divider flex p-b-12">
13+
<p-checkbox variant="filled" [value]="item" [inputId]="item.id" [(ngModel)]="selectedUsers"></p-checkbox>
14+
<label class="label ml-2" [for]="item.id">{{ item.fullName }}</label>
15+
</div>
16+
}
17+
18+
@if (!totalUsersCount() && !isInitialState()) {
19+
<div class="no-items">{{ 'common.search.noResultsFound' | translate }}</div>
20+
}
21+
}
22+
</div>
23+
24+
@if (totalUsersCount() > rows()) {
25+
<osf-custom-paginator
26+
class="mt-2"
27+
[first]="first()"
28+
[rows]="rows()"
29+
[totalCount]="totalUsersCount()"
30+
(pageChanged)="pageChanged($event)"
31+
></osf-custom-paginator>
32+
}
33+
34+
<div class="flex justify-content-end mt-4">
35+
<p-button
36+
class="secondary-add-btn"
37+
severity="secondary"
38+
[label]="'moderation.inviteModerator' | translate"
39+
(click)="inviteModerator()"
40+
/>
41+
</div>
42+
43+
<div class="flex gap-2 mt-6">
44+
<p-button
45+
class="w-full"
46+
styleClass="w-full"
47+
(click)="dialogRef.close()"
48+
severity="info"
49+
[label]="'common.buttons.cancel' | translate"
50+
>
51+
</p-button>
52+
53+
<p-button
54+
class="w-full"
55+
styleClass="w-full"
56+
(click)="addModerator()"
57+
[label]="'common.buttons.add' | translate"
58+
[disabled]="!selectedUsers().length"
59+
>
60+
</p-button>
61+
</div>
62+
</div>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@use "assets/styles/variables" as var;
2+
@use "assets/styles/mixins" as mix;
3+
4+
.label {
5+
color: var.$dark-blue-1;
6+
margin: 0;
7+
cursor: pointer;
8+
}
9+
10+
.users-list {
11+
height: 30vh;
12+
overflow: auto;
13+
}
14+
15+
.border-divider {
16+
border-bottom: 1px solid var.$grey-2;
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { AddModeratorDialogComponent } from './add-moderator-dialog.component';
4+
5+
describe('AddModeratorDialogComponent', () => {
6+
let component: AddModeratorDialogComponent;
7+
let fixture: ComponentFixture<AddModeratorDialogComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [AddModeratorDialogComponent],
12+
}).compileComponents();
13+
14+
fixture = TestBed.createComponent(AddModeratorDialogComponent);
15+
component = fixture.componentInstance;
16+
fixture.detectChanges();
17+
});
18+
19+
it('should create', () => {
20+
expect(component).toBeTruthy();
21+
});
22+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { createDispatchMap, select } from '@ngxs/store';
2+
3+
import { TranslatePipe } from '@ngx-translate/core';
4+
5+
import { Button } from 'primeng/button';
6+
import { Checkbox } from 'primeng/checkbox';
7+
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
8+
import { PaginatorState } from 'primeng/paginator';
9+
10+
import { debounceTime, distinctUntilChanged, filter, switchMap } from 'rxjs';
11+
12+
import { ChangeDetectionStrategy, Component, DestroyRef, inject, OnDestroy, OnInit, signal } from '@angular/core';
13+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
14+
import { FormControl, FormsModule } from '@angular/forms';
15+
16+
import { CustomPaginatorComponent, LoadingSpinnerComponent, SearchInputComponent } from '@osf/shared/components';
17+
18+
import { AddModeratorType } from '../../enums';
19+
import { ModeratorAddModel, ModeratorDialogAddModel } from '../../models';
20+
import { ClearUsers, ModerationSelectors, SearchUsers } from '../../store';
21+
22+
@Component({
23+
selector: 'osf-add-moderator-dialog',
24+
imports: [
25+
Button,
26+
Checkbox,
27+
FormsModule,
28+
TranslatePipe,
29+
SearchInputComponent,
30+
LoadingSpinnerComponent,
31+
CustomPaginatorComponent,
32+
],
33+
templateUrl: './add-moderator-dialog.component.html',
34+
styleUrl: './add-moderator-dialog.component.scss',
35+
changeDetection: ChangeDetectionStrategy.OnPush,
36+
})
37+
export class AddModeratorDialogComponent implements OnInit, OnDestroy {
38+
protected dialogRef = inject(DynamicDialogRef);
39+
private readonly destroyRef = inject(DestroyRef);
40+
readonly config = inject(DynamicDialogConfig);
41+
42+
protected users = select(ModerationSelectors.getUsers);
43+
protected isLoading = select(ModerationSelectors.isUsersLoading);
44+
protected totalUsersCount = select(ModerationSelectors.getUsersTotalCount);
45+
protected isInitialState = signal(true);
46+
47+
protected currentPage = signal(1);
48+
protected first = signal(0);
49+
protected rows = signal(10);
50+
51+
protected selectedUsers = signal<ModeratorAddModel[]>([]);
52+
protected searchControl = new FormControl<string>('');
53+
54+
protected actions = createDispatchMap({ searchUsers: SearchUsers, clearUsers: ClearUsers });
55+
56+
ngOnInit(): void {
57+
this.setSearchSubscription();
58+
this.selectedUsers.set([]);
59+
}
60+
61+
ngOnDestroy(): void {
62+
this.actions.clearUsers();
63+
}
64+
65+
addModerator(): void {
66+
const dialogData: ModeratorDialogAddModel = { data: this.selectedUsers(), type: AddModeratorType.Search };
67+
this.dialogRef.close(dialogData);
68+
}
69+
70+
inviteModerator() {
71+
const dialogData: ModeratorDialogAddModel = { data: [], type: AddModeratorType.Invite };
72+
this.dialogRef.close(dialogData);
73+
}
74+
75+
pageChanged(event: PaginatorState) {
76+
this.currentPage.set(event.page ? this.currentPage() + 1 : 1);
77+
this.first.set(event.first ?? 0);
78+
this.actions.searchUsers(this.searchControl.value, this.currentPage());
79+
}
80+
81+
private setSearchSubscription() {
82+
this.searchControl.valueChanges
83+
.pipe(
84+
filter((searchTerm) => !!searchTerm && searchTerm.trim().length > 0),
85+
debounceTime(500),
86+
distinctUntilChanged(),
87+
switchMap((searchTerm) => this.actions.searchUsers(searchTerm, this.currentPage())),
88+
takeUntilDestroyed(this.destroyRef)
89+
)
90+
.subscribe(() => {
91+
this.isInitialState.set(false);
92+
this.selectedUsers.set([]);
93+
});
94+
}
95+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<p>
2+
<span>{{ 'moderation.settingsMessage' | translate }}</span>
3+
<a class="ml-1 font-bold cursor-pointer" routerLink="/settings/notifications">
4+
{{ 'moderation.userSettings' | translate }}
5+
</a>
6+
</p>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { CollectionModerationSettingsComponent } from './collection-moderation-settings.component';
4+
5+
describe('CollectionModerationSettingsComponent', () => {
6+
let component: CollectionModerationSettingsComponent;
7+
let fixture: ComponentFixture<CollectionModerationSettingsComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [CollectionModerationSettingsComponent],
12+
}).compileComponents();
13+
14+
fixture = TestBed.createComponent(CollectionModerationSettingsComponent);
15+
component = fixture.componentInstance;
16+
fixture.detectChanges();
17+
});
18+
19+
it('should create', () => {
20+
expect(component).toBeTruthy();
21+
});
22+
});

0 commit comments

Comments
 (0)