Skip to content

Commit 44e2ee4

Browse files
committed
fix(users-search): changed api for user search
1 parent 7043b73 commit 44e2ee4

19 files changed

+419
-70
lines changed

src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
@for (item of users(); track $index) {
1212
<div class="border-divider flex pb-3">
1313
<p-checkbox variant="filled" [value]="item" [inputId]="item.id" [(ngModel)]="selectedUsers"></p-checkbox>
14-
<label class="label mb-0 ml-2 cursor-pointer" [for]="item.id">{{ item.fullName }}</label>
14+
<a class="ml-2 font-bold" [href]="item.id" target="_blank">{{ item.fullName }}</a>
1515
</div>
1616
}
1717

@@ -27,6 +27,7 @@
2727
[first]="first()"
2828
[rows]="rows()"
2929
[totalCount]="totalUsersCount()"
30+
[showPageLinks]="false"
3031
(pageChanged)="pageChanged($event)"
3132
></osf-custom-paginator>
3233
}

src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { SearchInputComponent } from '@osf/shared/components/search-input/search
1919

2020
import { AddModeratorType } from '../../enums';
2121
import { ModeratorAddModel, ModeratorDialogAddModel } from '../../models';
22-
import { ClearUsers, ModeratorsSelectors, SearchUsers } from '../../store/moderators';
22+
import { ClearUsers, ModeratorsSelectors, SearchUsers, SearchUsersPageChange } from '../../store/moderators';
2323

2424
@Component({
2525
selector: 'osf-add-moderator-dialog',
@@ -44,6 +44,8 @@ export class AddModeratorDialogComponent implements OnInit, OnDestroy {
4444
users = select(ModeratorsSelectors.getUsers);
4545
isLoading = select(ModeratorsSelectors.isUsersLoading);
4646
totalUsersCount = select(ModeratorsSelectors.getUsersTotalCount);
47+
usersNextLink = select(ModeratorsSelectors.getUsersNextLink);
48+
usersPreviousLink = select(ModeratorsSelectors.getUsersPreviousLink);
4749
isInitialState = signal(true);
4850

4951
currentPage = signal(1);
@@ -53,7 +55,11 @@ export class AddModeratorDialogComponent implements OnInit, OnDestroy {
5355
selectedUsers = signal<ModeratorAddModel[]>([]);
5456
searchControl = new FormControl<string>('');
5557

56-
actions = createDispatchMap({ searchUsers: SearchUsers, clearUsers: ClearUsers });
58+
actions = createDispatchMap({
59+
searchUsers: SearchUsers,
60+
searchUsersPageChange: SearchUsersPageChange,
61+
clearUsers: ClearUsers,
62+
});
5763

5864
ngOnInit(): void {
5965
this.setSearchSubscription();
@@ -74,10 +80,32 @@ export class AddModeratorDialogComponent implements OnInit, OnDestroy {
7480
this.dialogRef.close(dialogData);
7581
}
7682

77-
pageChanged(event: PaginatorState) {
78-
this.currentPage.set(event.page ? this.currentPage() + 1 : 1);
79-
this.first.set(event.first ?? 0);
80-
this.actions.searchUsers(this.searchControl.value, this.currentPage());
83+
pageChanged(event: PaginatorState): void {
84+
if (event.page === undefined) {
85+
return;
86+
}
87+
88+
const eventPageOneBased = event.page + 1;
89+
90+
if (eventPageOneBased === 1) {
91+
const searchTerm = this.searchControl.value?.trim();
92+
93+
if (searchTerm) {
94+
this.actions.searchUsers(searchTerm);
95+
this.currentPage.set(1);
96+
this.first.set(0);
97+
}
98+
99+
return;
100+
}
101+
102+
const link = eventPageOneBased > this.currentPage() ? this.usersNextLink() : this.usersPreviousLink();
103+
104+
if (link) {
105+
this.actions.searchUsersPageChange(link);
106+
this.currentPage.set(eventPageOneBased);
107+
this.first.set(event.first ?? 0);
108+
}
81109
}
82110

83111
private setSearchSubscription() {
@@ -86,7 +114,7 @@ export class AddModeratorDialogComponent implements OnInit, OnDestroy {
86114
filter((searchTerm) => !!searchTerm && searchTerm.trim().length > 0),
87115
debounceTime(500),
88116
distinctUntilChanged(),
89-
switchMap((searchTerm) => this.actions.searchUsers(searchTerm, this.currentPage())),
117+
switchMap((searchTerm) => this.actions.searchUsers(searchTerm)),
90118
takeUntilDestroyed(this.destroyRef)
91119
)
92120
.subscribe(() => {

src/app/features/moderation/services/moderators.service.ts

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
import { map, Observable } from 'rxjs';
1+
import { forkJoin, map, Observable } from 'rxjs';
22

33
import { inject, Injectable } from '@angular/core';
44

55
import { ENVIRONMENT } from '@core/provider/environment.provider';
66
import { ResourceType } from '@osf/shared/enums/resource-type.enum';
7-
import { JsonApiResponse, ResponseJsonApi } from '@osf/shared/models/common/json-api.model';
7+
import { parseSearchTotalCount } from '@osf/shared/helpers/search-total-count.helper';
8+
import { MapResources } from '@osf/shared/mappers/search';
9+
import { JsonApiResponse } from '@osf/shared/models/common/json-api.model';
810
import { PaginatedData } from '@osf/shared/models/paginated-data.model';
9-
import { UserDataJsonApi } from '@osf/shared/models/user/user-json-api.model';
11+
import { IndexCardSearchResponseJsonApi } from '@osf/shared/models/search/index-card-search-json-api.models';
12+
import { SearchUserDataModel } from '@osf/shared/models/user/search-user-data.model';
1013
import { JsonApiService } from '@osf/shared/services/json-api.service';
1114
import { StringOrNull } from '@shared/helpers/types.helper';
1215

13-
import { AddModeratorType } from '../enums';
16+
import { AddModeratorType, ModeratorPermission } from '../enums';
1417
import { ModerationMapper } from '../mappers';
1518
import { ModeratorAddModel, ModeratorDataJsonApi, ModeratorModel, ModeratorResponseJsonApi } from '../models';
1619

@@ -25,6 +28,14 @@ export class ModeratorsService {
2528
return `${this.environment.apiDomainUrl}/v2`;
2629
}
2730

31+
get shareTroveUrl() {
32+
return this.environment.shareTroveUrl;
33+
}
34+
35+
get webUrl() {
36+
return this.environment.webUrl;
37+
}
38+
2839
private readonly urlMap = new Map<ResourceType, string>([
2940
[ResourceType.Collection, 'providers/collections'],
3041
[ResourceType.Registration, 'providers/registrations'],
@@ -84,11 +95,86 @@ export class ModeratorsService {
8495
return this.jsonApiService.delete(baseUrl);
8596
}
8697

87-
searchUsers(value: string, page = 1): Observable<PaginatedData<ModeratorAddModel[]>> {
88-
const baseUrl = `${this.apiUrl}/search/users/?q=${value}*&page=${page}`;
98+
searchUsers(value: string, pageSize = 10): Observable<SearchUserDataModel<ModeratorAddModel[]>> {
99+
if (value.length === 5) {
100+
return forkJoin([this.searchUsersByName(value, pageSize), this.searchUsersById(value, pageSize)]).pipe(
101+
map(([nameResults, idResults]) => {
102+
const users = [...nameResults.users];
103+
const existingIds = new Set(users.map((u) => u.id));
104+
105+
idResults.users.forEach((user) => {
106+
if (!existingIds.has(user.id)) {
107+
users.push(user);
108+
existingIds.add(user.id);
109+
}
110+
});
111+
112+
return {
113+
users,
114+
totalCount: nameResults.totalCount + idResults.totalCount,
115+
next: nameResults.next,
116+
previous: nameResults.previous,
117+
};
118+
})
119+
);
120+
} else {
121+
return this.searchUsersByName(value, pageSize);
122+
}
123+
}
124+
125+
searchUsersByName(value: string, pageSize = 10): Observable<SearchUserDataModel<ModeratorAddModel[]>> {
126+
const baseUrl = `${this.shareTroveUrl}/index-card-search`;
127+
const params = {
128+
'cardSearchFilter[resourceType]': 'Person',
129+
'cardSearchFilter[accessService]': this.webUrl,
130+
'cardSearchText[name]': `${value}*`,
131+
acceptMediatype: 'application/vnd.api+json',
132+
'page[size]': pageSize,
133+
};
89134

90135
return this.jsonApiService
91-
.get<ResponseJsonApi<UserDataJsonApi[]>>(baseUrl)
92-
.pipe(map((response) => ModerationMapper.fromUsersWithPaginationGetResponse(response)));
136+
.get<IndexCardSearchResponseJsonApi>(baseUrl, params)
137+
.pipe(map((response) => this.handleResourcesRawResponse(response)));
138+
}
139+
140+
searchUsersById(value: string, pageSize = 10): Observable<SearchUserDataModel<ModeratorAddModel[]>> {
141+
const baseUrl = `${this.shareTroveUrl}/index-card-search`;
142+
const params = {
143+
'cardSearchFilter[resourceType]': 'Person',
144+
'cardSearchFilter[accessService]': this.webUrl,
145+
'cardSearchFilter[sameAs]': `${this.webUrl}/${value}`,
146+
acceptMediatype: 'application/vnd.api+json',
147+
'page[size]': pageSize,
148+
};
149+
150+
return this.jsonApiService
151+
.get<IndexCardSearchResponseJsonApi>(baseUrl, params)
152+
.pipe(map((response) => this.handleResourcesRawResponse(response)));
153+
}
154+
155+
getUsersByLink(link: string): Observable<SearchUserDataModel<ModeratorAddModel[]>> {
156+
return this.jsonApiService
157+
.get<IndexCardSearchResponseJsonApi>(link)
158+
.pipe(map((response) => this.handleResourcesRawResponse(response)));
159+
}
160+
161+
private handleResourcesRawResponse(
162+
response: IndexCardSearchResponseJsonApi
163+
): SearchUserDataModel<ModeratorAddModel[]> {
164+
const users = MapResources(response).map(
165+
(user) =>
166+
({
167+
id: user.absoluteUrl.split('/').pop(),
168+
fullName: user.name,
169+
permission: ModeratorPermission.Moderator,
170+
}) as ModeratorAddModel
171+
);
172+
173+
return {
174+
users,
175+
totalCount: parseSearchTotalCount(response),
176+
next: response.data?.relationships?.searchResultPage.links?.next?.href ?? null,
177+
previous: response.data?.relationships?.searchResultPage.links?.prev?.href ?? null,
178+
};
93179
}
94180
}

src/app/features/moderation/store/moderators/moderators.actions.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,13 @@ export class UpdateModeratorsSearchValue {
5454

5555
export class SearchUsers {
5656
static readonly type = `${ACTION_SCOPE} Search Users`;
57-
constructor(
58-
public searchValue: string | null,
59-
public page: number
60-
) {}
57+
constructor(public searchValue: string | null) {}
58+
}
59+
60+
export class SearchUsersPageChange {
61+
static readonly type = `${ACTION_SCOPE} Search Users Page Change`;
62+
63+
constructor(public link: string) {}
6164
}
6265

6366
export class ClearUsers {

src/app/features/moderation/store/moderators/moderators.model.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ import { ModeratorAddModel, ModeratorModel } from '../../models';
44

55
export interface ModeratorsStateModel {
66
moderators: ModeratorsDataStateModel;
7-
users: AsyncStateWithTotalCount<ModeratorAddModel[]>;
7+
users: UserListModel;
88
}
99

1010
interface ModeratorsDataStateModel extends AsyncStateWithTotalCount<ModeratorModel[]> {
1111
searchValue: string | null;
1212
}
1313

14+
interface UserListModel extends AsyncStateWithTotalCount<ModeratorAddModel[]> {
15+
next: string | null;
16+
previous: string | null;
17+
}
18+
1419
export const MODERATORS_STATE_DEFAULTS: ModeratorsStateModel = {
1520
moderators: {
1621
data: [],
@@ -24,5 +29,7 @@ export const MODERATORS_STATE_DEFAULTS: ModeratorsStateModel = {
2429
isLoading: false,
2530
error: null,
2631
totalCount: 0,
32+
next: null,
33+
previous: null,
2734
},
2835
};

src/app/features/moderation/store/moderators/moderators.selectors.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,14 @@ export class ModeratorsSelectors {
3939
static getUsersTotalCount(state: ModeratorsStateModel): number {
4040
return state.users.totalCount;
4141
}
42+
43+
@Selector([ModeratorsState])
44+
static getUsersNextLink(state: ModeratorsStateModel) {
45+
return state?.users?.next || null;
46+
}
47+
48+
@Selector([ModeratorsState])
49+
static getUsersPreviousLink(state: ModeratorsStateModel) {
50+
return state?.users?.previous || null;
51+
}
4252
}

src/app/features/moderation/store/moderators/moderators.state.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
DeleteModerator,
1717
LoadModerators,
1818
SearchUsers,
19+
SearchUsersPageChange,
1920
UpdateModerator,
2021
UpdateModeratorsSearchValue,
2122
} from './moderators.actions';
@@ -141,14 +142,43 @@ export class ModeratorsState {
141142
return of([]);
142143
}
143144

144-
return this.moderatorsService.searchUsers(action.searchValue, action.page).pipe(
145-
tap((users) => {
145+
return this.moderatorsService.searchUsers(action.searchValue).pipe(
146+
tap((response) => {
146147
ctx.patchState({
147148
users: {
148-
data: users.data.filter((user) => !addedModeratorsIds.includes(user.id!)),
149+
data: response.users.filter((user) => !addedModeratorsIds.includes(user.id!)),
149150
isLoading: false,
150151
error: '',
151-
totalCount: users.totalCount,
152+
totalCount: response.totalCount,
153+
next: response.next,
154+
previous: response.previous,
155+
},
156+
});
157+
}),
158+
catchError((error) => handleSectionError(ctx, 'users', error))
159+
);
160+
}
161+
162+
@Action(SearchUsersPageChange)
163+
searchUsersPageChange(ctx: StateContext<ModeratorsStateModel>, action: SearchUsersPageChange) {
164+
const state = ctx.getState();
165+
166+
ctx.patchState({
167+
users: { ...state.users, isLoading: true, error: null },
168+
});
169+
170+
const addedModeratorsIds = state.moderators.data.map((moderator) => moderator.userId);
171+
172+
return this.moderatorsService.getUsersByLink(action.link).pipe(
173+
tap((response) => {
174+
ctx.patchState({
175+
users: {
176+
data: response.users.filter((user) => !addedModeratorsIds.includes(user.id!)),
177+
isLoading: false,
178+
error: '',
179+
totalCount: response.totalCount,
180+
next: response.next,
181+
previous: response.previous,
152182
},
153183
});
154184
}),
@@ -158,6 +188,8 @@ export class ModeratorsState {
158188

159189
@Action(ClearUsers)
160190
clearUsers(ctx: StateContext<ModeratorsStateModel>) {
161-
ctx.patchState({ users: { data: [], isLoading: false, error: null, totalCount: 0 } });
191+
ctx.patchState({
192+
users: { data: [], isLoading: false, error: null, totalCount: 0, next: null, previous: null },
193+
});
162194
}
163195
}

src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<osf-custom-paginator
3232
[first]="first()"
3333
[totalCount]="totalUsersCount()"
34+
[showPageLinks]="false"
3435
(pageChanged)="pageChanged($event)"
3536
></osf-custom-paginator>
3637
}

0 commit comments

Comments
 (0)