Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
FormComponents.jsx1337 linesDownload Raw Back to components
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' }} />&nbsp;{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: {

Showing the first 1,200 of 1337 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai