mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
883 lines
25 KiB
JavaScript
883 lines
25 KiB
JavaScript
/////////////////////////////////////////////////////////////
|
|
//
|
|
// pgAdmin 4 - PostgreSQL Tools
|
|
//
|
|
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
|
// This software is released under the PostgreSQL Licence
|
|
//
|
|
//////////////////////////////////////////////////////////////
|
|
/* Common form components used in pgAdmin */
|
|
|
|
import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
|
|
import { makeStyles } from '@material-ui/core/styles';
|
|
import { Box, FormControl, OutlinedInput, FormHelperText,
|
|
Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel } from '@material-ui/core';
|
|
import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab';
|
|
import ReportProblemIcon from '@material-ui/icons/ReportProblemRounded';
|
|
import InfoIcon from '@material-ui/icons/InfoRounded';
|
|
import CloseIcon from '@material-ui/icons/CloseRounded';
|
|
import CheckIcon from '@material-ui/icons/CheckCircleOutlineRounded';
|
|
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
|
|
import FolderOpenRoundedIcon from '@material-ui/icons/FolderOpenRounded';
|
|
import Select, {components as RSComponents} from 'react-select';
|
|
import CreatableSelect from 'react-select/creatable';
|
|
import Pickr from '@simonwep/pickr';
|
|
import clsx from 'clsx';
|
|
import PropTypes from 'prop-types';
|
|
|
|
import CodeMirror from './CodeMirror';
|
|
import gettext from 'sources/gettext';
|
|
import { showFileDialog } from '../helpers/legacyConnector';
|
|
import _ from 'lodash';
|
|
import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons';
|
|
import CustomPropTypes from '../custom_prop_types';
|
|
|
|
|
|
const useStyles = makeStyles((theme) => ({
|
|
formRoot: {
|
|
padding: '1rem'
|
|
},
|
|
img: {
|
|
maxWidth: '100%',
|
|
height: 'auto'
|
|
},
|
|
info: {
|
|
color: theme.palette.info.main,
|
|
marginLeft: '0.25rem',
|
|
fontSize: '1rem',
|
|
},
|
|
formLabel: {
|
|
margin: theme.spacing(0.75, 0.75, 0.75, 0.75),
|
|
display: 'flex',
|
|
},
|
|
formLabelError: {
|
|
color: theme.palette.error.main,
|
|
},
|
|
sql: {
|
|
height: '100%',
|
|
},
|
|
optionIcon: {
|
|
...theme.mixins.nodeIcon,
|
|
},
|
|
colorBtn: {
|
|
height: theme.spacing(3.5),
|
|
minHeight: theme.spacing(3.5),
|
|
width: theme.spacing(3.5),
|
|
minWidth: theme.spacing(3.5),
|
|
}
|
|
}));
|
|
|
|
|
|
export const MESSAGE_TYPE = {
|
|
SUCCESS: 'Success',
|
|
ERROR: 'Error',
|
|
INFO: 'Info',
|
|
CLOSE: 'Close',
|
|
};
|
|
|
|
/* Icon based on MESSAGE_TYPE */
|
|
function FormIcon({type, close=false, ...props}) {
|
|
let TheIcon = null;
|
|
if(close) {
|
|
TheIcon = CloseIcon;
|
|
} else if(type === MESSAGE_TYPE.SUCCESS) {
|
|
TheIcon = CheckIcon;
|
|
} else if(type === MESSAGE_TYPE.ERROR) {
|
|
TheIcon = ReportProblemIcon;
|
|
} else if(type === MESSAGE_TYPE.INFO) {
|
|
TheIcon = InfoIcon;
|
|
}
|
|
|
|
return <TheIcon fontSize="small" {...props} />;
|
|
}
|
|
FormIcon.propTypes = {
|
|
type: PropTypes.oneOf(Object.values(MESSAGE_TYPE)),
|
|
close: PropTypes.bool,
|
|
};
|
|
|
|
/* Wrapper on any form component to add label, error indicator and help message */
|
|
export function FormInput({children, error, className, label, helpMessage, required, testcid}) {
|
|
const classes = useStyles();
|
|
const cid = testcid || _.uniqueId('c');
|
|
const helpid = `h${cid}`;
|
|
return (
|
|
<Grid container spacing={0} className={className}>
|
|
<Grid item lg={3} md={3} sm={3} xs={12}>
|
|
<InputLabel htmlFor={cid} className={clsx(classes.formLabel, error?classes.formLabelError : null)} required={required}>
|
|
{label}
|
|
<FormIcon type={MESSAGE_TYPE.ERROR} style={{marginLeft: 'auto', visibility: error ? 'unset' : 'hidden'}}/>
|
|
</InputLabel>
|
|
</Grid>
|
|
<Grid item lg={9} md={9} sm={9} xs={12}>
|
|
<FormControl error={Boolean(error)} fullWidth>
|
|
{React.cloneElement(children, {cid, helpid})}
|
|
</FormControl>
|
|
<FormHelperText id={helpid} variant="outlined">{helpMessage}</FormHelperText>
|
|
</Grid>
|
|
</Grid>
|
|
);
|
|
}
|
|
FormInput.propTypes = {
|
|
children: CustomPropTypes.children,
|
|
error: PropTypes.bool,
|
|
className: CustomPropTypes.className,
|
|
label: PropTypes.string,
|
|
helpMessage: PropTypes.string,
|
|
required: PropTypes.bool,
|
|
testcid: PropTypes.any,
|
|
};
|
|
|
|
export function InputSQL({value, options, onChange, ...props}) {
|
|
const classes = useStyles();
|
|
|
|
return (
|
|
<CodeMirror
|
|
value={value||''}
|
|
options={{
|
|
lineNumbers: true,
|
|
mode: 'text/x-pgsql',
|
|
...options,
|
|
}}
|
|
className={classes.sql}
|
|
events={{
|
|
change: (cm)=>{
|
|
onChange && onChange(cm.getValue(), cm);
|
|
},
|
|
}}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
InputSQL.propTypes = {
|
|
value: PropTypes.string,
|
|
options: PropTypes.object,
|
|
onChange: PropTypes.func
|
|
|
|
};
|
|
|
|
export function FormInputSQL({hasError, required, label, className, helpMessage, testcid, value, controlProps, ...props}) {
|
|
return (
|
|
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} >
|
|
<InputSQL value={value} options={controlProps} {...props}/>
|
|
</FormInput>
|
|
);
|
|
}
|
|
FormInputSQL.propTypes = {
|
|
hasError: PropTypes.bool,
|
|
required: PropTypes.bool,
|
|
label: PropTypes.string,
|
|
className: CustomPropTypes.className,
|
|
helpMessage: PropTypes.string,
|
|
testcid: PropTypes.string,
|
|
value: PropTypes.string,
|
|
controlProps: PropTypes.object,
|
|
change: PropTypes.func,
|
|
};
|
|
|
|
|
|
/* Use forwardRef to pass ref prop to OutlinedInput */
|
|
export const InputText = forwardRef(({
|
|
cid, helpid, readonly, disabled, maxlength=255, value, onChange, controlProps, ...props}, ref)=>{
|
|
|
|
const classes = useStyles();
|
|
|
|
let onChangeFinal = (changeVal)=>{
|
|
if(controlProps?.formatter) {
|
|
changeVal = controlProps.formatter.toRaw(changeVal);
|
|
}
|
|
onChange && onChange(changeVal);
|
|
};
|
|
|
|
let finalValue = (_.isNull(value) || _.isUndefined(value)) ? '' : value;
|
|
if(controlProps?.formatter) {
|
|
finalValue = controlProps.formatter.fromRaw(finalValue);
|
|
}
|
|
|
|
return(
|
|
<OutlinedInput
|
|
ref={ref}
|
|
color="primary"
|
|
fullWidth
|
|
className={classes.formInput}
|
|
inputProps={{
|
|
id: cid,
|
|
maxLength: maxlength,
|
|
'aria-describedby': helpid,
|
|
}}
|
|
readOnly={Boolean(readonly)}
|
|
disabled={Boolean(disabled)}
|
|
rows={4}
|
|
notched={false}
|
|
value={(_.isNull(finalValue) || _.isUndefined(finalValue)) ? '' : finalValue}
|
|
onChange={onChangeFinal}
|
|
{...controlProps}
|
|
{...props}
|
|
/>
|
|
);
|
|
});
|
|
InputText.displayName = 'InputText';
|
|
InputText.propTypes = {
|
|
cid: PropTypes.string,
|
|
helpid: PropTypes.string,
|
|
label: PropTypes.string,
|
|
readonly: PropTypes.bool,
|
|
disabled: PropTypes.bool,
|
|
maxlength: PropTypes.number,
|
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
onChange: PropTypes.func,
|
|
controlProps: PropTypes.object,
|
|
};
|
|
|
|
export function FormInputText({hasError, required, label, className, helpMessage, testcid, ...props}) {
|
|
return (
|
|
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
|
<InputText label={label} {...props}/>
|
|
</FormInput>
|
|
);
|
|
}
|
|
FormInputText.propTypes = {
|
|
hasError: PropTypes.bool,
|
|
required: PropTypes.bool,
|
|
label: PropTypes.string,
|
|
className: CustomPropTypes.className,
|
|
helpMessage: PropTypes.string,
|
|
testcid: PropTypes.string,
|
|
};
|
|
|
|
/* Using the existing file dialog functions using showFileDialog */
|
|
export function InputFileSelect({controlProps, onChange, disabled, readonly, ...props}) {
|
|
const inpRef = useRef();
|
|
const onFileSelect = (value)=>{
|
|
onChange && onChange(decodeURI(value));
|
|
inpRef.current.focus();
|
|
};
|
|
return (
|
|
<InputText ref={inpRef} disabled={disabled} readonly={readonly} {...props} endAdornment={
|
|
<IconButton onClick={()=>showFileDialog(controlProps, onFileSelect)}
|
|
disabled={disabled||readonly} aria-label={gettext('Select a file')}><FolderOpenRoundedIcon /></IconButton>
|
|
} />
|
|
);
|
|
}
|
|
InputFileSelect.propTypes = {
|
|
controlProps: PropTypes.object,
|
|
onChange: PropTypes.func,
|
|
disabled: PropTypes.bool,
|
|
readonly: PropTypes.bool,
|
|
};
|
|
|
|
export function FormInputFileSelect({
|
|
hasError, required, label, className, helpMessage, testcid, ...props}) {
|
|
|
|
return (
|
|
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
|
<InputFileSelect required={required} label={label} {...props}/>
|
|
</FormInput>
|
|
);
|
|
}
|
|
FormInputFileSelect.propTypes = {
|
|
hasError: PropTypes.bool,
|
|
required: PropTypes.bool,
|
|
label: PropTypes.string,
|
|
className: CustomPropTypes.className,
|
|
helpMessage: PropTypes.string,
|
|
testcid: PropTypes.string,
|
|
};
|
|
|
|
export function InputSwitch({cid, helpid, value, onChange, readonly, controlProps, ...props}) {
|
|
return (
|
|
<Switch color="primary"
|
|
checked={Boolean(value)}
|
|
onChange={
|
|
readonly ? ()=>{} : onChange
|
|
}
|
|
id={cid}
|
|
inputProps={{
|
|
'aria-describedby': helpid,
|
|
}}
|
|
{...controlProps}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
InputSwitch.propTypes = {
|
|
cid: PropTypes.string,
|
|
helpid: PropTypes.string,
|
|
value: PropTypes.any,
|
|
onChange: PropTypes.func,
|
|
readonly: PropTypes.bool,
|
|
controlProps: PropTypes.object,
|
|
};
|
|
|
|
export function FormInputSwitch({hasError, required, label, className, helpMessage, testcid, ...props}) {
|
|
|
|
return (
|
|
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
|
<InputSwitch {...props}/>
|
|
</FormInput>
|
|
);
|
|
}
|
|
FormInputSwitch.propTypes = {
|
|
hasError: PropTypes.bool,
|
|
required: PropTypes.bool,
|
|
label: PropTypes.string,
|
|
className: CustomPropTypes.className,
|
|
helpMessage: PropTypes.string,
|
|
testcid: PropTypes.string,
|
|
};
|
|
|
|
export function InputCheckbox({cid, helpid, value, onChange, controlProps, readonly, ...props}) {
|
|
controlProps = controlProps || {};
|
|
return (
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
id={cid}
|
|
checked={Boolean(value)}
|
|
onChange={readonly ? ()=>{} : onChange}
|
|
color="primary"
|
|
inputProps={{
|
|
'aria-describedby': helpid,
|
|
}}
|
|
{...props}
|
|
/>
|
|
}
|
|
label={controlProps.label}
|
|
/>
|
|
|
|
);
|
|
}
|
|
InputCheckbox.propTypes = {
|
|
cid: PropTypes.string,
|
|
helpid: PropTypes.string,
|
|
value: PropTypes.bool,
|
|
controlProps: PropTypes.object,
|
|
onChange: PropTypes.func,
|
|
readonly: PropTypes.bool,
|
|
};
|
|
|
|
export function FormInputCheckbox({hasError, required, label,
|
|
className, helpMessage, testcid, ...props}) {
|
|
|
|
return (
|
|
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
|
<InputCheckbox {...props}/>
|
|
</FormInput>
|
|
);
|
|
}
|
|
FormInputCheckbox.propTypes = {
|
|
hasError: PropTypes.bool,
|
|
required: PropTypes.bool,
|
|
label: PropTypes.string,
|
|
className: CustomPropTypes.className,
|
|
helpMessage: PropTypes.string,
|
|
testcid: PropTypes.string,
|
|
};
|
|
|
|
|
|
export function InputToggle({cid, value, onChange, options, disabled, readonly, ...props}) {
|
|
return (
|
|
<ToggleButtonGroup
|
|
id={cid}
|
|
value={value}
|
|
exclusive
|
|
onChange={(e, val)=>{val!==null && onChange(val);}}
|
|
{...props}
|
|
>
|
|
{
|
|
(options||[]).map((option)=>{
|
|
const isSelected = option.value === value;
|
|
const isDisabled = disabled || (readonly && isSelected);
|
|
return (
|
|
<ToggleButton key={option.label} value={option.value} component={isSelected ? PrimaryButton : DefaultButton}
|
|
disabled={isDisabled} aria-label={option.label}>
|
|
<CheckRoundedIcon style={{visibility: isSelected ? 'visible': 'hidden'}}/> {option.label}
|
|
</ToggleButton>
|
|
);
|
|
})
|
|
}
|
|
</ToggleButtonGroup>
|
|
);
|
|
}
|
|
InputToggle.propTypes = {
|
|
cid: PropTypes.string,
|
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
|
|
options: PropTypes.array,
|
|
controlProps: PropTypes.object,
|
|
onChange: PropTypes.func,
|
|
disabled: PropTypes.bool,
|
|
readonly: PropTypes.bool,
|
|
};
|
|
|
|
export function FormInputToggle({hasError, required, label,
|
|
className, helpMessage, testcid, ...props}) {
|
|
|
|
return (
|
|
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
|
<InputToggle {...props}/>
|
|
</FormInput>
|
|
);
|
|
}
|
|
FormInputToggle.propTypes = {
|
|
hasError: PropTypes.bool,
|
|
required: PropTypes.bool,
|
|
label: PropTypes.string,
|
|
className: CustomPropTypes.className,
|
|
helpMessage: PropTypes.string,
|
|
testcid: PropTypes.string,
|
|
};
|
|
|
|
/* react-select package is used for select input
|
|
* Customizing the select styles to fit existing theme
|
|
*/
|
|
const customReactSelectStyles = (theme, readonly)=>({
|
|
input: (provided) => {
|
|
return {...provided, padding: 0, margin: 0, color: 'inherit'};
|
|
},
|
|
singleValue: (provided) => {
|
|
return {
|
|
...provided,
|
|
color: 'inherit',
|
|
};
|
|
},
|
|
control: (provided, state) => ({
|
|
...provided,
|
|
minHeight: '0',
|
|
backgroundColor: readonly ? theme.otherVars.inputDisabledBg : theme.palette.background.default,
|
|
borderColor: theme.otherVars.inputBorderColor,
|
|
...(state.isFocused ? {
|
|
borderColor: theme.palette.primary.main,
|
|
boxShadow: 'inset 0 0 0 1px '+theme.palette.primary.main,
|
|
'&:hover': {
|
|
borderColor: theme.palette.primary.main,
|
|
}
|
|
} : {}),
|
|
}),
|
|
dropdownIndicator: (provided)=>({
|
|
...provided,
|
|
padding: '0rem 0.25rem',
|
|
}),
|
|
indicatorsContainer: (provided)=>({
|
|
...provided,
|
|
...(readonly ? {display: 'none'} : {})
|
|
}),
|
|
clearIndicator: (provided)=>({
|
|
...provided,
|
|
padding: '0rem 0.25rem',
|
|
}),
|
|
valueContainer: (provided)=>({
|
|
...provided,
|
|
padding: theme.otherVars.reactSelect.padding,
|
|
}),
|
|
menu: (provided)=>({
|
|
...provided,
|
|
backgroundColor: theme.palette.background.default,
|
|
color: theme.palette.text.primary,
|
|
boxShadow: 'none',
|
|
border: '1px solid ' + theme.otherVars.inputBorderColor,
|
|
}),
|
|
menuPortal: (provided)=>({
|
|
...provided, zIndex: 9999,
|
|
backgroundColor: 'inherit',
|
|
color: 'inherit',
|
|
}),
|
|
option: (provided, state)=>({
|
|
...provided,
|
|
padding: '0.5rem',
|
|
backgroundColor: state.isFocused ? theme.palette.grey[200] :
|
|
(state.isSelected ? theme.palette.primary.main : 'inherit'),
|
|
}),
|
|
multiValue: (provided)=>({
|
|
...provided,
|
|
backgroundColor: theme.palette.grey[400],
|
|
}),
|
|
multiValueLabel: (provided)=>({
|
|
...provided,
|
|
fontSize: '1em',
|
|
zIndex: 99,
|
|
color: theme.palette.text.primary
|
|
}),
|
|
multiValueRemove: (provided)=>({
|
|
...provided,
|
|
'&:hover': {
|
|
backgroundColor: 'unset',
|
|
color: theme.palette.error.main,
|
|
},
|
|
...(readonly ? {display: 'none'} : {})
|
|
}),
|
|
});
|
|
|
|
function OptionView({image, label}) {
|
|
const classes = useStyles();
|
|
return (
|
|
<>
|
|
{image && <span className={clsx(classes.optionIcon, image)}></span>}
|
|
<span>{label}</span>
|
|
</>
|
|
);
|
|
}
|
|
OptionView.propTypes = {
|
|
image: PropTypes.string,
|
|
label: PropTypes.string,
|
|
};
|
|
|
|
function CustomSelectOption(props) {
|
|
return (
|
|
<RSComponents.Option {...props}>
|
|
<OptionView image={props.data.image} label={props.data.label} />
|
|
</RSComponents.Option>
|
|
);
|
|
}
|
|
CustomSelectOption.propTypes = {
|
|
data: PropTypes.object,
|
|
};
|
|
|
|
function CustomSelectSingleValue(props) {
|
|
return (
|
|
<RSComponents.SingleValue {...props}>
|
|
<OptionView image={props.data.image} label={props.data.label} />
|
|
</RSComponents.SingleValue>
|
|
);
|
|
}
|
|
CustomSelectSingleValue.propTypes = {
|
|
data: PropTypes.object,
|
|
};
|
|
|
|
function getRealValue(options, value, creatable, formatter) {
|
|
let realValue = null;
|
|
if(_.isArray(value)) {
|
|
realValue = [...value];
|
|
/* If multi select options need to be in some format by UI, use formatter */
|
|
if(formatter) {
|
|
realValue = formatter.fromRaw(realValue);
|
|
}
|
|
if(creatable) {
|
|
realValue = realValue.map((val)=>({label:val, value: val}));
|
|
} else {
|
|
realValue = realValue.map((val)=>(_.find(options, (option)=>option.value==val)));
|
|
}
|
|
} else {
|
|
realValue = _.find(options, (option)=>option.value==value) || null;
|
|
}
|
|
return realValue;
|
|
}
|
|
|
|
export function InputSelect({
|
|
cid, onChange, options, readonly=false, value, controlProps={}, optionsLoaded, optionsReloadBasis, disabled, ...props}) {
|
|
const [[finalOptions, isLoading], setFinalOptions] = useState([[], true]);
|
|
const theme = useTheme();
|
|
|
|
/* React will always take options var as changed parameter. So,
|
|
We cannot run the below effect with options dependency as it will keep on
|
|
loading the options. optionsReloadBasis is helpful to avoid repeated
|
|
options load. If optionsReloadBasis value changes, then options will be loaded again.
|
|
*/
|
|
useEffect(()=>{
|
|
let optPromise = options, umounted=false;
|
|
if(typeof options === 'function') {
|
|
optPromise = options();
|
|
}
|
|
Promise.resolve(optPromise)
|
|
.then((res)=>{
|
|
/* If component unmounted, dont update state */
|
|
if(!umounted) {
|
|
optionsLoaded && optionsLoaded(res, value);
|
|
setFinalOptions([res || [], false]);
|
|
}
|
|
});
|
|
return ()=>umounted=true;
|
|
}, [optionsReloadBasis]);
|
|
|
|
const onChangeOption = useCallback((selectVal, action)=>{
|
|
if(_.isArray(selectVal)) {
|
|
/* If multi select options need to be in some format by UI, use formatter */
|
|
selectVal = selectVal.map((option)=>option.value);
|
|
if(controlProps.formatter) {
|
|
selectVal = controlProps.formatter.toRaw(selectVal);
|
|
}
|
|
onChange && onChange(selectVal, action.name);
|
|
} else {
|
|
onChange && onChange(selectVal ? selectVal.value : null, action.name);
|
|
}
|
|
}, [onChange]);
|
|
|
|
/* Apply filter if any */
|
|
const filteredOptions = (controlProps.filter && controlProps.filter(finalOptions)) || finalOptions;
|
|
const realValue = getRealValue(filteredOptions, value, controlProps.creatable, controlProps.formatter);
|
|
const otherProps = {
|
|
isSearchable: !readonly,
|
|
isClearable: !readonly && (!_.isUndefined(controlProps.allowClear) ? controlProps.allowClear : true),
|
|
isDisabled: Boolean(disabled),
|
|
};
|
|
|
|
const styles = customReactSelectStyles(theme, readonly || disabled);
|
|
|
|
const commonProps = {
|
|
components: {
|
|
Option: CustomSelectOption,
|
|
SingleValue: CustomSelectSingleValue,
|
|
},
|
|
isMulti: Boolean(controlProps.multiple),
|
|
openMenuOnClick: !readonly,
|
|
onChange: onChangeOption,
|
|
isLoading: isLoading,
|
|
options: filteredOptions,
|
|
value: realValue,
|
|
menuPortalTarget: document.body,
|
|
styles: styles,
|
|
inputId: cid,
|
|
...otherProps,
|
|
...props
|
|
};
|
|
if(!controlProps.creatable) {
|
|
return (
|
|
<Select {...commonProps}/>
|
|
);
|
|
} else {
|
|
return (
|
|
<CreatableSelect {...commonProps}/>
|
|
);
|
|
}
|
|
}
|
|
InputSelect.propTypes = {
|
|
cid: PropTypes.string,
|
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
|
|
options: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]),
|
|
controlProps: PropTypes.object,
|
|
optionsLoaded: PropTypes.func,
|
|
onChange: PropTypes.func,
|
|
disabled: PropTypes.bool,
|
|
readonly: PropTypes.bool,
|
|
};
|
|
|
|
|
|
export function FormInputSelect({
|
|
hasError, required, className, label, helpMessage, testcid, ...props}) {
|
|
|
|
return (
|
|
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
|
<InputSelect {...props}/>
|
|
</FormInput>
|
|
);
|
|
}
|
|
FormInputSelect.propTypes = {
|
|
hasError: PropTypes.bool,
|
|
required: PropTypes.bool,
|
|
label: PropTypes.string,
|
|
className: CustomPropTypes.className,
|
|
helpMessage: PropTypes.string,
|
|
testcid: PropTypes.string,
|
|
};
|
|
|
|
/* React wrapper on color pickr */
|
|
export function InputColor({value, controlProps, disabled, onChange, currObj}) {
|
|
const pickrOptions = {
|
|
showPalette: true,
|
|
allowEmpty: true,
|
|
colorFormat: 'HEX',
|
|
defaultColor: null,
|
|
position: 'right-middle',
|
|
clearText: gettext('No color'),
|
|
...controlProps,
|
|
disabled: disabled,
|
|
};
|
|
const eleRef = useRef();
|
|
const pickrObj = useRef();
|
|
const classes = useStyles();
|
|
|
|
const setColor = (value)=>{
|
|
pickrObj.current &&
|
|
pickrObj.current.setColor((_.isUndefined(value) || value == '') ? pickrOptions.defaultColor : value);
|
|
};
|
|
|
|
const destroyPickr = ()=>{
|
|
if(pickrObj.current) {
|
|
pickrObj.current.destroy();
|
|
pickrObj.current = null;
|
|
}
|
|
};
|
|
|
|
const initPickr = ()=>{
|
|
/* pickr does not have way to update options, need to
|
|
destroy and recreate pickr to reflect options */
|
|
destroyPickr();
|
|
|
|
pickrObj.current = new Pickr({
|
|
el: eleRef.current,
|
|
useAsButton: true,
|
|
theme: 'monolith',
|
|
swatches: [
|
|
'#000', '#666', '#ccc', '#fff', '#f90', '#ff0', '#0f0',
|
|
'#f0f', '#f4cccc', '#fce5cd', '#d0e0e3', '#cfe2f3', '#ead1dc', '#ea9999',
|
|
'#b6d7a8', '#a2c4c9', '#d5a6bd', '#e06666','#93c47d', '#76a5af', '#c27ba0',
|
|
'#f1c232', '#6aa84f', '#45818e', '#a64d79', '#bf9000', '#0c343d', '#4c1130',
|
|
],
|
|
position: pickrOptions.position,
|
|
strings: {
|
|
clear: pickrOptions.clearText,
|
|
},
|
|
components: {
|
|
palette: pickrOptions.showPalette,
|
|
preview: true,
|
|
hue: pickrOptions.showPalette,
|
|
interaction: {
|
|
clear: pickrOptions.allowEmpty,
|
|
defaultRepresentation: pickrOptions.colorFormat,
|
|
disabled: pickrOptions.disabled,
|
|
},
|
|
},
|
|
}).on('init', instance => {
|
|
setColor(value);
|
|
disabled && instance.disable();
|
|
|
|
const {lastColor} = instance.getRoot().preview;
|
|
const {clear} = instance.getRoot().interaction;
|
|
|
|
/* Cycle the keyboard navigation within the color picker */
|
|
clear.addEventListener('keydown', (e)=>{
|
|
if(e.keyCode === 9) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
lastColor.focus();
|
|
}
|
|
});
|
|
|
|
lastColor.addEventListener('keydown', (e)=>{
|
|
if(e.keyCode === 9 && e.shiftKey) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
clear.focus();
|
|
}
|
|
});
|
|
}).on('clear', () => {
|
|
onChange && onChange('');
|
|
}).on('change', (color) => {
|
|
onChange && onChange(color.toHEXA().toString());
|
|
}).on('show', (color, instance) => {
|
|
const {palette} = instance.getRoot().palette;
|
|
palette.focus();
|
|
}).on('hide', (instance) => {
|
|
const button = instance.getRoot().button;
|
|
button.focus();
|
|
});
|
|
|
|
if(currObj) {
|
|
currObj(pickrObj.current);
|
|
}
|
|
};
|
|
|
|
useEffect(()=>{
|
|
initPickr();
|
|
return ()=>{
|
|
destroyPickr();
|
|
};
|
|
}, [...Object.values(pickrOptions)]);
|
|
|
|
useEffect(()=>{
|
|
if(pickrObj.current) {
|
|
setColor(value);
|
|
}
|
|
}, [value]);
|
|
|
|
let btnStyles = {backgroundColor: value};
|
|
return (
|
|
// <Button variant="contained" ref={eleRef} className={classes.colorBtn} style={btnStyles} disabled={pickrOptions.disabled}>
|
|
// {(_.isUndefined(value) || _.isNull(value) || value === '') && <CloseIcon />}
|
|
// </Button>
|
|
<PgIconButton ref={eleRef} title={gettext('Select the color')} className={classes.colorBtn} style={btnStyles} disabled={pickrOptions.disabled}
|
|
icon={(_.isUndefined(value) || _.isNull(value) || value === '') && <CloseIcon />}
|
|
/>
|
|
);
|
|
}
|
|
InputColor.propTypes = {
|
|
value: PropTypes.string,
|
|
controlProps: PropTypes.object,
|
|
onChange: PropTypes.func,
|
|
disabled: PropTypes.bool,
|
|
currObj: PropTypes.func,
|
|
};
|
|
|
|
export function FormInputColor({
|
|
hasError, required, className, label, helpMessage, testcid, ...props}) {
|
|
|
|
return (
|
|
<FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid}>
|
|
<InputColor {...props}/>
|
|
</FormInput>
|
|
);
|
|
}
|
|
FormInputColor.propTypes = {
|
|
hasError: PropTypes.bool,
|
|
required: PropTypes.bool,
|
|
className: CustomPropTypes.className,
|
|
label: PropTypes.string,
|
|
helpMessage: PropTypes.string,
|
|
testcid: PropTypes.string,
|
|
};
|
|
|
|
|
|
const useStylesFormFooter = makeStyles((theme)=>({
|
|
root: {
|
|
padding: theme.spacing(0.5),
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
zIndex: 10,
|
|
},
|
|
container: {
|
|
borderWidth: '1px',
|
|
borderStyle: 'solid',
|
|
borderRadius: theme.shape.borderRadius,
|
|
padding: theme.spacing(0.5),
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
},
|
|
containerSuccess: {
|
|
borderColor: theme.palette.success.main,
|
|
backgroundColor: theme.palette.success.light,
|
|
},
|
|
iconSuccess: {
|
|
color: theme.palette.success.main,
|
|
},
|
|
containerError: {
|
|
borderColor: theme.palette.error.main,
|
|
backgroundColor: theme.palette.error.light,
|
|
},
|
|
iconError: {
|
|
color: theme.palette.error.main,
|
|
},
|
|
message: {
|
|
marginLeft: theme.spacing(0.5),
|
|
},
|
|
closeButton: {
|
|
marginLeft: 'auto',
|
|
},
|
|
}));
|
|
|
|
/* The form footer used mostly for showing error */
|
|
export function FormFooterMessage({type=MESSAGE_TYPE.SUCCESS, message, closable=true, onClose=()=>{}}) {
|
|
const classes = useStylesFormFooter();
|
|
|
|
if(!message) {
|
|
return <></>;
|
|
}
|
|
return (
|
|
<Box className={classes.root}>
|
|
<Box className={clsx(classes.container, classes[`container${type}`])}>
|
|
<FormIcon type={type} className={classes[`icon${type}`]}/>
|
|
<Box className={classes.message}>{message}</Box>
|
|
{closable && <IconButton className={clsx(classes.closeButton, classes[`icon${type}`])} onClick={onClose}>
|
|
<FormIcon close={true}/>
|
|
</IconButton>}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
FormFooterMessage.propTypes = {
|
|
type: PropTypes.oneOf(Object.values(MESSAGE_TYPE)).isRequired,
|
|
message: PropTypes.string,
|
|
closable: PropTypes.bool,
|
|
onClose: PropTypes.func,
|
|
};
|