mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 09:46:28 +08:00
ui: autocomplete: rewrite in typescript
This commit is contained in:
1 parent
370fc07914
commit
8f4a63006a
11 files changed
+318
-279
No files matched your search
+18
-4
@@ -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,
|
||||||
);
|
);
|
||||||
+19
-5
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+87
-52
@@ -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}
|
||||||
|
|||||||
Reference in new issue
Block a user