use-debounce#useDebouncedCallback TypeScript Examples

The following examples show how to use use-debounce#useDebouncedCallback. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: ActionRoleList.tsx    From amplication with Apache License 2.0 6 votes vote down vote up
ActionRoleList = ({
  availableRoles,
  selectedRoleIds,
  debounceMS,
  onChange,
}: Props) => {
  const [selectedRoleList, setSelectedRoleList] = useState<Set<string>>(
    selectedRoleIds
  );

  // onChange is wrapped with a useDebouncedCallback so it won't be called more often than defined in debounceMs
  // This function is called by handleRoleSelect as it can not manage dependencies
  const [debouncedOnChange] = useDebouncedCallback((value: Set<string>) => {
    onChange(value);
  }, debounceMS);

  const handleRoleSelect = useCallback(
    ({ id }: models.AppRole, checked: boolean) => {
      const newSet = new Set(selectedRoleList);
      if (checked) {
        newSet.add(id);
      } else {
        newSet.delete(id);
      }
      setSelectedRoleList(newSet);
      debouncedOnChange(newSet);
    },
    [setSelectedRoleList, selectedRoleList, debouncedOnChange]
  );

  return availableRoles.map((role) => (
    <ActionRole
      key={role.id}
      role={role}
      onClick={handleRoleSelect}
      selected={selectedRoleList.has(role.id)}
    />
  ));
}
Example #2
Source File: NameField.tsx    From amplication with Apache License 2.0 6 votes vote down vote up
NameField = ({ capitalized, ...rest }: Props) => {
  const [regexp, pattern, helpText] = capitalized
    ? [CAPITALIZED_NAME_REGEX, CAPITALIZED_NAME_PATTERN, CAPITALIZED_HELP_TEXT]
    : [NAME_REGEX, NAME_PATTERN, HELP_TEXT];
  // @ts-ignore
  const [field, meta] = useField<string>({
    ...rest,
    validate: (value) => (value.match(regexp) ? undefined : helpText),
  });
  const [showMessage, setShowMessage] = useState<boolean>(false);

  const [debouncedHideMessage] = useDebouncedCallback(() => {
    setShowMessage(false);
  }, SHOW_MESSAGE_DURATION);

  useEffect(() => {
    if (meta.error) {
      setShowMessage(true);
    } else {
      debouncedHideMessage();
    }
  }, [meta.error, setShowMessage, debouncedHideMessage]);

  return (
    <div className={CLASS_NAME}>
      <TextInput
        {...field}
        {...rest}
        label="Name"
        autoComplete="off"
        minLength={1}
        pattern={pattern}
      />
      {showMessage && (
        <div className={`${CLASS_NAME}__tooltip`}>{helpText}</div>
      )}
    </div>
  );
}
Example #3
Source File: useAccountsData.ts    From sail with Apache License 2.0 5 votes vote down vote up
useAccountsData = (
  keys: (PublicKey | null | undefined)[]
): readonly AccountDatum[] => {
  const { getDatum, onBatchCache, fetchKeys, onError } = useSail();

  const [data, setData] = useState<{ [cacheKey: string]: AccountDatum }>(() =>
    loadKeysFromCache(getDatum, keys)
  );

  // TODO: add cancellation
  const fetchAndSetKeys = useDebouncedCallback(
    async (
      fetchKeys: FetchKeysFn,
      keys: readonly (PublicKey | null | undefined)[]
    ) => {
      const keysData = await fetchKeysMaybe(fetchKeys, keys);

      const nextData: Record<string, AccountDatum> = {};
      keys.forEach((key, keyIndex) => {
        if (key) {
          const keyData = keysData[keyIndex];
          if (keyData) {
            nextData[getCacheKeyOfPublicKey(key)] = keyData.data;
          } else {
            nextData[getCacheKeyOfPublicKey(key)] = keyData;
          }
        }
      });
      startTransition(() => {
        setData(nextData);
      });
    },
    100
  );

  useEffect(() => {
    void (async () => {
      await fetchAndSetKeys(fetchKeys, keys)?.catch((e) => {
        onError(new SailCacheRefetchError(e, keys));
      });
    })();
  }, [keys, fetchAndSetKeys, fetchKeys, onError]);

  useAccountsSubscribe(keys);

  // refresh from the cache whenever the cache is updated
  useEffect(() => {
    return onBatchCache((e) => {
      if (keys.find((key) => key && e.hasKey(key))) {
        void fetchAndSetKeys(fetchKeys, keys)?.catch((e) => {
          onError(new SailCacheRefetchError(e, keys));
        });
      }
    });
  }, [keys, fetchAndSetKeys, fetchKeys, onError, onBatchCache]);

  // unload debounces when the component dismounts
  useEffect(() => {
    return () => {
      fetchAndSetKeys.cancel();
    };
  }, [fetchAndSetKeys]);

  return useMemo(() => {
    return keys.map((key) => {
      if (key) {
        return data[getCacheKeyOfPublicKey(key)];
      }

      return key;
    });
  }, [data, keys]);
}
Example #4
Source File: converter.tsx    From genql with MIT License 5 votes vote down vote up
function Page({}) {
    const [code, setCode] = useState(DEFAULT_QUERY)
    const [onCodeChange] = useDebouncedCallback(() => {
        setInvalid('')
        try {
            const query = gql(code)
            console.log('parsed')
            setGenqlTranslation('\n' + print(query, {}))
        } catch (e) {
            console.error(e)
            setInvalid(e.message)
        }
    }, 400)
    useEffect(() => {
        onCodeChange()
        return
    }, [code])
    const [genqlTranslation, setGenqlTranslation] = useState('')
    const [invalid, setInvalid] = useState('')
    return (
        <Stack align='stretch'>
            <SectionTitle
                dark
                heading='Convert graphql to genql'
                subheading='Easily migrate from graphql strings to type safe genql queries'
                mb='40px'
            />
            <Stack p='10' align='stretch'>
                <Stack
                    spacing='20'
                    justify='stretch'
                    width='100%'
                    align={['center', null, null, 'flex-start']}
                    direction={['column', null, null, 'row']}
                >
                    <Stack minWidth='0' align='stretch' flex='1'>
                        {invalid && (
                            <Stack
                                as='pre'
                                px='20px'
                                zIndex={1000}
                                mb='-40px'
                                color='red.500'
                            >
                                {invalid}
                            </Stack>
                        )}
                        <Code value={code} onChange={setCode} />
                    </Stack>
                    <Code
                        hideLinesNumbers
                        flex='1'
                        readOnly
                        value={genqlTranslation}
                    />
                </Stack>
            </Stack>
        </Stack>
    )
}
Example #5
Source File: TableCell.tsx    From firetable with Apache License 2.0 5 votes vote down vote up
export default function Date_({
  rowIdx,
  column,
  value,
  onSubmit,
  disabled,
}: IHeavyCellProps) {
  const classes = useStyles();
  const { dataGridRef } = useFiretableContext();

  const transformedValue = transformValue(value);

  const [handleDateChange] = useDebouncedCallback<DatePickerProps["onChange"]>(
    (date) => {
      const sanitized = sanitizeValue(date);
      if (sanitized === undefined) return;

      onSubmit(sanitized);
      if (dataGridRef?.current?.selectCell)
        dataGridRef.current.selectCell({ rowIdx, idx: column.idx });
    },
    500
  );

  if (disabled)
    return (
      <div className={classes.disabledCell}>
        <BasicCell
          value={value}
          type={(column as any).type}
          name={column.key}
        />
      </div>
    );

  return (
    <MuiPickersUtilsProvider utils={DateFnsUtils}>
      <KeyboardDatePicker
        value={transformedValue}
        onChange={handleDateChange}
        onClick={(e) => e.stopPropagation()}
        format={column.config?.format ?? DATE_FORMAT}
        fullWidth
        clearable
        keyboardIcon={<DateIcon />}
        className={clsx("cell-collapse-padding", classes.root)}
        inputVariant="standard"
        InputProps={{
          disableUnderline: true,
          classes: { root: classes.inputBase, input: classes.input },
        }}
        InputAdornmentProps={{
          position: "start",
          classes: { root: classes.inputAdornment },
        }}
        KeyboardButtonProps={{
          size: "small",
          classes: { root: !disabled ? "row-hover-iconButton" : undefined },
        }}
        DialogProps={{ onClick: (e) => e.stopPropagation() }}
        disabled={disabled}
      />
    </MuiPickersUtilsProvider>
  );
}
Example #6
Source File: TableCell.tsx    From firetable with Apache License 2.0 5 votes vote down vote up
export default function DateTime({
  rowIdx,
  column,
  value,
  onSubmit,
  disabled,
}: IHeavyCellProps) {
  const classes = useStyles();
  const { dataGridRef } = useFiretableContext();

  const transformedValue = transformValue(value);

  const [handleDateChange] = useDebouncedCallback<DatePickerProps["onChange"]>(
    (date) => {
      const sanitized = sanitizeValue(date);
      if (sanitized === undefined) return;

      onSubmit(sanitized);
      if (dataGridRef?.current?.selectCell)
        dataGridRef.current.selectCell({ rowIdx, idx: column.idx });
    },
    500
  );

  if (disabled)
    return (
      <div className={classes.disabledCell}>
        <BasicCell
          value={value}
          type={(column as any).type}
          name={column.key}
        />
      </div>
    );

  return (
    <MuiPickersUtilsProvider utils={DateFnsUtils}>
      <KeyboardDateTimePicker
        value={transformedValue}
        onChange={handleDateChange}
        onClick={(e) => e.stopPropagation()}
        format={DATE_TIME_FORMAT}
        fullWidth
        clearable
        keyboardIcon={<DateTimeIcon />}
        className={clsx("cell-collapse-padding", classes.root)}
        inputVariant="standard"
        InputProps={{
          disableUnderline: true,
          classes: { root: classes.inputBase, input: classes.input },
        }}
        InputAdornmentProps={{
          position: "start",
          classes: { root: classes.inputAdornment },
        }}
        KeyboardButtonProps={{
          size: "small",
          classes: { root: "row-hover-iconButton" },
        }}
        DialogProps={{ onClick: (e) => e.stopPropagation() }}
        dateRangeIcon={<DateRangeIcon className={classes.dateTabIcon} />}
        timeIcon={<TimeIcon className={classes.dateTabIcon} />}
      />
    </MuiPickersUtilsProvider>
  );
}
Example #7
Source File: useFormButton.tsx    From SQForm with MIT License 5 votes vote down vote up
export function useFormButton<Values>({
  isDisabled = false,
  shouldRequireFieldUpdates = false,
  onClick,
  buttonType,
}: UseFormButtonProps): UseFormButtonReturnType<Values> {
  const {values, initialValues, isValid, dirty, ...rest} =
    useFormikContext<Values>();

  const isButtonDisabled = React.useMemo(() => {
    if (isDisabled) {
      return true;
    }

    if (buttonType === BUTTON_TYPES.SUBMIT) {
      if (!isValid) {
        return true;
      }

      if (shouldRequireFieldUpdates && !dirty) {
        return true;
      }
    }

    if (buttonType === BUTTON_TYPES.RESET) {
      if (!dirty) {
        return true;
      }
    }

    return false;
  }, [buttonType, dirty, isDisabled, isValid, shouldRequireFieldUpdates]);

  const handleClick = useDebouncedCallback<
    React.MouseEventHandler<HTMLButtonElement>
  >((event) => onClick && onClick(event), 500, {
    leading: true,
    trailing: false,
  });

  return {
    isButtonDisabled,
    values,
    initialValues,
    isValid,
    handleClick,
    dirty,
    ...rest,
  };
}
Example #8
Source File: SQForm.tsx    From SQForm with MIT License 5 votes vote down vote up
function SQForm<Values extends FormikValues>({
  children,
  enableReinitialize = false,
  initialValues,
  muiGridProps = {},
  onSubmit,
  validationSchema,
}: SQFormProps<Values>): JSX.Element {
  const initialErrors = useInitialRequiredErrors(
    validationSchema,
    initialValues
  );

  // HACK: This is a workaround for: https://github.com/mui-org/material-ui-pickers/issues/2112
  // Remove this reset handler when the issue is fixed.
  const handleReset = () => {
    document &&
      document.activeElement &&
      (document.activeElement as HTMLElement).blur();
  };

  const handleSubmit = useDebouncedCallback(
    (values: Values, formikHelpers: FormikHelpers<Values>) =>
      onSubmit(values, formikHelpers),
    500,
    {leading: true, trailing: false}
  );

  return (
    <Formik<Values>
      enableReinitialize={enableReinitialize}
      initialErrors={initialErrors}
      initialValues={initialValues}
      onSubmit={handleSubmit}
      onReset={handleReset}
      validationSchema={validationSchema}
      validateOnMount={true}
    >
      {(_props) => {
        return (
          <Form>
            <Grid
              {...muiGridProps}
              container
              spacing={muiGridProps.spacing ?? 2}
            >
              {children}
            </Grid>
          </Form>
        );
      }}
    </Formik>
  );
}
Example #9
Source File: EventPropertiesStats.tsx    From posthog-foss with MIT License 4 votes vote down vote up
export function EventPropertiesStats(): JSX.Element {
    const { eventPropertiesDefinitions, eventsSnippet, eventPropertiesDefinitionTags, tagLoading } =
        useValues(definitionDrawerLogic)
    const { setNewEventPropertyTag, deleteEventPropertyTag, setEventPropertyDescription, saveAll } =
        useActions(definitionDrawerLogic)
    const propertyExamples = eventsSnippet[0]?.properties
    const tableColumns = [
        {
            title: 'Property',
            key: 'property',
            render: function renderProperty({ name }: PropertyDefinition) {
                return <span className="text-default">{name}</span>
            },
        },
        {
            title: 'Description',
            key: 'description',
            render: function renderDescription({ description, id }: PropertyDefinition) {
                const [newDescription, setNewDescription] = useState(description)
                const debouncePropertyDescription = useDebouncedCallback((value) => {
                    setEventPropertyDescription(value, id)
                }, 200)

                return (
                    <Input.TextArea
                        placeholder="Add description"
                        value={newDescription || ''}
                        onChange={(e) => {
                            setNewDescription(e.target.value)
                            debouncePropertyDescription(e.target.value)
                        }}
                        onKeyDown={(e) => {
                            if (e.key === 'Enter') {
                                saveAll()
                            }
                        }}
                    />
                )
            },
        },
        {
            title: 'Tags',
            key: 'tags',
            render: function renderTags({ id, tags }: PropertyDefinition) {
                return (
                    <ObjectTags
                        id={id}
                        tags={tags || []}
                        onTagSave={(tag, currentTags, propertyId) =>
                            setNewEventPropertyTag(tag, currentTags, propertyId)
                        }
                        onTagDelete={(tag, currentTags, propertyId) =>
                            deleteEventPropertyTag(tag, currentTags, propertyId)
                        }
                        saving={tagLoading}
                        tagsAvailable={eventPropertiesDefinitionTags?.filter((tag) => !tags?.includes(tag))}
                    />
                )
            },
        },
        {
            title: 'Example',
            key: 'example',
            render: function renderExample({ name }: PropertyDefinition) {
                let example = propertyExamples[name]
                // Just show a json stringify if it's an array or object
                example =
                    Array.isArray(example) || example instanceof Object
                        ? JSON.stringify(example).substr(0, 140)
                        : example
                return (
                    <div style={{ backgroundColor: '#F0F0F0', padding: '4px, 15px', textAlign: 'center' }}>
                        <span style={{ fontSize: 10, fontWeight: 400, fontFamily: 'monaco' }}>{example}</span>
                    </div>
                )
            },
        },
    ]

    return (
        <>
            <Row style={{ paddingBottom: 16 }}>
                <span className="text-default text-muted">
                    Top properties that are sent with this event. Please note that description and tags are shared
                    across events. Posthog properties are <b>excluded</b> from this list.
                </span>
            </Row>
            <Table
                dataSource={eventPropertiesDefinitions}
                columns={tableColumns}
                rowKey={(row) => row.id}
                size="small"
                tableLayout="fixed"
                pagination={{ pageSize: 5, hideOnSinglePage: true }}
            />
        </>
    )
}
Example #10
Source File: PDFViewerPage.tsx    From react-view-pdf with MIT License 4 votes vote down vote up
PDFViewerPageInner: React.FC<PDFViewerPageProps> = props => {
  const { document, pageNumber, scale, onPageVisibilityChanged, onPageLoaded, loaded } = props;
  const [page, setPage] = React.useState<PdfJs.Page>();
  const [isCalculated, setIsCalculated] = React.useState(false);
  const canvasRef = React.createRef<HTMLCanvasElement>();
  const renderTask = React.useRef<PdfJs.PageRenderTask>();

  const debouncedLoad = useDebouncedCallback(() => loadPage(), 100, { leading: true });
  const debouncedRender = useDebouncedCallback(() => renderPage(), 100, { leading: true });

  const intersectionThreshold = [...Array(10)].map((_, i) => i / 10);

  React.useEffect(() => {
    debouncedRender();
  }, [page, scale]);

  function loadPage() {
    if (document && !page && !isCalculated) {
      setIsCalculated(true);
      document.getPage(pageNumber).then(page => {
        const viewport = page.getViewport({ scale: 1 });
        onPageLoaded(pageNumber, viewport.width, viewport.height);
        setPage(page);
      });
    }
  }

  function renderPage() {
    if (page) {
      const task = renderTask.current;

      if (task) {
        task.cancel();
      }

      const canvasEle = canvasRef.current as HTMLCanvasElement;
      if (!canvasEle) {
        return;
      }
      const viewport = page.getViewport({ scale });
      canvasEle.height = viewport.height;
      canvasEle.width = viewport.width;

      const canvasContext = canvasEle.getContext('2d', { alpha: false }) as CanvasRenderingContext2D;

      renderTask.current = page.render({
        canvasContext,
        viewport,
      });
      renderTask.current.promise.then(
        // eslint-disable-next-line @typescript-eslint/no-empty-function
        () => {},
        // eslint-disable-next-line @typescript-eslint/no-empty-function
        () => {},
      );
    }
  }

  function visibilityChanged(params: VisibilityChanged) {
    const ratio = params.isVisible ? params.ratio : 0;
    const changed = onPageVisibilityChanged(pageNumber, ratio);
    if (params.isVisible && changed) {
      debouncedLoad();
    }
  }

  return (
    <Page
      disableSelect={props.disableSelect}
      style={{
        width: !loaded && '70%',
        height: !loaded && '1200px',
        padding: !loaded && distance.large,
      }}>
      <Observer onVisibilityChanged={visibilityChanged} threshold={intersectionThreshold}>
        {!loaded && (
          <>
            <Skeleton width="30%" height="3em" />
            <br />
            <br />
            {range(5).map(index => (
              <React.Fragment key={index}>
                <Skeleton width="80%" height="1em" />
                <br />
                <Skeleton width="70%" height="1em" />
                <br />
                <Skeleton width="85%" height="1em" />
                <br />
                <Skeleton width="60%" height="1em" />
                <br />
                <Skeleton width="80%" height="1em" />
                <br />
                <Skeleton width="25%" height="1em" />
                <br />
                <Skeleton width="60%" height="1em" />
                <br />
                <Skeleton width="38%" height="1em" />
                <br />
                <Skeleton width="50%" height="1em" />
                <br />
              </React.Fragment>
            ))}
          </>
        )}
        <canvas ref={canvasRef} />
      </Observer>
    </Page>
  );
}
Example #11
Source File: SQFormScrollableCard.tsx    From SQForm with MIT License 4 votes vote down vote up
function SQFormScrollableCard<Values>({
  cardContentStyles = {},
  children,
  enableReinitialize = false,
  height,
  helperErrorText,
  helperFailText,
  helperValidText,
  initialValues,
  isDisabled = false,
  isFailedState = false,
  isSelfBounding,
  muiGridProps = {},
  onSubmit,
  resetButtonText = 'Reset',
  shouldRenderHelperText = true,
  shouldRequireFieldUpdates = false,
  submitButtonText = 'Submit',
  SubHeaderComponent,
  title,
  validationSchema,
  isHeaderDisabled = false,
  isSquareCorners = true,
}: SQFormScrollableCardProps<Values>): React.ReactElement {
  const hasSubHeader = Boolean(SubHeaderComponent);

  const initialErrors = useInitialRequiredErrors(
    validationSchema,
    initialValues
  );

  const classes = useStyles({hasSubHeader, cardContentStyles});

  const handleSubmit = useDebouncedCallback(
    (formValues: Values, formikBag: FormikHelpers<Values>) =>
      onSubmit(formValues, formikBag),
    500,
    {leading: true, trailing: false}
  );

  const cardID = React.useMemo(() => {
    if (title) {
      return title.replace(/\s/g, '-');
    } else {
      // Ensures IDs are present if no title is given and are random
      // incase multiple SQFormScrollableCards exist in the dom
      // Statistically unique for the lifetime of the components
      return (Date.now() * Math.random()).toString();
    }
  }, [title]);

  const [calculatedHeight, setCalculatedHeight] =
    React.useState<React.CSSProperties['height']>(0);

  React.useEffect(() => {
    const currentElement = document.getElementById(
      `sqform-scrollable-card-id-${cardID}`
    );

    if (!currentElement?.parentElement) {
      return;
    }

    const topOffset = currentElement?.getBoundingClientRect().top;
    const offsetBasedHeight = `calc(100vh - ${topOffset}px - 24px)`;

    const parentHeight = currentElement.parentElement.clientHeight;
    const parentTopOffset =
      currentElement.parentElement.getBoundingClientRect().top;
    const topDifferential = topOffset - parentTopOffset;
    const maxOffsetBasedHeight = `calc(${parentHeight}px - ${topDifferential}px)`;

    const calculatedHeight = `min(${offsetBasedHeight}, ${maxOffsetBasedHeight})`;

    setCalculatedHeight(calculatedHeight);
  }, [cardID]);

  const heightToUse =
    height || (isSelfBounding && calculatedHeight ? calculatedHeight : '100%');

  return (
    <div
      id={`sqform-scrollable-card-id-${cardID}`}
      style={{height: heightToUse}}
    >
      <Formik
        enableReinitialize={enableReinitialize}
        initialErrors={initialErrors}
        initialValues={initialValues}
        onSubmit={handleSubmit}
        validationSchema={validationSchema}
        validateOnMount={true}
      >
        {(_props) => {
          return (
            <Form className={classes.form}>
              <Card
                raised={true}
                elevation={1}
                square={isSquareCorners}
                className={classes.card}
              >
                {!isHeaderDisabled && (
                  <CardHeader
                    title={title}
                    className={classes.cardHeader}
                    titleTypographyProps={{variant: 'h5'}}
                  />
                )}

                <CardContent className={classes.cardContent}>
                  {SubHeaderComponent}
                  <Grid
                    {...muiGridProps}
                    container
                    spacing={muiGridProps.spacing ?? 2}
                    className={classes.childrenContainer}
                  >
                    {children}
                  </Grid>
                </CardContent>
                <CardActions className={classes.cardFooter}>
                  <SQFormButton type="reset" title="Reset Form">
                    {resetButtonText}
                  </SQFormButton>
                  {shouldRenderHelperText && (
                    <SQFormHelperText
                      isFailedState={isFailedState}
                      errorText={helperErrorText}
                      failText={helperFailText}
                      validText={helperValidText}
                    />
                  )}
                  <SQFormButton
                    isDisabled={isDisabled}
                    shouldRequireFieldUpdates={shouldRequireFieldUpdates}
                  >
                    {submitButtonText}
                  </SQFormButton>
                </CardActions>
              </Card>
            </Form>
          );
        }}
      </Formik>
    </div>
  );
}
Example #12
Source File: FunnelConversionWindowFilter.tsx    From posthog-foss with MIT License 4 votes vote down vote up
export function FunnelConversionWindowFilter({ horizontal }: { horizontal?: boolean }): JSX.Element {
    const { insightProps } = useValues(insightLogic)
    const { conversionWindow, aggregationTargetLabel, filters } = useValues(funnelLogic(insightProps))
    const { setFilters } = useActions(funnelLogic(insightProps))
    const [localConversionWindow, setLocalConversionWindow] = useState<FunnelConversionWindow>(conversionWindow)
    const timeUnitRef: React.RefObject<RefSelectProps> | null = useRef(null)

    const options = Object.keys(TIME_INTERVAL_BOUNDS).map((unit) => ({
        label: pluralize(conversionWindow.funnel_window_interval ?? 7, unit, `${unit}s`, false),
        value: unit,
    }))
    const intervalBounds =
        TIME_INTERVAL_BOUNDS[conversionWindow.funnel_window_interval_unit ?? FunnelConversionWindowTimeUnit.Day]

    const setConversionWindow = useDebouncedCallback((): void => {
        if (
            localConversionWindow.funnel_window_interval !== conversionWindow.funnel_window_interval ||
            localConversionWindow.funnel_window_interval_unit !== conversionWindow.funnel_window_interval_unit
        ) {
            setFilters(localConversionWindow)
        }
    }, 200)

    return (
        <div
            className={clsx('funnel-options-container', horizontal && 'flex-center')}
            style={horizontal ? { flexDirection: 'row' } : undefined}
        >
            <span className="funnel-options-label">
                Conversion window limit{' '}
                <Tooltip
                    title={
                        <>
                            <b>Recommended!</b> Limit to {aggregationTargetLabel.plural}{' '}
                            {filters.aggregation_group_type_index != undefined ? 'that' : 'who'} converted within a
                            specific time frame. {capitalizeFirstLetter(aggregationTargetLabel.plural)}{' '}
                            {filters.aggregation_group_type_index != undefined ? 'that' : 'who'} do not convert in this
                            time frame will be considered as drop-offs.
                        </>
                    }
                >
                    <InfoCircleOutlined className="info-indicator" />
                </Tooltip>
            </span>
            <Row className="funnel-options-inputs" style={horizontal ? { paddingLeft: 8 } : undefined}>
                <InputNumber
                    className="time-value-input"
                    min={intervalBounds[0]}
                    max={intervalBounds[1]}
                    defaultValue={conversionWindow.funnel_window_interval}
                    value={localConversionWindow.funnel_window_interval}
                    onChange={(funnel_window_interval) => {
                        setLocalConversionWindow((state) => ({
                            ...state,
                            funnel_window_interval: Number(funnel_window_interval),
                        }))
                        setConversionWindow()
                    }}
                    onBlur={setConversionWindow}
                    onPressEnter={setConversionWindow}
                />
                <Select
                    ref={timeUnitRef}
                    className="time-unit-input"
                    defaultValue={conversionWindow.funnel_window_interval_unit}
                    dropdownMatchSelectWidth={false}
                    value={localConversionWindow.funnel_window_interval_unit}
                    onChange={(funnel_window_interval_unit: FunnelConversionWindowTimeUnit) => {
                        setLocalConversionWindow((state) => ({ ...state, funnel_window_interval_unit }))
                        setConversionWindow()
                    }}
                    onBlur={setConversionWindow}
                >
                    {options.map(({ value, label }) => (
                        <Select.Option value={value} key={value}>
                            {label}
                        </Select.Option>
                    ))}
                </Select>
            </Row>
        </div>
    )
}
Example #13
Source File: PopupContents.tsx    From firetable with Apache License 2.0 4 votes vote down vote up
// TODO: Implement infinite scroll here
export default function PopupContents({
  value = [],
  onChange,
  config,
  row,
  docRef,
}: IPopupContentsProps) {
  const url = config.url;
  const titleKey = config.titleKey ?? config.primaryKey;
  const subtitleKey = config.subtitleKey;
  const resultsKey = config.resultsKey;
  const primaryKey = config.primaryKey;
  const multiple = Boolean(config.multiple);

  const classes = useStyles();

  // Webservice search query
  const [query, setQuery] = useState("");
  // Webservice response
  const [response, setResponse] = useState<any | null>(null);

  const [docData, setDocData] = useState<any | null>(null);
  useEffect(() => {
    docRef.get().then((d) => setDocData(d.data()));
  }, []);

  const hits: any["hits"] = _get(response, resultsKey) ?? [];
  const [search] = useDebouncedCallback(
    async (query: string) => {
      if (!docData) return;
      if (!url) return;
      const uri = new URL(url),
        params = { q: query };
      Object.keys(params).forEach((key) =>
        uri.searchParams.append(key, params[key])
      );

      const resp = await fetch(uri.toString(), {
        method: "POST",
        body: JSON.stringify(docData),
        headers: { "content-type": "application/json" },
      });

      const jsonBody = await resp.json();
      setResponse(jsonBody);
    },
    1000,
    { leading: true }
  );

  useEffect(() => {
    search(query);
  }, [query, docData]);

  if (!response) return <Loading />;

  const select = (hit: any) => () => {
    if (multiple) onChange([...value, hit]);
    else onChange([hit]);
  };
  const deselect = (hit: any) => () => {
    if (multiple)
      onChange(value.filter((v) => v[primaryKey] !== hit[primaryKey]));
    else onChange([]);
  };

  const selectedValues = value?.map((item) => _get(item, primaryKey));

  const clearSelection = () => onChange([]);

  return (
    <Grid container direction="column" className={classes.grid}>
      <Grid item className={classes.searchRow}>
        <TextField
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          fullWidth
          variant="filled"
          margin="dense"
          label="Search items"
          className={classes.noMargins}
          InputProps={{
            endAdornment: (
              <InputAdornment position="end">
                <SearchIcon />
              </InputAdornment>
            ),
          }}
          onClick={(e) => e.stopPropagation()}
          onKeyDown={(e) => e.stopPropagation()}
        />
      </Grid>

      <Grid item xs className={classes.listRow}>
        <List className={classes.list}>
          {hits.map((hit) => {
            const isSelected =
              selectedValues.indexOf(_get(hit, primaryKey)) !== -1;
            console.log(`Selected Values: ${selectedValues}`);
            return (
              <React.Fragment key={_get(hit, primaryKey)}>
                <MenuItem
                  dense
                  onClick={isSelected ? deselect(hit) : select(hit)}
                >
                  <ListItemIcon className={classes.checkboxContainer}>
                    {multiple ? (
                      <Checkbox
                        edge="start"
                        checked={isSelected}
                        tabIndex={-1}
                        color="secondary"
                        className={classes.checkbox}
                        disableRipple
                        inputProps={{
                          "aria-labelledby": `label-${_get(hit, primaryKey)}`,
                        }}
                      />
                    ) : (
                      <Radio
                        edge="start"
                        checked={isSelected}
                        tabIndex={-1}
                        color="secondary"
                        className={classes.checkbox}
                        disableRipple
                        inputProps={{
                          "aria-labelledby": `label-${_get(hit, primaryKey)}`,
                        }}
                      />
                    )}
                  </ListItemIcon>
                  <ListItemText
                    id={`label-${_get(hit, primaryKey)}`}
                    primary={_get(hit, titleKey)}
                    secondary={!subtitleKey ? "" : _get(hit, subtitleKey)}
                  />
                </MenuItem>
                <Divider className={classes.divider} />
              </React.Fragment>
            );
          })}
        </List>
      </Grid>

      {multiple && (
        <Grid item className={clsx(classes.footerRow, classes.selectedRow)}>
          <Grid
            container
            direction="row"
            justify="space-between"
            alignItems="center"
          >
            <Typography
              variant="button"
              color="textSecondary"
              className={classes.selectedNum}
            >
              {value?.length} of {hits?.length}
            </Typography>

            <Button
              disabled={!value || value.length === 0}
              onClick={clearSelection}
              color="primary"
              className={classes.selectAllButton}
            >
              Clear Selection
            </Button>
          </Grid>
        </Grid>
      )}
    </Grid>
  );
}
Example #14
Source File: index.tsx    From jsonschema-editor-react with Apache License 2.0 4 votes vote down vote up
SchemaItem: React.FunctionComponent<SchemaItemProps> = (
	props: React.PropsWithChildren<SchemaItemProps>
) => {
	const {
		name,
		itemStateProp,
		showadvanced,
		required,
		parentStateProp,
		isReadOnly,
	} = props;

	// const itemState = useState(itemStateProp);
	const parentState = useState(parentStateProp);
	const parentStateOrNull: State<JSONSchema7> | undefined = parentState.ornull;
	const propertiesOrNull:
		| State<{
				[key: string]: JSONSchema7Definition;
		  }>
		| undefined = parentStateOrNull.properties.ornull;

	const nameState = useState(name);
	const isReadOnlyState = useState(isReadOnly);

	const itemState = useState(
		(parentStateProp.properties as State<{
			[key: string]: JSONSchema7;
		}>).nested(nameState.value)
	);

	const { length } = parentState.path.filter((name) => name !== "properties");
	const tagPaddingLeftStyle = {
		paddingLeft: `${20 * (length + 1)}px`,
	};

	const isRequired = required
		? required.length > 0 && required.includes(name)
		: false;
	const toast = useToast();

	// Debounce callback
	const debounced = useDebouncedCallback(
		// function
		(newValue: string) => {
			// Todo: make toast for duplicate properties
			if (propertiesOrNull && propertiesOrNull[newValue].value) {
				toast({
					title: "Duplicate Property",
					description: "Property already exists!",
					status: "error",
					duration: 1000,
					isClosable: true,
					position: "top",
				});
			} else {
				const oldName = name;
				const proptoupdate = newValue;

				const newobj = renameKeys(
					{ [oldName]: proptoupdate },
					parentState.properties.value
				);
				parentStateOrNull.properties.set(JSON.parse(JSON.stringify(newobj)));
			}
		},
		// delay in ms
		1000
	);

	if (!itemState.value) {
		return <></>;
	}

	return (
		<div>
			<Flex
				alignContent="space-evenly"
				direction="row"
				wrap="nowrap"
				className="schema-item"
				style={tagPaddingLeftStyle}
			>
				<Input
					isDisabled={isReadOnlyState.value}
					defaultValue={nameState.value}
					size="sm"
					margin={2}
					variant="outline"
					placeholder="Enter property name"
					onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
						debounced(evt.target.value);
					}}
				/>
				<Checkbox
					isDisabled={isReadOnlyState.value}
					isChecked={isRequired}
					margin={2}
					colorScheme="blue"
					onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
						if (!evt.target.checked && required.includes(name)) {
							(parentState.required as State<string[]>)[
								required.indexOf(name)
							].set(none);
						} else {
							parentState.required.merge([name]);
						}
					}}
				/>
				<Select
					isDisabled={false}
					variant="outline"
					value={itemState.type.value}
					size="sm"
					margin={2}
					placeholder="Choose data type"
					onChange={(evt: React.ChangeEvent<HTMLSelectElement>) => {
						const newSchema = handleTypeChange(
							evt.target.value as JSONSchema7TypeName,
							false
						);
						itemState.set(newSchema as JSONSchema7);
					}}
				>
					{SchemaTypes.map((item, index) => {
						return (
							<option key={String(index)} value={item}>
								{item}
							</option>
						);
					})}
				</Select>
				<Input
					isDisabled={isReadOnlyState.value}
					value={itemState.title.value || ""}
					size="sm"
					margin={2}
					variant="outline"
					placeholder="Add Title"
					onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
						itemState.title.set(evt.target.value);
					}}
				/>
				<Input
					isDisabled={isReadOnlyState.value}
					value={itemState.description.value || ""}
					size="sm"
					margin={2}
					variant="outline"
					placeholder="Add Description"
					onChange={(evt: React.ChangeEvent<HTMLInputElement>) => {
						itemState.description.set(evt.target.value);
					}}
				/>

				{itemState.type.value !== "object" && itemState.type.value !== "array" && (
					<Tooltip
						hasArrow
						aria-label="Advanced Settings"
						label="Advanced Settings"
						placement="top"
					>
						<IconButton
							isRound
							isDisabled={isReadOnlyState.value}
							size="sm"
							mt={2}
							mb={2}
							ml={1}
							variant="link"
							colorScheme="blue"
							fontSize="16px"
							icon={<FiSettings />}
							aria-label="Advanced Settings"
							onClick={() => {
								showadvanced(name);
							}}
						/>
					</Tooltip>
				)}

				<Tooltip
					hasArrow
					aria-label="Remove Node"
					label="Remove Node"
					placement="top"
				>
					<IconButton
						isRound
						isDisabled={isReadOnlyState.value}
						size="sm"
						mt={2}
						mb={2}
						ml={1}
						variant="link"
						colorScheme="red"
						fontSize="16px"
						icon={<AiOutlineDelete />}
						aria-label="Remove Node"
						onClick={() => {
							const updatedState = deleteKey(
								nameState.value,
								JSON.parse(JSON.stringify(parentState.properties.value))
							);
							parentState.properties.set(updatedState);
						}}
					/>
				</Tooltip>

				{itemState.type?.value === "object" ? (
					<DropPlus
						isDisabled={isReadOnlyState.value}
						parentStateProp={parentState}
						itemStateProp={itemStateProp}
					/>
				) : (
					<Tooltip
						hasArrow
						aria-label="Add Sibling Node"
						label="Add Sibling Node"
						placement="top"
					>
						<IconButton
							isRound
							isDisabled={isReadOnlyState.value}
							size="sm"
							mt={2}
							mb={2}
							mr={2}
							variant="link"
							colorScheme="green"
							fontSize="16px"
							icon={<IoIosAddCircleOutline />}
							aria-label="Add Sibling Node"
							onClick={() => {
								if (propertiesOrNull) {
									const fieldName = `field_${random()}`;
									propertiesOrNull
										?.nested(fieldName)
										.set(getDefaultSchema(DataType.string) as JSONSchema7);
								}
							}}
						/>
					</Tooltip>
				)}
			</Flex>
			{itemState.type?.value === "object" && (
				<SchemaObject isReadOnly={isReadOnlyState} schemaState={itemState} />
			)}
			{itemState.type?.value === "array" && (
				<SchemaArray isReadOnly={isReadOnlyState} schemaState={itemState} />
			)}
		</div>
	);
}
Example #15
Source File: SessionItemComponent.tsx    From caritas-onlineBeratung-frontend with GNU Affero General Public License v3.0 4 votes vote down vote up
SessionItemComponent = (props: SessionItemProps) => {
	const { rcGroupId: groupIdFromParam } = useParams();

	const activeSession = useContext(ActiveSessionContext);
	let { sessionsData, setSessionsData } = useContext(SessionsDataContext);
	const { userData } = useContext(UserDataContext);
	const [monitoringButtonVisible, setMonitoringButtonVisible] =
		useState(false);
	const [overlayItem, setOverlayItem] = useState<OverlayItem>(null);
	const [currentGroupId, setCurrentGroupId] = useState(null);
	const { setAcceptedGroupId } = useContext(AcceptedGroupIdContext);
	const chatItem = getChatItemForSession(
		activeSession
	) as SessionItemInterface;
	const isGroupChat = isGroupChatForSessionItem(activeSession);
	const isLiveChat = isAnonymousSession(activeSession?.session);
	const messages = useMemo(() => props.messages, [props && props.messages]); // eslint-disable-line react-hooks/exhaustive-deps
	const [initialScrollCompleted, setInitialScrollCompleted] = useState(false);
	const [isRequestInProgress, setIsRequestInProgress] = useState(false);
	const scrollContainerRef = React.useRef<HTMLDivElement>(null);
	const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
	const [newMessages, setNewMessages] = useState(0);
	const { setUpdateSessionList } = useContext(UpdateSessionListContext);
	const [sessionListTab] = useState(
		new URLSearchParams(useLocation().search).get('sessionListTab')
	);
	const getSessionListTab = () =>
		`${sessionListTab ? `?sessionListTab=${sessionListTab}` : ''}`;

	const { isAnonymousEnquiry } = props;

	const resetUnreadCount = () => {
		if (!isAnonymousEnquiry) {
			setNewMessages(0);
			initMessageCount = messages?.length;
			scrollContainerRef.current
				.querySelectorAll('.messageItem__divider--lastRead')
				.forEach((e) => e.remove());
		}
	};

	useEffect(() => {
		if (scrollContainerRef.current) {
			resetUnreadCount();
		}
	}, [scrollContainerRef]); // eslint-disable-line

	useEffect(() => {
		if (!isAnonymousEnquiry && messages) {
			if (
				initialScrollCompleted &&
				isMyMessage(messages[messages.length - 1]?.userId)
			) {
				resetUnreadCount();
				scrollToEnd(0, true);
			} else {
				// if first unread message -> prepend element
				if (newMessages === 0 && !isScrolledToBottom) {
					const scrollContainer = scrollContainerRef.current;
					const firstUnreadItem = Array.from(
						scrollContainer.querySelectorAll('.messageItem')
					).pop();
					const lastReadDivider = document.createElement('div');
					lastReadDivider.innerHTML = translate(
						'session.divider.lastRead'
					);
					lastReadDivider.className =
						'messageItem__divider messageItem__divider--lastRead';
					firstUnreadItem.prepend(lastReadDivider);
				}

				if (isScrolledToBottom && initialScrollCompleted) {
					resetUnreadCount();
					scrollToEnd(0, true);
				}

				setNewMessages(messages.length - initMessageCount);
			}
		}
	}, [messages?.length]); // eslint-disable-line

	useEffect(() => {
		if (isScrolledToBottom) {
			resetUnreadCount();
		}
	}, [isScrolledToBottom]); // eslint-disable-line

	const [resortData, setResortData] = useState<ConsultingTypeInterface>();
	useEffect(() => {
		let isCanceled = false;
		apiGetConsultingType({
			consultingTypeId: chatItem?.consultingType
		}).then((response) => {
			if (isCanceled) return;
			setResortData(response);
		});
		return () => {
			isCanceled = true;
		};
	}, [chatItem?.consultingType]);

	const getPlaceholder = () => {
		if (isGroupChat) {
			return translate('enquiry.write.input.placeholder.groupChat');
		} else if (hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData)) {
			return translate('enquiry.write.input.placeholder');
		} else if (
			hasUserAuthority(AUTHORITIES.VIEW_ALL_PEER_SESSIONS, userData) &&
			activeSession.isFeedbackSession
		) {
			return translate('enquiry.write.input.placeholder.feedback.main');
		} else if (
			hasUserAuthority(AUTHORITIES.CONSULTANT_DEFAULT, userData) &&
			activeSession.isFeedbackSession
		) {
			return translate('enquiry.write.input.placeholder.feedback.peer');
		} else if (hasUserAuthority(AUTHORITIES.CONSULTANT_DEFAULT, userData)) {
			return translate('enquiry.write.input.placeholder.consultant');
		}
		return translate('enquiry.write.input.placeholder');
	};

	const handleButtonClick = (sessionId: any, sessionGroupId: string) => {
		if (isRequestInProgress) {
			return null;
		}
		setIsRequestInProgress(true);

		apiEnquiryAcceptance(sessionId, isAnonymousEnquiry)
			.then(() => {
				setOverlayItem(enquirySuccessfullyAcceptedOverlayItem);
				setCurrentGroupId(sessionGroupId);
			})
			.catch((error) => {
				if (error.message === FETCH_ERRORS.CONFLICT) {
					setOverlayItem(enquiryTakenByOtherConsultantOverlayItem);
				} else {
					console.log(error);
				}
			});
	};

	const handleOverlayAction = (buttonFunction: string) => {
		switch (buttonFunction) {
			case OVERLAY_FUNCTIONS.REDIRECT:
				setOverlayItem(null);
				setIsRequestInProgress(false);
				setAcceptedGroupId(currentGroupId);
				setSessionsData({ ...sessionsData, enquiries: [] });
				history.push(`/sessions/consultant/sessionView/`);
				break;
			case OVERLAY_FUNCTIONS.CLOSE:
				setOverlayItem(null);
				setUpdateSessionList(SESSION_LIST_TYPES.ENQUIRY);
				history.push(
					`/sessions/consultant/sessionPreview${getSessionListTab()}`
				);
				break;
			default:
			// Should never be executed as `handleOverlayAction` is only called
			// with a non-null `overlayItem`
		}
	};

	/* eslint-disable */
	const handleScroll = useDebouncedCallback((e) => {
		const scrollPosition = Math.round(
			e.target.scrollHeight - e.target.scrollTop
		);
		const containerHeight = e.target.clientHeight;
		const isBottom =
			scrollPosition >= containerHeight - 1 &&
			scrollPosition <= containerHeight + 1;

		setIsScrolledToBottom(isBottom);
	}, 100);
	/* eslint-enable */

	const handleScrollToBottomButtonClick = () => {
		if (newMessages > 0) {
			const scrollContainer = scrollContainerRef.current;
			const sessionHeader =
				scrollContainer.parentElement.getElementsByClassName(
					'sessionInfo'
				)[0] as HTMLElement;
			const messageItems = scrollContainer.querySelectorAll(
				'.messageItem:not(.messageItem--right)'
			);
			const firstUnreadItem = messageItems[
				messageItems.length - newMessages
			] as HTMLElement;
			const firstUnreadItemOffet =
				firstUnreadItem.offsetTop - sessionHeader.offsetHeight;

			if (scrollContainer.scrollTop < firstUnreadItemOffet) {
				smoothScroll({
					duration: 1000,
					element: scrollContainer,
					to: firstUnreadItemOffet
				});
			} else {
				scrollToEnd(0, true);
			}
		} else {
			scrollToEnd(0, true);
		}
	};

	const enableInitialScroll = () => {
		if (!initialScrollCompleted) {
			setInitialScrollCompleted(true);
			scrollToEnd(500, true);
		}
	};

	const isOnlyEnquiry = typeIsEnquiry(getTypeOfLocation());

	const buttonItem: ButtonItem = {
		label: isAnonymousEnquiry
			? translate('enquiry.acceptButton.anonymous')
			: translate('enquiry.acceptButton'),
		type: BUTTON_TYPES.PRIMARY
	};

	const enquirySuccessfullyAcceptedOverlayItem: OverlayItem = {
		svg: CheckIcon,
		headline: translate('session.acceptance.overlayHeadline'),
		buttonSet: [
			{
				label: translate('session.acceptance.buttonLabel'),
				function: OVERLAY_FUNCTIONS.REDIRECT,
				type: BUTTON_TYPES.PRIMARY
			}
		]
	};

	const enquiryTakenByOtherConsultantOverlayItem: OverlayItem = {
		svg: XIcon,
		headline: translate(
			'session.anonymous.takenByOtherConsultant.overlayHeadline'
		),
		illustrationBackground: 'error',
		buttonSet: [
			{
				label: translate(
					'session.anonymous.takenByOtherConsultant.buttonLabel'
				),
				function: OVERLAY_FUNCTIONS.CLOSE,
				type: BUTTON_TYPES.PRIMARY
			}
		]
	};

	const monitoringButtonItem: ButtonItem = {
		label: translate('session.monitoring.buttonLabel'),
		type: 'PRIMARY',
		function: ''
	};

	const scrollBottomButtonItem: ButtonItem = {
		icon: <ArrowDoubleDownIcon />,
		type: BUTTON_TYPES.SMALL_ICON,
		smallIconBackgroundColor: 'alternate'
	};

	return (
		<div
			className={
				activeSession.isFeedbackSession
					? `session session--yellowTheme`
					: `session`
			}
		>
			<SessionHeaderComponent
				consultantAbsent={
					activeSession.consultant && activeSession.consultant.absent
						? activeSession.consultant
						: null
				}
				hasUserInitiatedStopOrLeaveRequest={
					props.hasUserInitiatedStopOrLeaveRequest
				}
				legalComponent={props.legalComponent}
				bannedUsers={props.bannedUsers}
			/>

			{!isAnonymousEnquiry && (
				<div
					id="session-scroll-container"
					className="session__content"
					ref={scrollContainerRef}
					onScroll={(e) => handleScroll(e)}
				>
					{messages &&
						resortData &&
						messages.map((message: MessageItem, index) => (
							<React.Fragment key={index}>
								<MessageItemComponent
									clientName={
										getContact(activeSession).username
									}
									askerRcId={chatItem.askerRcId}
									type={getTypeOfLocation()}
									isOnlyEnquiry={isOnlyEnquiry}
									isMyMessage={isMyMessage(message.userId)}
									resortData={resortData}
									bannedUsers={props.bannedUsers}
									{...message}
								/>
								{index === messages.length - 1 &&
									enableInitialScroll()}
							</React.Fragment>
						))}
					<div
						className={`session__scrollToBottom ${
							isScrolledToBottom
								? 'session__scrollToBottom--disabled'
								: ''
						}`}
					>
						{newMessages > 0 && (
							<span className="session__unreadCount">
								{newMessages > 99
									? translate('session.unreadCount.maxValue')
									: newMessages}
							</span>
						)}
						<Button
							item={scrollBottomButtonItem}
							isLink={false}
							buttonHandle={handleScrollToBottomButtonClick}
						/>
					</div>
				</div>
			)}

			{isAnonymousEnquiry && (
				<div className="session__content session__content--anonymousEnquiry">
					<Headline
						semanticLevel="3"
						text={`${translate(
							'enquiry.anonymous.infoLabel.start'
						)}${getContact(activeSession).username}${translate(
							'enquiry.anonymous.infoLabel.end'
						)}`}
					/>
				</div>
			)}

			{chatItem.monitoring &&
				!hasUserAuthority(AUTHORITIES.ASKER_DEFAULT, userData) &&
				!activeSession.isFeedbackSession &&
				!typeIsEnquiry(getTypeOfLocation()) &&
				monitoringButtonVisible &&
				!isLiveChat && (
					<Link
						to={`/sessions/consultant/${getViewPathForType(
							getTypeOfLocation()
						)}/${chatItem.groupId}/${
							chatItem.id
						}/userProfile/monitoring${getSessionListTab()}`}
					>
						<div className="monitoringButton">
							<Button item={monitoringButtonItem} isLink={true} />
						</div>
					</Link>
				)}

			{typeIsEnquiry(getTypeOfLocation()) ? (
				<div className="session__acceptance messageItem">
					{!isLiveChat &&
					hasUserAuthority(
						AUTHORITIES.VIEW_ALL_PEER_SESSIONS,
						userData
					) ? (
						<SessionAssign />
					) : (
						<Button
							item={buttonItem}
							buttonHandle={() =>
								handleButtonClick(chatItem.id, chatItem.groupId)
							}
						/>
					)}
				</div>
			) : null}

			{!isAnonymousEnquiry &&
				(!typeIsEnquiry(getTypeOfLocation()) ||
					(typeIsEnquiry(getTypeOfLocation()) &&
						hasUserAuthority(
							AUTHORITIES.VIEW_ALL_PEER_SESSIONS,
							userData
						))) && (
					<MessageSubmitInterfaceComponent
						handleSendButton={() => {}}
						isTyping={props.isTyping}
						className={clsx(
							'session__submit-interface',
							!isScrolledToBottom &&
								'session__submit-interface--scrolled-up'
						)}
						placeholder={getPlaceholder()}
						showMonitoringButton={() => {
							setMonitoringButtonVisible(true);
						}}
						type={getTypeOfLocation()}
						typingUsers={props.typingUsers}
						groupIdFromParam={groupIdFromParam}
					/>
				)}
			{overlayItem && (
				<OverlayWrapper>
					<Overlay
						item={overlayItem}
						handleOverlay={handleOverlayAction}
					/>
				</OverlayWrapper>
			)}
		</div>
	);
}
Example #16
Source File: useTableConfig.ts    From firetable with Apache License 2.0 4 votes vote down vote up
useTableConfig = (tablePath?: string) => {
  const [tableConfigState, documentDispatch] = useDoc({
    path: tablePath ? formatPath(tablePath) : "",
  });

  useEffect(() => {
    const { doc, columns, rowHeight } = tableConfigState;
    // TODO: REMOVE THIS
    // Copy columns, rowHeight to tableConfigState
    if (doc && columns !== doc.columns) {
      documentDispatch({ columns: doc.columns });
    }
    if (doc && rowHeight !== doc.rowHeight) {
      documentDispatch({ rowHeight: doc.rowHeight });
    }
  }, [tableConfigState.doc]);
  /**  used for specifying the table in use
   *  @param table firestore collection path
   */
  const setTable = (table: string) => {
    documentDispatch({
      path: formatPath(table),
      columns: [],
      doc: null,
      ref: db.doc(formatPath(table)),
      loading: true,
    });
  };
  /**  used for creating a new column
   *  @param name of column.
   *  @param type of column
   *  @param data additional column properties
   */
  const addColumn = (name: string, type: FieldType, data?: any) => {
    //TODO: validation
    const { columns } = tableConfigState;
    const newIndex = Object.keys(columns).length ?? 0;
    let updatedColumns = { ...columns };
    const key = _camelCase(name);
    updatedColumns[key] = { name, key, type, ...data, index: newIndex ?? 0 };
    documentDispatch({
      action: DocActions.update,
      data: { columns: updatedColumns },
    });
  };

  /**  used for updating the width of column
   *  @param index of column.
   *  @param width number of pixels, eg: 120
   */
  const [resize] = useDebouncedCallback((index: number, width: number) => {
    const { columns } = tableConfigState;
    const numberOfFixedColumns = Object.values(columns).filter(
      (col: any) => col.fixed && !col.hidden
    ).length;
    const columnsArray = _sortBy(
      Object.values(columns).filter((col: any) => !col.hidden && !col.fixed),
      "index"
    );
    let column: any = columnsArray[index - numberOfFixedColumns];
    column.width = width;
    let updatedColumns = columns;
    updatedColumns[column.key] = column;
    documentDispatch({
      action: DocActions.update,
      data: { columns: updatedColumns },
    });
  }, 1000);
  type updatable = { field: string; value: unknown };

  /**  used for updating column properties such as type,name etc.
   *  @param index of column.
   *  @param {updatable[]} updatables properties to be updated
   */
  const updateColumn = (key: string, updates: any) => {
    const { columns } = tableConfigState;

    const updatedColumns = {
      ...columns,
      [key]: { ...columns[key], ...updates },
    };

    documentDispatch({
      action: DocActions.update,
      data: { columns: updatedColumns },
    });
  };
  /** remove column by index
   *  @param index of column.
   */
  const remove = (key: string) => {
    const { columns } = tableConfigState;
    let updatedColumns = columns;
    updatedColumns[key] = deleteField();
    documentDispatch({
      action: DocActions.update,
      data: { columns: updatedColumns },
    });
  };
  /** reorder columns by key
   * @param draggedColumnKey column being repositioned.
   * @param droppedColumnKey column being .
   */
  const reorder = (draggedColumnKey: string, droppedColumnKey: string) => {
    const { columns } = tableConfigState;
    const oldIndex = columns[draggedColumnKey].index;
    const newIndex = columns[droppedColumnKey].index;
    const columnsArray = _sortBy(Object.values(columns), "index");
    arrayMover(columnsArray, oldIndex, newIndex);
    let updatedColumns = columns;

    columnsArray
      .filter((c) => c) // arrayMover has a bug creating undefined items
      .forEach((column: any, index) => {
        updatedColumns[column.key] = { ...column, index };
      });
    documentDispatch({
      action: DocActions.update,
      data: { columns: updatedColumns },
    });
  };
  /** changing table configuration used for things such as row height
   * @param key name of parameter eg. rowHeight
   * @param value new value eg. 65
   */
  const updateConfig = (key: string, value: unknown) => {
    documentDispatch({
      action: DocActions.update,
      data: { [key]: value },
    });
  };
  const actions = {
    updateColumn,
    updateConfig,
    addColumn,
    resize,
    setTable,
    remove,
    reorder,
  };
  return [tableConfigState, actions];
}
Example #17
Source File: PopupContents.tsx    From firetable with Apache License 2.0 4 votes vote down vote up
// TODO: Implement infinite scroll here
export default function PopupContents({
  value = [],
  onChange,
  config,
  docRef,
}: IPopupContentsProps) {
  const url = config.url;
  const titleKey = config.titleKey ?? config.primaryKey;
  const subtitleKey = config.subtitleKey;
  const resultsKey = config.resultsKey;
  const primaryKey = config.primaryKey;
  const multiple = Boolean(config.multiple);

  const classes = useStyles();

  // Webservice search query
  const [query, setQuery] = useState("");
  // Webservice response
  const [response, setResponse] = useState<any | null>(null);

  const [docData, setDocData] = useState<any | null>(null);
  useEffect(() => {
    docRef.get().then((d) => setDocData(d.data()));
  }, []);

  const hits: any["hits"] = _get(response, resultsKey) ?? [];
  const [search] = useDebouncedCallback(
    async (query: string) => {
      if (!docData) return;
      if (!url) return;
      const uri = new URL(url),
        params = { q: query };
      Object.keys(params).forEach((key) =>
        uri.searchParams.append(key, params[key])
      );

      const resp = await fetch(uri.toString(), {
        method: "POST",
        body: JSON.stringify(docData),
        headers: { "content-type": "application/json" },
      });

      const jsonBody = await resp.json();
      setResponse(jsonBody);
    },
    1000,
    { leading: true }
  );

  useEffect(() => {
    search(query);
  }, [query, docData]);

  if (!response) return <Loading />;

  const select = (hit: any) => () => {
    if (multiple) onChange([...value, hit]);
    else onChange([hit]);
  };
  const deselect = (hit: any) => () => {
    if (multiple)
      onChange(value.filter((v) => v[primaryKey] !== hit[primaryKey]));
    else onChange([]);
  };

  const selectedValues = value?.map((item) => _get(item, primaryKey));

  const clearSelection = () => onChange([]);

  return (
    <Grid container direction="column" className={classes.grid}>
      <Grid item className={classes.searchRow}>
        <TextField
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          fullWidth
          variant="filled"
          margin="dense"
          label="Search items"
          className={classes.noMargins}
          InputProps={{
            endAdornment: (
              <InputAdornment position="end">
                <SearchIcon />
              </InputAdornment>
            ),
          }}
          onClick={(e) => e.stopPropagation()}
          onKeyDown={(e) => e.stopPropagation()}
        />
      </Grid>

      <Grid item xs className={classes.listRow}>
        <List className={classes.list}>
          {hits.map((hit) => {
            const isSelected =
              selectedValues.indexOf(_get(hit, primaryKey)) !== -1;
            return (
              <React.Fragment key={_get(hit, primaryKey)}>
                <MenuItem
                  dense
                  onClick={isSelected ? deselect(hit) : select(hit)}
                >
                  <ListItemIcon className={classes.checkboxContainer}>
                    {multiple ? (
                      <Checkbox
                        edge="start"
                        checked={isSelected}
                        tabIndex={-1}
                        color="secondary"
                        className={classes.checkbox}
                        disableRipple
                        inputProps={{
                          "aria-labelledby": `label-${_get(hit, primaryKey)}`,
                        }}
                      />
                    ) : (
                      <Radio
                        edge="start"
                        checked={isSelected}
                        tabIndex={-1}
                        color="secondary"
                        className={classes.checkbox}
                        disableRipple
                        inputProps={{
                          "aria-labelledby": `label-${_get(hit, primaryKey)}`,
                        }}
                      />
                    )}
                  </ListItemIcon>
                  <ListItemText
                    id={`label-${_get(hit, primaryKey)}`}
                    primary={_get(hit, titleKey)}
                    secondary={!subtitleKey ? "" : _get(hit, subtitleKey)}
                  />
                </MenuItem>
                <Divider className={classes.divider} />
              </React.Fragment>
            );
          })}
        </List>
      </Grid>

      {multiple && (
        <Grid item className={clsx(classes.footerRow, classes.selectedRow)}>
          <Grid
            container
            direction="row"
            justify="space-between"
            alignItems="center"
          >
            <Typography
              variant="button"
              color="textSecondary"
              className={classes.selectedNum}
            >
              {value?.length} of {hits?.length}
            </Typography>

            <Button
              disabled={!value || value.length === 0}
              onClick={clearSelection}
              color="primary"
              className={classes.selectAllButton}
            >
              Clear Selection
            </Button>
          </Grid>
        </Grid>
      )}
    </Grid>
  );
}
Example #18
Source File: ImportCsv.tsx    From firetable with Apache License 2.0 4 votes vote down vote up
export default function ImportCsv({ render, PopoverProps }: IImportCsvProps) {
  const classes = useStyles();

  const [open, setOpen] = useState<HTMLButtonElement | null>(null);
  const [tab, setTab] = useState("upload");
  const [csvData, setCsvData] = useState<IImportCsvWizardProps["csvData"]>(
    null
  );
  const [error, setError] = useState("");
  const validCsv =
    csvData !== null && csvData?.columns.length > 0 && csvData?.rows.length > 0;

  const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) =>
    setOpen(event.currentTarget);
  const handleClose = () => {
    setOpen(null);
    setCsvData(null);
    setTab("upload");
    setError("");
  };
  const popoverId = open ? "csv-popover" : undefined;

  const parseCsv = (csvString: string) =>
    parse(csvString, {}, (err, rows) => {
      if (err) {
        setError(err.message);
      } else {
        const columns = rows.shift() ?? [];
        if (columns.length === 0) {
          setError("No columns detected");
        } else {
          const mappedRows = rows.map((row) =>
            row.reduce((a, c, i) => ({ ...a, [columns[i]]: c }), {})
          );
          setCsvData({ columns, rows: mappedRows });
          setError("");
        }
      }
    });

  const onDrop = useCallback(async (acceptedFiles) => {
    const file = acceptedFiles[0];
    const reader = new FileReader();
    reader.onload = (event: any) => parseCsv(event.target.result);
    reader.readAsText(file);
  }, []);
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    multiple: false,
    accept: "text/csv",
  });

  const [handlePaste] = useDebouncedCallback(
    (value: string) => parseCsv(value),
    1000
  );

  const [loading, setLoading] = useState(false);
  const [handleUrl] = useDebouncedCallback((value: string) => {
    setLoading(true);
    setError("");
    fetch(value, { mode: "no-cors" })
      .then((res) => res.text())
      .then((data) => {
        parseCsv(data);
        setLoading(false);
      })
      .catch((e) => {
        setError(e.message);
        setLoading(false);
      });
  }, 1000);

  const [openWizard, setOpenWizard] = useState(false);

  return (
    <>
      {render ? (
        render(handleOpen)
      ) : (
        <TableHeaderButton
          title="Import CSV"
          onClick={handleOpen}
          icon={<ImportIcon />}
        />
      )}

      <Popover
        id={popoverId}
        open={!!open}
        anchorEl={open}
        onClose={handleClose}
        anchorOrigin={{
          vertical: "bottom",
          horizontal: "right",
        }}
        transformOrigin={{
          vertical: "top",
          horizontal: "right",
        }}
        {...PopoverProps}
      >
        <TabContext value={tab}>
          <TabList
            onChange={(_, v) => {
              setTab(v);
              setCsvData(null);
              setError("");
            }}
            aria-label="Import CSV method tabs"
            action={(actions) =>
              setTimeout(() => actions?.updateIndicator(), 200)
            }
          >
            <Tab label="Upload" value="upload" />
            <Tab label="Paste" value="paste" />
            <Tab label="URL" value="url" />
          </TabList>

          <TabPanel value="upload" className={classes.tabPanel}>
            <Grid
              container
              justify="center"
              alignContent="center"
              alignItems="center"
              direction="column"
              {...getRootProps()}
              className={clsx(classes.dropzone, error && classes.error)}
            >
              <input {...getInputProps()} />
              {isDragActive ? (
                <Typography variant="overline">Drop CSV file here…</Typography>
              ) : (
                <>
                  <Grid item>
                    {validCsv ? (
                      <CheckIcon color="inherit" />
                    ) : (
                      <FileUploadIcon color="inherit" />
                    )}
                  </Grid>
                  <Grid item>
                    <Typography variant="overline" color="inherit">
                      {validCsv
                        ? "Valid CSV"
                        : "Click to upload or drop CSV file here"}
                    </Typography>
                  </Grid>
                </>
              )}
            </Grid>

            {error && (
              <FormHelperText error className={classes.dropzoneError}>
                {error}
              </FormHelperText>
            )}
          </TabPanel>

          <TabPanel value="paste" className={classes.tabPanel}>
            <TextField
              variant="filled"
              multiline
              inputProps={{ minRows: 5 }}
              autoFocus
              fullWidth
              label="Paste your CSV here"
              onChange={(e) => {
                if (csvData !== null) setCsvData(null);
                handlePaste(e.target.value);
              }}
              InputProps={{
                classes: {
                  root: classes.pasteField,
                  input: classes.pasteInput,
                },
              }}
              helperText={error}
              error={!!error}
            />
          </TabPanel>

          <TabPanel value="url" className={classes.tabPanel}>
            <TextField
              variant="filled"
              autoFocus
              fullWidth
              label="Paste the link to the CSV file here"
              onChange={(e) => {
                if (csvData !== null) setCsvData(null);
                handleUrl(e.target.value);
              }}
              helperText={loading ? "Fetching CSV…" : error}
              error={!!error}
            />
          </TabPanel>
        </TabContext>

        <Button
          endIcon={<GoIcon />}
          disabled={!validCsv}
          className={classes.continueButton}
          onClick={() => setOpenWizard(true)}
        >
          Continue
        </Button>
      </Popover>

      {openWizard && csvData && (
        <ImportCsvWizard
          handleClose={() => setOpenWizard(false)}
          csvData={csvData}
        />
      )}
    </>
  );
}
Example #19
Source File: AlgoliaFilters.tsx    From firetable with Apache License 2.0 4 votes vote down vote up
export default function AlgoliaFilters({
  index,
  request,
  requestDispatch,
  requiredFilters,
  label,
  filters,
  search = true,
}: IAlgoliaFiltersProps) {
  const classes = useStyles();

  // Store filter values
  const [filterValues, setFilterValues] = useState<Record<string, string[]>>(
    {}
  );
  // Push filter values to dispatch
  useEffect(() => {
    const filtersString = generateFiltersString(filterValues, requiredFilters);
    if (filtersString === null) return;
    requestDispatch({ filters: filtersString });
  }, [filterValues]);

  // Store facet values
  const [facetValues, setFacetValues] = useState<
    Record<string, readonly FacetHit[]>
  >({});
  // Get facet values
  useEffect(() => {
    if (!index) return;

    filters.forEach((filter) => {
      const params = { ...request, maxFacetHits: 100 };
      // Ignore current user-selected value for these filters so all options
      // continue to show up
      params.filters =
        generateFiltersString(
          { ...filterValues, [filter.facet]: [] },
          requiredFilters
        ) ?? "";

      index
        .searchForFacetValues(filter.facet, "", params)
        .then(({ facetHits }) =>
          setFacetValues((other) => ({ ...other, [filter.facet]: facetHits }))
        );
    });
  }, [filters, index, filterValues, requiredFilters]);

  // Reset filters
  const handleResetFilters = () => {
    setFilterValues({});
    setQuery("");
    requestDispatch({ filters: requiredFilters ?? "", query: "" });
  };

  // Store search query
  const [query, setQuery] = useState("");
  const [handleQueryChange] = useDebouncedCallback(
    (query: string) => requestDispatch({ query }),
    500
  );

  return (
    <div>
      <Grid container spacing={1} alignItems="center">
        <Grid item xs>
          <Typography variant="overline">
            Filter{label ? " " + label : "s"}
          </Typography>
        </Grid>

        <Grid item>
          <Button
            color="primary"
            onClick={handleResetFilters}
            className={classes.resetFilters}
            disabled={query === "" && Object.keys(filterValues).length === 0}
          >
            Reset Filters
          </Button>
        </Grid>
      </Grid>

      <Grid
        container
        spacing={2}
        alignItems="center"
        className={classes.filterGrid}
      >
        {search && (
          <Grid item xs={12} md={4} lg={3}>
            <TextField
              value={query}
              onChange={(e) => {
                setQuery(e.target.value);
                handleQueryChange(e.target.value);
              }}
              variant="filled"
              type="search"
              InputProps={{
                startAdornment: (
                  <InputAdornment position="start">
                    <SearchIcon />
                  </InputAdornment>
                ),
              }}
              aria-label={`Search${label ? " " + label : ""}`}
              placeholder={`Search${label ? " " + label : ""}`}
              hiddenLabel
              fullWidth
            />
          </Grid>
        )}

        {filters.map((filter) => (
          <Grid item key={filter.facet} xs={12} sm={6} md={4} lg={3}>
            <MultiSelect
              label={filter.label}
              value={filterValues[filter.facet] ?? []}
              onChange={(value) =>
                setFilterValues((other) => ({
                  ...other,
                  [filter.facet]: value,
                }))
              }
              options={
                facetValues[filter.facet]?.map((item) => ({
                  value: item.value,
                  label: filter.labelTransformer
                    ? filter.labelTransformer(item.value)
                    : item.value,
                  count: item.count,
                })) ?? []
              }
              itemRenderer={(option) => (
                <React.Fragment key={option.value}>
                  {option.label}
                  <ListItemSecondaryAction className={classes.count}>
                    <Typography
                      variant="body2"
                      color="inherit"
                      component="span"
                    >
                      {(option as any).count}
                    </Typography>
                  </ListItemSecondaryAction>
                </React.Fragment>
              )}
              searchable={facetValues[filter.facet]?.length > 10}
            />
          </Grid>
        ))}
      </Grid>
    </div>
  );
}