feat: Territory autocomplete (#2124)
* feat: Territory autocomplete Closes #992. * refactor: refactor UserSuggest and TerritorySuggest components * style: lint * refactor: unify user and territory autocomplete logic * simplify a bit and fix unrelated onSelect re-query * fix skipping empty string on forward draft population --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
parent
d4e3853f27
commit
0edf68cab9
@ -35,6 +35,27 @@ export async function getSub (parent, { name }, { models, me }) {
|
|||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
sub: getSub,
|
sub: getSub,
|
||||||
|
subSuggestions: async (parent, { q, limit = 5 }, { models }) => {
|
||||||
|
let subs = []
|
||||||
|
if (q) {
|
||||||
|
subs = await models.$queryRaw`
|
||||||
|
SELECT name
|
||||||
|
FROM "Sub"
|
||||||
|
WHERE status = 'ACTIVE'
|
||||||
|
AND SIMILARITY(name, ${q}) > 0.1
|
||||||
|
ORDER BY SIMILARITY(name, ${q}) DESC
|
||||||
|
LIMIT ${limit}`
|
||||||
|
} else {
|
||||||
|
subs = await models.$queryRaw`
|
||||||
|
SELECT name
|
||||||
|
FROM "Sub"
|
||||||
|
WHERE status = 'ACTIVE'
|
||||||
|
ORDER BY name ASC
|
||||||
|
LIMIT ${limit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return subs
|
||||||
|
},
|
||||||
subs: async (parent, args, { models, me }) => {
|
subs: async (parent, args, { models, me }) => {
|
||||||
if (me) {
|
if (me) {
|
||||||
const currentUser = await models.user.findUnique({ where: { id: me.id } })
|
const currentUser = await models.user.findUnique({ where: { id: me.id } })
|
||||||
|
@ -7,6 +7,7 @@ export default gql`
|
|||||||
subs: [Sub!]!
|
subs: [Sub!]!
|
||||||
topSubs(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
|
topSubs(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
|
||||||
userSubs(name: String!, cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
|
userSubs(name: String!, cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
|
||||||
|
subSuggestions(q: String!, limit: Limit): [Sub!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subs {
|
type Subs {
|
||||||
|
@ -198,7 +198,7 @@ export default function AdvPostForm ({ children, item, sub, storageKeyPrefix })
|
|||||||
for (let i = 0; i < MAX_FORWARDS; i++) {
|
for (let i = 0; i < MAX_FORWARDS; i++) {
|
||||||
['nym', 'pct'].forEach(key => {
|
['nym', 'pct'].forEach(key => {
|
||||||
const value = window.localStorage.getItem(`${storageKeyPrefix}-forward[${i}].${key}`)
|
const value = window.localStorage.getItem(`${storageKeyPrefix}-forward[${i}].${key}`)
|
||||||
if (value) {
|
if (value !== undefined && value !== null) {
|
||||||
formik?.setFieldValue(`forward[${i}].${key}`, value)
|
formik?.setFieldValue(`forward[${i}].${key}`, value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -16,6 +16,7 @@ import AddIcon from '@/svgs/add-fill.svg'
|
|||||||
import CloseIcon from '@/svgs/close-line.svg'
|
import CloseIcon from '@/svgs/close-line.svg'
|
||||||
import { gql, useLazyQuery } from '@apollo/client'
|
import { gql, useLazyQuery } from '@apollo/client'
|
||||||
import { USER_SUGGESTIONS } from '@/fragments/users'
|
import { USER_SUGGESTIONS } from '@/fragments/users'
|
||||||
|
import { SUB_SUGGESTIONS } from '@/fragments/subs'
|
||||||
import TextareaAutosize from 'react-textarea-autosize'
|
import TextareaAutosize from 'react-textarea-autosize'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
import { numWithUnits } from '@/lib/format'
|
import { numWithUnits } from '@/lib/format'
|
||||||
@ -139,6 +140,94 @@ function setNativeValue (textarea, value) {
|
|||||||
textarea.dispatchEvent(new Event('input', { bubbles: true, value }))
|
textarea.dispatchEvent(new Event('input', { bubbles: true, value }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useEntityAutocomplete ({
|
||||||
|
prefix,
|
||||||
|
meta,
|
||||||
|
helpers,
|
||||||
|
innerRef,
|
||||||
|
setSelectionRange,
|
||||||
|
SuggestComponent
|
||||||
|
}) {
|
||||||
|
const [entityData, setEntityData] = useState()
|
||||||
|
|
||||||
|
const handleSelect = useCallback((name) => {
|
||||||
|
if (entityData?.start === undefined || entityData?.end === undefined) return
|
||||||
|
const { start, end } = entityData
|
||||||
|
setEntityData(undefined)
|
||||||
|
const first = `${meta?.value.substring(0, start)}${prefix}${name}`
|
||||||
|
const second = meta?.value.substring(end)
|
||||||
|
const updatedValue = `${first}${second}`
|
||||||
|
helpers.setValue(updatedValue)
|
||||||
|
setSelectionRange({ start: first.length, end: first.length })
|
||||||
|
innerRef.current.focus()
|
||||||
|
}, [entityData, meta?.value, helpers, prefix, setSelectionRange, innerRef])
|
||||||
|
|
||||||
|
const handleTextChange = useCallback((e) => {
|
||||||
|
const { value, selectionStart } = e.target
|
||||||
|
if (!value || selectionStart === undefined) {
|
||||||
|
setEntityData(undefined)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let priorSpace = -1
|
||||||
|
for (let i = selectionStart - 1; i >= 0; i--) {
|
||||||
|
if (/[^\w@~]/.test(value[i])) {
|
||||||
|
priorSpace = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextSpace = value.length
|
||||||
|
for (let i = selectionStart; i <= value.length; i++) {
|
||||||
|
if (/[^\w]/.test(value[i])) {
|
||||||
|
nextSpace = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSegment = value.substring(priorSpace + 1, nextSpace)
|
||||||
|
const regexPattern = new RegExp(`^\\${prefix}\\w*$`)
|
||||||
|
|
||||||
|
if (regexPattern.test(currentSegment)) {
|
||||||
|
const { top, left } = textAreaCaret(e.target, e.target.selectionStart)
|
||||||
|
setEntityData({
|
||||||
|
query: currentSegment,
|
||||||
|
start: priorSpace + 1,
|
||||||
|
end: nextSpace,
|
||||||
|
style: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: `${top + Number(window.getComputedStyle(e.target).lineHeight.replace('px', ''))}px`,
|
||||||
|
left: `${left}px`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
setEntityData(undefined)
|
||||||
|
return false
|
||||||
|
}, [prefix])
|
||||||
|
|
||||||
|
// Return a function that takes a render prop instead of directly returning the component
|
||||||
|
return {
|
||||||
|
entityData,
|
||||||
|
handleSelect,
|
||||||
|
handleTextChange,
|
||||||
|
renderSuggest: (renderProps) => {
|
||||||
|
if (!entityData) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SuggestComponent
|
||||||
|
query={entityData?.query}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
dropdownStyle={entityData?.style}
|
||||||
|
>
|
||||||
|
{renderProps}
|
||||||
|
</SuggestComponent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKeyDown, innerRef, ...props }) {
|
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKeyDown, innerRef, ...props }) {
|
||||||
const [tab, setTab] = useState('write')
|
const [tab, setTab] = useState('write')
|
||||||
const [, meta, helpers] = useField(props)
|
const [, meta, helpers] = useField(props)
|
||||||
@ -198,18 +287,23 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||||||
}
|
}
|
||||||
}, [innerRef, selectionRange.start, selectionRange.end])
|
}, [innerRef, selectionRange.start, selectionRange.end])
|
||||||
|
|
||||||
const [mention, setMention] = useState()
|
const userAutocomplete = useEntityAutocomplete({
|
||||||
const insertMention = useCallback((name) => {
|
prefix: '@',
|
||||||
if (mention?.start === undefined || mention?.end === undefined) return
|
meta,
|
||||||
const { start, end } = mention
|
helpers,
|
||||||
setMention(undefined)
|
innerRef,
|
||||||
const first = `${meta?.value.substring(0, start)}@${name}`
|
setSelectionRange,
|
||||||
const second = meta?.value.substring(end)
|
SuggestComponent: UserSuggest
|
||||||
const updatedValue = `${first}${second}`
|
})
|
||||||
helpers.setValue(updatedValue)
|
|
||||||
setSelectionRange({ start: first.length, end: first.length })
|
const territoryAutocomplete = useEntityAutocomplete({
|
||||||
innerRef.current.focus()
|
prefix: '~',
|
||||||
}, [mention, meta?.value, helpers?.setValue])
|
meta,
|
||||||
|
helpers,
|
||||||
|
innerRef,
|
||||||
|
setSelectionRange,
|
||||||
|
SuggestComponent: TerritorySuggest
|
||||||
|
})
|
||||||
|
|
||||||
const uploadFeesUpdate = useDebounceCallback(
|
const uploadFeesUpdate = useDebounceCallback(
|
||||||
(text) => {
|
(text) => {
|
||||||
@ -219,50 +313,16 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||||||
|
|
||||||
const onChangeInner = useCallback((formik, e) => {
|
const onChangeInner = useCallback((formik, e) => {
|
||||||
if (onChange) onChange(formik, e)
|
if (onChange) onChange(formik, e)
|
||||||
// check for mention editing
|
// check for mentions and territory suggestions
|
||||||
const { value, selectionStart } = e.target
|
uploadFeesUpdate(e.target.value)
|
||||||
uploadFeesUpdate(value)
|
|
||||||
|
|
||||||
if (!value || selectionStart === undefined) {
|
// Try to match user mentions first, then territories
|
||||||
setMention(undefined)
|
if (!userAutocomplete.handleTextChange(e)) {
|
||||||
return
|
territoryAutocomplete.handleTextChange(e)
|
||||||
}
|
}
|
||||||
|
}, [onChange, uploadFeesUpdate, userAutocomplete, territoryAutocomplete])
|
||||||
|
|
||||||
let priorSpace = -1
|
const onKeyDownInner = useCallback((userSuggestOnKeyDown, territorySuggestOnKeyDown) => {
|
||||||
for (let i = selectionStart - 1; i >= 0; i--) {
|
|
||||||
if (/[^\w@]/.test(value[i])) {
|
|
||||||
priorSpace = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let nextSpace = value.length
|
|
||||||
for (let i = selectionStart; i <= value.length; i++) {
|
|
||||||
if (/[^\w]/.test(value[i])) {
|
|
||||||
nextSpace = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const currentSegment = value.substring(priorSpace + 1, nextSpace)
|
|
||||||
|
|
||||||
// set the query to the current character segment and note where it appears
|
|
||||||
if (/^@\w*$/.test(currentSegment)) {
|
|
||||||
const { top, left } = textAreaCaret(e.target, e.target.selectionStart)
|
|
||||||
setMention({
|
|
||||||
query: currentSegment,
|
|
||||||
start: priorSpace + 1,
|
|
||||||
end: nextSpace,
|
|
||||||
style: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: `${top + Number(window.getComputedStyle(e.target).lineHeight.replace('px', ''))}px`,
|
|
||||||
left: `${left}px`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setMention(undefined)
|
|
||||||
}
|
|
||||||
}, [onChange, setMention, uploadFeesUpdate])
|
|
||||||
|
|
||||||
const onKeyDownInner = useCallback((userSuggestOnKeyDown) => {
|
|
||||||
return (e) => {
|
return (e) => {
|
||||||
const metaOrCtrl = e.metaKey || e.ctrlKey
|
const metaOrCtrl = e.metaKey || e.ctrlKey
|
||||||
if (metaOrCtrl) {
|
if (metaOrCtrl) {
|
||||||
@ -293,12 +353,16 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!metaOrCtrl) {
|
if (!metaOrCtrl) {
|
||||||
userSuggestOnKeyDown(e)
|
if (userAutocomplete.entityData) {
|
||||||
|
userSuggestOnKeyDown(e)
|
||||||
|
} else if (territoryAutocomplete.entityData) {
|
||||||
|
territorySuggestOnKeyDown(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onKeyDown) onKeyDown(e)
|
if (onKeyDown) onKeyDown(e)
|
||||||
}
|
}
|
||||||
}, [innerRef, helpers?.setValue, setSelectionRange, onKeyDown])
|
}, [innerRef, helpers?.setValue, setSelectionRange, onKeyDown, userAutocomplete.entityData, territoryAutocomplete.entityData])
|
||||||
|
|
||||||
const onPaste = useCallback((event) => {
|
const onPaste = useCallback((event) => {
|
||||||
const items = event.clipboardData.items
|
const items = event.clipboardData.items
|
||||||
@ -409,22 +473,32 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||||||
</Nav>
|
</Nav>
|
||||||
<div className={`position-relative ${tab === 'write' ? '' : 'd-none'}`}>
|
<div className={`position-relative ${tab === 'write' ? '' : 'd-none'}`}>
|
||||||
<UserSuggest
|
<UserSuggest
|
||||||
query={mention?.query}
|
query={userAutocomplete.entityData?.query}
|
||||||
onSelect={insertMention}
|
onSelect={userAutocomplete.handleSelect}
|
||||||
dropdownStyle={mention?.style}
|
dropdownStyle={userAutocomplete.entityData?.style}
|
||||||
>{({ onKeyDown: userSuggestOnKeyDown, resetSuggestions }) => (
|
>{({ onKeyDown: userSuggestOnKeyDown, resetSuggestions: resetUserSuggestions }) => (
|
||||||
<InputInner
|
<TerritorySuggest
|
||||||
innerRef={innerRef}
|
query={territoryAutocomplete.entityData?.query}
|
||||||
{...props}
|
onSelect={territoryAutocomplete.handleSelect}
|
||||||
onChange={onChangeInner}
|
dropdownStyle={territoryAutocomplete.entityData?.style}
|
||||||
onKeyDown={onKeyDownInner(userSuggestOnKeyDown)}
|
>{({ onKeyDown: territorySuggestOnKeyDown, resetSuggestions: resetTerritorySuggestions }) => (
|
||||||
onBlur={() => setTimeout(resetSuggestions, 500)}
|
<InputInner
|
||||||
onDragEnter={onDragEnter}
|
innerRef={innerRef}
|
||||||
onDragLeave={onDragLeave}
|
{...props}
|
||||||
onDrop={onDrop}
|
onChange={onChangeInner}
|
||||||
onPaste={onPaste}
|
onKeyDown={onKeyDownInner(userSuggestOnKeyDown, territorySuggestOnKeyDown)}
|
||||||
className={dragStyle === 'over' ? styles.dragOver : ''}
|
onBlur={() => {
|
||||||
/>)}
|
setTimeout(resetUserSuggestions, 500)
|
||||||
|
setTimeout(resetTerritorySuggestions, 500)
|
||||||
|
}}
|
||||||
|
onDragEnter={onDragEnter}
|
||||||
|
onDragLeave={onDragLeave}
|
||||||
|
onDrop={onDrop}
|
||||||
|
onPaste={onPaste}
|
||||||
|
className={dragStyle === 'over' ? styles.dragOver : ''}
|
||||||
|
/>)}
|
||||||
|
</TerritorySuggest>
|
||||||
|
)}
|
||||||
</UserSuggest>
|
</UserSuggest>
|
||||||
</div>
|
</div>
|
||||||
{tab !== 'write' &&
|
{tab !== 'write' &&
|
||||||
@ -617,34 +691,34 @@ function InputInner ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_SUGGESTIONS = { array: [], index: 0 }
|
const INITIAL_SUGGESTIONS = { array: [], index: 0 }
|
||||||
export function UserSuggest ({
|
|
||||||
query, onSelect, dropdownStyle, children,
|
export function BaseSuggest ({
|
||||||
transformUser = user => user, selectWithTab = true, filterUsers = () => true
|
query, onSelect, dropdownStyle,
|
||||||
|
transformItem = item => item, selectWithTab = true, filterItems = () => true,
|
||||||
|
getSuggestionsQuery, queryName, itemsField,
|
||||||
|
children
|
||||||
}) {
|
}) {
|
||||||
const [getSuggestions] = useLazyQuery(USER_SUGGESTIONS, {
|
const [getSuggestions] = useLazyQuery(getSuggestionsQuery, {
|
||||||
onCompleted: data => {
|
onCompleted: data => {
|
||||||
query !== undefined && setSuggestions({
|
query !== undefined && setSuggestions({
|
||||||
array: data.userSuggestions
|
array: data[itemsField]
|
||||||
.filter((...args) => filterUsers(query, ...args))
|
.filter((...args) => filterItems(query, ...args))
|
||||||
.map(transformUser),
|
.map(transformItem),
|
||||||
index: 0
|
index: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS)
|
const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS)
|
||||||
const resetSuggestions = useCallback(() => setSuggestions(INITIAL_SUGGESTIONS), [])
|
const resetSuggestions = useCallback(() => setSuggestions(INITIAL_SUGGESTIONS), [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query !== undefined) {
|
if (query !== undefined) {
|
||||||
// remove both the leading @ and any @domain after nym
|
// remove the leading character and any trailing spaces
|
||||||
const q = query?.replace(/^[@ ]+|[ ]+$/g, '').replace(/@[^\s]*$/, '')
|
const q = query?.replace(/^[@ ~]+|[ ]+$/g, '').replace(/@[^\s]*$/, '').replace(/~[^\s]*$/, '')
|
||||||
getSuggestions({ variables: { q, limit: 5 } })
|
getSuggestions({ variables: { q, limit: 5 } })
|
||||||
} else {
|
} else {
|
||||||
resetSuggestions()
|
resetSuggestions()
|
||||||
}
|
}
|
||||||
}, [query, resetSuggestions, getSuggestions])
|
}, [query, resetSuggestions, getSuggestions])
|
||||||
|
|
||||||
const onKeyDown = useCallback(e => {
|
const onKeyDown = useCallback(e => {
|
||||||
switch (e.code) {
|
switch (e.code) {
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
@ -689,7 +763,6 @@ export function UserSuggest ({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, [onSelect, resetSuggestions, suggestions])
|
}, [onSelect, resetSuggestions, suggestions])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{children?.({ onKeyDown, resetSuggestions })}
|
{children?.({ onKeyDown, resetSuggestions })}
|
||||||
@ -712,17 +785,17 @@ export function UserSuggest ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InputUserSuggest ({
|
function BaseInputSuggest ({
|
||||||
label, groupClassName, transformUser, filterUsers,
|
label, groupClassName, transformItem, filterItems,
|
||||||
selectWithTab, onChange, transformQuery, ...props
|
selectWithTab, onChange, transformQuery, SuggestComponent, prefixRegex, ...props
|
||||||
}) {
|
}) {
|
||||||
const [ovalue, setOValue] = useState()
|
const [ovalue, setOValue] = useState()
|
||||||
const [query, setQuery] = useState()
|
const [query, setQuery] = useState()
|
||||||
return (
|
return (
|
||||||
<FormGroup label={label} className={groupClassName}>
|
<FormGroup label={label} className={groupClassName}>
|
||||||
<UserSuggest
|
<SuggestComponent
|
||||||
transformUser={transformUser}
|
transformItem={transformItem}
|
||||||
filterUsers={filterUsers}
|
filterItems={filterItems}
|
||||||
selectWithTab={selectWithTab}
|
selectWithTab={selectWithTab}
|
||||||
onSelect={(v) => {
|
onSelect={(v) => {
|
||||||
// HACK ... ovalue does not trigger onChange
|
// HACK ... ovalue does not trigger onChange
|
||||||
@ -737,19 +810,85 @@ export function InputUserSuggest ({
|
|||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
onChange={(formik, e) => {
|
onChange={(formik, e) => {
|
||||||
onChange && onChange(formik, e)
|
onChange && onChange(formik, e)
|
||||||
|
if (e.target.value === ovalue) {
|
||||||
|
// we don't need to set the ovalue or query if the value is the same
|
||||||
|
return
|
||||||
|
}
|
||||||
setOValue(e.target.value)
|
setOValue(e.target.value)
|
||||||
setQuery(e.target.value.replace(/^[@ ]+|[ ]+$/g, ''))
|
setQuery(e.target.value.replace(prefixRegex, ''))
|
||||||
}}
|
}}
|
||||||
overrideValue={ovalue}
|
overrideValue={ovalue}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onBlur={() => setTimeout(resetSuggestions, 500)}
|
onBlur={() => setTimeout(resetSuggestions, 500)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</UserSuggest>
|
</SuggestComponent>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function InputUserSuggest ({
|
||||||
|
transformUser, filterUsers, ...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<BaseInputSuggest
|
||||||
|
transformItem={transformUser}
|
||||||
|
filterItems={filterUsers}
|
||||||
|
SuggestComponent={UserSuggest}
|
||||||
|
prefixRegex={/^[@ ]+|[ ]+$/g}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InputTerritorySuggest ({
|
||||||
|
transformSub, filterSubs, ...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<BaseInputSuggest
|
||||||
|
transformItem={transformSub}
|
||||||
|
filterItems={filterSubs}
|
||||||
|
SuggestComponent={TerritorySuggest}
|
||||||
|
prefixRegex={/^[~ ]+|[ ]+$/g}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserSuggest ({
|
||||||
|
transformUser = user => user, filterUsers = () => true,
|
||||||
|
children, ...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<BaseSuggest
|
||||||
|
transformItem={transformUser}
|
||||||
|
filterItems={filterUsers}
|
||||||
|
getSuggestionsQuery={USER_SUGGESTIONS}
|
||||||
|
itemsField='userSuggestions'
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BaseSuggest>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TerritorySuggest ({
|
||||||
|
transformSub = sub => sub, filterSubs = () => true,
|
||||||
|
children, ...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<BaseSuggest
|
||||||
|
transformItem={transformSub}
|
||||||
|
filterItems={filterSubs}
|
||||||
|
getSuggestionsQuery={SUB_SUGGESTIONS}
|
||||||
|
itemsField='subSuggestions'
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BaseSuggest>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function Input ({ label, groupClassName, under, ...props }) {
|
export function Input ({ label, groupClassName, under, ...props }) {
|
||||||
return (
|
return (
|
||||||
<FormGroup label={label} className={groupClassName}>
|
<FormGroup label={label} className={groupClassName}>
|
||||||
|
@ -122,6 +122,14 @@ export const SUB_SEARCH = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const SUB_SUGGESTIONS = gql`
|
||||||
|
query subSuggestions($q: String!, $limit: Limit) {
|
||||||
|
subSuggestions(q: $q, limit: $limit) {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const TOP_SUBS = gql`
|
export const TOP_SUBS = gql`
|
||||||
${SUB_FULL_FIELDS}
|
${SUB_FULL_FIELDS}
|
||||||
query TopSubs($cursor: String, $when: String, $from: String, $to: String, $by: String, ) {
|
query TopSubs($cursor: String, $when: String, $from: String, $to: String, $by: String, ) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user