codekingpro/portable-devtools
114k
1/////////////////////////////////////////////////////////////2//3// pgAdmin 4 - PostgreSQL Tools4//5// Copyright (C) 2013 - 2024, The pgAdmin Development Team6// This software is released under the PostgreSQL Licence7//8//////////////////////////////////////////////////////////////9/* Common form components used in pgAdmin */10 11import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';12import { makeStyles } from '@mui/styles';13import {14 Box, FormControl, OutlinedInput, FormHelperText, ToggleButton, ToggleButtonGroup,15 Grid, IconButton, FormControlLabel, Switch, Checkbox, useTheme, InputLabel, Paper, Select as MuiSelect, Radio, Tooltip,16} from '@mui/material';17import ErrorRoundedIcon from '@mui/icons-material/ErrorOutlineRounded';18import InfoRoundedIcon from '@mui/icons-material/InfoRounded';19import CloseIcon from '@mui/icons-material/CloseRounded';20import CheckRoundedIcon from '@mui/icons-material/CheckRounded';21import WarningRoundedIcon from '@mui/icons-material/WarningRounded';22import FolderOpenRoundedIcon from '@mui/icons-material/FolderOpenRounded';23import DescriptionIcon from '@mui/icons-material/Description';24import AssignmentTurnedIn from '@mui/icons-material/AssignmentTurnedIn';25import Select, { components as RSComponents } from 'react-select';26import CreatableSelect from 'react-select/creatable';27import clsx from 'clsx';28import PropTypes from 'prop-types';29import HTMLReactParse from 'html-react-parser';30 31import { DateTimePicker, DatePicker, TimePicker} from '@mui/x-date-pickers';32import * as DateFns from 'date-fns';33 34import CodeMirror from './ReactCodeMirror';35import gettext from 'sources/gettext';36import _ from 'lodash';37import { DefaultButton, PrimaryButton, PgIconButton } from './Buttons';38import CustomPropTypes from '../custom_prop_types';39import KeyboardShortcuts from './KeyboardShortcuts';40import QueryThresholds from './QueryThresholds';41import SelectThemes from './SelectThemes';42import { showFileManager } from '../helpers/showFileManager';43import { withColorPicker } from '../helpers/withColorPicker';44import { useWindowSize } from '../custom_hooks';45import PgTreeView from '../PgTreeView';46import Loader from 'sources/components/Loader';47 48 49const useStyles = makeStyles((theme) => ({50 formRoot: {51 padding: '1rem'52 },53 img: {54 maxWidth: '100%',55 height: 'auto'56 },57 info: {58 color: theme.palette.info.main,59 marginLeft: '0.25rem',60 fontSize: '1rem',61 },62 formLabel: {63 margin: theme.spacing(0.75, 0.75, 0.75, 0.75),64 display: 'flex',65 wordBreak: 'break-word'66 },67 formLabelError: {68 color: theme.palette.error.main,69 },70 sql: {71 border: '1px solid ' + theme.otherVars.inputBorderColor,72 borderRadius: theme.shape.borderRadius,73 height: '100%',74 },75 optionIcon: {76 ...theme.mixins.nodeIcon,77 },78 colorBtn: {79 height: theme.spacing(3.5),80 minHeight: theme.spacing(3.5),81 width: theme.spacing(3.5),82 minWidth: theme.spacing(3.5),83 },84 noteRoot: {85 display: 'flex',86 backgroundColor: theme.otherVars.borderColor,87 padding: theme.spacing(1),88 },89 readOnlySwitch: {90 opacity: 0.75,91 '& .MuiSwitch-track': {92 opacity: theme.palette.action.disabledOpacity,93 }94 }95}));96 97 98export const MESSAGE_TYPE = {99 SUCCESS: 'Success',100 ERROR: 'Error',101 INFO: 'Info',102 CLOSE: 'Close',103 WARNING: 'Warning'104};105 106/* Icon based on MESSAGE_TYPE */107function FormIcon({ type, close = false, ...props }) {108 let TheIcon = null;109 if (close) {110 TheIcon = CloseIcon;111 } else if (type === MESSAGE_TYPE.SUCCESS) {112 TheIcon = CheckRoundedIcon;113 } else if (type === MESSAGE_TYPE.ERROR) {114 TheIcon = ErrorRoundedIcon;115 } else if (type === MESSAGE_TYPE.INFO) {116 TheIcon = InfoRoundedIcon;117 } else if (type === MESSAGE_TYPE.WARNING) {118 TheIcon = WarningRoundedIcon;119 }120 121 return <TheIcon fontSize="small" {...props} data-testid={close ? 'Close' : type}/>;122}123FormIcon.propTypes = {124 type: PropTypes.oneOf(Object.values(MESSAGE_TYPE)),125 close: PropTypes.bool,126};127 128/* Wrapper on any form component to add label, error indicator and help message */129export function FormInput({ children, error, className, label, helpMessage, required, testcid, lid, withContainer=true, labelGridBasis=3, controlGridBasis=9, labelTooltip='' }) {130 const classes = useStyles();131 const cid = testcid || _.uniqueId('c');132 const helpid = `h${cid}`;133 if(!withContainer) {134 return (135 <>136 <Grid item lg={labelGridBasis} md={labelGridBasis} sm={12} xs={12}>137 <InputLabel id={lid} htmlFor={lid ? undefined : cid} className={clsx(classes.formLabel, error ? classes.formLabelError : null)} required={required}>138 {label}139 <FormIcon type={MESSAGE_TYPE.ERROR} style={{ marginLeft: 'auto', visibility: error ? 'unset' : 'hidden' }} />140 </InputLabel>141 </Grid>142 <Grid item lg={controlGridBasis} md={controlGridBasis} sm={12} xs={12}>143 <FormControl error={Boolean(error)} fullWidth>144 {React.cloneElement(children, { cid, helpid })}145 </FormControl>146 <FormHelperText id={helpid} variant="outlined">{HTMLReactParse(helpMessage || '')}</FormHelperText>147 </Grid>148 </>149 );150 }151 152 let labelComponent = <InputLabel id={lid} htmlFor={lid ? undefined : cid} className={clsx(classes.formLabel, error ? classes.formLabelError : null)} required={required}>153 {label}154 <FormIcon type={MESSAGE_TYPE.ERROR} style={{ marginLeft: 'auto', visibility: error ? 'unset' : 'hidden' }} />155 </InputLabel>;156 return (157 <Grid container spacing={0} className={className} data-testid="form-input">158 <Grid item lg={labelGridBasis} md={labelGridBasis} sm={12} xs={12}>159 {160 labelTooltip ?161 <Tooltip title={labelTooltip}>162 {labelComponent}163 </Tooltip> : labelComponent164 }165 </Grid>166 <Grid item lg={controlGridBasis} md={controlGridBasis} sm={12} xs={12}>167 <FormControl error={Boolean(error)} fullWidth>168 {React.cloneElement(children, { cid, helpid })}169 </FormControl>170 <FormHelperText id={helpid} variant="outlined">{HTMLReactParse(helpMessage || '')}</FormHelperText>171 </Grid>172 </Grid>173 );174}175FormInput.propTypes = {176 children: CustomPropTypes.children,177 error: PropTypes.bool,178 className: CustomPropTypes.className,179 label: PropTypes.string,180 helpMessage: PropTypes.string,181 required: PropTypes.bool,182 testcid: PropTypes.any,183 lid: PropTypes.any,184 withContainer: PropTypes.bool,185 labelGridBasis: PropTypes.number,186 controlGridBasis: PropTypes.number,187 labelTooltip: PropTypes.string188};189 190export function InputSQL({ value, options, onChange, className, controlProps, inputRef, ...props }) {191 const classes = useStyles();192 const editor = useRef();193 194 return (195 <CodeMirror196 currEditor={(obj) => {197 editor.current = obj;198 inputRef?.(obj);199 }}200 value={value || ''}201 options={{202 ...options,203 }}204 className={clsx(classes.sql, className)}205 onChange={onChange}206 {...controlProps}207 {...props}208 />209 );210}211InputSQL.propTypes = {212 value: PropTypes.string,213 options: PropTypes.object,214 onChange: PropTypes.func,215 readonly: PropTypes.bool,216 className: CustomPropTypes.className,217 controlProps: PropTypes.object,218 inputRef: CustomPropTypes.ref,219};220 221export function FormInputSQL({ hasError, required, label, className, helpMessage, testcid, value, controlProps, noLabel, ...props }) {222 if (noLabel) {223 return <InputSQL value={value} options={controlProps} {...props} />;224 } else {225 const lid = _.uniqueId('l');226 return (227 <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} lid={lid}>228 <InputSQL value={value} options={controlProps} labelledBy={lid} {...props} />229 </FormInput>230 );231 }232}233FormInputSQL.propTypes = {234 hasError: PropTypes.bool,235 required: PropTypes.bool,236 label: PropTypes.string,237 className: CustomPropTypes.className,238 helpMessage: PropTypes.string,239 testcid: PropTypes.string,240 value: PropTypes.string,241 controlProps: PropTypes.object,242 noLabel: PropTypes.bool,243 change: PropTypes.func,244};245 246/* https://date-fns.org/v2.24.0/docs/format */247const DATE_TIME_FORMAT = {248 DATE_TIME_12: 'yyyy-MM-dd hh:mm:ss aa xxx',249 DATE_TIME_24: 'yyyy-MM-dd HH:mm:ss xxx',250 DATE: 'yyyy-MM-dd',251 TIME_12: 'hh:mm:ss aa',252 TIME_24: 'HH:mm:ss',253};254 255export function InputDateTimePicker({ value, onChange, readonly, controlProps, ...props }) {256 let format = '';257 let placeholder = '';258 let regExp = /[a-zA-Z]/;259 let timeZoneString = '';260 if (controlProps?.pickerType === 'Date') {261 format = controlProps.format || DATE_TIME_FORMAT.DATE;262 placeholder = controlProps.placeholder || 'YYYY-MM-DD';263 } else if (controlProps?.pickerType === 'Time') {264 format = controlProps.format || (controlProps.ampm ? DATE_TIME_FORMAT.TIME_12 : DATE_TIME_FORMAT.TIME_24);265 placeholder = controlProps.placeholder || 'HH:mm:ss';266 } else {267 format = controlProps.format || (controlProps.ampm ? DATE_TIME_FORMAT.DATE_TIME_12 : DATE_TIME_FORMAT.DATE_TIME_24);268 placeholder = controlProps.placeholder || 'YYYY-MM-DD HH:mm:ss Z';269 }270 271 const handleChange = (dateVal) => {272 if(DateFns.isValid(dateVal)) {273 onChange(DateFns.format(dateVal, format));274 } else{275 onChange(null);276 }277 };278 279 /* Value should be a date object instead of string */280 value = _.isUndefined(value) || regExp.test(value) ? null : value;281 if (!_.isNull(value)) {282 timeZoneString = value.slice(-6);283 let parseValue = DateFns.parse(value, format, new Date());284 if (!DateFns.isValid(parseValue)) {285 parseValue = DateFns.parseISO(value);286 }287 value = !DateFns.isValid(parseValue) ? value : parseValue;288 }289 290 if (readonly) {291 return (<InputText value={value ? DateFns.format(value, format) : value}292 readonly={readonly} controlProps={{ placeholder: controlProps.placeholder }} {...props} />);293 }294 295 let commonProps = {296 ...props,297 value: value,298 format: format.replace('xxx', timeZoneString),299 label: '',300 variant: 'inline',301 ampm: controlProps.ampm ? controlProps.ampm : undefined,302 disablePast: controlProps.disablePast || false,303 onChange: handleChange,304 slotProps: {textField: {placeholder:placeholder}}305 };306 307 if (controlProps?.pickerType === 'Date') {308 return (309 <DatePicker {...commonProps} />310 );311 } else if (controlProps?.pickerType === 'Time') {312 return (313 <TimePicker {...commonProps} />314 );315 }316 return (317 <DateTimePicker {...commonProps} />318 );319}320InputDateTimePicker.propTypes = {321 value: CustomPropTypes.className,322 options: PropTypes.object,323 onChange: PropTypes.func,324 readonly: PropTypes.bool,325 controlProps: PropTypes.object,326};327 328export function FormInputDateTimePicker({ hasError, required, label, className, helpMessage, testcid, labelTooltip, ...props }) {329 return (330 <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} labelTooltip={labelTooltip}>331 <InputDateTimePicker {...props} />332 </FormInput>333 );334}335FormInputDateTimePicker.propTypes = {336 hasError: PropTypes.bool,337 required: PropTypes.bool,338 label: PropTypes.string,339 className: CustomPropTypes.className,340 helpMessage: PropTypes.string,341 testcid: PropTypes.string,342 value: CustomPropTypes.className,343 controlProps: PropTypes.object,344 change: PropTypes.func,345 labelTooltip: PropTypes.string346};347 348/* Use forwardRef to pass ref prop to OutlinedInput */349export const InputText = forwardRef(({350 cid, helpid, readonly, disabled, value, onChange, controlProps, type, size, inputStyle, ...props }, ref) => {351 352 const maxlength = typeof(controlProps?.maxLength) != 'undefined' ? controlProps.maxLength : 255;353 354 const patterns = {355 'numeric': '^-?[0-9]\\d*\\.?\\d*$',356 'int': '^-?[0-9]\\d*$',357 };358 let onChangeFinal = (e) => {359 let changeVal = e.target.value;360 361 /* For type number, we set type as tel with number regex to get validity.*/362 if (['numeric', 'int', 'tel'].indexOf(type) > -1) {363 if (!e.target.validity.valid && changeVal !== '' && changeVal !== '-') {364 return;365 }366 }367 if (controlProps?.formatter) {368 changeVal = controlProps.formatter.toRaw(changeVal);369 }370 onChange?.(changeVal);371 };372 373 let finalValue = (_.isNull(value) || _.isUndefined(value)) ? '' : value;374 375 if (controlProps?.formatter) {376 finalValue = controlProps.formatter.fromRaw(finalValue);377 }378 379 const filteredProps = _.pickBy(props, (_v, key)=>(380 /* When used in ButtonGroup, following props should be skipped */381 !['color', 'disableElevation', 'disableFocusRipple', 'disableRipple'].includes(key)382 ));383 384 return (385 <OutlinedInput386 ref={ref}387 color="primary"388 fullWidth389 size={size}390 margin={size == 'small' ? 'dense' : 'none'}391 inputProps={{392 id: cid,393 maxLength: controlProps?.multiline ? null : maxlength,394 'aria-describedby': helpid,395 ...(type ? { pattern: !_.isUndefined(controlProps) && !_.isUndefined(controlProps.pattern) ? controlProps.pattern : patterns[type] } : {}),396 style: inputStyle || {},397 autoComplete: _.isUndefined(controlProps?.autoComplete) ? 'off' : controlProps?.autoComplete,398 'data-testid': 'input-text',399 title: controlProps?.title,400 }}401 readOnly={Boolean(readonly)}402 disabled={Boolean(disabled)}403 rows={4}404 notched={false}405 value={(_.isNull(finalValue) || _.isUndefined(finalValue)) ? '' : finalValue}406 onChange={onChangeFinal}407 {408 ...(controlProps?.onKeyDown && { onKeyDown: controlProps.onKeyDown })409 }410 {...controlProps}411 {...filteredProps}412 {...(['numeric', 'int'].indexOf(type) > -1 ? { type: 'tel' } : { type: type })}413 />414 );415});416InputText.displayName = 'InputText';417InputText.propTypes = {418 cid: PropTypes.string,419 helpid: PropTypes.string,420 label: PropTypes.string,421 readonly: PropTypes.bool,422 disabled: PropTypes.bool,423 value: PropTypes.any,424 onChange: PropTypes.func,425 controlProps: PropTypes.object,426 type: PropTypes.string,427 size: PropTypes.string,428 inputStyle: PropTypes.object429};430 431export function FormInputText({ hasError, required, label, className, helpMessage, testcid, labelTooltip, ...props }) {432 return (433 <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} labelTooltip={labelTooltip} >434 <InputText label={label} {...props} />435 </FormInput>436 );437}438FormInputText.propTypes = {439 hasError: PropTypes.bool,440 required: PropTypes.bool,441 label: PropTypes.string,442 className: CustomPropTypes.className,443 helpMessage: PropTypes.string,444 testcid: PropTypes.string,445 labelTooltip: PropTypes.string446};447 448export function InputFileSelect({ controlProps, onChange, disabled, readonly, isvalidate = false, hideBrowseButton=false,validate, ...props }) {449 const inpRef = useRef();450 let textControlProps = {};451 if(controlProps?.placeholder) {452 const {placeholder} = controlProps;453 textControlProps = {placeholder};454 }455 const showFileDialog = ()=>{456 let params = {457 supported_types: controlProps.supportedTypes || [],458 dialog_type: controlProps.dialogType || 'select_file',459 dialog_title: controlProps.dialogTitle || '',460 btn_primary: controlProps.btnPrimary || '',461 };462 showFileManager(params, (fileName)=>{463 onChange?.(decodeURI(fileName));464 inpRef.current.focus();465 });466 };467 468 return (469 <InputText ref={inpRef} disabled={disabled} readonly={readonly} onChange={onChange} controlProps={textControlProps} {...props} endAdornment={470 <>471 {!hideBrowseButton &&472 <IconButton onClick={showFileDialog}473 disabled={disabled || readonly} aria-label={gettext('Select a file')}><FolderOpenRoundedIcon /></IconButton>474 }475 {isvalidate &&476 <PgIconButton title={gettext('Validate')} style={{ border: 'none' }} disabled={!props.value} onClick={() => { validate(props.value); }} icon={<AssignmentTurnedIn />}></PgIconButton>477 }478 </>479 } />480 );481}482InputFileSelect.propTypes = {483 controlProps: PropTypes.object,484 onChange: PropTypes.func,485 disabled: PropTypes.bool,486 readonly: PropTypes.bool,487 isvalidate: PropTypes.bool,488 validate: PropTypes.func,489 value: PropTypes.string,490 hideBrowseButton: PropTypes.bool491};492 493export function FormInputFileSelect({494 hasError, required, label, className, helpMessage, testcid, labelTooltip, ...props }) {495 496 return (497 <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} labelTooltip={labelTooltip}>498 <InputFileSelect required={required} label={label} {...props} />499 </FormInput>500 );501}502FormInputFileSelect.propTypes = {503 hasError: PropTypes.bool,504 required: PropTypes.bool,505 label: PropTypes.string,506 className: CustomPropTypes.className,507 helpMessage: PropTypes.string,508 testcid: PropTypes.string,509 labelTooltip: PropTypes.string510};511 512export function InputSwitch({ cid, helpid, value, onChange, readonly, controlProps, ...props }) {513 const classes = useStyles();514 return (515 <Switch color="primary"516 checked={Boolean(value)}517 onChange={518 readonly ? () => {/*This is intentional (SonarQube)*/ } : onChange519 }520 id={cid}521 inputProps={{522 'aria-describedby': helpid,523 }}524 {...controlProps}525 {...props}526 className={(readonly || props.disabled) ? classes.readOnlySwitch : null}527 />528 );529}530InputSwitch.propTypes = {531 cid: PropTypes.string,532 helpid: PropTypes.string,533 value: PropTypes.any,534 onChange: PropTypes.func,535 readonly: PropTypes.bool,536 disabled: PropTypes.bool,537 controlProps: PropTypes.object,538};539 540export function FormInputSwitch({ hasError, required, label, className, helpMessage, testcid, withContainer, controlGridBasis, labelTooltip, ...props }) {541 return (542 <FormInput required={required} label={label} error={hasError} className={className}543 helpMessage={helpMessage} testcid={testcid} withContainer={withContainer} controlGridBasis={controlGridBasis} labelTooltip={labelTooltip}>544 <InputSwitch {...props} />545 </FormInput>546 );547}548FormInputSwitch.propTypes = {549 hasError: PropTypes.bool,550 required: PropTypes.bool,551 label: PropTypes.string,552 className: CustomPropTypes.className,553 helpMessage: PropTypes.string,554 testcid: PropTypes.string,555 withContainer: PropTypes.bool,556 controlGridBasis: PropTypes.number,557 labelTooltip: PropTypes.string558};559 560export function InputCheckbox({ cid, helpid, value, onChange, controlProps, readonly, labelPlacement, ...props }) {561 controlProps = controlProps || {};562 return (563 <FormControlLabel564 control={565 <Checkbox566 id={cid}567 checked={Boolean(value)}568 onChange={readonly ? () => {/*This is intentional (SonarQube)*/ } : onChange}569 color="primary"570 inputProps={{ 'aria-describedby': helpid, 'title': controlProps.label}}571 {...props} />572 }573 label={controlProps.label}574 labelPlacement={labelPlacement}575 />576 );577}578InputCheckbox.propTypes = {579 cid: PropTypes.string,580 helpid: PropTypes.string,581 value: PropTypes.bool,582 controlProps: PropTypes.object,583 onChange: PropTypes.func,584 readonly: PropTypes.bool,585 labelPlacement: PropTypes.string586};587 588export function FormInputCheckbox({ hasError, required, label,589 className, helpMessage, testcid, labelTooltip, ...props }) {590 591 return (592 <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} labelTooltip={labelTooltip}>593 <InputCheckbox {...props} />594 </FormInput>595 );596}597FormInputCheckbox.propTypes = {598 hasError: PropTypes.bool,599 required: PropTypes.bool,600 label: PropTypes.string,601 className: CustomPropTypes.className,602 helpMessage: PropTypes.string,603 testcid: PropTypes.string,604 labelTooltip: PropTypes.string605};606 607export function InputRadio({ helpid, value, onChange, controlProps, readonly, labelPlacement, ...props }) {608 const classes = useStyles();609 controlProps = controlProps || {};610 return (611 <FormControlLabel612 control={613 <Radio614 color="primary"615 checked={props?.disabled ? false : value }616 onChange={617 readonly ? () => {618 /*This is intentional (SonarQube)*/ } : onChange619 }620 value={value}621 name="radio-button-demo"622 inputProps={{ 'aria-label': value, 'aria-describedby': helpid }}623 style={{ padding: 0 }}624 disableRipple625 {...props}626 />627 628 }629 label={controlProps.label}630 labelPlacement={labelPlacement}631 className={(readonly || props.disabled) ? classes.readOnlySwitch : null}632 />633 );634}635InputRadio.propTypes = {636 helpid: PropTypes.string,637 value: PropTypes.bool,638 controlProps: PropTypes.object,639 onChange: PropTypes.func,640 readonly: PropTypes.bool,641 disabled: PropTypes.bool,642 labelPlacement: PropTypes.string643};644 645export const InputToggle = forwardRef(({ cid, value, onChange, options, disabled, readonly, helpid, ...props }, ref) => {646 return (647 <>648 <ToggleButtonGroup649 value={value}650 exclusive651 onChange={(e, val) => { val !== null && onChange(val); }}652 {...props}653 >654 {655 (options || []).map((option, i) => {656 const isSelected = option.value === value;657 const isDisabled = disabled || option.disabled || (readonly && !isSelected);658 return (659 <ToggleButton ref={i == 0 ? ref : null} key={option.label} value={option.value} component={isSelected ? PrimaryButton : DefaultButton}660 disabled={isDisabled} aria-label={option.label}>661 <CheckRoundedIcon style={{ visibility: isSelected ? 'visible' : 'hidden' }} /> {option.label}662 </ToggleButton>663 );664 })665 }666 </ToggleButtonGroup>667 {helpid && <input style={{display: 'none'}} defaultValue={options?.find((o)=>o.value==value)?.label} id={cid} aria-describedby={helpid} />}668 </>669 );670});671InputToggle.displayName = 'InputToggle';672InputToggle.propTypes = {673 cid: PropTypes.string,674 helpid: PropTypes.string,675 value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),676 options: PropTypes.array,677 controlProps: PropTypes.object,678 onChange: PropTypes.func,679 disabled: PropTypes.bool,680 readonly: PropTypes.bool,681};682 683export function FormInputToggle({ hasError, required, label,684 className, helpMessage, testcid, inputRef, labelTooltip, ...props }) {685 return (686 <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} labelTooltip={labelTooltip}>687 <InputToggle ref={inputRef} {...props} />688 </FormInput>689 );690}691FormInputToggle.propTypes = {692 hasError: PropTypes.bool,693 required: PropTypes.bool,694 label: PropTypes.string,695 className: CustomPropTypes.className,696 helpMessage: PropTypes.string,697 testcid: PropTypes.string,698 inputRef: CustomPropTypes.ref,699 labelTooltip: PropTypes.string700};701 702/* react-select package is used for select input703 * Customizing the select styles to fit existing theme704 */705const customReactSelectStyles = (theme, readonly) => ({706 input: (provided) => {707 return { ...provided, padding: 0, margin: 0, color: 'inherit' };708 },709 singleValue: (provided) => {710 return {711 ...provided,712 color: 'inherit',713 };714 },715 control: (provided, state) => ({716 ...provided,717 minHeight: '0',718 backgroundColor: readonly ? theme.otherVars.inputDisabledBg : theme.palette.background.default,719 color: readonly ? theme.palette.text.muted : theme.palette.text.primary,720 borderColor: theme.otherVars.inputBorderColor,721 ...(state.isFocused ? {722 borderColor: theme.palette.primary.main,723 boxShadow: 'inset 0 0 0 1px ' + theme.palette.primary.main,724 '&:hover': {725 borderColor: theme.palette.primary.main,726 }727 } : {}),728 }),729 dropdownIndicator: (provided) => ({730 ...provided,731 padding: '0rem 0.25rem',732 }),733 indicatorsContainer: (provided) => ({734 ...provided,735 ...(readonly ? { display: 'none' } : {})736 }),737 clearIndicator: (provided) => ({738 ...provided,739 padding: '0rem 0.25rem',740 }),741 valueContainer: (provided) => ({742 ...provided,743 padding: theme.otherVars.reactSelect.padding,744 }),745 groupHeading: (provided) => ({746 ...provided,747 color: 'inherit',748 fontSize: '0.85em',749 fontWeight: 'bold',750 textTransform: 'none',751 }),752 menu: (provided) => ({753 ...provided,754 backgroundColor: theme.palette.background.default,755 color: theme.palette.text.primary,756 boxShadow: 'none',757 border: '1px solid ' + theme.otherVars.inputBorderColor,758 marginTop: '2px',759 }),760 menuPortal: (provided) => ({761 ...provided, zIndex: 9999,762 backgroundColor: 'inherit',763 color: 'inherit',764 }),765 option: (provided, state) => {766 let bgColor = 'inherit';767 if (state.isFocused) {768 bgColor = theme.palette.grey[400];769 } else if (state.isSelected) {770 bgColor = theme.palette.primary.light;771 }772 return {773 ...provided,774 padding: '0.5rem',775 color: 'inherit',776 backgroundColor: bgColor,777 };778 },779 multiValue: (provided) => ({780 ...provided,781 backgroundColor: theme.palette.grey[400],782 }),783 multiValueLabel: (provided) => ({784 ...provided,785 fontSize: '1em',786 zIndex: 99,787 color: theme.palette.text.primary788 }),789 multiValueRemove: (provided) => ({790 ...provided,791 '&:hover': {792 backgroundColor: 'unset',793 color: theme.palette.error.main,794 },795 ...(readonly ? { display: 'none' } : {})796 }),797});798 799function OptionView({ image, imageUrl, label }) {800 const classes = useStyles();801 return (802 <>803 {image && <span className={clsx(classes.optionIcon, image)}></span>}804 {imageUrl && <img style={{height: '20px', marginRight: '4px'}} src={imageUrl} />}805 <span>{label}</span>806 </>807 );808}809OptionView.propTypes = {810 image: PropTypes.string,811 imageUrl: PropTypes.string,812 label: PropTypes.string,813};814 815function CustomSelectInput(props) {816 const { maxLength } = props.selectProps;817 return (818 <RSComponents.Input {...props} maxLength={maxLength} autoComplete='off' autoCorrect='off' spellCheck='off' />819 );820}821CustomSelectInput.propTypes = {822 selectProps: PropTypes.object,823};824 825function CustomSelectOption(props) {826 return (827 <RSComponents.Option {...props}>828 <OptionView image={props.data.image} imageUrl={props.data.imageUrl} label={props.data.label} />829 </RSComponents.Option>830 );831}832CustomSelectOption.propTypes = {833 data: PropTypes.object,834};835 836function CustomSelectSingleValue(props) {837 return (838 <RSComponents.SingleValue {...props}>839 <OptionView image={props.data.image} imageUrl={props.data.imageUrl} label={props.data.label} />840 </RSComponents.SingleValue>841 );842}843CustomSelectSingleValue.propTypes = {844 data: PropTypes.object,845};846 847export function flattenSelectOptions(options) {848 return _.flatMap(options, (option) => {849 if (option.options) {850 return option.options;851 } else {852 return option;853 }854 });855}856 857function getRealValue(options, value, creatable, formatter) {858 let realValue = null;859 if(options?.length == 0 && !creatable) {860 return realValue;861 }862 if (_.isArray(value)) {863 realValue = [...value];864 /* If multi select options need to be in some format by UI, use formatter */865 if (formatter) {866 realValue = formatter.fromRaw(realValue, options);867 } else if (creatable) {868 realValue = realValue.map((val) => ({ label: val, value: val }));869 } else {870 realValue = realValue.map((val) => (_.find(options, (option) => _.isEqual(option.value, val))));871 }872 } else {873 let flatOptions = flattenSelectOptions(options);874 realValue = _.find(flatOptions, (option) => option.value == value) ||875 (creatable && !_.isUndefined(value) && !_.isNull(value) ? { label: value, value: value } : null);876 }877 return realValue;878}879export function InputSelectNonSearch({ options, ...props }) {880 return <MuiSelect native {...props} variant="outlined">881 {(options || []).map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}882 </MuiSelect>;883}884InputSelectNonSearch.propTypes = {885 options: PropTypes.arrayOf(PropTypes.shape({886 label: PropTypes.shape,887 value: PropTypes.any,888 })),889};890 891export const InputSelect = forwardRef(({892 cid, helpid, onChange, options, readonly = false, value, controlProps = {}, optionsLoaded, optionsReloadBasis, disabled, ...props }, ref) => {893 const [[finalOptions, isLoading], setFinalOptions] = useState([[], true]);894 const theme = useTheme();895 896 useWindowSize();897 898 /* React will always take options let as changed parameter. So,899 We cannot run the below effect with options dependency as it will keep on900 loading the options. optionsReloadBasis is helpful to avoid repeated901 options load. If optionsReloadBasis value changes, then options will be loaded again.902 */903 useEffect(() => {904 let optPromise = options, umounted = false;905 if (typeof options === 'function') {906 optPromise = options();907 }908 setFinalOptions([[], true]);909 Promise.resolve(optPromise)910 .then((res) => {911 /* If component unmounted, dont update state */912 if (!umounted) {913 optionsLoaded?.(res, value);914 /* Auto select if any option has key as selected */915 const flatRes = flattenSelectOptions(res || []);916 let selectedVal;917 if (controlProps.multiple) {918 selectedVal = _.filter(flatRes, (o) => o.selected)?.map((o) => o.value);919 } else {920 selectedVal = _.find(flatRes, (o) => o.selected)?.value;921 }922 923 if ((!_.isUndefined(selectedVal) && !_.isArray(selectedVal)) || (_.isArray(selectedVal) && selectedVal.length != 0)) {924 onChange?.(selectedVal);925 }926 setFinalOptions([res || [], false]);927 }928 });929 return () => umounted = true;930 }, [optionsReloadBasis]);931 932 933 /* Apply filter if any */934 const filteredOptions = (controlProps.filter?.(finalOptions)) || finalOptions;935 const flatFiltered = flattenSelectOptions(filteredOptions);936 let realValue = getRealValue(flatFiltered, value, controlProps.creatable, controlProps.formatter);937 if (realValue && _.isPlainObject(realValue) && _.isUndefined(realValue.value)) {938 console.error('Undefined option value not allowed', realValue, filteredOptions);939 }940 const otherProps = {941 isSearchable: !readonly,942 isClearable: !readonly && (!_.isUndefined(controlProps.allowClear) ? controlProps.allowClear : true),943 isDisabled: Boolean(disabled),944 };945 946 const styles = customReactSelectStyles(theme, readonly || disabled);947 948 const onChangeOption = useCallback((selectVal) => {949 if (_.isArray(selectVal)) {950 // Check if select all option is selected951 if (!_.isUndefined(selectVal.find(x => x.label === '<Select All>'))) {952 selectVal = filteredOptions;953 }954 /* If multi select options need to be in some format by UI, use formatter */955 if (controlProps.formatter) {956 selectVal = controlProps.formatter.toRaw(selectVal, filteredOptions);957 } else {958 selectVal = selectVal.map((option) => option.value);959 }960 onChange?.(selectVal);961 } else {962 onChange?.(selectVal ? selectVal.value : null);963 }964 }, [onChange, filteredOptions]);965 966 const commonProps = {967 components: {968 Option: CustomSelectOption,969 SingleValue: CustomSelectSingleValue,970 IndicatorSeparator: controlProps.noDropdown ? null: RSComponents.IndicatorSeparator,971 DropdownIndicator: controlProps.noDropdown ? null: RSComponents.DropdownIndicator,972 Input: CustomSelectInput,973 },974 isMulti: Boolean(controlProps.multiple),975 openMenuOnClick: !readonly,976 onChange: onChangeOption,977 isLoading: isLoading,978 options: controlProps.allowSelectAll ? [{ label: gettext('<Select All>'), value: '*' }, ...filteredOptions] : filteredOptions,979 value: realValue,980 menuPortalTarget: document.body,981 styles: styles,982 inputId: cid,983 placeholder: (readonly || disabled) ? '' : controlProps.placeholder || gettext('Select an item...'),984 maxLength: controlProps.maxLength,985 ...otherProps,986 ...props,987 };988 const selectValue = useMemo(()=>{989 if(_.isArray(realValue)) {990 return realValue.map((o)=>o?.label)?.join(',');991 }992 return realValue?.label;993 }, [realValue]);994 if (!controlProps.creatable) {995 return (996 <>997 <Select ref={ref} {...commonProps} />998 {helpid && <input data-testid="select-value" style={{display: 'none'}} defaultValue={selectValue} id={cid} aria-describedby={helpid} />}999 </>1000 );1001 } else {1002 return (1003 <>1004 <CreatableSelect1005 ref={ref}1006 {...commonProps}1007 noOptionsMessage={() =>1008 !controlProps.noDropdown ? 'No options' : null1009 }1010 />1011 {helpid && <input data-testid="select-value" style={{display: 'none'}} defaultValue={selectValue} id={cid} aria-describedby={helpid} />}1012 </>1013 );1014 }1015});1016InputSelect.displayName = 'InputSelect';1017InputSelect.propTypes = {1018 cid: PropTypes.string,1019 helpid: PropTypes.string,1020 value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.bool]),1021 options: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]),1022 controlProps: PropTypes.object,1023 optionsLoaded: PropTypes.func,1024 optionsReloadBasis: PropTypes.any,1025 onChange: PropTypes.func,1026 disabled: PropTypes.bool,1027 readonly: PropTypes.bool,1028};1029 1030 1031export function FormInputSelect({1032 hasError, required, className, label, helpMessage, testcid, labelTooltip, ...props }) {1033 return (1034 <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} labelTooltip={labelTooltip}>1035 <InputSelect ref={props.inputRef} {...props} />1036 </FormInput>1037 );1038}1039FormInputSelect.propTypes = {1040 hasError: PropTypes.bool,1041 required: PropTypes.bool,1042 label: PropTypes.string,1043 className: CustomPropTypes.className,1044 helpMessage: PropTypes.string,1045 testcid: PropTypes.string,1046 inputRef: CustomPropTypes.ref,1047 labelTooltip: PropTypes.string1048};1049 1050const ColorButton = withColorPicker(PgIconButton);1051export function InputColor({ value, controlProps, disabled, onChange, currObj }) {1052 const classes = useStyles();1053 1054 let btnStyles = { backgroundColor: value };1055 return (1056 <ColorButton title={gettext('Select the color')} className={classes.colorBtn} style={btnStyles} disabled={disabled}1057 icon={(_.isUndefined(value) || _.isNull(value) || value === '') && <CloseIcon data-label="CloseIcon" />} options={{1058 ...controlProps,1059 disabled: disabled1060 }} onChange={onChange} value={value} currObj={currObj}1061 />1062 );1063}1064InputColor.propTypes = {1065 value: PropTypes.string,1066 controlProps: PropTypes.object,1067 onChange: PropTypes.func,1068 disabled: PropTypes.bool,1069 currObj: PropTypes.func,1070};1071 1072export function FormInputColor({1073 hasError, required, className, label, helpMessage, testcid, labelTooltip, ...props }) {1074 1075 return (1076 <FormInput required={required} label={label} error={hasError} className={className} helpMessage={helpMessage} testcid={testcid} labelTooltip={labelTooltip}>1077 <InputColor {...props} />1078 </FormInput>1079 );1080}1081FormInputColor.propTypes = {1082 hasError: PropTypes.bool,1083 required: PropTypes.bool,1084 className: CustomPropTypes.className,1085 label: PropTypes.string,1086 helpMessage: PropTypes.string,1087 testcid: PropTypes.string,1088 labelTooltip: PropTypes.string1089};1090 1091export function PlainString({ controlProps, value }) {1092 let finalValue = value;1093 if (controlProps?.formatter) {1094 finalValue = controlProps.formatter.fromRaw(finalValue);1095 }1096 return <span>{finalValue}</span>;1097}1098PlainString.propTypes = {1099 controlProps: PropTypes.object,1100 value: PropTypes.any,1101};1102 1103export function FormNote({ text, className, controlProps }) {1104 const classes = useStyles();1105 /* If raw, then remove the styles and icon */1106 return (1107 <Box className={className}>1108 <Paper elevation={0} className={controlProps?.raw ? '' : classes.noteRoot}>1109 {!controlProps?.raw && <Box paddingRight="0.25rem"><DescriptionIcon fontSize="small" /></Box>}1110 <Box>{HTMLReactParse(text || '')}</Box>1111 </Paper>1112 </Box>1113 );1114}1115FormNote.propTypes = {1116 text: PropTypes.string,1117 className: CustomPropTypes.className,1118 controlProps: PropTypes.object,1119};1120 1121const useStylesFormFooter = makeStyles((theme) => ({1122 root: {1123 padding: theme.spacing(0.5),1124 position: 'absolute',1125 bottom: 0,1126 left: 0,1127 right: 0,1128 zIndex: 10,1129 },1130 container: {1131 borderWidth: '1px',1132 borderStyle: 'solid',1133 borderRadius: theme.shape.borderRadius,1134 padding: theme.spacing(0.5),1135 display: 'flex',1136 alignItems: 'center',1137 minHeight: '36px',1138 },1139 containerSuccess: {1140 borderColor: theme.palette.success.main,1141 backgroundColor: theme.palette.success.light,1142 },1143 iconSuccess: {1144 color: theme.palette.success.main,1145 },1146 containerError: {1147 borderColor: theme.palette.error.main,1148 backgroundColor: theme.palette.error.light,1149 },1150 iconError: {1151 color: theme.palette.error.main,1152 },1153 containerInfo: {1154 borderColor: theme.palette.primary.main,1155 backgroundColor: theme.palette.primary.light,1156 },1157 iconInfo: {1158 color: theme.palette.primary.main,1159 },1160 containerWarning: {1161 borderColor: theme.palette.warning.main,1162 backgroundColor: theme.palette.warning.light,1163 },1164 iconWarning: {1165 color: theme.palette.warning.main,1166 },1167 message: {1168 color: theme.palette.text.primary,1169 marginLeft: theme.spacing(0.5),1170 },1171 messageCenter: {1172 color: theme.palette.text.primary,1173 margin: 'auto',1174 },1175 closeButton: {1176 marginLeft: 'auto',1177 },1178}));1179 1180/* The form footer used mostly for showing error */1181export function FormFooterMessage({style, ...props}) {1182 const classes = useStylesFormFooter();1183 1184 if (!props.message) {1185 return <></>;1186 }1187 return (1188 <Box className={classes.root} style={style}>1189 <NotifierMessage {...props}></NotifierMessage>1190 </Box>1191 );1192}1193 1194FormFooterMessage.propTypes = {1195 style: PropTypes.object,1196 message: PropTypes.string,1197};1198 1199const useStylesKeyboardShortcut = makeStyles(() => ({1200 customRow: {