| 1 | import { dirname, enforceFinal, join } from './misc' |
| 2 | import _ from 'lodash' |
| 3 | import { useApiList } from './api' |
| 4 | import { ChangeEvent, createElement as h, useMemo } from 'react' |
| 5 | import { Autocomplete, AutocompleteProps, TextField } from '@mui/material' |
| 6 | import { FieldProps } from '@hfs/mui-grid-form' |
| 7 | |
| 8 | interface VfsPathFieldProps extends FieldProps<string> { |
| 9 | autocompleteProps: Partial<AutocompleteProps<string, false, true, undefined>> |
| 10 | } |
| 11 | |
| 12 | export default function VfsPathField({ |
| 13 | value='', onChange, helperText, setApi, autocompleteProps, folders=true, files=true, |
| 14 | InputLabelProps, slotProps, ...props |
| 15 | }: VfsPathFieldProps) { |
| 16 | const uri = dirname(value.replace(/\/{2,}/g, '/')) |
| 17 | const { list, loading } = useApiList('get_file_list', { |
| 18 | uri, |
| 19 | admin: true, |
| 20 | fileMask: typeof files === 'string' ? files : undefined, |
| 21 | onlyFolders: !files |
| 22 | }) |
| 23 | const options = useMemo(() => { |
| 24 | const ret = _.uniq([uri && (dirname(uri) + '/'), enforceFinal('/', uri)].filter(Boolean)) |
| 25 | .concat(list.map(x => join(uri, x.n))) |
| 26 | if (value && !ret.includes(value)) ret.push(value) // allow re-selection of the same value without issuing a console warning |
| 27 | return ret |
| 28 | }, [list, uri]) |
| 29 | setApi?.({ |
| 30 | getError() { |
| 31 | return !folders && value?.endsWith('/') && "must be a file" || false |
| 32 | } |
| 33 | }) |
| 34 | return h(Autocomplete<string, false, true, undefined>, { |
| 35 | value, |
| 36 | options, |
| 37 | isOptionEqualToValue: (o,v) => o === v || o === v + '/', |
| 38 | loading, |
| 39 | disableClearable: true, |
| 40 | disableCloseOnSelect: true, |
| 41 | renderInput: params => h(TextField, { |
| 42 | helperText, |
| 43 | onBlur(event) { |
| 44 | // if the user specified a folder without the final slash, try to enforce it |
| 45 | const v = enforceFinal('/', event.target.value) |
| 46 | if (options.includes(v)) |
| 47 | onChange(v, { was: value, event }) |
| 48 | }, |
| 49 | ...params, |
| 50 | ...props, |
| 51 | slotProps: { |
| 52 | ...slotProps, |
| 53 | input: { |
| 54 | ...slotProps?.input, |
| 55 | ...params.slotProps.input, |
| 56 | }, |
| 57 | inputLabel: { |
| 58 | shrink: true, |
| 59 | ...params.slotProps.inputLabel, |
| 60 | ...InputLabelProps, |
| 61 | ...slotProps?.inputLabel, |
| 62 | }, |
| 63 | htmlInput: { |
| 64 | ...slotProps?.htmlInput, |
| 65 | ...params.slotProps.htmlInput, |
| 66 | onChange(event: ChangeEvent<HTMLInputElement>) { |
| 67 | params.slotProps.htmlInput.onChange?.(event) |
| 68 | const v = event.target.value |
| 69 | if (files || !v || v.endsWith('/')) |
| 70 | onChange(v, { was: value, event }) |
| 71 | }, |
| 72 | }, |
| 73 | }, |
| 74 | }), |
| 75 | onChange: (event, sel) => onChange(sel, { was: value, event }), |
| 76 | ...autocompleteProps, |
| 77 | }) |
| 78 | } |