Skip to content

Commit

Permalink
Merge pull request #3127 from goratt12/2409-validate-collaborator-use…
Browse files Browse the repository at this point in the history
…rnames

feat(research): select and validate research collaborators usernames
  • Loading branch information
iSCJT authored Jan 22, 2024
2 parents 3e1f44f + 459db42 commit 381407e
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/components/src/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export const Select = (props: Props) => {
options={options}
onChange={(v) => props.onChange && props.onChange(v)}
value={props.value}
onInputChange={props.onInputChange}
/>
)
}
4 changes: 3 additions & 1 deletion src/pages/Research/Content/Common/Research.form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from 'src/utils/validators'
import { Box, Card, Flex, Heading, Label } from 'theme-ui'

import { UserNameSelect } from '../../../common/UserNameSelect/UserNameSelect'
import {
RESEARCH_MAX_LENGTH,
RESEARCH_TITLE_MAX_LENGTH,
Expand Down Expand Up @@ -330,8 +331,9 @@ const ResearchForm = observer((props: IProps) => {
</ResearchFormLabel>
<Field
name="collaborators"
component={FieldInput}
component={UserNameSelect}
placeholder={collaborators.placeholder}
defaultOptions={[]}
/>
</Flex>
<Flex sx={{ flexDirection: 'column' }} mb={3}>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Research/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const overview: ILabels = {
title: 'Which categories fit your research?',
},
collaborators: {
placeholder: 'A comma separated list of usernames.',
placeholder: 'Select collaborators or start typing to find them',
title: 'Who have you been collaborating on this Research with?',
},
description: {
Expand Down
36 changes: 36 additions & 0 deletions src/pages/common/UserNameSelect/LoadUserNameOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { IUserPP } from 'src/models'
import type { UserStore } from 'src/stores/User/user.store'

const USER_RESULTS_LIMIT = 20

export interface IOption {
value: string
label: string
}

export const loadUserNameOptions = async (
userStore: UserStore,
defaultOptions: string[] | undefined,
inputValue: string,
) => {
// if there is no user input, use defaultOptions
if (inputValue == '') {
const selectOptions: IOption[] = defaultOptions?.length
? defaultOptions
.filter((user) => user != '')
.map((user) => ({
value: user,
label: user,
}))
: []
return selectOptions
} else {
const usersStartingWithInput: IUserPP[] =
await userStore.getUsersStartingWith(inputValue, USER_RESULTS_LIMIT)
const selectOptions: IOption[] = usersStartingWithInput.map((user) => ({
value: user.userName,
label: user.userName,
}))
return selectOptions
}
}
59 changes: 59 additions & 0 deletions src/pages/common/UserNameSelect/UserNameSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect, useState } from 'react'
import { observer } from 'mobx-react'
import { Select } from 'oa-components'
import { useCommonStores } from 'src/index'

import { loadUserNameOptions } from './LoadUserNameOptions'

import type { IOption } from './LoadUserNameOptions'

interface IProps {
input: {
value: string[]
onChange: (v: string[]) => void
}
defaultOptions?: string[]
placeholder?: string
isForm?: boolean
}

export const UserNameSelect = observer((props: IProps) => {
const { defaultOptions, input, isForm, placeholder } = props
const { userStore } = useCommonStores().stores
const [inputValue, setInputValue] = useState('')
const [options, setOptions] = useState<IOption[]>([])

const loadOptions = async (inputVal: string) => {
const selectOptions = await loadUserNameOptions(
userStore,
defaultOptions,
inputVal,
)
setOptions(selectOptions)
}

// Effect to load options whenever the input value changes
useEffect(() => {
loadOptions(inputValue)
}, [inputValue])

const value = input.value?.length
? input.value.map((user) => ({
value: user,
label: user,
}))
: []

return (
<Select
variant={isForm ? 'form' : undefined}
options={options}
placeholder={placeholder}
value={value}
onChange={(v: IOption[]) => input.onChange(v.map((user) => user.value))}
onInputChange={setInputValue}
isClearable={true}
isMulti={true}
/>
)
})
29 changes: 29 additions & 0 deletions src/stores/User/user.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
signInWithEmailAndPassword,
updateProfile,
} from 'firebase/auth'
import { uniqBy } from 'lodash'
import { action, computed, makeObservable, observable, toJS } from 'mobx'
import { EmailNotificationFrequency, IModerationStatus } from 'oa-shared'

Expand Down Expand Up @@ -60,6 +61,34 @@ export class UserStore extends ModuleStore {
return this.aggregationsStore.aggregations.users_verified || {}
}

@action
public getAllUsers() {
return this.allDocs$
}

@action
public async getUsersStartingWith(prefix: string, limit?: number) {
// getWhere with the '>=' operator will return every userName that is lexicographically greater than prefix, so adding filter to avoid getting not relvant userNames
const users: IUserPP[] = await this.db
.collection<IUserPP>(COLLECTION_NAME)
.getWhere('userName', '>=', prefix, limit)
const uniqueUsers: IUserPP[] = uniqBy(
users.filter((user) => user.userName?.startsWith(prefix)),
(user) => user.userName,
)
return uniqueUsers
}

@action
private updateActiveUser(user?: IUserPPDB | null) {
this.user = user
}

@action
public setUpdateStatus(update: keyof IUserUpdateStatus) {
this.updateStatus[update] = true
}

// when registering a new user create firebase auth profile as well as database user profile
public async registerNewUser(
email: string,
Expand Down
8 changes: 7 additions & 1 deletion src/stores/databaseV2/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ export interface DBQueryWhereOptions {
value: DBQueryWhereValue
}

export type DBQueryWhereOperator = '>' | '<' | '==' | '!=' | 'array-contains'
export type DBQueryWhereOperator =
| '>'
| '>='
| '<'
| '=='
| '!='
| 'array-contains'
export type DBQueryWhereValue = string | number

/**
Expand Down

0 comments on commit 381407e

Please sign in to comment.