Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
components.form.fields.InputSelectField.js Maven / Gradle / Ivy
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Autocomplete, Box, Chip, CircularProgress, TextField, Typography } from "@mui/material";
import { useApolloClient } from "@apollo/client";
import InputError from "components/form/fields/InputError";
import Listbox from "components/form/fields/select/Listbox";
import { isEmptyValue, useDebounce } from "utils/utils";
import Report from "helpers/report";
import { validateField } from "components/form/utils/validate-utils";
import { useNotifier } from "hooks/notification";
import { useFormInfo } from 'hooks/form';
import { useFieldInfo } from "hooks/field";
import { GET_CHOICES } from "queries/task";
import { toOption } from "utils/option-utils";
import { useDependants } from "hooks/dependants";
import { useAutoSubmitSignal } from "hooks/autosubmit";
import { fieldMinWidthStyle } from "components/form/utils/field-utils";
import { useConfig } from "hooks/config";
import { useTranslator } from "hooks/translator";
const InputSelectField = (props) => (
)
const InputSelectContent = (props) => {
const { setFocus, selectProps, setViewOpen } = props
const {mode} = selectProps
const { t } = useTranslation()
const { translator } = useTranslator()
const client = useApolloClient()
const notifier = useNotifier()
const [options, setOptions] = useState([])
const formInfo = useFormInfo()
const { augProps, fieldProps, info } = useFieldInfo()
const [selected, setSelected] = React.useState(getDefaultValue(mode, fieldProps.value)); // this value is never allowed to be 'undefined'!!
const [open, setOpen] = React.useState(false);
const pageSize = 100
const [tabLoading, setTabLoading] = useState(false)
const [loading, setLoading] = useState(false)
const [filter, setFilter] = useState(undefined)
const [hasNextPage, setHasNextPage] = useState(true)
const [highlighted, setHighlighted] = useState({option: null, index: -1})
const [request, setRequest] = useState(undefined)
const dependantValues = useDependants()
const [input, setInput, debouncedInput, skipDebounce] = useDebounce("", 500)
const {signal} = useAutoSubmitSignal()
const {props: {taskRendering}} = useConfig()
useEffect(() => {
if (_.isEmpty(dependantValues))
return undefined
const value = getDefaultValue(mode, fieldProps.value)
setSelected(value)
setInput("")
setOptions([])
handleValidate(null, value)
},[dependantValues])
// adjust selected value, provided by formik
React.useEffect(() => {
var selected = fieldProps.value
if (mode == 'multiple') {
selected = selected && Array.isArray(selected) ? selected.map(toOption) : []
} else {
selected = toOption(selected)
}
setSelected(selected)
}, [fieldProps.value]);
// load new options based on input
React.useEffect(() => {
if (!open){
request?.cancel()
setOptions([])
setFilter(undefined)
return undefined;
}
if (input != debouncedInput) {
request?.cancel()
} else if (options.length == 0){
loadOptions(input, true, true)
} else if (debouncedInput !== selected?.label) { // TODO: does this handle well with multiple select... I think not.
loadOptions(debouncedInput, true)
}
}, [open, debouncedInput, input]);
function updateOptions(newOptions, filter) {
setOptions(newOptions)
setFilter(filter)
setHasNextPage(!(newOptions.length < pageSize))
}
const loadOptions = async (newFilter, reset, overwrite) => {
request?.cancel()
if (!open)
return undefined
if (!overwrite && reset && newFilter === filter)
return undefined
const nextPage = reset ? 1 : Number(Math.floor(options.length / pageSize)) + 1
const usedFilter = reset ? newFilter : filter
setLoading(true)
let cancelled = false
const cancel = () => {
if (!cancelled) {
cancelled = true
setLoading(false)
}
}
function updateOptionsLocal(newOptions) {
if (!cancelled) {
const combinedOptions = reset ? newOptions : [...options].concat(newOptions)
updateOptions(combinedOptions, usedFilter)
}
}
const variables = {
...formInfo.reference,
path: info.rpath,
filter: usedFilter,
start: (nextPage - 1) * pageSize,
count: pageSize,
values: dependantValues
}
console.log("field %o requests choices with: %o", info.rpath, variables)
const newRequest = client.query({ query: GET_CHOICES, variables })
newRequest.cancel = cancel
setRequest(newRequest)
newRequest
.then(
result => {
if (!cancelled){
console.debug("loadOptions: searchQuery=%o, page=%o, variables=%o, result=%o", usedFilter, nextPage, variables, result)
const newOptions = result.data.choices
updateOptionsLocal(newOptions)
}
},
error => {
if (!cancelled) {
console.error("failed to get options: %o", error.message)
const report = Report.from(error, translator, { category: Report.backend })
report.addToNotifier(notifier)
}
}
)
.catch(reason => {
if (!cancelled) {
notifier.error("Could not retrieve more options")
console.error("the frontend has an issue with retrieving options: " + reason)
}
})
.finally(() => {
if (!cancelled){
setLoading(false)
}
})
}
const loadTabOption = (e, filter) => {
request?.cancel()
setTabLoading(true)
const variables = {
...formInfo.reference,
path : info.rpath,
filter: filter,
start : 0,
count : pageSize,
}
console.log("field %o requests tab choices with: %o", info.rpath, variables)
const newRequest = client.query({ query: GET_CHOICES, variables })
newRequest
.then(
result => {
console.debug("loadTabOptions: filter=%o, result=%o", filter, result)
const newOptions = result.data.choices
updateOptions(newOptions)
if (newOptions.length > 0)
handleChange(e, newOptions[0])
else
notifier.error("Could not select a value in field '" + info.rpath + "' given filter '"+ filter + "'.")
},
error => {
console.error("the frontend has an issue with retrieving options: " + JSON.stringify(error))
}
)
.catch(reason => {
console.error("the frontend has an issue with retrieving options: " + reason)
})
.finally(() => {
setTabLoading(false)
})
}
const loadNextPage = () => {
loadOptions(input, false)
}
function handleKeyDown(e) {
switch (e.key) {
case "ArrowDown":
case "ArrowUp":
const focusElement = document.getElementsByClassName("MuiAutocomplete-option Mui-focused")
if (focusElement.length > 0) {
const focusType = e.key === "ArrowUp"
? {
behavior: 'smooth',
block: 'center',
inline: 'center'
} :{
behavior: 'smooth',
block: 'start',
inline: 'start'
}
focusElement[0].scrollIntoView(focusType);
}
break;
case "Tab": // tab completion
if (input == "")
return
if ((mode == "multiple" && input.length == 0) || (mode == "single" && input == selected?.label))
return undefined
if (mode == "multiple") {
e.preventDefault();
e.stopPropagation()
}
const option = highlighted?.option
const tabAlreadyMatches = Boolean(option?.label?.toLowerCase().includes(input?.toLowerCase()))
if (tabAlreadyMatches){
handleChange(e, option)
} else {
skipDebounce()
loadTabOption(e, input)
}
break;
case "Enter": // enter completion
e.preventDefault();
if (input !== debouncedInput) {
e.stopPropagation()
skipDebounce()
}
break;
default:
}
}
const getHighlightedIndex = (e, option, reason) => {
switch (reason) {
case "keyboard":
case "auto": return option ? options.indexOf(option) : -1
case "mouse": return e?.target?.dataset?.optionIndex || -1
default: return -1
}
}
const handleHighlightChange = (e, value, reason) => {
const index = getHighlightedIndex(e, value, reason)
if (highlighted?.option?.value != value?.value)
setHighlighted({ option: value ? value : null, index})
}
function handleValidate(e, option) {
switch (mode) {
case 'multiple':
augProps.setError(validateField("multiselect", fieldProps.required, e, option))
break
case 'single':
augProps.setError(validateField("select", fieldProps.required, e, option))
break
default:
}
}
function handleBlur (e) {
setFocus(false)
fieldProps.onBlur(e)
handleValidate(e, selected)
signal()
}
function handleFocus(e) {
setFocus(true)
fieldProps.onFocus(e)
}
// store selected option locally and with formik. This HAS to be in two places, as a workaround for re-render behavior.
const getSelectedChange = (value, previousSelected, allowRemoval) => {
const matchesOption = (option) => option.value === value?.value || option.value === value
const addValueTo = (selected) => {
if (selected.some(matchesOption))
return allowRemoval ? selected.filter(o => !matchesOption(o)) : selected
else
return [...selected, value]
}
if (Array.isArray(value))
return value
else {
if (mode == "multiple") {
const selected = !Array.isArray(previousSelected) ? [] : previousSelected
return value ? addValueTo(selected) : selected
} else
return value
}
}
function handleChange(e, value) {
request?.cancel()
augProps.setRuntimeError(undefined)
const newSelected = getSelectedChange(value, selected, true)
setSelected(newSelected)
augProps.setValue(newSelected)
handleValidate(e, newSelected)
// in case of multiple select, trigger autosubmit on change
if (mode == "multiple" && !isEmptyValue(value))
signal()
}
function handleInputChange(e, value) {
setInput(value)
}
const listboxProps = {
highlightedIndex: highlighted.index,
hasNextPage,
isNextPageLoading: loading,
loadNextPage
}
const multipleProps = {
renderTags: (value, getTagProps) =>
value.map((option, index) =>
{option.label}}
/>
),
multiple: true,
limitTags: 5,
disableCloseOnSelect: true
}
const autocompleteProps = mode === 'multiple' ? multipleProps : {}
const { onChange, ...localFieldProps } = fieldProps
return (
x}
options={options}
isOptionEqualToValue={(option, value) => option.value === value?.value}
value={!selected ? (mode === 'multiple' ? [] : null) : selected}
onChange={handleChange}
inputValue={input}
onInputChange={handleInputChange}
autoHighlight
onHighlightChange={handleHighlightChange}
open={open}
onOpen={() => { setOpen(true); setViewOpen(true) }}
onClose={() => { setOpen(false); setViewOpen(false) }}
// localization options
loadingText={t("select.loading")}
clearText={t("select.clear")}
closeText={t("select.close")}
openText={t("select.open")}
noOptionsText={t("select.nooptions")}
fullWidth
disableListWrap
ListboxProps={{listboxProps}}
ListboxComponent={Listbox}
//renderInput={(params) =>
//
//}
renderInput={(params) => {
params.inputProps.onKeyDown = handleKeyDown; // add onKey event here, because it is overwritten if you give it to 'autocomplete'
return (
{tabLoading || loading ? : null}
{params.InputProps.endAdornment}
),
inputProps: {
...params.inputProps,
"data-state": "dynamic"
}
}}
size={fieldProps.size}
style={{flexGrow: 1, ...(taskRendering == 'standard' ? fieldMinWidthStyle(formInfo, info.field) : undefined)}}
fullWidth
/>
)
}}
renderOption={(props, option, state) => renderOptionbox(props, option, state, highlighted)}
/>
);
}
function getDefaultValue(mode, value) {
const defaultValue = mode == 'multiple' ? [] : null
if (value instanceof Boolean || value)
return value
else
return defaultValue
}
function renderOptionbox(props, option, state, highlighted) {
const index = props['data-option-index']
const className = index == highlighted.index ? 'MuiAutocomplete-option Mui-focused' : 'MuiAutocomplete-option'
return (
{option.label}
)
}
const wrapStyle = {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}
export default InputSelectField