ui: autocomplete: rewrite in typescript

This commit is contained in:
undefined committed 2022-04-05 03:26:30 +08:00
1 parent 370fc07914
commit 8f4a63006a
11 files changed
+318 -279

No files matched your search

@@ -5,6 +5,22 @@ import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete from '.'; import AutoComplete from '.';
import DomainSelectAutoCompleteFC from './components/DomainSelectAutoComplete'; 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 (
<DomainSelectAutoCompleteFC
ref={ref as any}
height="34px"
selectedKeys={value.split(',').map((i) => i.trim())}
onChange={(v) => {
setValue(v);
props.onChange(v);
}}
multi={props.multi}
/>
);
});
export default class DomainSelectAutoComplete extends AutoComplete { export default class DomainSelectAutoComplete extends AutoComplete {
static DOMAttachKey = 'ucwDomainSelectAutoCompleteInstance'; static DOMAttachKey = 'ucwDomainSelectAutoCompleteInstance';
@@ -18,13 +34,11 @@ export default class DomainSelectAutoComplete extends AutoComplete {
attach() { attach() {
const value = this.$dom.val(); const value = this.$dom.val();
ReactDOM.render( ReactDOM.render(
<DomainSelectAutoCompleteFC <Component
ref={(ref) => { this.ref = ref; }} ref={(ref) => { this.ref = ref; }}
height="34px" value={value}
defaultItems={value}
onChange={this.onChange} onChange={this.onChange}
multi={this.options.multi} multi={this.options.multi}
freeSolo={this.options.multi}
/>, />,
this.container, this.container,
); );
@@ -5,6 +5,22 @@ import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete from '.'; import AutoComplete from '.';
import ProblemSelectAutoCompleteFC from './components/ProblemSelectAutoComplete'; import ProblemSelectAutoCompleteFC from './components/ProblemSelectAutoComplete';
const Component = React.forwardRef<any, any>((props, ref) => {
const [value, setValue] = React.useState(props.value);
return (
<ProblemSelectAutoCompleteFC
ref={ref as any}
height="34px"
selectedKeys={value.split(',').map((i) => i.trim())}
onChange={(v) => {
setValue(v);
props.onChange(v);
}}
multi={props.multi}
/>
);
});
export default class ProblemSelectAutoComplete extends AutoComplete { export default class ProblemSelectAutoComplete extends AutoComplete {
static DOMAttachKey = 'ucwProblemSelectAutoCompleteInstance'; static DOMAttachKey = 'ucwProblemSelectAutoCompleteInstance';
@@ -18,13 +34,11 @@ export default class ProblemSelectAutoComplete extends AutoComplete {
attach() { attach() {
const value = this.$dom.val(); const value = this.$dom.val();
ReactDOM.render( ReactDOM.render(
<ProblemSelectAutoCompleteFC <Component
ref={(ref) => { this.ref = ref; }} ref={(ref) => { this.ref = ref; }}
height="34px" value={value}
defaultItems={value}
onChange={this.onChange}
multi={this.options.multi} multi={this.options.multi}
freeSolo={this.options.multi} onChange={this.onChange}
/>, />,
this.container, this.container,
); );
@@ -1,24 +1,28 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { assign } from 'lodash'; import { assign } from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject'; import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete, { AutoCompleteOptions } from '.'; import AutoComplete, { AutoCompleteOptions } from '.';
import UserSelectAutoCompleteFC from './components/UserSelectAutoComplete'; import UserSelectAutoCompleteFC from './components/UserSelectAutoComplete';
const Component = React.forwardRef<any, any>((props, ref) => {
const [value, setValue] = React.useState(props.value);
return (
<UserSelectAutoCompleteFC
ref={ref as any}
height="34px"
selectedKeys={value.split(',').map((i) => i.trim())}
onChange={(v) => {
setValue(v);
props.onChange(v);
}}
multi={props.multi}
/>
);
});
export default class UserSelectAutoComplete<Multi extends boolean> extends AutoComplete { export default class UserSelectAutoComplete<Multi extends boolean> extends AutoComplete {
static DOMAttachKey = 'ucwUserSelectAutoCompleteInstance'; static DOMAttachKey = 'ucwUserSelectAutoCompleteInstance';
client = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
retry: false,
staleTime: Infinity,
},
},
});
constructor($dom, options: AutoCompleteOptions<Multi>) { constructor($dom, options: AutoCompleteOptions<Multi>) {
super($dom, { super($dom, {
@@ -35,16 +39,12 @@ export default class UserSelectAutoComplete<Multi extends boolean> extends AutoC
attach() { attach() {
const value = this.$dom.val(); const value = this.$dom.val();
ReactDOM.render( ReactDOM.render(
<QueryClientProvider client={this.client}> <Component
<UserSelectAutoCompleteFC
ref={(ref) => { this.ref = ref; }} ref={(ref) => { this.ref = ref; }}
height={this.options.height || '34px'} value={value}
defaultItems={value}
onChange={this.onChange}
multi={this.options.multi} multi={this.options.multi}
freeSolo={this.options.multi} onChange={this.onChange}
/> />,
</QueryClientProvider>,
this.container, this.container,
); );
} }
@@ -7,38 +7,89 @@ import PropTypes from 'prop-types';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import Icon from 'vj/components/react/IconComponent'; import Icon from 'vj/components/react/IconComponent';
export interface AutoCompleteProps<Item> {
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[]> | Item[];
fetchItems?: (ids: string[]) => Promise<Item[]> | 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<Item> {
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 // eslint-disable-next-line prefer-arrow-callback
const AutoComplete = forwardRef(function AutoComplete(props, ref) { const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, ref: React.Ref<AutoCompleteHandle<T>>) {
const width = props.width ?? '100%'; 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 height = props.height ?? 'auto';
const disabled = props.disabled ?? false; const disabled = props.disabled ?? false;
const disabledHint = props.disabledHint ?? ''; const disabledHint = props.disabledHint ?? '';
const listStyle = props.listStyle ?? {}; const listStyle = props.listStyle ?? {};
const itemsFn = props.itemsFn ?? (async () => []); const queryItems = props.queryItems ?? (() => []);
const renderItem = props.renderItem ?? ((item) => item); 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 itemKey = props.itemKey ?? itemText;
const onChange = props.onChange ?? (() => { }); const onChange = props.onChange ?? (() => { });
const multi = props.multi ?? false; 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 allowEmptyQuery = props.allowEmptyQuery ?? false;
const freeSolo = props.freeSolo ?? false; const freeSolo = props.freeSolo ?? false;
const freeSoloConverter = freeSolo ? props.freeSoloConverter ?? ((i) => i) : ((i) => i); const freeSoloConverter = freeSolo ? props.freeSoloConverter ?? ((i) => i) : ((i) => i);
const [focused, setFocused] = useState(false); // is focused const [focused, setFocused] = useState(false); // is focused
const [selected, setSelected] = useState(multi ? defaultItems : []); // selected items const [selected, setSelected] = useState([]); // selected items
const [selectedKeys, setSelectedKeys] = useState(multi ? defaultItems.map((i) => itemKey(i)) : []); // keys of selected items const [selectedKeys, setSelectedKeys] = useState(props.selectedKeys); // keys of selected items
const [itemList, setItemList] = useState([]); // items list const [itemList, setItemList] = useState([]); // items list
const [currentItem, setCurrentItem] = useState(null); // index of current item (in item list) const [currentItem, setCurrentItem] = useState(null); // index of current item (in item list)
const [rerender, setRerender] = useState(false);
const inputRef = useRef(); const inputRef = useRef<HTMLInputElement>();
const listRef = useRef(); const listRef = useRef<HTMLUListElement>();
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) => { const queryList = async (query) => {
if (!query && !allowEmptyQuery) { if (!query && !allowEmptyQuery) {
@@ -46,27 +97,19 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
setCurrentItem(null); setCurrentItem(null);
return; return;
} }
let items; if (!queryCache[query]) queryCache[query] = await queryItems(query);
if (cache.current[query]) { for (const item of queryCache[query]) valueCache[itemKey(item)] = item;
items = cache.current[query]; setItemList(queryCache[query]);
} else {
items = await itemsFn(query);
cache.current[query] = items;
}
setItemList(items);
setCurrentItem(null); setCurrentItem(null);
}; };
const calculateValue = () => { const calculateValue = () => {
const query = inputRef.current?.value; const query = inputRef.current?.value;
if (!query) return multi ? selectedKeys.join(',') : ''; 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 dispatchChange = () => onChange(calculateValue());
const value = calculateValue();
onChange(value);
};
useEffect(() => { useEffect(() => {
dispatchChange(); dispatchChange();
@@ -78,8 +121,7 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
queryList(value); queryList(value);
}, 300); }, 300);
const toggleItem = (item) => { const toggleItem = (item: T, key = itemKey(item)) => {
const key = itemKey(item);
if (multi) { if (multi) {
const idx = selectedKeys.indexOf(key); const idx = selectedKeys.indexOf(key);
if (idx !== -1) { if (idx !== -1) {
@@ -159,6 +201,7 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getSelectedItems: () => selected, getSelectedItems: () => selected,
getSelectedItemKeys: () => selectedKeys,
getSelectedItemsAsString: () => selectedKeys.join(','), getSelectedItemsAsString: () => selectedKeys.join(','),
setSelectedItems: (items) => { setSelectedItems: (items) => {
setSelected(items); setSelected(items);
@@ -175,12 +218,6 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
}, },
getValue: () => (multi ? selectedKeys.join(',') : (inputRef.current.value ?? '')), getValue: () => (multi ? selectedKeys.join(',') : (inputRef.current.value ?? '')),
getValueArray: () => (multi ? selected : [inputRef.current?.value].filter((i) => !!i)), 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: () => { clear: () => {
setSelected([]); setSelected([]);
setSelectedKeys([]); setSelectedKeys([]);
@@ -198,12 +235,15 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
className={focused ? 'autocomplete-wrapper focused' : 'autocomplete-wrapper'} className={focused ? 'autocomplete-wrapper focused' : 'autocomplete-wrapper'}
style={{ width, height }} style={{ width, height }}
> >
{multi && selected.map((item, idx) => ( {multi && selectedKeys.map((key) => {
<div className="autocomplete-tag" key={selectedKeys[idx]}> const item = valueCache[key];
<div>{itemText(item)}</div> return (
<Icon name="close" onClick={() => toggleItem(item)} /> <div className="autocomplete-tag" key={item ? key : `draft-${key}`}>
<div>{item ? itemText(item) : key}</div>
<Icon name="close" onClick={() => toggleItem(item, key)} />
</div> </div>
))} );
})}
{disabled && ( {disabled && (
<input <input
disabled disabled
@@ -219,15 +259,10 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
dispatchChange(); dispatchChange();
handleInputChange(e); handleInputChange(e);
}} }}
onFocus={() => { onFocus={() => setFocused(true)}
setFocused(true);
if (allowEmptyQuery) {
handleInputChange();
}
}}
onBlur={() => setFocused(false)} onBlur={() => setFocused(false)}
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
defaultValue={multi ? '' : defaultItems.join(',')} defaultValue={multi ? '' : selectedKeys.join(',')}
/> />
</div> </div>
{focused && itemList.length > 0 && ( {focused && itemList.length > 0 && (
@@ -241,14 +276,14 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
data-focus={idx === currentItem} data-focus={idx === currentItem}
> >
<div>{renderItem(item)}</div> <div>{renderItem(item)}</div>
<Icon name="check" /> {selectedKeys.includes(itemKey(item)) && <Icon name="check" />}
</li> </li>
))} ))}
</ul> </ul>
)} )}
</div> </div>
); );
}); }) as (<T>(props: AutoCompleteProps<T> & { ref: React.Ref<AutoCompleteHandle<T>> }) => React.ReactElement) & React.FC;
AutoComplete.propTypes = { AutoComplete.propTypes = {
width: PropTypes.string, width: PropTypes.string,
@@ -256,13 +291,13 @@ AutoComplete.propTypes = {
disabled: PropTypes.bool, disabled: PropTypes.bool,
disabledHint: PropTypes.string, disabledHint: PropTypes.string,
listStyle: PropTypes.object, listStyle: PropTypes.object,
itemsFn: PropTypes.func.isRequired, queryItems: PropTypes.func.isRequired,
itemKey: PropTypes.func, itemKey: PropTypes.func,
renderItem: PropTypes.func, renderItem: PropTypes.func,
itemText: PropTypes.func, itemText: PropTypes.func,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
multi: PropTypes.bool, multi: PropTypes.bool,
defaultItems: PropTypes.oneOfType([(PropTypes.arrayOf(PropTypes.any)), PropTypes.string]), selectedKeys: PropTypes.arrayOf(PropTypes.string),
allowEmptyQuery: PropTypes.bool, allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool, freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func, freeSoloConverter: PropTypes.func,
@@ -275,9 +310,9 @@ AutoComplete.defaultProps = {
disabledHint: '', disabledHint: '',
listStyle: {}, listStyle: {},
renderItem: (item) => item, renderItem: (item) => item,
itemText: (item) => item, itemText: (item) => item.toString(),
multi: false, multi: false,
defaultItems: [], selectedKeys: [],
allowEmptyQuery: false, allowEmptyQuery: false,
freeSolo: false, freeSolo: false,
freeSoloConverter: (input) => input, freeSoloConverter: (input) => input,
@@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import AutoComplete from './AutoComplete';
const CustomSelectAutoComplete = React.forwardRef<any, any>((props, ref) => (
<AutoComplete<any>
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;
@@ -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) => (
<div className="media">
<div className="media__left medium">
<img className="small domain-profile-avatar" src={domain.avatarUrl} alt="" width="30" height="30" />
</div>
<div className="media__body medium">
<div className="domain-select__name">{domain.name}</div>
<div className="domain-select__id">ID = {domain._id}</div>
</div>
</div>
);
return (
<AutoComplete
ref={ref}
itemsFn={itemsFn}
itemText={itemKey}
itemKey={itemKey}
renderItem={renderItem}
{...props}
/>
);
});
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;
@@ -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<AutoCompleteHandle<DomainDoc>, AutoCompleteProps<DomainDoc>>((props, ref) => (
<AutoComplete<DomainDoc>
ref={ref as any}
cacheKey="domain"
renderItem={(domain) => (
<div className="media">
<div className="media__left medium">
<img className="small domain-profile-avatar" src={domain.avatarUrl} alt="" width="30" height="30" />
</div>
<div className="media__body medium">
<div className="domain-select__name">{domain.name}</div>
<div className="domain-select__id">ID = {domain._id}</div>
</div>
</div>
)}
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;
@@ -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) => (
<div className="media">
<div className="media__body medium">
<div className="problem-select__name">{pdoc.pid ? `${pdoc.pid} ` : ''}{pdoc.title}</div>
<div className="problem-select__id">ID = {pdoc.docId}</div>
</div>
</div>
);
return (
<AutoComplete
ref={ref}
itemsFn={itemsFn}
itemText={itemKey}
itemKey={itemKey}
renderItem={renderItem}
{...props}
/>
);
});
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;
@@ -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<AutoCompleteHandle<ProblemDoc>, AutoCompleteProps<ProblemDoc>>((props, ref) => (
<AutoComplete<ProblemDoc>
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) => (
<div className="media">
<div className="media__body medium">
<div className="problem-select__name">{pdoc.pid ? `${pdoc.pid} ` : ''}{pdoc.title}</div>
<div className="problem-select__id">ID = {pdoc.docId}</div>
</div>
</div>
)}
{...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;
@@ -1,64 +1,31 @@
import React, { import React, { forwardRef } from 'react';
forwardRef, useImperativeHandle, useRef, useEffect,
} from 'react';
import { useQuery } from 'react-query';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import api, { gql } from 'vj/utils/api'; import api, { gql } from 'vj/utils/api';
import AutoComplete from './AutoComplete'; import type { Udoc } from 'hydrooj/src/interface';
import type { AutoCompleteOptions } from '..'; import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete';
// eslint-disable-next-line prefer-arrow-callback // eslint-disable-next-line prefer-arrow-callback
const UserSelectAutoComplete = forwardRef(function UserSelectAutoComplete( const UserSelectAutoComplete = forwardRef<AutoCompleteHandle<Udoc>, AutoCompleteProps<Udoc>>((props, ref) => (
props: AutoCompleteOptions, ref: React.ForwardedRef<AutoComplete>, <AutoComplete<Udoc>
) { ref={ref as any}
const multi = props.multi ?? false; cacheKey="user"
const rawDefaultItems = props.defaultItems ?? ''; {...props}
const defaultItems = multi ? rawDefaultItems queryItems={(query) => api(gql`
.split(',')
.map((i) => +i.trim())
.filter((i) => i) : rawDefaultItems;
const comRef = useRef<AutoComplete>();
const itemsBox = useRef([]);
const { isLoading } = useQuery(['default_user', defaultItems], async () => {
if (!multi || defaultItems.length === 0) return;
const items = await api(gql`
users(ids: ${defaultItems}) {
_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}) { users(search: ${query}) {
_id _id
uname uname
avatarUrl avatarUrl
} }
`, ['data', 'users']); `, ['data', 'users'])}
fetchItems={(ids) => api(gql`
const itemText = (user) => user.uname || user; users(ids: ${ids.map((i) => +i)}) {
_id
const itemKey = (user) => user._id; uname
}
const renderItem = (user) => ( `, ['data', 'users'])}
itemText={(user) => user.uname}
itemKey={(user) => (props.multi ? user._id.toString() : user.uname)}
renderItem={(user) => (
<div className="media"> <div className="media">
<div className="media__left medium"> <div className="media__left medium">
<img className="small user-profile-avatar" alt="" src={user.avatarUrl} width="30" height="30" /> <img className="small user-profile-avatar" alt="" src={user.avatarUrl} width="30" height="30" />
@@ -68,22 +35,9 @@ const UserSelectAutoComplete = forwardRef(function UserSelectAutoComplete(
<div className="user-select__uid">UID = {user._id}</div> <div className="user-select__uid">UID = {user._id}</div>
</div> </div>
</div> </div>
); )}
return (
<AutoComplete
ref={comRef}
{...props}
disabled={isLoading}
disabledHint="Loading..."
itemsFn={itemsFn}
itemText={itemText}
itemKey={multi ? itemKey : itemText}
renderItem={renderItem}
defaultItems={multi ? [] : rawDefaultItems}
/> />
); ));
});
UserSelectAutoComplete.propTypes = { UserSelectAutoComplete.propTypes = {
width: PropTypes.string, width: PropTypes.string,
@@ -91,7 +45,7 @@ UserSelectAutoComplete.propTypes = {
listStyle: PropTypes.object, listStyle: PropTypes.object,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
multi: PropTypes.bool, multi: PropTypes.bool,
defaultItems: PropTypes.string, selectedKeys: PropTypes.arrayOf(PropTypes.string),
allowEmptyQuery: PropTypes.bool, allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool, freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func, freeSoloConverter: PropTypes.func,
@@ -102,7 +56,7 @@ UserSelectAutoComplete.defaultProps = {
height: 'auto', height: 'auto',
listStyle: {}, listStyle: {},
multi: false, multi: false,
defaultItems: '', selectedKeys: [],
allowEmptyQuery: false, allowEmptyQuery: false,
freeSolo: false, freeSolo: false,
freeSoloConverter: (input) => input, freeSoloConverter: (input) => input,
@@ -68,10 +68,10 @@ export default class AutoComplete extends DOMAttachedObject {
<AutoCompleteFC <AutoCompleteFC
ref={(ref) => { this.ref = ref; }} ref={(ref) => { this.ref = ref; }}
height="34px" height="34px"
itemsFn={this.options.items} queryItems={this.options.items}
renderItem={this.options.render} renderItem={this.options.render}
itemText={this.options.text} itemText={this.options.text}
defaultItems={value} selectedKeys={value.split(',').map((i) => i.trim())}
onChange={this.onChange} onChange={this.onChange}
multi={this.options.multi} multi={this.options.multi}
freeSolo={this.options.multi} freeSolo={this.options.multi}