parent
53d8c9ba8d
commit
e1cbc4a782
@ -0,0 +1,29 @@
|
|||||||
|
interface BasePath {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
lastModified: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface File extends BasePath {
|
||||||
|
type: 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Folder extends BasePath {
|
||||||
|
type: 'folder';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PathType = 'file' | 'folder' | 'drive' | 'computer' | 'parent';
|
||||||
|
export type Path = File | Folder;
|
||||||
|
|
||||||
|
interface PathsAppState {
|
||||||
|
currentPath: string;
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
directories: Folder[];
|
||||||
|
files: File[];
|
||||||
|
parent: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PathsAppState;
|
@ -1,34 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import styles from './Alert.css';
|
|
||||||
|
|
||||||
function Alert(props) {
|
|
||||||
const { className, kind, children, ...otherProps } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles[kind]
|
|
||||||
)}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Alert.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
kind: PropTypes.oneOf(kinds.all),
|
|
||||||
children: PropTypes.node.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Alert.defaultProps = {
|
|
||||||
className: styles.alert,
|
|
||||||
kind: kinds.INFO
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Alert;
|
|
@ -0,0 +1,18 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import { Kind } from 'Helpers/Props/kinds';
|
||||||
|
import styles from './Alert.css';
|
||||||
|
|
||||||
|
interface AlertProps {
|
||||||
|
className?: string;
|
||||||
|
kind?: Extract<Kind, keyof typeof styles>;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Alert(props: AlertProps) {
|
||||||
|
const { className = styles.alert, kind = 'info', children } = props;
|
||||||
|
|
||||||
|
return <div className={classNames(className, styles[kind])}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Alert;
|
@ -1,60 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import styles from './Card.css';
|
|
||||||
|
|
||||||
class Card extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
overlayClassName,
|
|
||||||
overlayContent,
|
|
||||||
children,
|
|
||||||
onPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (overlayContent) {
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<Link
|
|
||||||
className={styles.underlay}
|
|
||||||
onPress={onPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={overlayClassName}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className={className}
|
|
||||||
onPress={onPress}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Card.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
overlayClassName: PropTypes.string.isRequired,
|
|
||||||
overlayContent: PropTypes.bool.isRequired,
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
onPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Card.defaultProps = {
|
|
||||||
className: styles.card,
|
|
||||||
overlayClassName: styles.overlay,
|
|
||||||
overlayContent: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Card;
|
|
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Link, { LinkProps } from 'Components/Link/Link';
|
||||||
|
import styles from './Card.css';
|
||||||
|
|
||||||
|
interface CardProps extends Pick<LinkProps, 'onPress'> {
|
||||||
|
// TODO: Consider using different properties for classname depending if it's overlaying content or not
|
||||||
|
className?: string;
|
||||||
|
overlayClassName?: string;
|
||||||
|
overlayContent?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Card(props: CardProps) {
|
||||||
|
const {
|
||||||
|
className = styles.card,
|
||||||
|
overlayClassName = styles.overlay,
|
||||||
|
overlayContent = false,
|
||||||
|
children,
|
||||||
|
onPress,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (overlayContent) {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Link className={styles.underlay} onPress={onPress} />
|
||||||
|
|
||||||
|
<div className={overlayClassName}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link className={className} onPress={onPress}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Card;
|
@ -1,33 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import styles from './DescriptionList.css';
|
|
||||||
|
|
||||||
class DescriptionList extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dl className={className}>
|
|
||||||
{children}
|
|
||||||
</dl>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DescriptionList.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.node
|
|
||||||
};
|
|
||||||
|
|
||||||
DescriptionList.defaultProps = {
|
|
||||||
className: styles.descriptionList
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DescriptionList;
|
|
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './DescriptionList.css';
|
||||||
|
|
||||||
|
interface DescriptionListProps {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DescriptionList(props: DescriptionListProps) {
|
||||||
|
const { className = styles.descriptionList, children } = props;
|
||||||
|
|
||||||
|
return <dl className={className}>{children}</dl>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DescriptionList;
|
@ -1,46 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import DescriptionListItemDescription from './DescriptionListItemDescription';
|
|
||||||
import DescriptionListItemTitle from './DescriptionListItemTitle';
|
|
||||||
|
|
||||||
class DescriptionListItem extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
titleClassName,
|
|
||||||
descriptionClassName,
|
|
||||||
title,
|
|
||||||
data
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<DescriptionListItemTitle
|
|
||||||
className={titleClassName}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</DescriptionListItemTitle>
|
|
||||||
|
|
||||||
<DescriptionListItemDescription
|
|
||||||
className={descriptionClassName}
|
|
||||||
>
|
|
||||||
{data}
|
|
||||||
</DescriptionListItemDescription>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DescriptionListItem.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
titleClassName: PropTypes.string,
|
|
||||||
descriptionClassName: PropTypes.string,
|
|
||||||
title: PropTypes.string,
|
|
||||||
data: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node])
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DescriptionListItem;
|
|
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DescriptionListItemDescription, {
|
||||||
|
DescriptionListItemDescriptionProps,
|
||||||
|
} from './DescriptionListItemDescription';
|
||||||
|
import DescriptionListItemTitle, {
|
||||||
|
DescriptionListItemTitleProps,
|
||||||
|
} from './DescriptionListItemTitle';
|
||||||
|
|
||||||
|
interface DescriptionListItemProps {
|
||||||
|
className?: string;
|
||||||
|
titleClassName?: DescriptionListItemTitleProps['className'];
|
||||||
|
descriptionClassName?: DescriptionListItemDescriptionProps['className'];
|
||||||
|
title?: DescriptionListItemTitleProps['children'];
|
||||||
|
data?: DescriptionListItemDescriptionProps['children'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function DescriptionListItem(props: DescriptionListItemProps) {
|
||||||
|
const { className, titleClassName, descriptionClassName, title, data } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<DescriptionListItemTitle className={titleClassName}>
|
||||||
|
{title}
|
||||||
|
</DescriptionListItemTitle>
|
||||||
|
|
||||||
|
<DescriptionListItemDescription className={descriptionClassName}>
|
||||||
|
{data}
|
||||||
|
</DescriptionListItemDescription>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DescriptionListItem;
|
@ -1,27 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './DescriptionListItemDescription.css';
|
|
||||||
|
|
||||||
function DescriptionListItemDescription(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dd className={className}>
|
|
||||||
{children}
|
|
||||||
</dd>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DescriptionListItemDescription.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node])
|
|
||||||
};
|
|
||||||
|
|
||||||
DescriptionListItemDescription.defaultProps = {
|
|
||||||
className: styles.description
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DescriptionListItemDescription;
|
|
@ -0,0 +1,17 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import styles from './DescriptionListItemDescription.css';
|
||||||
|
|
||||||
|
export interface DescriptionListItemDescriptionProps {
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DescriptionListItemDescription(
|
||||||
|
props: DescriptionListItemDescriptionProps
|
||||||
|
) {
|
||||||
|
const { className = styles.description, children } = props;
|
||||||
|
|
||||||
|
return <dd className={className}>{children}</dd>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DescriptionListItemDescription;
|
@ -1,27 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './DescriptionListItemTitle.css';
|
|
||||||
|
|
||||||
function DescriptionListItemTitle(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dt className={className}>
|
|
||||||
{children}
|
|
||||||
</dt>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DescriptionListItemTitle.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
DescriptionListItemTitle.defaultProps = {
|
|
||||||
className: styles.title
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DescriptionListItemTitle;
|
|
@ -0,0 +1,15 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import styles from './DescriptionListItemTitle.css';
|
||||||
|
|
||||||
|
export interface DescriptionListItemTitleProps {
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DescriptionListItemTitle(props: DescriptionListItemTitleProps) {
|
||||||
|
const { className = styles.title, children } = props;
|
||||||
|
|
||||||
|
return <dt className={className}>{children}</dt>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DescriptionListItemTitle;
|
@ -1,22 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './DragPreviewLayer.css';
|
|
||||||
|
|
||||||
function DragPreviewLayer({ children, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<div {...otherProps}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DragPreviewLayer.propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
className: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
DragPreviewLayer.defaultProps = {
|
|
||||||
className: styles.dragLayer
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragPreviewLayer;
|
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './DragPreviewLayer.css';
|
||||||
|
|
||||||
|
interface DragPreviewLayerProps {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DragPreviewLayer({
|
||||||
|
className = styles.dragLayer,
|
||||||
|
children,
|
||||||
|
...otherProps
|
||||||
|
}: DragPreviewLayerProps) {
|
||||||
|
return (
|
||||||
|
<div className={className} {...otherProps}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DragPreviewLayer;
|
@ -1,62 +0,0 @@
|
|||||||
import * as sentry from '@sentry/browser';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
class ErrorBoundary extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
error: null,
|
|
||||||
info: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error, info) {
|
|
||||||
this.setState({
|
|
||||||
error,
|
|
||||||
info
|
|
||||||
});
|
|
||||||
|
|
||||||
sentry.captureException(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
children,
|
|
||||||
errorComponent: ErrorComponent,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
error,
|
|
||||||
info
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<ErrorComponent
|
|
||||||
error={error}
|
|
||||||
info={info}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorBoundary.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
errorComponent: PropTypes.elementType.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ErrorBoundary;
|
|
@ -0,0 +1,46 @@
|
|||||||
|
import * as sentry from '@sentry/browser';
|
||||||
|
import React, { Component, ErrorInfo } from 'react';
|
||||||
|
|
||||||
|
interface ErrorBoundaryProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
errorComponent: React.ElementType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorBoundaryState {
|
||||||
|
error: Error | null;
|
||||||
|
info: ErrorInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Class component until componentDidCatch is supported in functional components
|
||||||
|
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
|
constructor(props: ErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
error: null,
|
||||||
|
info: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||||
|
this.setState({
|
||||||
|
error,
|
||||||
|
info,
|
||||||
|
});
|
||||||
|
|
||||||
|
sentry.captureException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children, errorComponent: ErrorComponent } = this.props;
|
||||||
|
const { error, info } = this.state;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorComponent error={error} info={info} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
@ -1,41 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import styles from './FieldSet.css';
|
|
||||||
|
|
||||||
class FieldSet extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
size,
|
|
||||||
legend,
|
|
||||||
children
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<fieldset className={styles.fieldSet}>
|
|
||||||
<legend className={classNames(styles.legend, (size === sizes.SMALL) && styles.small)}>
|
|
||||||
{legend}
|
|
||||||
</legend>
|
|
||||||
{children}
|
|
||||||
</fieldset>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
FieldSet.propTypes = {
|
|
||||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
|
||||||
legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
|
||||||
children: PropTypes.node
|
|
||||||
};
|
|
||||||
|
|
||||||
FieldSet.defaultProps = {
|
|
||||||
size: sizes.MEDIUM
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FieldSet;
|
|
@ -0,0 +1,29 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { ComponentProps } from 'react';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import { Size } from 'Helpers/Props/sizes';
|
||||||
|
import styles from './FieldSet.css';
|
||||||
|
|
||||||
|
interface FieldSetProps {
|
||||||
|
size?: Size;
|
||||||
|
legend?: ComponentProps<'legend'>['children'];
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldSet({ size = sizes.MEDIUM, legend, children }: FieldSetProps) {
|
||||||
|
return (
|
||||||
|
<fieldset className={styles.fieldSet}>
|
||||||
|
<legend
|
||||||
|
className={classNames(
|
||||||
|
styles.legend,
|
||||||
|
size === sizes.SMALL && styles.small
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{legend}
|
||||||
|
</legend>
|
||||||
|
{children}
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FieldSet;
|
@ -1,39 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import FileBrowserModalContentConnector from './FileBrowserModalContentConnector';
|
|
||||||
import styles from './FileBrowserModal.css';
|
|
||||||
|
|
||||||
class FileBrowserModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
onModalClose,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className={styles.modal}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<FileBrowserModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileBrowserModal;
|
|
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import FileBrowserModalContent, {
|
||||||
|
FileBrowserModalContentProps,
|
||||||
|
} from './FileBrowserModalContent';
|
||||||
|
import styles from './FileBrowserModal.css';
|
||||||
|
|
||||||
|
interface FileBrowserModalProps extends FileBrowserModalContentProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileBrowserModal(props: FileBrowserModalProps) {
|
||||||
|
const { isOpen, onModalClose, ...otherProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal className={styles.modal} isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<FileBrowserModalContent {...otherProps} onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileBrowserModal;
|
@ -1,246 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import PathInput from 'Components/Form/PathInput';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import FileBrowserRow from './FileBrowserRow';
|
|
||||||
import styles from './FileBrowserModalContent.css';
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'type',
|
|
||||||
label: () => translate('Type'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: () => translate('Name'),
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
class FileBrowserModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._scrollerRef = React.createRef();
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isFileBrowserModalOpen: false,
|
|
||||||
currentPath: props.value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
const {
|
|
||||||
currentPath
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentPath !== this.state.currentPath &&
|
|
||||||
currentPath !== prevState.currentPath
|
|
||||||
) {
|
|
||||||
this.setState({ currentPath });
|
|
||||||
this._scrollerRef.current.scrollTop = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPathInputChange = ({ value }) => {
|
|
||||||
this.setState({ currentPath: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRowPress = (path) => {
|
|
||||||
this.props.onFetchPaths(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
onOkPress = () => {
|
|
||||||
this.props.onChange({
|
|
||||||
name: this.props.name,
|
|
||||||
value: this.state.currentPath
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.onClearPaths();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
parent,
|
|
||||||
directories,
|
|
||||||
files,
|
|
||||||
isWindowsService,
|
|
||||||
onModalClose,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const emptyParent = parent === '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('FileBrowser')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody
|
|
||||||
className={styles.modalBody}
|
|
||||||
scrollDirection={scrollDirections.NONE}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isWindowsService &&
|
|
||||||
<Alert
|
|
||||||
className={styles.mappedDrivesWarning}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
>
|
|
||||||
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService', { url: 'https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server' })} />
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
<PathInput
|
|
||||||
className={styles.pathInput}
|
|
||||||
placeholder={translate('FileBrowserPlaceholderText')}
|
|
||||||
hasFileBrowser={false}
|
|
||||||
{...otherProps}
|
|
||||||
value={this.state.currentPath}
|
|
||||||
onChange={this.onPathInputChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Scroller
|
|
||||||
ref={this._scrollerRef}
|
|
||||||
className={styles.scroller}
|
|
||||||
scrollDirection={scrollDirections.BOTH}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
!!error &&
|
|
||||||
<div>{translate('ErrorLoadingContents')}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !error &&
|
|
||||||
<Table
|
|
||||||
horizontalScroll={false}
|
|
||||||
columns={columns}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
emptyParent &&
|
|
||||||
<FileBrowserRow
|
|
||||||
type="computer"
|
|
||||||
name={translate('MyComputer')}
|
|
||||||
path={parent}
|
|
||||||
onPress={this.onRowPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!emptyParent && parent &&
|
|
||||||
<FileBrowserRow
|
|
||||||
type="parent"
|
|
||||||
name="..."
|
|
||||||
path={parent}
|
|
||||||
onPress={this.onRowPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
directories.map((directory) => {
|
|
||||||
return (
|
|
||||||
<FileBrowserRow
|
|
||||||
key={directory.path}
|
|
||||||
type={directory.type}
|
|
||||||
name={directory.name}
|
|
||||||
path={directory.path}
|
|
||||||
onPress={this.onRowPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
files.map((file) => {
|
|
||||||
return (
|
|
||||||
<FileBrowserRow
|
|
||||||
key={file.path}
|
|
||||||
type={file.type}
|
|
||||||
name={file.name}
|
|
||||||
path={file.path}
|
|
||||||
onPress={this.onRowPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
}
|
|
||||||
</Scroller>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
{
|
|
||||||
isFetching &&
|
|
||||||
<LoadingIndicator
|
|
||||||
className={styles.loading}
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onPress={this.onOkPress}
|
|
||||||
>
|
|
||||||
{translate('Ok')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModalContent.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
parent: PropTypes.string,
|
|
||||||
currentPath: PropTypes.string.isRequired,
|
|
||||||
directories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
files: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isWindowsService: PropTypes.bool.isRequired,
|
|
||||||
onFetchPaths: PropTypes.func.isRequired,
|
|
||||||
onClearPaths: PropTypes.func.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileBrowserModalContent;
|
|
@ -0,0 +1,237 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import PathInput from 'Components/Form/PathInput';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||||
|
import { clearPaths, fetchPaths } from 'Store/Actions/pathActions';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import createPathsSelector from './createPathsSelector';
|
||||||
|
import FileBrowserRow from './FileBrowserRow';
|
||||||
|
import styles from './FileBrowserModalContent.css';
|
||||||
|
|
||||||
|
const columns: Column[] = [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
label: () => translate('Type'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: () => translate('Name'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleClearPaths = () => {};
|
||||||
|
|
||||||
|
export interface FileBrowserModalContentProps {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
includeFiles?: boolean;
|
||||||
|
onChange: (args: InputChanged<string>) => unknown;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileBrowserModalContent(props: FileBrowserModalContentProps) {
|
||||||
|
const { name, value, includeFiles = true, onChange, onModalClose } = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { isWindows, mode } = useSelector(createSystemStatusSelector());
|
||||||
|
const { isFetching, isPopulated, error, parent, directories, files, paths } =
|
||||||
|
useSelector(createPathsSelector());
|
||||||
|
|
||||||
|
const [currentPath, setCurrentPath] = useState(value);
|
||||||
|
const scrollerRef = useRef(null);
|
||||||
|
const previousValue = usePrevious(value);
|
||||||
|
|
||||||
|
const emptyParent = parent === '';
|
||||||
|
const isWindowsService = isWindows && mode === 'service';
|
||||||
|
|
||||||
|
const handlePathInputChange = useCallback(
|
||||||
|
({ value }: InputChanged<string>) => {
|
||||||
|
setCurrentPath(value);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRowPress = useCallback(
|
||||||
|
(path: string) => {
|
||||||
|
setCurrentPath(path);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
fetchPaths({
|
||||||
|
path,
|
||||||
|
allowFoldersWithoutTrailingSlashes: true,
|
||||||
|
includeFiles,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[includeFiles, dispatch, setCurrentPath]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOkPress = useCallback(() => {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: currentPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(clearPaths());
|
||||||
|
onModalClose();
|
||||||
|
}, [name, currentPath, dispatch, onChange, onModalClose]);
|
||||||
|
|
||||||
|
const handleFetchPaths = useCallback(
|
||||||
|
(path: string) => {
|
||||||
|
dispatch(
|
||||||
|
fetchPaths({
|
||||||
|
path,
|
||||||
|
allowFoldersWithoutTrailingSlashes: true,
|
||||||
|
includeFiles,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[includeFiles, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value !== previousValue && value !== currentPath) {
|
||||||
|
setCurrentPath(value);
|
||||||
|
}
|
||||||
|
}, [value, previousValue, currentPath, setCurrentPath]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
dispatch(
|
||||||
|
fetchPaths({
|
||||||
|
path: currentPath,
|
||||||
|
allowFoldersWithoutTrailingSlashes: true,
|
||||||
|
includeFiles,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearPaths());
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// This should only run once when the component mounts,
|
||||||
|
// so we don't need to include the other dependencies.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('FileBrowser')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody
|
||||||
|
className={styles.modalBody}
|
||||||
|
scrollDirection={scrollDirections.NONE}
|
||||||
|
>
|
||||||
|
{isWindowsService ? (
|
||||||
|
<Alert className={styles.mappedDrivesWarning} kind={kinds.WARNING}>
|
||||||
|
<InlineMarkdown
|
||||||
|
data={translate('MappedNetworkDrivesWindowsService', {
|
||||||
|
url: 'https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<PathInput
|
||||||
|
className={styles.pathInput}
|
||||||
|
placeholder={translate('FileBrowserPlaceholderText')}
|
||||||
|
hasFileBrowser={false}
|
||||||
|
includeFiles={includeFiles}
|
||||||
|
paths={paths}
|
||||||
|
name={name}
|
||||||
|
value={currentPath}
|
||||||
|
onChange={handlePathInputChange}
|
||||||
|
onFetchPaths={handleFetchPaths}
|
||||||
|
onClearPaths={handleClearPaths}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Scroller
|
||||||
|
ref={scrollerRef}
|
||||||
|
className={styles.scroller}
|
||||||
|
scrollDirection="both"
|
||||||
|
>
|
||||||
|
{error ? <div>{translate('ErrorLoadingContents')}</div> : null}
|
||||||
|
|
||||||
|
{isPopulated && !error ? (
|
||||||
|
<Table horizontalScroll={false} columns={columns}>
|
||||||
|
<TableBody>
|
||||||
|
{emptyParent ? (
|
||||||
|
<FileBrowserRow
|
||||||
|
type="computer"
|
||||||
|
name={translate('MyComputer')}
|
||||||
|
path={parent}
|
||||||
|
onPress={handleRowPress}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!emptyParent && parent ? (
|
||||||
|
<FileBrowserRow
|
||||||
|
type="parent"
|
||||||
|
name="..."
|
||||||
|
path={parent}
|
||||||
|
onPress={handleRowPress}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{directories.map((directory) => {
|
||||||
|
return (
|
||||||
|
<FileBrowserRow
|
||||||
|
key={directory.path}
|
||||||
|
type={directory.type}
|
||||||
|
name={directory.name}
|
||||||
|
path={directory.path}
|
||||||
|
onPress={handleRowPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{files.map((file) => {
|
||||||
|
return (
|
||||||
|
<FileBrowserRow
|
||||||
|
key={file.path}
|
||||||
|
type={file.type}
|
||||||
|
name={file.name}
|
||||||
|
path={file.path}
|
||||||
|
onPress={handleRowPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
</Scroller>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
{isFetching ? (
|
||||||
|
<LoadingIndicator className={styles.loading} size={20} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button onPress={handleOkPress}>{translate('Ok')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileBrowserModalContent;
|
@ -1,119 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearPaths, fetchPaths } from 'Store/Actions/pathActions';
|
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
|
||||||
import FileBrowserModalContent from './FileBrowserModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.paths,
|
|
||||||
createSystemStatusSelector(),
|
|
||||||
(paths, systemStatus) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
parent,
|
|
||||||
currentPath,
|
|
||||||
directories,
|
|
||||||
files
|
|
||||||
} = paths;
|
|
||||||
|
|
||||||
const filteredPaths = _.filter([...directories, ...files], ({ path }) => {
|
|
||||||
return path.toLowerCase().startsWith(currentPath.toLowerCase());
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
parent,
|
|
||||||
currentPath,
|
|
||||||
directories,
|
|
||||||
files,
|
|
||||||
paths: filteredPaths,
|
|
||||||
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchPaths: fetchPaths,
|
|
||||||
dispatchClearPaths: clearPaths
|
|
||||||
};
|
|
||||||
|
|
||||||
class FileBrowserModalContentConnector extends Component {
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
includeFiles,
|
|
||||||
dispatchFetchPaths
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
dispatchFetchPaths({
|
|
||||||
path: value,
|
|
||||||
allowFoldersWithoutTrailingSlashes: true,
|
|
||||||
includeFiles
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFetchPaths = (path) => {
|
|
||||||
const {
|
|
||||||
includeFiles,
|
|
||||||
dispatchFetchPaths
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
dispatchFetchPaths({
|
|
||||||
path,
|
|
||||||
allowFoldersWithoutTrailingSlashes: true,
|
|
||||||
includeFiles
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onClearPaths = () => {
|
|
||||||
// this.props.dispatchClearPaths();
|
|
||||||
};
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.props.dispatchClearPaths();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<FileBrowserModalContent
|
|
||||||
onFetchPaths={this.onFetchPaths}
|
|
||||||
onClearPaths={this.onClearPaths}
|
|
||||||
{...this.props}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModalContentConnector.propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
includeFiles: PropTypes.bool.isRequired,
|
|
||||||
dispatchFetchPaths: PropTypes.func.isRequired,
|
|
||||||
dispatchClearPaths: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
FileBrowserModalContentConnector.defaultProps = {
|
|
||||||
includeFiles: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(FileBrowserModalContentConnector);
|
|
@ -1,62 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableRowButton from 'Components/Table/TableRowButton';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import styles from './FileBrowserRow.css';
|
|
||||||
|
|
||||||
function getIconName(type) {
|
|
||||||
switch (type) {
|
|
||||||
case 'computer':
|
|
||||||
return icons.COMPUTER;
|
|
||||||
case 'drive':
|
|
||||||
return icons.DRIVE;
|
|
||||||
case 'file':
|
|
||||||
return icons.FILE;
|
|
||||||
case 'parent':
|
|
||||||
return icons.PARENT;
|
|
||||||
default:
|
|
||||||
return icons.FOLDER;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FileBrowserRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
this.props.onPress(this.props.path);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
name
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowButton onPress={this.onPress}>
|
|
||||||
<TableRowCell className={styles.type}>
|
|
||||||
<Icon name={getIconName(type)} />
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell>{name}</TableRowCell>
|
|
||||||
</TableRowButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserRow.propTypes = {
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
path: PropTypes.string.isRequired,
|
|
||||||
onPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileBrowserRow;
|
|
@ -0,0 +1,49 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { PathType } from 'App/State/PathsAppState';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableRowButton from 'Components/Table/TableRowButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import styles from './FileBrowserRow.css';
|
||||||
|
|
||||||
|
function getIconName(type: PathType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'computer':
|
||||||
|
return icons.COMPUTER;
|
||||||
|
case 'drive':
|
||||||
|
return icons.DRIVE;
|
||||||
|
case 'file':
|
||||||
|
return icons.FILE;
|
||||||
|
case 'parent':
|
||||||
|
return icons.PARENT;
|
||||||
|
default:
|
||||||
|
return icons.FOLDER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileBrowserRowProps {
|
||||||
|
type: PathType;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
onPress: (path: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileBrowserRow(props: FileBrowserRowProps) {
|
||||||
|
const { type, name, path, onPress } = props;
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
onPress(path);
|
||||||
|
}, [path, onPress]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowButton onPress={handlePress}>
|
||||||
|
<TableRowCell className={styles.type}>
|
||||||
|
<Icon name={getIconName(type)} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>{name}</TableRowCell>
|
||||||
|
</TableRowButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileBrowserRow;
|
@ -0,0 +1,36 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
function createPathsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.paths,
|
||||||
|
(paths) => {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
parent,
|
||||||
|
currentPath,
|
||||||
|
directories,
|
||||||
|
files,
|
||||||
|
} = paths;
|
||||||
|
|
||||||
|
const filteredPaths = [...directories, ...files].filter(({ path }) => {
|
||||||
|
return path.toLowerCase().startsWith(currentPath.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
parent,
|
||||||
|
currentPath,
|
||||||
|
directories,
|
||||||
|
files,
|
||||||
|
paths: filteredPaths,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createPathsSelector;
|
@ -1,50 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './LoadingIndicator.css';
|
|
||||||
|
|
||||||
function LoadingIndicator({ className, rippleClassName, size }) {
|
|
||||||
const sizeInPx = `${size}px`;
|
|
||||||
const width = sizeInPx;
|
|
||||||
const height = sizeInPx;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={className}
|
|
||||||
style={{ height }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={styles.rippleContainer}
|
|
||||||
style={{ width, height }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={rippleClassName}
|
|
||||||
style={{ width, height }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={rippleClassName}
|
|
||||||
style={{ width, height }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={rippleClassName}
|
|
||||||
style={{ width, height }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadingIndicator.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
rippleClassName: PropTypes.string,
|
|
||||||
size: PropTypes.number
|
|
||||||
};
|
|
||||||
|
|
||||||
LoadingIndicator.defaultProps = {
|
|
||||||
className: styles.loading,
|
|
||||||
rippleClassName: styles.ripple,
|
|
||||||
size: 50
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoadingIndicator;
|
|
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './LoadingIndicator.css';
|
||||||
|
|
||||||
|
interface LoadingIndicatorProps {
|
||||||
|
className?: string;
|
||||||
|
rippleClassName?: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadingIndicator({
|
||||||
|
className = styles.loading,
|
||||||
|
rippleClassName = styles.ripple,
|
||||||
|
size = 50,
|
||||||
|
}: LoadingIndicatorProps) {
|
||||||
|
const sizeInPx = `${size}px`;
|
||||||
|
const width = sizeInPx;
|
||||||
|
const height = sizeInPx;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} style={{ height }}>
|
||||||
|
<div className={styles.rippleContainer} style={{ width, height }}>
|
||||||
|
<div className={rippleClassName} style={{ width, height }} />
|
||||||
|
|
||||||
|
<div className={rippleClassName} style={{ width, height }} />
|
||||||
|
|
||||||
|
<div className={rippleClassName} style={{ width, height }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadingIndicator;
|
@ -1,74 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
|
|
||||||
class InlineMarkdown extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
data,
|
|
||||||
blockClassName
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// For now only replace links or code blocks (not both)
|
|
||||||
const markdownBlocks = [];
|
|
||||||
if (data) {
|
|
||||||
const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
|
|
||||||
|
|
||||||
let endIndex = 0;
|
|
||||||
let match = null;
|
|
||||||
|
|
||||||
while ((match = linkRegex.exec(data)) !== null) {
|
|
||||||
if (match.index > endIndex) {
|
|
||||||
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
|
|
||||||
endIndex = match.index + match[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endIndex !== data.length && markdownBlocks.length > 0) {
|
|
||||||
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g);
|
|
||||||
|
|
||||||
endIndex = 0;
|
|
||||||
match = null;
|
|
||||||
let matchedCode = false;
|
|
||||||
|
|
||||||
while ((match = codeRegex.exec(data)) !== null) {
|
|
||||||
matchedCode = true;
|
|
||||||
|
|
||||||
if (match.index > endIndex) {
|
|
||||||
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
markdownBlocks.push(<code key={`code-${match.index}`} className={blockClassName ?? null}>{match[0].substring(1, match[0].length - 1)}</code>);
|
|
||||||
endIndex = match.index + match[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) {
|
|
||||||
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (markdownBlocks.length === 0) {
|
|
||||||
markdownBlocks.push(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span className={className}>{markdownBlocks}</span>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
InlineMarkdown.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
data: PropTypes.string,
|
|
||||||
blockClassName: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InlineMarkdown;
|
|
@ -0,0 +1,75 @@
|
|||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
|
||||||
|
interface InlineMarkdownProps {
|
||||||
|
className?: string;
|
||||||
|
data?: string;
|
||||||
|
blockClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InlineMarkdown(props: InlineMarkdownProps) {
|
||||||
|
const { className, data, blockClassName } = props;
|
||||||
|
|
||||||
|
// For now only replace links or code blocks (not both)
|
||||||
|
const markdownBlocks: (ReactElement | string)[] = [];
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
|
||||||
|
|
||||||
|
let endIndex = 0;
|
||||||
|
let match = null;
|
||||||
|
|
||||||
|
while ((match = linkRegex.exec(data)) !== null) {
|
||||||
|
if (match.index > endIndex) {
|
||||||
|
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
markdownBlocks.push(
|
||||||
|
<Link key={match.index} to={match[2]}>
|
||||||
|
{match[1]}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
endIndex = match.index + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endIndex !== data.length && markdownBlocks.length > 0) {
|
||||||
|
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g);
|
||||||
|
|
||||||
|
endIndex = 0;
|
||||||
|
match = null;
|
||||||
|
let matchedCode = false;
|
||||||
|
|
||||||
|
while ((match = codeRegex.exec(data)) !== null) {
|
||||||
|
matchedCode = true;
|
||||||
|
|
||||||
|
if (match.index > endIndex) {
|
||||||
|
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
markdownBlocks.push(
|
||||||
|
<code
|
||||||
|
key={`code-${match.index}`}
|
||||||
|
className={blockClassName ?? undefined}
|
||||||
|
>
|
||||||
|
{match[0].substring(1, match[0].length - 1)}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
endIndex = match.index + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) {
|
||||||
|
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markdownBlocks.length === 0) {
|
||||||
|
markdownBlocks.push(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span className={className}>{markdownBlocks}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InlineMarkdown;
|
@ -1,80 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './MonitorToggleButton.css';
|
|
||||||
|
|
||||||
function getTooltip(monitored, isDisabled) {
|
|
||||||
if (isDisabled) {
|
|
||||||
return translate('ToggleMonitoredSeriesUnmonitored ');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monitored) {
|
|
||||||
return translate('ToggleMonitoredToUnmonitored');
|
|
||||||
}
|
|
||||||
|
|
||||||
return translate('ToggleUnmonitoredToMonitored');
|
|
||||||
}
|
|
||||||
|
|
||||||
class MonitorToggleButton extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = (event) => {
|
|
||||||
const shiftKey = event.nativeEvent.shiftKey;
|
|
||||||
|
|
||||||
this.props.onPress(!this.props.monitored, { shiftKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
monitored,
|
|
||||||
isDisabled,
|
|
||||||
isSaving,
|
|
||||||
size,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const iconName = monitored ? icons.MONITORED : icons.UNMONITORED;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SpinnerIconButton
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
isDisabled && styles.isDisabled
|
|
||||||
)}
|
|
||||||
name={iconName}
|
|
||||||
size={size}
|
|
||||||
title={getTooltip(monitored, isDisabled)}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
isSpinning={isSaving}
|
|
||||||
{...otherProps}
|
|
||||||
onPress={this.onPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MonitorToggleButton.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
size: PropTypes.number,
|
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
onPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
MonitorToggleButton.defaultProps = {
|
|
||||||
className: styles.toggleButton,
|
|
||||||
isDisabled: false,
|
|
||||||
isSaving: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MonitorToggleButton;
|
|
@ -0,0 +1,65 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { SyntheticEvent, useCallback, useMemo } from 'react';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './MonitorToggleButton.css';
|
||||||
|
|
||||||
|
interface MonitorToggleButtonProps {
|
||||||
|
className?: string;
|
||||||
|
monitored: boolean;
|
||||||
|
size?: number;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isSaving?: boolean;
|
||||||
|
onPress: (value: boolean, options: { shiftKey: boolean }) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MonitorToggleButton(props: MonitorToggleButtonProps) {
|
||||||
|
const {
|
||||||
|
className = styles.toggleButton,
|
||||||
|
monitored,
|
||||||
|
isDisabled = false,
|
||||||
|
isSaving = false,
|
||||||
|
size,
|
||||||
|
onPress,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const iconName = monitored ? icons.MONITORED : icons.UNMONITORED;
|
||||||
|
|
||||||
|
const title = useMemo(() => {
|
||||||
|
if (isDisabled) {
|
||||||
|
return translate('ToggleMonitoredSeriesUnmonitored');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitored) {
|
||||||
|
return translate('ToggleMonitoredToUnmonitored');
|
||||||
|
}
|
||||||
|
|
||||||
|
return translate('ToggleUnmonitoredToMonitored');
|
||||||
|
}, [monitored, isDisabled]);
|
||||||
|
|
||||||
|
const handlePress = useCallback(
|
||||||
|
(event: SyntheticEvent<HTMLLinkElement, MouseEvent>) => {
|
||||||
|
const shiftKey = event.nativeEvent.shiftKey;
|
||||||
|
|
||||||
|
onPress(!monitored, { shiftKey });
|
||||||
|
},
|
||||||
|
[monitored, onPress]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpinnerIconButton
|
||||||
|
className={classNames(className, isDisabled && styles.isDisabled)}
|
||||||
|
name={iconName}
|
||||||
|
size={size}
|
||||||
|
title={title}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
{...otherProps}
|
||||||
|
onPress={handlePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MonitorToggleButton;
|
@ -1,18 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
|
|
||||||
function Portal(props) {
|
|
||||||
const { children, target } = props;
|
|
||||||
return ReactDOM.createPortal(children, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
Portal.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
target: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Portal.defaultProps = {
|
|
||||||
target: document.getElementById('portal-root')
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Portal;
|
|
@ -0,0 +1,20 @@
|
|||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
interface PortalProps {
|
||||||
|
children: Parameters<typeof ReactDOM.createPortal>[0];
|
||||||
|
target?: Parameters<typeof ReactDOM.createPortal>[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTarget = document.getElementById('portal-root');
|
||||||
|
|
||||||
|
function Portal(props: PortalProps) {
|
||||||
|
const { children, target = defaultTarget } = props;
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(children, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Portal;
|
@ -1,44 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Switch as RouterSwitch } from 'react-router-dom';
|
|
||||||
import { map } from 'Helpers/elementChildren';
|
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
|
||||||
|
|
||||||
class Switch extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
children
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RouterSwitch>
|
|
||||||
{
|
|
||||||
map(children, (child) => {
|
|
||||||
const {
|
|
||||||
path: childPath,
|
|
||||||
addUrlBase = true
|
|
||||||
} = child.props;
|
|
||||||
|
|
||||||
if (!childPath) {
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = addUrlBase ? getPathWithUrlBase(childPath) : childPath;
|
|
||||||
|
|
||||||
return React.cloneElement(child, { path });
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</RouterSwitch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Switch.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Switch;
|
|
@ -0,0 +1,38 @@
|
|||||||
|
import React, { Children, ReactElement, ReactNode } from 'react';
|
||||||
|
import { Switch as RouterSwitch } from 'react-router-dom';
|
||||||
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
|
|
||||||
|
interface ExtendedRoute {
|
||||||
|
path: string;
|
||||||
|
addUrlBase?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SwitchProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Switch({ children }: SwitchProps) {
|
||||||
|
return (
|
||||||
|
<RouterSwitch>
|
||||||
|
{Children.map(children, (child) => {
|
||||||
|
if (!React.isValidElement<ExtendedRoute>(child)) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elementChild: ReactElement<ExtendedRoute> = child;
|
||||||
|
|
||||||
|
const { path: childPath, addUrlBase = true } = elementChild.props;
|
||||||
|
|
||||||
|
if (!childPath) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = addUrlBase ? getPathWithUrlBase(childPath) : childPath;
|
||||||
|
|
||||||
|
return React.cloneElement(child, { path });
|
||||||
|
})}
|
||||||
|
</RouterSwitch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Switch;
|
@ -1,179 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Scrollbars } from 'react-custom-scrollbars-2';
|
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
|
||||||
import styles from './OverlayScroller.css';
|
|
||||||
|
|
||||||
const SCROLLBAR_SIZE = 10;
|
|
||||||
|
|
||||||
class OverlayScroller extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._scroller = null;
|
|
||||||
this._isScrolling = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
scrollTop
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!this._isScrolling &&
|
|
||||||
scrollTop != null &&
|
|
||||||
scrollTop !== prevProps.scrollTop
|
|
||||||
) {
|
|
||||||
this._scroller.scrollTop(scrollTop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
_setScrollRef = (ref) => {
|
|
||||||
this._scroller = ref;
|
|
||||||
|
|
||||||
if (ref) {
|
|
||||||
this.props.registerScroller(ref.view);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_renderThumb = (props) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={this.props.trackClassName}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
_renderTrackHorizontal = ({ style, props }) => {
|
|
||||||
const finalStyle = {
|
|
||||||
...style,
|
|
||||||
right: 2,
|
|
||||||
bottom: 2,
|
|
||||||
left: 2,
|
|
||||||
borderRadius: 3,
|
|
||||||
height: SCROLLBAR_SIZE
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.track}
|
|
||||||
style={finalStyle}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
_renderTrackVertical = ({ style, props }) => {
|
|
||||||
const finalStyle = {
|
|
||||||
...style,
|
|
||||||
right: 2,
|
|
||||||
bottom: 2,
|
|
||||||
top: 2,
|
|
||||||
borderRadius: 3,
|
|
||||||
width: SCROLLBAR_SIZE
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.track}
|
|
||||||
style={finalStyle}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
_renderView = (props) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={this.props.className}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listers
|
|
||||||
|
|
||||||
onScrollStart = () => {
|
|
||||||
this._isScrolling = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
onScrollStop = () => {
|
|
||||||
this._isScrolling = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
onScroll = (event) => {
|
|
||||||
const {
|
|
||||||
scrollTop,
|
|
||||||
scrollLeft
|
|
||||||
} = event.currentTarget;
|
|
||||||
|
|
||||||
this._isScrolling = true;
|
|
||||||
const onScroll = this.props.onScroll;
|
|
||||||
|
|
||||||
if (onScroll) {
|
|
||||||
onScroll({ scrollTop, scrollLeft });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
autoHide,
|
|
||||||
autoScroll,
|
|
||||||
children
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Scrollbars
|
|
||||||
ref={this._setScrollRef}
|
|
||||||
autoHide={autoHide}
|
|
||||||
hideTracksWhenNotNeeded={autoScroll}
|
|
||||||
renderTrackHorizontal={this._renderTrackHorizontal}
|
|
||||||
renderTrackVertical={this._renderTrackVertical}
|
|
||||||
renderThumbHorizontal={this._renderThumb}
|
|
||||||
renderThumbVertical={this._renderThumb}
|
|
||||||
renderView={this._renderView}
|
|
||||||
onScrollStart={this.onScrollStart}
|
|
||||||
onScrollStop={this.onScrollStop}
|
|
||||||
onScroll={this.onScroll}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Scrollbars>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
OverlayScroller.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
trackClassName: PropTypes.string,
|
|
||||||
scrollTop: PropTypes.number,
|
|
||||||
scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]).isRequired,
|
|
||||||
autoHide: PropTypes.bool.isRequired,
|
|
||||||
autoScroll: PropTypes.bool.isRequired,
|
|
||||||
children: PropTypes.node,
|
|
||||||
onScroll: PropTypes.func,
|
|
||||||
registerScroller: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
OverlayScroller.defaultProps = {
|
|
||||||
className: styles.scroller,
|
|
||||||
trackClassName: styles.thumb,
|
|
||||||
scrollDirection: scrollDirections.VERTICAL,
|
|
||||||
autoHide: false,
|
|
||||||
autoScroll: true,
|
|
||||||
registerScroller: () => { /* no-op */ }
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OverlayScroller;
|
|
@ -0,0 +1,127 @@
|
|||||||
|
import React, { ComponentPropsWithoutRef, useCallback, useRef } from 'react';
|
||||||
|
import { Scrollbars } from 'react-custom-scrollbars-2';
|
||||||
|
import { ScrollDirection } from 'Helpers/Props/scrollDirections';
|
||||||
|
import { OnScroll } from './Scroller';
|
||||||
|
import styles from './OverlayScroller.css';
|
||||||
|
|
||||||
|
const SCROLLBAR_SIZE = 10;
|
||||||
|
|
||||||
|
interface OverlayScrollerProps {
|
||||||
|
className?: string;
|
||||||
|
trackClassName?: string;
|
||||||
|
scrollTop?: number;
|
||||||
|
scrollDirection: ScrollDirection;
|
||||||
|
autoHide: boolean;
|
||||||
|
autoScroll: boolean;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onScroll?: (payload: OnScroll) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScrollbarTrackProps {
|
||||||
|
style: React.CSSProperties;
|
||||||
|
props: ComponentPropsWithoutRef<'div'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverlayScroller(props: OverlayScrollerProps) {
|
||||||
|
const {
|
||||||
|
autoHide = false,
|
||||||
|
autoScroll = true,
|
||||||
|
className = styles.scroller,
|
||||||
|
trackClassName = styles.thumb,
|
||||||
|
children,
|
||||||
|
onScroll,
|
||||||
|
} = props;
|
||||||
|
const scrollBarRef = useRef<Scrollbars>(null);
|
||||||
|
const isScrolling = useRef(false);
|
||||||
|
|
||||||
|
const handleScrollStart = useCallback(() => {
|
||||||
|
isScrolling.current = true;
|
||||||
|
}, []);
|
||||||
|
const handleScrollStop = useCallback(() => {
|
||||||
|
isScrolling.current = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
if (!scrollBarRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { scrollTop, scrollLeft } = scrollBarRef.current.getValues();
|
||||||
|
isScrolling.current = true;
|
||||||
|
|
||||||
|
if (onScroll) {
|
||||||
|
onScroll({ scrollTop, scrollLeft });
|
||||||
|
}
|
||||||
|
}, [onScroll]);
|
||||||
|
|
||||||
|
const renderThumb = useCallback(
|
||||||
|
(props: ComponentPropsWithoutRef<'div'>) => {
|
||||||
|
return <div className={trackClassName} {...props} />;
|
||||||
|
},
|
||||||
|
[trackClassName]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTrackHorizontal = useCallback(
|
||||||
|
({ style, props: trackProps }: ScrollbarTrackProps) => {
|
||||||
|
const finalStyle = {
|
||||||
|
...style,
|
||||||
|
right: 2,
|
||||||
|
bottom: 2,
|
||||||
|
left: 2,
|
||||||
|
borderRadius: 3,
|
||||||
|
height: SCROLLBAR_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.track} style={finalStyle} {...trackProps} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTrackVertical = useCallback(
|
||||||
|
({ style, props: trackProps }: ScrollbarTrackProps) => {
|
||||||
|
const finalStyle = {
|
||||||
|
...style,
|
||||||
|
right: 2,
|
||||||
|
bottom: 2,
|
||||||
|
top: 2,
|
||||||
|
borderRadius: 3,
|
||||||
|
width: SCROLLBAR_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.track} style={finalStyle} {...trackProps} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderView = useCallback(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(props: any) => {
|
||||||
|
return <div className={className} {...props} />;
|
||||||
|
},
|
||||||
|
[className]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Scrollbars
|
||||||
|
ref={scrollBarRef}
|
||||||
|
autoHide={autoHide}
|
||||||
|
hideTracksWhenNotNeeded={autoScroll}
|
||||||
|
renderTrackHorizontal={renderTrackHorizontal}
|
||||||
|
renderTrackVertical={renderTrackVertical}
|
||||||
|
renderThumbHorizontal={renderThumb}
|
||||||
|
renderThumbVertical={renderThumb}
|
||||||
|
renderView={renderView}
|
||||||
|
onScrollStart={handleScrollStart}
|
||||||
|
onScrollStop={handleScrollStop}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Scrollbars>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OverlayScroller;
|
@ -1,43 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { tooltipPositions } from 'Helpers/Props';
|
|
||||||
import Tooltip from './Tooltip';
|
|
||||||
import styles from './Popover.css';
|
|
||||||
|
|
||||||
function Popover(props) {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
{...otherProps}
|
|
||||||
bodyClassName={styles.tooltipBody}
|
|
||||||
tooltip={
|
|
||||||
<div>
|
|
||||||
<div className={styles.title}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.body}>
|
|
||||||
{body}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Popover.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
bodyClassName: PropTypes.string,
|
|
||||||
anchor: PropTypes.node.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
|
||||||
position: PropTypes.oneOf(tooltipPositions.all),
|
|
||||||
canFlip: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Popover;
|
|
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Tooltip, { TooltipProps } from './Tooltip';
|
||||||
|
import styles from './Popover.css';
|
||||||
|
|
||||||
|
interface PopoverProps extends Omit<TooltipProps, 'tooltip' | 'bodyClassName'> {
|
||||||
|
title: string;
|
||||||
|
body: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Popover({ title, body, ...otherProps }: PopoverProps) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
{...otherProps}
|
||||||
|
bodyClassName={styles.tooltipBody}
|
||||||
|
tooltip={
|
||||||
|
<div>
|
||||||
|
<div className={styles.title}>{title}</div>
|
||||||
|
|
||||||
|
<div className={styles.body}>{body}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Popover;
|
@ -1,235 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
|
||||||
import Portal from 'Components/Portal';
|
|
||||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
|
||||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
|
||||||
import styles from './Tooltip.css';
|
|
||||||
|
|
||||||
let maxWidth = null;
|
|
||||||
|
|
||||||
function getMaxWidth() {
|
|
||||||
const windowWidth = window.innerWidth;
|
|
||||||
|
|
||||||
if (windowWidth >= parseInt(dimensions.breakpointLarge)) {
|
|
||||||
maxWidth = 800;
|
|
||||||
} else if (windowWidth >= parseInt(dimensions.breakpointMedium)) {
|
|
||||||
maxWidth = 650;
|
|
||||||
} else if (windowWidth >= parseInt(dimensions.breakpointSmall)) {
|
|
||||||
maxWidth = 500;
|
|
||||||
} else {
|
|
||||||
maxWidth = 450;
|
|
||||||
}
|
|
||||||
|
|
||||||
return maxWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Tooltip extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._scheduleUpdate = null;
|
|
||||||
this._closeTimeout = null;
|
|
||||||
this._maxWidth = maxWidth || getMaxWidth();
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
if (this._scheduleUpdate && this.state.isOpen) {
|
|
||||||
this._scheduleUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this._closeTimeout) {
|
|
||||||
this._closeTimeout = clearTimeout(this._closeTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
computeMaxSize = (data) => {
|
|
||||||
const {
|
|
||||||
top,
|
|
||||||
right,
|
|
||||||
bottom,
|
|
||||||
left
|
|
||||||
} = data.offsets.reference;
|
|
||||||
|
|
||||||
const windowWidth = window.innerWidth;
|
|
||||||
const windowHeight = window.innerHeight;
|
|
||||||
|
|
||||||
if ((/^top/).test(data.placement)) {
|
|
||||||
data.styles.maxHeight = top - 20;
|
|
||||||
} else if ((/^bottom/).test(data.placement)) {
|
|
||||||
data.styles.maxHeight = windowHeight - bottom - 20;
|
|
||||||
} else if ((/^right/).test(data.placement)) {
|
|
||||||
data.styles.maxWidth = Math.min(this._maxWidth, windowWidth - right - 20);
|
|
||||||
data.styles.maxHeight = top - 20;
|
|
||||||
} else {
|
|
||||||
data.styles.maxWidth = Math.min(this._maxWidth, left - 20);
|
|
||||||
data.styles.maxHeight = top - 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMeasure = ({ width }) => {
|
|
||||||
this.setState({ width });
|
|
||||||
};
|
|
||||||
|
|
||||||
onClick = () => {
|
|
||||||
if (isMobileUtil()) {
|
|
||||||
this.setState({ isOpen: !this.state.isOpen });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMouseEnter = () => {
|
|
||||||
if (this._closeTimeout) {
|
|
||||||
this._closeTimeout = clearTimeout(this._closeTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ isOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onMouseLeave = () => {
|
|
||||||
this._closeTimeout = setTimeout(() => {
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
bodyClassName,
|
|
||||||
anchor,
|
|
||||||
tooltip,
|
|
||||||
kind,
|
|
||||||
position,
|
|
||||||
canFlip
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Manager>
|
|
||||||
<Reference>
|
|
||||||
{({ ref }) => (
|
|
||||||
<span
|
|
||||||
ref={ref}
|
|
||||||
className={className}
|
|
||||||
onClick={this.onClick}
|
|
||||||
onMouseEnter={this.onMouseEnter}
|
|
||||||
onMouseLeave={this.onMouseLeave}
|
|
||||||
>
|
|
||||||
{anchor}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Reference>
|
|
||||||
|
|
||||||
<Portal>
|
|
||||||
<Popper
|
|
||||||
placement={position}
|
|
||||||
// Disable events to improve performance when many tooltips
|
|
||||||
// are shown (Quality Definitions for example).
|
|
||||||
eventsEnabled={false}
|
|
||||||
modifiers={{
|
|
||||||
computeMaxHeight: {
|
|
||||||
order: 851,
|
|
||||||
enabled: true,
|
|
||||||
fn: this.computeMaxSize
|
|
||||||
},
|
|
||||||
preventOverflow: {
|
|
||||||
// Fixes positioning for tooltips in the queue
|
|
||||||
// and likely others.
|
|
||||||
escapeWithReference: false
|
|
||||||
},
|
|
||||||
flip: {
|
|
||||||
enabled: canFlip
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
|
|
||||||
this._scheduleUpdate = scheduleUpdate;
|
|
||||||
|
|
||||||
const popperPlacement = placement ? placement.split('-')[0] : position;
|
|
||||||
const vertical = popperPlacement === 'top' || popperPlacement === 'bottom';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={classNames(
|
|
||||||
styles.tooltipContainer,
|
|
||||||
vertical ? styles.verticalContainer : styles.horizontalContainer
|
|
||||||
)}
|
|
||||||
style={style}
|
|
||||||
onMouseEnter={this.onMouseEnter}
|
|
||||||
onMouseLeave={this.onMouseLeave}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={this.state.isOpen ? classNames(
|
|
||||||
styles.arrow,
|
|
||||||
styles[kind],
|
|
||||||
styles[popperPlacement]
|
|
||||||
) : styles.arrowDisabled}
|
|
||||||
ref={arrowProps.ref}
|
|
||||||
style={arrowProps.style}
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
this.state.isOpen ?
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.tooltip,
|
|
||||||
styles[kind]
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={bodyClassName}
|
|
||||||
>
|
|
||||||
{tooltip}
|
|
||||||
</div>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Popper>
|
|
||||||
</Portal>
|
|
||||||
</Manager>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Tooltip.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
bodyClassName: PropTypes.string.isRequired,
|
|
||||||
anchor: PropTypes.node.isRequired,
|
|
||||||
tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
|
||||||
kind: PropTypes.oneOf([kinds.DEFAULT, kinds.INVERSE]),
|
|
||||||
position: PropTypes.oneOf(tooltipPositions.all),
|
|
||||||
canFlip: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Tooltip.defaultProps = {
|
|
||||||
bodyClassName: styles.body,
|
|
||||||
kind: kinds.DEFAULT,
|
|
||||||
position: tooltipPositions.TOP,
|
|
||||||
canFlip: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Tooltip;
|
|
@ -0,0 +1,216 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
|
import Portal from 'Components/Portal';
|
||||||
|
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import { Kind } from 'Helpers/Props/kinds';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||||
|
import styles from './Tooltip.css';
|
||||||
|
|
||||||
|
export interface TooltipProps {
|
||||||
|
className?: string;
|
||||||
|
bodyClassName?: string;
|
||||||
|
anchor: React.ReactNode;
|
||||||
|
tooltip: string | React.ReactNode;
|
||||||
|
kind?: Extract<Kind, keyof typeof styles>;
|
||||||
|
position?: (typeof tooltipPositions.all)[number];
|
||||||
|
canFlip?: boolean;
|
||||||
|
}
|
||||||
|
function Tooltip(props: TooltipProps) {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
bodyClassName = styles.body,
|
||||||
|
anchor,
|
||||||
|
tooltip,
|
||||||
|
kind = kinds.DEFAULT,
|
||||||
|
position = tooltipPositions.TOP,
|
||||||
|
canFlip = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const closeTimeout = useRef(0);
|
||||||
|
const updater = useRef<(() => void) | null>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
if (!isMobileUtil()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsOpen((isOpen) => {
|
||||||
|
return !isOpen;
|
||||||
|
});
|
||||||
|
}, [setIsOpen]);
|
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(() => {
|
||||||
|
// Mobile will fire mouse enter and click events rapidly,
|
||||||
|
// this causes the tooltip not to open on the first press.
|
||||||
|
// Ignore the mouse enter event on mobile.
|
||||||
|
if (isMobileUtil()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeTimeout.current) {
|
||||||
|
window.clearTimeout(closeTimeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsOpen(true);
|
||||||
|
}, [setIsOpen]);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
// Still listen for mouse leave on mobile to allow clicks outside to close the tooltip.
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}, 100);
|
||||||
|
}, [setIsOpen]);
|
||||||
|
|
||||||
|
const maxWidth = useMemo(() => {
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
|
||||||
|
if (windowWidth >= parseInt(dimensions.breakpointLarge)) {
|
||||||
|
return 800;
|
||||||
|
} else if (windowWidth >= parseInt(dimensions.breakpointMedium)) {
|
||||||
|
return 650;
|
||||||
|
} else if (windowWidth >= parseInt(dimensions.breakpointSmall)) {
|
||||||
|
return 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 450;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const computeMaxSize = useCallback(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(data: any) => {
|
||||||
|
const { top, right, bottom, left } = data.offsets.reference;
|
||||||
|
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
|
if (/^top/.test(data.placement)) {
|
||||||
|
data.styles.maxHeight = top - 20;
|
||||||
|
} else if (/^bottom/.test(data.placement)) {
|
||||||
|
data.styles.maxHeight = windowHeight - bottom - 20;
|
||||||
|
} else if (/^right/.test(data.placement)) {
|
||||||
|
data.styles.maxWidth = Math.min(maxWidth, windowWidth - right - 20);
|
||||||
|
data.styles.maxHeight = top - 20;
|
||||||
|
} else {
|
||||||
|
data.styles.maxWidth = Math.min(maxWidth, left - 20);
|
||||||
|
data.styles.maxHeight = top - 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
[maxWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentTimeout = closeTimeout.current;
|
||||||
|
|
||||||
|
if (updater.current && isOpen) {
|
||||||
|
updater.current();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (currentTimeout) {
|
||||||
|
window.clearTimeout(currentTimeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Manager>
|
||||||
|
<Reference>
|
||||||
|
{({ ref }) => (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
className={className}
|
||||||
|
onClick={handleClick}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
{anchor}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Reference>
|
||||||
|
|
||||||
|
<Portal>
|
||||||
|
<Popper
|
||||||
|
// @ts-expect-error - PopperJS types are not in sync with our position types.
|
||||||
|
placement={position}
|
||||||
|
// Disable events to improve performance when many tooltips
|
||||||
|
// are shown (Quality Definitions for example).
|
||||||
|
eventsEnabled={false}
|
||||||
|
modifiers={{
|
||||||
|
computeMaxHeight: {
|
||||||
|
order: 851,
|
||||||
|
enabled: true,
|
||||||
|
fn: computeMaxSize,
|
||||||
|
},
|
||||||
|
preventOverflow: {
|
||||||
|
// Fixes positioning for tooltips in the queue
|
||||||
|
// and likely others.
|
||||||
|
escapeWithReference: false,
|
||||||
|
},
|
||||||
|
flip: {
|
||||||
|
enabled: canFlip,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
|
||||||
|
updater.current = scheduleUpdate;
|
||||||
|
|
||||||
|
const popperPlacement = placement
|
||||||
|
? placement.split('-')[0]
|
||||||
|
: position;
|
||||||
|
const vertical =
|
||||||
|
popperPlacement === 'top' || popperPlacement === 'bottom';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={classNames(
|
||||||
|
styles.tooltipContainer,
|
||||||
|
vertical
|
||||||
|
? styles.verticalContainer
|
||||||
|
: styles.horizontalContainer
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={arrowProps.ref}
|
||||||
|
className={
|
||||||
|
isOpen
|
||||||
|
? classNames(
|
||||||
|
styles.arrow,
|
||||||
|
styles[kind],
|
||||||
|
// @ts-expect-error - is a string that may not exist in styles
|
||||||
|
styles[popperPlacement]
|
||||||
|
)
|
||||||
|
: styles.arrowDisabled
|
||||||
|
}
|
||||||
|
style={arrowProps.style}
|
||||||
|
/>
|
||||||
|
{isOpen ? (
|
||||||
|
<div className={classNames(styles.tooltip, styles[kind])}>
|
||||||
|
<div className={bodyClassName}>{tooltip}</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Popper>
|
||||||
|
</Portal>
|
||||||
|
</Manager>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tooltip;
|
@ -1,8 +0,0 @@
|
|||||||
enum ScrollDirection {
|
|
||||||
Horizontal = 'horizontal',
|
|
||||||
Vertical = 'vertical',
|
|
||||||
None = 'none',
|
|
||||||
Both = 'both',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ScrollDirection;
|
|
@ -1,6 +0,0 @@
|
|||||||
enum SortDirection {
|
|
||||||
Ascending = 'ascending',
|
|
||||||
Descending = 'descending',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SortDirection;
|
|
@ -1,3 +0,0 @@
|
|||||||
type TooltipPosition = 'top' | 'right' | 'bottom' | 'left';
|
|
||||||
|
|
||||||
export default TooltipPosition;
|
|
Loading…
Reference in new issue