Skip to content

Commit

Permalink
use prefetched user list in select user component (#4647)
Browse files Browse the repository at this point in the history
* use prefetched user list in select user component

there was an issue when fetching desk members
for a desk with many users, it would fail on
too big request when fetching such users by id.

SDCP-849
  • Loading branch information
petrjasek committed Sep 26, 2024
1 parent 0abca10 commit eefae69
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 47 deletions.
8 changes: 7 additions & 1 deletion scripts/api/desks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {IDesk, IStage} from 'superdesk-api';
import {IDesk, IStage, IUser} from 'superdesk-api';
import ng from 'core/services/ng';
import {OrderedMap} from 'immutable';

Expand Down Expand Up @@ -49,6 +49,10 @@ function getDeskById(id: IDesk['_id']): IDesk {
return getAllDesks().get(id);
}

function getDeskMembers(deskId: IDesk['_id']): Array<IUser> {
return ng.get('desks').deskMembers[deskId] ?? [];
}

interface IDesksApi {
/** Desk is considered active if it is being viewed in monitoring at the moment */
getActiveDeskId(): IDesk['_id'] | null;
Expand All @@ -58,6 +62,7 @@ interface IDesksApi {
getDeskById(id: IDesk['_id']): IDesk ;
getDeskStages(deskId: IDesk['_id']): OrderedMap<IStage['_id'], IStage>;
getCurrentUserDesks(): Array<IDesk>; // desks that current user has access to
getDeskMembers(deskId: IDesk['_id']): Array<IUser>; // members of the desk
}

export const desks: IDesksApi = {
Expand All @@ -68,4 +73,5 @@ export const desks: IDesksApi = {
getDeskById,
getDeskStages,
getCurrentUserDesks,
getDeskMembers,
};
49 changes: 5 additions & 44 deletions scripts/core/ui/components/SelectUser.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/* eslint-disable react/no-multi-comp */
import React from 'react';
import {IPropsSelectUser, IUser, IRestApiResponse} from 'superdesk-api';
import {gettext, getUserSearchMongoQuery} from 'core/utils';
import {IPropsSelectUser, IUser} from 'superdesk-api';
import {gettext, searchUsers} from 'core/utils';
import {UserAvatar} from 'apps/users/components/UserAvatar';
import {SelectWithTemplate, Spacer} from 'superdesk-ui-framework/react';
import {httpRequestJsonLocal} from 'core/helpers/network';
import {SuperdeskReactComponent} from 'core/SuperdeskReactComponent';
import {sdApi} from 'api';

Expand Down Expand Up @@ -118,47 +117,9 @@ export class SelectUser extends SuperdeskReactComponent<IPropsSelectUser, IState
inlineLabel={true}
labelHidden={true}
getItems={(searchString) => {
this.abortController?.abort();
this.abortController = new AbortController();

let query = {$and: []};
const desk = sdApi.desks.getDeskById(this.props.deskId);
const deskMemberIds = (desk?.members ?? []).map((member) => member.user);

if (this.props.deskId != null && this.props.deskId != '') {
query.$and.push({_id: {$in: deskMemberIds}});
}

if (searchString != null && searchString.length > 0) {
query.$and.push(getUserSearchMongoQuery(searchString));
}

const urlParams = {max_results: 50};

if (query.$and.length > 0) {
urlParams['where'] = query;
}

// Wrapping into additional promise in order to avoid having to handle rejected promise
// in `SelectWithTemplate` component. The component takes a generic promise
// as an argument and not a fetch result so it wouldn't be good to handle
// fetch-specific rejections there.
return new Promise((resolve) => {
httpRequestJsonLocal<IRestApiResponse<IUser>>({
method: 'GET',
path: '/users',
urlParams,
abortSignal: this.abortController.signal,
}).then((res) => {
resolve(res._items);
}).catch((err) => {
// If user types something in the filter input all unfinished requests will be aborted.
// This is expected behaviour here and should not throw an error.
if (err?.name !== 'AbortError') {
throw err;
}
});
});
const deskMembers = sdApi.desks.getDeskMembers(this.props.deskId);

return Promise.resolve(searchUsers(deskMembers, searchString));
}}
value={this.state.selectedUser}
onChange={(user) => {
Expand Down
39 changes: 37 additions & 2 deletions scripts/core/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import React from 'react';
import gettextjs from 'gettext.js';
import {appConfig, getUserInterfaceLanguage} from 'appConfig';
import {IVocabularyItem, IArticle, IBaseRestApiResponse, ILockInfo, IListViewFieldWithOptions} from 'superdesk-api';
import {
IVocabularyItem,
IArticle,
IBaseRestApiResponse,
ILockInfo,
IListViewFieldWithOptions,
IUser,
} from 'superdesk-api';
import {assertNever} from './helpers/typescript-helpers';
import {isObject, omit} from 'lodash';
import formatISO from 'date-fns/formatISO';
Expand Down Expand Up @@ -283,7 +290,15 @@ export function translateArticleType(type: IArticle['type']) {
}
}

export function getUserSearchMongoQuery(searchString: string) {
type IUserFieldQuery = {
[field in keyof IUser]?: {$regex: string, $options: string};
};

interface IMongoQuery {
$or: Array<IUserFieldQuery>;
}

export function getUserSearchMongoQuery(searchString: string): IMongoQuery {
return {
$or: [
{username: {$regex: searchString, $options: 'i'}},
Expand All @@ -296,6 +311,26 @@ export function getUserSearchMongoQuery(searchString: string) {
};
}

/**
* Should match the logic of `getUserSearchMongoQuery`
*/
export function searchUsers(users: Array<IUser>, searchString: string): Array<IUser> {
if (!searchString) {
return users;
}

const query = getUserSearchMongoQuery(searchString);
const regex = new RegExp(escapeRegExp(searchString), 'i');

return users.filter((user) => {
return query['$or'].some((orQuery) => {
return Object.keys(orQuery).every((key) => {
return user[key] ? regex.test(user[key]) : false;
});
});
});
}

export function getItemTypes() {
const item_types = [
{type: 'all', label: gettext('all')},
Expand Down

0 comments on commit eefae69

Please sign in to comment.