diff --git a/packages/ui-default/components/autocomplete/DomainSelectAutoComplete.jsx b/packages/ui-default/components/autocomplete/DomainSelectAutoComplete.tsx similarity index 64% rename from packages/ui-default/components/autocomplete/DomainSelectAutoComplete.jsx rename to packages/ui-default/components/autocomplete/DomainSelectAutoComplete.tsx index ffab53bad..ca54c1b3b 100644 --- a/packages/ui-default/components/autocomplete/DomainSelectAutoComplete.jsx +++ b/packages/ui-default/components/autocomplete/DomainSelectAutoComplete.tsx @@ -5,6 +5,22 @@ import DOMAttachedObject from 'vj/components/DOMAttachedObject'; import AutoComplete from '.'; import DomainSelectAutoCompleteFC from './components/DomainSelectAutoComplete'; +const Component = React.forwardRef((props: { value: string, multi: boolean, onChange: (v: string) => void }, ref) => { + const [value, setValue] = React.useState(props.value); + return ( + i.trim())} + onChange={(v) => { + setValue(v); + props.onChange(v); + }} + multi={props.multi} + /> + ); +}); + export default class DomainSelectAutoComplete extends AutoComplete { static DOMAttachKey = 'ucwDomainSelectAutoCompleteInstance'; @@ -18,13 +34,11 @@ export default class DomainSelectAutoComplete extends AutoComplete { attach() { const value = this.$dom.val(); ReactDOM.render( - { this.ref = ref; }} - height="34px" - defaultItems={value} + value={value} onChange={this.onChange} multi={this.options.multi} - freeSolo={this.options.multi} />, this.container, ); diff --git a/packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.jsx b/packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.tsx similarity index 67% rename from packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.jsx rename to packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.tsx index 13d6572ce..62abdb568 100644 --- a/packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.jsx +++ b/packages/ui-default/components/autocomplete/ProblemSelectAutoComplete.tsx @@ -5,6 +5,22 @@ import DOMAttachedObject from 'vj/components/DOMAttachedObject'; import AutoComplete from '.'; import ProblemSelectAutoCompleteFC from './components/ProblemSelectAutoComplete'; +const Component = React.forwardRef((props, ref) => { + const [value, setValue] = React.useState(props.value); + return ( + i.trim())} + onChange={(v) => { + setValue(v); + props.onChange(v); + }} + multi={props.multi} + /> + ); +}); + export default class ProblemSelectAutoComplete extends AutoComplete { static DOMAttachKey = 'ucwProblemSelectAutoCompleteInstance'; @@ -18,13 +34,11 @@ export default class ProblemSelectAutoComplete extends AutoComplete { attach() { const value = this.$dom.val(); ReactDOM.render( - { this.ref = ref; }} - height="34px" - defaultItems={value} - onChange={this.onChange} + value={value} multi={this.options.multi} - freeSolo={this.options.multi} + onChange={this.onChange} />, this.container, ); diff --git a/packages/ui-default/components/autocomplete/UserSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/UserSelectAutoComplete.tsx index 817c79b8b..ba83ca9f7 100644 --- a/packages/ui-default/components/autocomplete/UserSelectAutoComplete.tsx +++ b/packages/ui-default/components/autocomplete/UserSelectAutoComplete.tsx @@ -1,24 +1,28 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { QueryClient, QueryClientProvider } from 'react-query'; import { assign } from 'lodash'; import DOMAttachedObject from 'vj/components/DOMAttachedObject'; import AutoComplete, { AutoCompleteOptions } from '.'; import UserSelectAutoCompleteFC from './components/UserSelectAutoComplete'; +const Component = React.forwardRef((props, ref) => { + const [value, setValue] = React.useState(props.value); + return ( + i.trim())} + onChange={(v) => { + setValue(v); + props.onChange(v); + }} + multi={props.multi} + /> + ); +}); + export default class UserSelectAutoComplete extends AutoComplete { static DOMAttachKey = 'ucwUserSelectAutoCompleteInstance'; - client = new QueryClient({ - defaultOptions: { - queries: { - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchOnReconnect: false, - retry: false, - staleTime: Infinity, - }, - }, - }); constructor($dom, options: AutoCompleteOptions) { super($dom, { @@ -35,16 +39,12 @@ export default class UserSelectAutoComplete extends AutoC attach() { const value = this.$dom.val(); ReactDOM.render( - - { this.ref = ref; }} - height={this.options.height || '34px'} - defaultItems={value} - onChange={this.onChange} - multi={this.options.multi} - freeSolo={this.options.multi} - /> - , + { this.ref = ref; }} + value={value} + multi={this.options.multi} + onChange={this.onChange} + />, this.container, ); } diff --git a/packages/ui-default/components/autocomplete/components/AutoComplete.jsx b/packages/ui-default/components/autocomplete/components/AutoComplete.tsx similarity index 65% rename from packages/ui-default/components/autocomplete/components/AutoComplete.jsx rename to packages/ui-default/components/autocomplete/components/AutoComplete.tsx index 410a84fb5..3c9dc5e45 100644 --- a/packages/ui-default/components/autocomplete/components/AutoComplete.jsx +++ b/packages/ui-default/components/autocomplete/components/AutoComplete.tsx @@ -7,38 +7,89 @@ import PropTypes from 'prop-types'; import { debounce } from 'lodash'; import Icon from 'vj/components/react/IconComponent'; +export interface AutoCompleteProps { + width?: string; + /** + * if you need fix height, set to at least "30px" + * for Hydro, no less then "34px" can be better + */ + height?: string; + disabled?: boolean; + disabledHint?: string; + listStyle?: React.CSSProperties; + cacheKey?: string; + renderItem?: (item: Item) => any; + queryItems?: (query: string) => Promise | Item[]; + fetchItems?: (ids: string[]) => Promise | Item[]; + itemText?: (item: Item) => string; + itemKey?: (item: Item) => string; + onChange?: (value: string) => any; + multi?: boolean; + selectedKeys?: string[]; + allowEmptyQuery?: boolean; + freeSolo?: boolean; + freeSoloConverter?: (value: string) => string; +} + +export interface AutoCompleteHandle { + getSelectedItems: () => Item[]; + getSelectedItemKeys: () => string[]; + getSelectedItemsAsString: () => string; + setSelectedItems: (items: Item[]) => void; + getQuery: () => string; + setQuery: (query: string) => void; + triggerQuery: () => any; + closeList: () => void; + /** returns comma seperated value text */ + getValue: () => string; + /** return value detail */ + getValueArray: () => Item[]; + clear: () => void; + focus: () => void; +} + +const superCache = {}; + // eslint-disable-next-line prefer-arrow-callback -const AutoComplete = forwardRef(function AutoComplete(props, ref) { +const AutoComplete = forwardRef(function Impl(props: AutoCompleteProps, ref: React.Ref>) { const width = props.width ?? '100%'; - // if you need fix height, set to at least "30px" - // for Hydro, no less then "34px" can be better const height = props.height ?? 'auto'; const disabled = props.disabled ?? false; const disabledHint = props.disabledHint ?? ''; const listStyle = props.listStyle ?? {}; - const itemsFn = props.itemsFn ?? (async () => []); + const queryItems = props.queryItems ?? (() => []); const renderItem = props.renderItem ?? ((item) => item); - const itemText = props.itemText ?? ((item) => item); + const itemText = props.itemText ?? ((item) => item.toString()); const itemKey = props.itemKey ?? itemText; const onChange = props.onChange ?? (() => { }); const multi = props.multi ?? false; - const rawDefaultItems = props.defaultItems ?? []; - const defaultItems = typeof rawDefaultItems === 'string' - ? rawDefaultItems.split(',').map((i) => i.trim()).filter((i) => i.length > 0) : rawDefaultItems; const allowEmptyQuery = props.allowEmptyQuery ?? false; const freeSolo = props.freeSolo ?? false; const freeSoloConverter = freeSolo ? props.freeSoloConverter ?? ((i) => i) : ((i) => i); const [focused, setFocused] = useState(false); // is focused - const [selected, setSelected] = useState(multi ? defaultItems : []); // selected items - const [selectedKeys, setSelectedKeys] = useState(multi ? defaultItems.map((i) => itemKey(i)) : []); // keys of selected items + const [selected, setSelected] = useState([]); // selected items + const [selectedKeys, setSelectedKeys] = useState(props.selectedKeys); // keys of selected items const [itemList, setItemList] = useState([]); // items list const [currentItem, setCurrentItem] = useState(null); // index of current item (in item list) + const [rerender, setRerender] = useState(false); - const inputRef = useRef(); - const listRef = useRef(); + const inputRef = useRef(); + const listRef = useRef(); - const cache = useRef({}); + if (props.cacheKey) superCache[props.cacheKey] ||= { query: {}, value: {} }; + const queryCache = props.cacheKey ? superCache[props.cacheKey].query : useRef({}).current; + const valueCache = props.cacheKey ? superCache[props.cacheKey].value : useRef({}).current; + + useEffect(() => { + const ids = []; + for (const key of selectedKeys) if (!valueCache[key]) ids.push(key); + if (!ids.length) return; + Promise.resolve(props.fetchItems(ids)).then((items) => { + for (const item of items) valueCache[itemKey(item)] = item; + setRerender(!rerender); + }); + }, [selectedKeys]); const queryList = async (query) => { if (!query && !allowEmptyQuery) { @@ -46,27 +97,19 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) { setCurrentItem(null); return; } - let items; - if (cache.current[query]) { - items = cache.current[query]; - } else { - items = await itemsFn(query); - cache.current[query] = items; - } - setItemList(items); + if (!queryCache[query]) queryCache[query] = await queryItems(query); + for (const item of queryCache[query]) valueCache[itemKey(item)] = item; + setItemList(queryCache[query]); setCurrentItem(null); }; const calculateValue = () => { const query = inputRef.current?.value; if (!query) return multi ? selectedKeys.join(',') : ''; - return (multi && selectedKeys.length > 0) ? `${selectedKeys.join(',')},${query}` : query; + return (multi && selectedKeys.length) ? `${selectedKeys.join(',')},${query}` : query; }; - const dispatchChange = () => { - const value = calculateValue(); - onChange(value); - }; + const dispatchChange = () => onChange(calculateValue()); useEffect(() => { dispatchChange(); @@ -78,8 +121,7 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) { queryList(value); }, 300); - const toggleItem = (item) => { - const key = itemKey(item); + const toggleItem = (item: T, key = itemKey(item)) => { if (multi) { const idx = selectedKeys.indexOf(key); if (idx !== -1) { @@ -159,6 +201,7 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) { useImperativeHandle(ref, () => ({ getSelectedItems: () => selected, + getSelectedItemKeys: () => selectedKeys, getSelectedItemsAsString: () => selectedKeys.join(','), setSelectedItems: (items) => { setSelected(items); @@ -175,12 +218,6 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) { }, getValue: () => (multi ? selectedKeys.join(',') : (inputRef.current.value ?? '')), getValueArray: () => (multi ? selected : [inputRef.current?.value].filter((i) => !!i)), - getValueWithQuery: () => calculateValue(), - getValueArrayWithQuery: () => { - const query = inputRef.current?.value; - if (!query) return multi ? selected : ['']; - return multi ? [...selected, query] : [query]; - }, clear: () => { setSelected([]); setSelectedKeys([]); @@ -198,12 +235,15 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) { className={focused ? 'autocomplete-wrapper focused' : 'autocomplete-wrapper'} style={{ width, height }} > - {multi && selected.map((item, idx) => ( -
-
{itemText(item)}
- toggleItem(item)} /> -
- ))} + {multi && selectedKeys.map((key) => { + const item = valueCache[key]; + return ( +
+
{item ? itemText(item) : key}
+ toggleItem(item, key)} /> +
+ ); + })} {disabled && ( { - setFocused(true); - if (allowEmptyQuery) { - handleInputChange(); - } - }} + onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} onKeyDown={handleInputKeyDown} - defaultValue={multi ? '' : defaultItems.join(',')} + defaultValue={multi ? '' : selectedKeys.join(',')} /> {focused && itemList.length > 0 && ( @@ -241,14 +276,14 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) { data-focus={idx === currentItem} >
{renderItem(item)}
- + {selectedKeys.includes(itemKey(item)) && } ))} )} ); -}); +}) as ((props: AutoCompleteProps & { ref: React.Ref> }) => React.ReactElement) & React.FC; AutoComplete.propTypes = { width: PropTypes.string, @@ -256,13 +291,13 @@ AutoComplete.propTypes = { disabled: PropTypes.bool, disabledHint: PropTypes.string, listStyle: PropTypes.object, - itemsFn: PropTypes.func.isRequired, + queryItems: PropTypes.func.isRequired, itemKey: PropTypes.func, renderItem: PropTypes.func, itemText: PropTypes.func, onChange: PropTypes.func.isRequired, multi: PropTypes.bool, - defaultItems: PropTypes.oneOfType([(PropTypes.arrayOf(PropTypes.any)), PropTypes.string]), + selectedKeys: PropTypes.arrayOf(PropTypes.string), allowEmptyQuery: PropTypes.bool, freeSolo: PropTypes.bool, freeSoloConverter: PropTypes.func, @@ -275,9 +310,9 @@ AutoComplete.defaultProps = { disabledHint: '', listStyle: {}, renderItem: (item) => item, - itemText: (item) => item, + itemText: (item) => item.toString(), multi: false, - defaultItems: [], + selectedKeys: [], allowEmptyQuery: false, freeSolo: false, freeSoloConverter: (input) => input, diff --git a/packages/ui-default/components/autocomplete/components/CustomSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/components/CustomSelectAutoComplete.tsx new file mode 100644 index 000000000..1a922049c --- /dev/null +++ b/packages/ui-default/components/autocomplete/components/CustomSelectAutoComplete.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import AutoComplete from './AutoComplete'; + +const CustomSelectAutoComplete = React.forwardRef((props, ref) => ( + + ref={ref as any} + queryItems={(query) => props.data.filter((i) => (i.name ? i.name.match(query) : i.match(query)))} + itemText={(item) => `${item.name || item}`} + itemKey={(item) => `${item._id?.toString() || item.name || item}`} + renderItem={(item) => `${item.name || item}`} + {...props} + /> +)); + +CustomSelectAutoComplete.propTypes = { + width: PropTypes.string, + height: PropTypes.string, + listStyle: PropTypes.object, + onChange: PropTypes.func.isRequired, + multi: PropTypes.bool, + selectedKeys: PropTypes.arrayOf(PropTypes.string), + allowEmptyQuery: PropTypes.bool, + freeSolo: PropTypes.bool, + freeSoloConverter: PropTypes.func, + data: PropTypes.any, +}; + +CustomSelectAutoComplete.displayName = 'CustomSelectAutoComplete'; + +CustomSelectAutoComplete.defaultProps = { + width: '100%', + height: 'auto', + listStyle: {}, + multi: false, + selectedKeys: [], + allowEmptyQuery: false, + freeSolo: false, + freeSoloConverter: (input) => input, +}; + +export default CustomSelectAutoComplete; diff --git a/packages/ui-default/components/autocomplete/components/DomainSelectAutoComplete.jsx b/packages/ui-default/components/autocomplete/components/DomainSelectAutoComplete.jsx deleted file mode 100644 index d9ac74d85..000000000 --- a/packages/ui-default/components/autocomplete/components/DomainSelectAutoComplete.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { forwardRef } from 'react'; -import PropTypes from 'prop-types'; -import request from 'vj/utils/request'; -import AutoComplete from './AutoComplete'; - -// eslint-disable-next-line prefer-arrow-callback -const DomainSelectAutoComplete = forwardRef(function DomainSelectAutoComplete(props, ref) { - const itemsFn = (query) => request.get('/domain/search', { q: query }); - - /* const itemText = (domain) => { - return domain.name; - }; */ - - const itemKey = (domain) => domain._id || domain; - - const renderItem = (domain) => ( -
-
- -
-
-
{domain.name}
-
ID = {domain._id}
-
-
- ); - - return ( - - ); -}); - -DomainSelectAutoComplete.propTypes = { - width: PropTypes.string, - height: PropTypes.string, - listStyle: PropTypes.object, - onChange: PropTypes.func.isRequired, - multi: PropTypes.bool, - defaultItems: PropTypes.oneOfType([(PropTypes.arrayOf(PropTypes.any)), PropTypes.string]), - allowEmptyQuery: PropTypes.bool, - freeSolo: PropTypes.bool, - freeSoloConverter: PropTypes.func, -}; - -DomainSelectAutoComplete.defaultProps = { - width: '100%', - height: 'auto', - listStyle: {}, - multi: false, - defaultItems: [], - allowEmptyQuery: false, - freeSolo: false, - freeSoloConverter: (input) => input, -}; - -DomainSelectAutoComplete.displayName = 'DomainSelectAutoComplete'; - -export default DomainSelectAutoComplete; diff --git a/packages/ui-default/components/autocomplete/components/DomainSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/components/DomainSelectAutoComplete.tsx new file mode 100644 index 000000000..6eb83820c --- /dev/null +++ b/packages/ui-default/components/autocomplete/components/DomainSelectAutoComplete.tsx @@ -0,0 +1,54 @@ +import React, { forwardRef } from 'react'; +import PropTypes from 'prop-types'; +import request from 'vj/utils/request'; +import type { DomainDoc } from 'hydrooj/src/interface'; +import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete'; + +const DomainSelectAutoComplete = forwardRef, AutoCompleteProps>((props, ref) => ( + + ref={ref as any} + cacheKey="domain" + renderItem={(domain) => ( +
+
+ +
+
+
{domain.name}
+
ID = {domain._id}
+
+
+ )} + queryItems={(query) => request.get('/domain/search', { q: query })} + itemText={(domain) => domain.name} + itemKey={(domain) => domain._id} + {...props} + /> +)); + +DomainSelectAutoComplete.propTypes = { + width: PropTypes.string, + height: PropTypes.string, + listStyle: PropTypes.object, + onChange: PropTypes.func.isRequired, + multi: PropTypes.bool, + selectedKeys: PropTypes.arrayOf(PropTypes.string), + allowEmptyQuery: PropTypes.bool, + freeSolo: PropTypes.bool, + freeSoloConverter: PropTypes.func, +}; + +DomainSelectAutoComplete.defaultProps = { + width: '100%', + height: 'auto', + listStyle: {}, + multi: false, + selectedKeys: [], + allowEmptyQuery: false, + freeSolo: false, + freeSoloConverter: (input) => input, +}; + +DomainSelectAutoComplete.displayName = 'DomainSelectAutoComplete'; + +export default DomainSelectAutoComplete; diff --git a/packages/ui-default/components/autocomplete/components/ProblemSelectAutoComplete.jsx b/packages/ui-default/components/autocomplete/components/ProblemSelectAutoComplete.jsx deleted file mode 100644 index bc49b0bd1..000000000 --- a/packages/ui-default/components/autocomplete/components/ProblemSelectAutoComplete.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { forwardRef } from 'react'; -import PropTypes from 'prop-types'; -import request from 'vj/utils/request'; -import AutoComplete from './AutoComplete'; - -// eslint-disable-next-line prefer-arrow-callback -const ProblemSelectAutoComplete = forwardRef(function ProblemSelectAutoComplete(props, ref) { - const itemsFn = (query) => request.get(`/d/${UiContext.domainId}/problem/list`, { prefix: query }); - - /* const itemText = (pdoc) => { - return (pdoc.pid ? `${pdoc.pid} ` : '') + pdoc.title; - }; */ - - const itemKey = (pdoc) => `${pdoc.docId || pdoc}`; // force string - - const renderItem = (pdoc) => ( -
-
-
{pdoc.pid ? `${pdoc.pid} ` : ''}{pdoc.title}
-
ID = {pdoc.docId}
-
-
- ); - - return ( - - ); -}); - -ProblemSelectAutoComplete.propTypes = { - width: PropTypes.string, - height: PropTypes.string, - listStyle: PropTypes.object, - onChange: PropTypes.func.isRequired, - multi: PropTypes.bool, - defaultItems: PropTypes.oneOfType([(PropTypes.arrayOf(PropTypes.any)), PropTypes.string]), - allowEmptyQuery: PropTypes.bool, - freeSolo: PropTypes.bool, - freeSoloConverter: PropTypes.func, -}; - -ProblemSelectAutoComplete.defaultProps = { - width: '100%', - height: 'auto', - listStyle: {}, - multi: false, - defaultItems: [], - allowEmptyQuery: false, - freeSolo: false, - freeSoloConverter: (input) => input, -}; - -ProblemSelectAutoComplete.displayName = 'ProblemSelectAutoComplete'; - -export default ProblemSelectAutoComplete; diff --git a/packages/ui-default/components/autocomplete/components/ProblemSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/components/ProblemSelectAutoComplete.tsx new file mode 100644 index 000000000..b0eb18ad2 --- /dev/null +++ b/packages/ui-default/components/autocomplete/components/ProblemSelectAutoComplete.tsx @@ -0,0 +1,53 @@ +import React, { forwardRef } from 'react'; +import PropTypes from 'prop-types'; +import request from 'vj/utils/request'; +import type { ProblemDoc } from 'hydrooj/src/interface'; +import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete'; + +const ProblemSelectAutoComplete = forwardRef, AutoCompleteProps>((props, ref) => ( + + ref={ref as any} + cacheKey={`problem-${UiContext.domainId}`} + queryItems={(query) => request.get(`/d/${UiContext.domainId}/problem/list`, { prefix: query })} + // FIXME fetch items + fetchItems={() => []} + itemText={(pdoc) => `${pdoc.docId || pdoc}`} + itemKey={(pdoc) => `${pdoc.docId || pdoc}`} + renderItem={(pdoc) => ( +
+
+
{pdoc.pid ? `${pdoc.pid} ` : ''}{pdoc.title}
+
ID = {pdoc.docId}
+
+
+ )} + {...props} + /> +)); + +ProblemSelectAutoComplete.propTypes = { + width: PropTypes.string, + height: PropTypes.string, + listStyle: PropTypes.object, + onChange: PropTypes.func.isRequired, + multi: PropTypes.bool, + selectedKeys: PropTypes.arrayOf(PropTypes.string), + allowEmptyQuery: PropTypes.bool, + freeSolo: PropTypes.bool, + freeSoloConverter: PropTypes.func, +}; + +ProblemSelectAutoComplete.defaultProps = { + width: '100%', + height: 'auto', + listStyle: {}, + multi: false, + selectedKeys: [], + allowEmptyQuery: false, + freeSolo: false, + freeSoloConverter: (input) => input, +}; + +ProblemSelectAutoComplete.displayName = 'ProblemSelectAutoComplete'; + +export default ProblemSelectAutoComplete; diff --git a/packages/ui-default/components/autocomplete/components/UserSelectAutoComplete.tsx b/packages/ui-default/components/autocomplete/components/UserSelectAutoComplete.tsx index 0f759b94e..42274ce46 100644 --- a/packages/ui-default/components/autocomplete/components/UserSelectAutoComplete.tsx +++ b/packages/ui-default/components/autocomplete/components/UserSelectAutoComplete.tsx @@ -1,89 +1,43 @@ -import React, { - forwardRef, useImperativeHandle, useRef, useEffect, -} from 'react'; -import { useQuery } from 'react-query'; +import React, { forwardRef } from 'react'; import PropTypes from 'prop-types'; import api, { gql } from 'vj/utils/api'; -import AutoComplete from './AutoComplete'; -import type { AutoCompleteOptions } from '..'; +import type { Udoc } from 'hydrooj/src/interface'; +import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete'; // eslint-disable-next-line prefer-arrow-callback -const UserSelectAutoComplete = forwardRef(function UserSelectAutoComplete( - props: AutoCompleteOptions, ref: React.ForwardedRef, -) { - const multi = props.multi ?? false; - const rawDefaultItems = props.defaultItems ?? ''; - const defaultItems = multi ? rawDefaultItems - .split(',') - .map((i) => +i.trim()) - .filter((i) => i) : rawDefaultItems; - - const comRef = useRef(); - const itemsBox = useRef([]); - const { isLoading } = useQuery(['default_user', defaultItems], async () => { - if (!multi || defaultItems.length === 0) return; - const items = await api(gql` - users(ids: ${defaultItems}) { +const UserSelectAutoComplete = forwardRef, AutoCompleteProps>((props, ref) => ( + + ref={ref as any} + cacheKey="user" + {...props} + queryItems={(query) => api(gql` + users(search: ${query}) { + _id + uname + avatarUrl + } + `, ['data', 'users'])} + fetchItems={(ids) => api(gql` + users(ids: ${ids.map((i) => +i)}) { _id uname } - `, ['data', 'users']); - itemsBox.current = items; - }); - - useEffect(() => { - if (multi && defaultItems.length > 0) { - comRef.current.setSelectedItems(itemsBox.current); - } - }, [itemsBox.current]); - - useImperativeHandle(ref, () => new Proxy(comRef, { - get(target, propKey) { - if (typeof target.current?.[propKey] === 'function') { - return (...args: any[]) => target.current[propKey](...args); - } - return target.current?.[propKey]; - }, - })); - - const itemsFn = (query) => api(gql` - users(search: ${query}) { - _id - uname - avatarUrl - } - `, ['data', 'users']); - - const itemText = (user) => user.uname || user; - - const itemKey = (user) => user._id; - - const renderItem = (user) => ( -
-
- + `, ['data', 'users'])} + itemText={(user) => user.uname} + itemKey={(user) => (props.multi ? user._id.toString() : user.uname)} + renderItem={(user) => ( +
+
+ +
+
+
{user.uname}
+
UID = {user._id}
+
-
-
{user.uname}
-
UID = {user._id}
-
-
- ); - - return ( - - ); -}); + )} + /> +)); UserSelectAutoComplete.propTypes = { width: PropTypes.string, @@ -91,7 +45,7 @@ UserSelectAutoComplete.propTypes = { listStyle: PropTypes.object, onChange: PropTypes.func.isRequired, multi: PropTypes.bool, - defaultItems: PropTypes.string, + selectedKeys: PropTypes.arrayOf(PropTypes.string), allowEmptyQuery: PropTypes.bool, freeSolo: PropTypes.bool, freeSoloConverter: PropTypes.func, @@ -102,7 +56,7 @@ UserSelectAutoComplete.defaultProps = { height: 'auto', listStyle: {}, multi: false, - defaultItems: '', + selectedKeys: [], allowEmptyQuery: false, freeSolo: false, freeSoloConverter: (input) => input, diff --git a/packages/ui-default/components/autocomplete/index.tsx b/packages/ui-default/components/autocomplete/index.tsx index 977aeae37..bee7074a1 100644 --- a/packages/ui-default/components/autocomplete/index.tsx +++ b/packages/ui-default/components/autocomplete/index.tsx @@ -68,10 +68,10 @@ export default class AutoComplete extends DOMAttachedObject { { this.ref = ref; }} height="34px" - itemsFn={this.options.items} + queryItems={this.options.items} renderItem={this.options.render} itemText={this.options.text} - defaultItems={value} + selectedKeys={value.split(',').map((i) => i.trim())} onChange={this.onChange} multi={this.options.multi} freeSolo={this.options.multi}