Implemented first draft of issue modal
This commit is contained in:
@@ -26,6 +26,21 @@ export const StyledButton = styled.button`
|
||||
}
|
||||
`;
|
||||
|
||||
const colored = css`
|
||||
color: #fff;
|
||||
background: ${props => color[props.color]};
|
||||
${font.medium}
|
||||
&:not(:disabled) {
|
||||
&:hover {
|
||||
background: ${props => mixin.lighten(color[props.color], 0.15)};
|
||||
}
|
||||
&:active {
|
||||
background: ${props => mixin.darken(color[props.color], 0.1)};
|
||||
}
|
||||
${props => props.isActive && `background: ${mixin.darken(color[props.color], 0.1)} !important;`}
|
||||
}
|
||||
`;
|
||||
|
||||
const secondaryAndEmptyShared = css`
|
||||
color: ${color.textDark};
|
||||
${font.regular}
|
||||
@@ -35,36 +50,21 @@ const secondaryAndEmptyShared = css`
|
||||
}
|
||||
&:active {
|
||||
color: ${color.primary};
|
||||
background: ${mixin.rgba(color.primary, 0.15)};
|
||||
background: ${color.backgroundLightPrimary};
|
||||
}
|
||||
${props =>
|
||||
props.isActive &&
|
||||
`
|
||||
color: ${color.primary};
|
||||
background: ${mixin.rgba(color.primary, 0.15)} !important;
|
||||
background: ${color.backgroundLightPrimary} !important;
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
const buttonColors = {
|
||||
primary: css`
|
||||
color: #fff;
|
||||
background: ${color.primary};
|
||||
${font.medium}
|
||||
&:not(:disabled) {
|
||||
&:hover {
|
||||
background: ${mixin.lighten(color.primary, 0.15)};
|
||||
}
|
||||
&:active {
|
||||
background: ${mixin.darken(color.primary, 0.1)};
|
||||
}
|
||||
${props =>
|
||||
props.isActive &&
|
||||
`
|
||||
background: ${mixin.darken(color.primary, 0.1)} !important;
|
||||
`}
|
||||
}
|
||||
`,
|
||||
primary: colored,
|
||||
success: colored,
|
||||
danger: colored,
|
||||
secondary: css`
|
||||
background: ${color.secondary};
|
||||
${secondaryAndEmptyShared};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { forwardRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { color } from 'shared/utils/styles';
|
||||
@@ -8,8 +8,8 @@ import { StyledButton, StyledSpinner } from './Styles';
|
||||
const propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
color: PropTypes.oneOf(['primary', 'secondary', 'empty']),
|
||||
icon: PropTypes.string,
|
||||
color: PropTypes.oneOf(['primary', 'secondary', 'empty', 'success', 'danger']),
|
||||
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
iconSize: PropTypes.number,
|
||||
disabled: PropTypes.bool,
|
||||
working: PropTypes.bool,
|
||||
@@ -27,44 +27,55 @@ const defaultProps = {
|
||||
onClick: () => {},
|
||||
};
|
||||
|
||||
const Button = ({
|
||||
children,
|
||||
color: propsColor,
|
||||
icon,
|
||||
iconSize,
|
||||
disabled,
|
||||
working,
|
||||
onClick = () => {},
|
||||
...buttonProps
|
||||
}) => (
|
||||
<StyledButton
|
||||
{...buttonProps}
|
||||
onClick={() => {
|
||||
const Button = forwardRef(
|
||||
(
|
||||
{
|
||||
children,
|
||||
color: propsColor,
|
||||
icon,
|
||||
iconSize,
|
||||
disabled,
|
||||
working,
|
||||
onClick = () => {},
|
||||
...buttonProps
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const handleClick = () => {
|
||||
if (!disabled && !working) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
color={propsColor}
|
||||
disabled={disabled || working}
|
||||
working={working}
|
||||
iconOnly={!children}
|
||||
>
|
||||
{working && (
|
||||
};
|
||||
const renderSpinner = () => (
|
||||
<StyledSpinner
|
||||
iconOnly={!children}
|
||||
size={26}
|
||||
color={propsColor === 'primary' ? '#fff' : color.textDark}
|
||||
/>
|
||||
)}
|
||||
{!working && icon && (
|
||||
);
|
||||
const renderIcon = () => (
|
||||
<Icon
|
||||
type={icon}
|
||||
size={iconSize}
|
||||
color={propsColor === 'primary' ? '#fff' : color.textDark}
|
||||
/>
|
||||
)}
|
||||
<div>{children}</div>
|
||||
</StyledButton>
|
||||
);
|
||||
return (
|
||||
<StyledButton
|
||||
{...buttonProps}
|
||||
onClick={handleClick}
|
||||
color={propsColor}
|
||||
disabled={disabled || working}
|
||||
working={working}
|
||||
iconOnly={!children}
|
||||
ref={ref}
|
||||
>
|
||||
{working && renderSpinner()}
|
||||
{!working && icon && (typeof icon !== 'string' ? icon : renderIcon())}
|
||||
<div>{children}</div>
|
||||
</StyledButton>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.propTypes = propTypes;
|
||||
|
||||
@@ -11,21 +11,21 @@ export const StyledConfirmModal = styled(Modal)`
|
||||
|
||||
export const Title = styled.div`
|
||||
padding-bottom: 25px;
|
||||
${font.bold}
|
||||
${font.size(24)}
|
||||
${font.medium}
|
||||
${font.size(22)}
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
export const Message = styled.p`
|
||||
padding-bottom: 25px;
|
||||
white-space: pre-wrap;
|
||||
${font.size(16)}
|
||||
${font.size(15)}
|
||||
`;
|
||||
|
||||
export const InputLabel = styled.div`
|
||||
padding-bottom: 12px;
|
||||
${font.bold}
|
||||
${font.size(16)}
|
||||
${font.size(15)}
|
||||
`;
|
||||
|
||||
export const StyledInput = styled(Input)`
|
||||
@@ -33,6 +33,10 @@ export const StyledInput = styled(Input)`
|
||||
max-width: 220px;
|
||||
`;
|
||||
|
||||
export const Actions = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const StyledButton = styled(Button)`
|
||||
margin: 5px 20px 0 0;
|
||||
`;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Message,
|
||||
InputLabel,
|
||||
StyledInput,
|
||||
Actions,
|
||||
StyledButton,
|
||||
} from './Styles';
|
||||
|
||||
@@ -76,17 +77,19 @@ const ConfirmModal = ({
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
<StyledButton hollow onClick={modal.close}>
|
||||
{cancelText}
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
color={type}
|
||||
disabled={confirmInput && !isConfirmEnabled}
|
||||
working={isWorking}
|
||||
onClick={() => handleConfirm(modal)}
|
||||
>
|
||||
{confirmText}
|
||||
</StyledButton>
|
||||
<Actions>
|
||||
<StyledButton hollow onClick={modal.close}>
|
||||
{cancelText}
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
color={type}
|
||||
disabled={confirmInput && !isConfirmEnabled}
|
||||
working={isWorking}
|
||||
onClick={() => handleConfirm(modal)}
|
||||
>
|
||||
{confirmText}
|
||||
</StyledButton>
|
||||
</Actions>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
21
client/src/shared/components/CopyLinkButton.jsx
Normal file
21
client/src/shared/components/CopyLinkButton.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { copyToClipboard } from 'shared/utils/clipboard';
|
||||
import { Button } from 'shared/components';
|
||||
|
||||
const CopyLinkButton = ({ ...otherProps }) => {
|
||||
const [isLinkCopied, setLinkCopied] = useState(false);
|
||||
|
||||
const handleLinkCopy = () => {
|
||||
setLinkCopied(true);
|
||||
setTimeout(() => setLinkCopied(false), 2000);
|
||||
copyToClipboard(window.location.href);
|
||||
};
|
||||
return (
|
||||
<Button icon="link" onClick={handleLinkCopy} {...otherProps}>
|
||||
{isLinkCopied ? 'Link Copied' : 'Copy link'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyLinkButton;
|
||||
@@ -14,7 +14,7 @@ export const Dropdown = styled.div`
|
||||
width: 270px;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
${mixin.boxShadowBorderMedium}
|
||||
${mixin.boxShadowDropdown}
|
||||
${props => (props.withTime ? withTimeStyles : '')}
|
||||
`;
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ const codes = {
|
||||
[`issues`]: '\\e908',
|
||||
[`settings`]: '\\e909',
|
||||
[`close`]: '\\e913',
|
||||
[`help-filled`]: '\\e912',
|
||||
[`feedback`]: '\\e915',
|
||||
[`feedback`]: '\\e918',
|
||||
[`trash`]: '\\e912',
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
|
||||
43
client/src/shared/components/InputDebounced.jsx
Normal file
43
client/src/shared/components/InputDebounced.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { Input } from 'shared/components';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
};
|
||||
|
||||
const InputDebounced = ({ onChange, value: propsValue, ...props }) => {
|
||||
const [value, setValue] = useState(propsValue);
|
||||
|
||||
const handleChange = useCallback(
|
||||
debounce(newValue => onChange(newValue), 500),
|
||||
[],
|
||||
);
|
||||
|
||||
const valueRef = useRef(value);
|
||||
valueRef.current = value;
|
||||
|
||||
useEffect(() => {
|
||||
if (propsValue !== valueRef.current) {
|
||||
setValue(propsValue);
|
||||
}
|
||||
}, [propsValue]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
value={value}
|
||||
onChange={newValue => {
|
||||
setValue(newValue);
|
||||
handleChange(newValue);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
InputDebounced.propTypes = propTypes;
|
||||
|
||||
export default InputDebounced;
|
||||
9
client/src/shared/components/IssuePriorityIcon/Styles.js
Normal file
9
client/src/shared/components/IssuePriorityIcon/Styles.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Icon } from 'shared/components';
|
||||
import { issuePriorityColors } from 'shared/utils/styles';
|
||||
|
||||
export const PriorityIcon = styled(Icon)`
|
||||
font-size: 18px;
|
||||
color: ${props => issuePriorityColors[props.color]};
|
||||
`;
|
||||
21
client/src/shared/components/IssuePriorityIcon/index.jsx
Normal file
21
client/src/shared/components/IssuePriorityIcon/index.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { IssuePriority } from 'shared/constants/issues';
|
||||
import { PriorityIcon } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
priority: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const IssuePriorityIcon = ({ priority, ...otherProps }) => {
|
||||
const iconType = [IssuePriority.LOW || IssuePriority.LOWEST].includes(priority)
|
||||
? 'arrow-down'
|
||||
: 'arrow-up';
|
||||
|
||||
return <PriorityIcon type={iconType} color={priority} {...otherProps} />;
|
||||
};
|
||||
|
||||
IssuePriorityIcon.propTypes = propTypes;
|
||||
|
||||
export default IssuePriorityIcon;
|
||||
9
client/src/shared/components/IssueTypeIcon/Styles.js
Normal file
9
client/src/shared/components/IssueTypeIcon/Styles.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Icon } from 'shared/components';
|
||||
import { issueTypeColors } from 'shared/utils/styles';
|
||||
|
||||
export const TypeIcon = styled(Icon)`
|
||||
font-size: 18px;
|
||||
color: ${props => issueTypeColors[props.color]};
|
||||
`;
|
||||
16
client/src/shared/components/IssueTypeIcon/index.jsx
Normal file
16
client/src/shared/components/IssueTypeIcon/index.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { TypeIcon } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const IssueTypeIcon = ({ type, ...otherProps }) => (
|
||||
<TypeIcon type={type} color={type} {...otherProps} />
|
||||
);
|
||||
|
||||
IssueTypeIcon.propTypes = propTypes;
|
||||
|
||||
export default IssueTypeIcon;
|
||||
@@ -41,14 +41,14 @@ export const StyledModal = styled.div`
|
||||
|
||||
const modalStyles = {
|
||||
center: css`
|
||||
max-width: 600px;
|
||||
max-width: ${props => props.width}px;
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
${mixin.boxShadowMedium}
|
||||
`,
|
||||
aside: css`
|
||||
min-height: 100vh;
|
||||
max-width: 500px;
|
||||
max-width: ${props => props.width}px;
|
||||
text-align: left;
|
||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15);
|
||||
`,
|
||||
@@ -57,7 +57,7 @@ const modalStyles = {
|
||||
export const CloseIcon = styled(Icon)`
|
||||
position: absolute;
|
||||
font-size: 25px;
|
||||
color: ${color.textDark};
|
||||
color: ${color.textMedium};
|
||||
${mixin.clickable}
|
||||
${props => closeIconStyles[props.variant]}
|
||||
`;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { uniqueId as uniqueIncreasingIntegerId } from 'lodash';
|
||||
|
||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
|
||||
@@ -10,6 +9,8 @@ import { ScrollOverlay, ClickableOverlay, StyledModal, CloseIcon } from './Style
|
||||
const propTypes = {
|
||||
className: PropTypes.string,
|
||||
variant: PropTypes.oneOf(['center', 'aside']),
|
||||
width: PropTypes.number,
|
||||
withCloseIcon: PropTypes.bool,
|
||||
isOpen: PropTypes.bool,
|
||||
onClose: PropTypes.func,
|
||||
renderLink: PropTypes.func,
|
||||
@@ -19,6 +20,8 @@ const propTypes = {
|
||||
const defaultProps = {
|
||||
className: undefined,
|
||||
variant: 'center',
|
||||
width: 600,
|
||||
withCloseIcon: true,
|
||||
isOpen: undefined,
|
||||
onClose: () => {},
|
||||
renderLink: () => {},
|
||||
@@ -27,6 +30,8 @@ const defaultProps = {
|
||||
const Modal = ({
|
||||
className,
|
||||
variant,
|
||||
width,
|
||||
withCloseIcon,
|
||||
isOpen: propsIsOpen,
|
||||
onClose: tellParentToClose,
|
||||
renderLink,
|
||||
@@ -37,12 +42,9 @@ const Modal = ({
|
||||
const isOpen = isControlled ? propsIsOpen : stateIsOpen;
|
||||
|
||||
const $modalRef = useRef();
|
||||
const modalIdRef = useRef(uniqueIncreasingIntegerId());
|
||||
const $clickableOverlayRef = useRef();
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
if (hasChildModal(modalIdRef.current)) {
|
||||
return;
|
||||
}
|
||||
if (!isControlled) {
|
||||
setStateOpen(false);
|
||||
} else {
|
||||
@@ -50,15 +52,15 @@ const Modal = ({
|
||||
}
|
||||
}, [isControlled, tellParentToClose]);
|
||||
|
||||
useOnOutsideClick($modalRef, isOpen, closeModal);
|
||||
useOnOutsideClick($modalRef, isOpen, closeModal, $clickableOverlayRef);
|
||||
useOnEscapeKeyDown(isOpen, closeModal);
|
||||
useEffect(setBodyScrollLock, [isOpen]);
|
||||
|
||||
const renderModal = () => (
|
||||
<ScrollOverlay data-jira-modal-id={modalIdRef.current}>
|
||||
<ClickableOverlay variant={variant}>
|
||||
<StyledModal className={className} variant={variant} ref={$modalRef}>
|
||||
<CloseIcon type="close" variant={variant} onClick={closeModal} />
|
||||
<ScrollOverlay data-jira-modal="true">
|
||||
<ClickableOverlay variant={variant} ref={$clickableOverlayRef}>
|
||||
<StyledModal className={className} variant={variant} width={width} ref={$modalRef}>
|
||||
{withCloseIcon && <CloseIcon type="close" variant={variant} onClick={closeModal} />}
|
||||
{renderContent({ close: closeModal })}
|
||||
</StyledModal>
|
||||
</ClickableOverlay>
|
||||
@@ -75,15 +77,8 @@ const Modal = ({
|
||||
|
||||
const $root = document.getElementById('root');
|
||||
|
||||
const getIdsOfAllOpenModals = () => {
|
||||
const $modalNodes = Array.from(document.querySelectorAll('[data-jira-modal-id]'));
|
||||
return $modalNodes.map($node => parseInt($node.getAttribute('data-jira-modal-id')));
|
||||
};
|
||||
|
||||
const hasChildModal = modalId => getIdsOfAllOpenModals().some(id => id > modalId);
|
||||
|
||||
const setBodyScrollLock = () => {
|
||||
const areAnyModalsOpen = getIdsOfAllOpenModals().length > 0;
|
||||
const areAnyModalsOpen = !!document.querySelector('[data-jira-modal]');
|
||||
document.body.style.overflow = areAnyModalsOpen ? 'hidden' : 'visible';
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@ import styled from 'styled-components';
|
||||
|
||||
export default styled.div`
|
||||
width: 100%;
|
||||
padding-top: 200px;
|
||||
padding: 200px 0;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
@@ -16,12 +16,14 @@ const propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onCreate: PropTypes.func,
|
||||
isMulti: PropTypes.bool,
|
||||
propsRenderOption: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
value: undefined,
|
||||
onCreate: undefined,
|
||||
isMulti: false,
|
||||
propsRenderOption: undefined,
|
||||
};
|
||||
|
||||
const SelectDropdown = ({
|
||||
@@ -35,6 +37,7 @@ const SelectDropdown = ({
|
||||
onChange,
|
||||
onCreate,
|
||||
isMulti,
|
||||
propsRenderOption,
|
||||
}) => {
|
||||
const [isCreatingOption, setCreatingOption] = useState(false);
|
||||
|
||||
@@ -143,27 +146,33 @@ const SelectDropdown = ({
|
||||
.includes(searchValue.toLowerCase()),
|
||||
);
|
||||
|
||||
const removeSelectedOptions = opts => opts.filter(option => !value.includes(option.value));
|
||||
const removeSelectedOptionsMulti = opts => opts.filter(option => !value.includes(option.value));
|
||||
const removeSelectedOptionsSingle = opts => opts.filter(option => value !== option.value);
|
||||
|
||||
const filteredOptions = isMulti
|
||||
? removeSelectedOptions(optionsFilteredBySearchValue)
|
||||
: optionsFilteredBySearchValue;
|
||||
? removeSelectedOptionsMulti(optionsFilteredBySearchValue)
|
||||
: removeSelectedOptionsSingle(optionsFilteredBySearchValue);
|
||||
|
||||
const searchValueNotInOptions = !options.map(option => option.label).includes(searchValue);
|
||||
const isOptionCreatable = onCreate && searchValue && searchValueNotInOptions;
|
||||
|
||||
const renderSelectableOption = (option, i) => (
|
||||
<Option
|
||||
key={option.value}
|
||||
className={i === 0 ? activeOptionClass : undefined}
|
||||
isSelected={option.value === value}
|
||||
data-select-option-value={option.value}
|
||||
onMouseEnter={handleOptionMouseEnter}
|
||||
onClick={() => selectOptionValue(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</Option>
|
||||
);
|
||||
const renderSelectableOption = (option, i) => {
|
||||
const optionProps = {
|
||||
key: option.value,
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
className: i === 0 ? activeOptionClass : undefined,
|
||||
isSelected: option.value === value,
|
||||
'data-select-option-value': option.value,
|
||||
onMouseEnter: handleOptionMouseEnter,
|
||||
onClick: () => selectOptionValue(option.value),
|
||||
};
|
||||
return propsRenderOption ? (
|
||||
propsRenderOption(optionProps)
|
||||
) : (
|
||||
<Option {...optionProps}>{option.label}</Option>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCreatableOption = () => (
|
||||
<Option
|
||||
|
||||
@@ -6,63 +6,54 @@ import Icon from 'shared/components/Icon';
|
||||
export const StyledSelect = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
border: 1px solid ${color.borderLight};
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${color.borderLightest};
|
||||
background: #fff;
|
||||
${font.size(14)}
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 1px solid ${color.borderMedium};
|
||||
background: #fff;
|
||||
border: 1px solid ${color.borderInputFocus};
|
||||
box-shadow: 0 0 0 1px ${color.borderInputFocus};
|
||||
}
|
||||
${props => (props.hasIcon ? 'padding-left: 25px;' : '')}
|
||||
${props => (props.invalid ? `&, &:focus { border: 1px solid ${color.danger}; }` : '')}
|
||||
`;
|
||||
|
||||
export const StyledIcon = styled(Icon)`
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 14px;
|
||||
font-size: 16px;
|
||||
color: ${color.textMedium};
|
||||
${props => props.invalid && `&, &:focus { border: 1px solid ${color.danger}; }`}
|
||||
`;
|
||||
|
||||
export const ValueContainer = styled.div`
|
||||
min-height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
width: 100%;
|
||||
padding: 8px 5px 8px 10px;
|
||||
`;
|
||||
|
||||
export const ChevronIcon = styled(Icon)`
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 11px;
|
||||
margin-left: auto;
|
||||
font-size: 18px;
|
||||
color: ${color.textMedium};
|
||||
`;
|
||||
|
||||
export const Placeholder = styled.div`
|
||||
padding: 11px 0 0 15px;
|
||||
color: ${color.textLight};
|
||||
`;
|
||||
|
||||
export const ValueSingle = styled.div`
|
||||
padding: 11px 0 0 15px;
|
||||
`;
|
||||
|
||||
export const ValueMulti = styled.div`
|
||||
padding: 15px 5px 10px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 5px;
|
||||
`;
|
||||
|
||||
export const ValueMultiItem = styled.div`
|
||||
margin: 0 5px 5px 0;
|
||||
${mixin.tag}
|
||||
${mixin.tag()}
|
||||
`;
|
||||
|
||||
export const AddMore = styled.div`
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
line-height: 22px;
|
||||
padding-right: 5px;
|
||||
${font.size(12)}
|
||||
margin-bottom: 3px;
|
||||
padding: 3px 0;
|
||||
${font.size(12.5)}
|
||||
${mixin.link()}
|
||||
i {
|
||||
margin-right: 3px;
|
||||
@@ -77,12 +68,13 @@ export const Dropdown = styled.div`
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
${mixin.boxShadowBorderMedium}
|
||||
${mixin.boxShadowDropdown}
|
||||
`;
|
||||
|
||||
export const DropdownInput = styled.input`
|
||||
padding: 10px 15px 8px;
|
||||
padding: 10px 12px 8px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
color: ${color.textDarkest};
|
||||
@@ -118,7 +110,7 @@ export const Option = styled.div`
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
&.jira-select-option-is-active {
|
||||
background: ${mixin.lighten(color.backgroundMedium, 0.05)};
|
||||
background: ${color.backgroundLightPrimary};
|
||||
}
|
||||
${props => (props.isSelected ? selectedOptionStyles : '')}
|
||||
`;
|
||||
|
||||
@@ -7,11 +7,9 @@ import Icon from 'shared/components/Icon';
|
||||
import Dropdown from './Dropdown';
|
||||
import {
|
||||
StyledSelect,
|
||||
StyledIcon,
|
||||
ValueContainer,
|
||||
ChevronIcon,
|
||||
Placeholder,
|
||||
ValueSingle,
|
||||
ValueMulti,
|
||||
ValueMultiItem,
|
||||
AddMore,
|
||||
@@ -19,8 +17,7 @@ import {
|
||||
|
||||
const propTypes = {
|
||||
className: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
|
||||
defaultValue: PropTypes.any,
|
||||
placeholder: PropTypes.string,
|
||||
invalid: PropTypes.bool,
|
||||
@@ -28,22 +25,24 @@ const propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onCreate: PropTypes.func,
|
||||
isMulti: PropTypes.bool,
|
||||
renderValue: PropTypes.func,
|
||||
renderOption: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
className: undefined,
|
||||
icon: undefined,
|
||||
value: undefined,
|
||||
defaultValue: undefined,
|
||||
placeholder: '',
|
||||
invalid: false,
|
||||
onCreate: undefined,
|
||||
isMulti: false,
|
||||
renderValue: undefined,
|
||||
renderOption: undefined,
|
||||
};
|
||||
|
||||
const Select = ({
|
||||
className,
|
||||
icon,
|
||||
value: propsValue,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
@@ -52,6 +51,8 @@ const Select = ({
|
||||
onChange,
|
||||
onCreate,
|
||||
isMulti,
|
||||
renderValue: propsRenderValue,
|
||||
renderOption: propsRenderOption,
|
||||
}) => {
|
||||
const [stateValue, setStateValue] = useState(defaultValue || (isMulti ? [] : null));
|
||||
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
||||
@@ -79,11 +80,18 @@ const Select = ({
|
||||
|
||||
useOnOutsideClick($selectRef, isDropdownOpen, deactivateDropdown);
|
||||
|
||||
const ensureValueType = newValue => {
|
||||
if (typeof value === 'number') {
|
||||
return isMulti ? newValue.map(parseInt) : parseInt(newValue);
|
||||
}
|
||||
return newValue;
|
||||
};
|
||||
|
||||
const handleChange = newValue => {
|
||||
if (!isControlled) {
|
||||
setStateValue(newValue);
|
||||
setStateValue(ensureValueType(newValue));
|
||||
}
|
||||
onChange(newValue);
|
||||
onChange(ensureValueType(newValue));
|
||||
};
|
||||
|
||||
const removeOptionValue = optionValue => {
|
||||
@@ -106,16 +114,21 @@ const Select = ({
|
||||
|
||||
const isValueEmpty = isMulti ? !value.length : !getOption(value);
|
||||
|
||||
const renderSingleValue = () => <ValueSingle>{getOptionLabel(value)}</ValueSingle>;
|
||||
const renderSingleValue = () =>
|
||||
propsRenderValue ? propsRenderValue({ value }) : getOptionLabel(value);
|
||||
|
||||
const renderMultiValue = () => (
|
||||
<ValueMulti>
|
||||
{value.map(optionValue => (
|
||||
<ValueMultiItem key={optionValue} onClick={() => removeOptionValue(optionValue)}>
|
||||
{getOptionLabel(optionValue)}
|
||||
<Icon type="close" />
|
||||
</ValueMultiItem>
|
||||
))}
|
||||
{value.map(optionValue =>
|
||||
propsRenderValue ? (
|
||||
propsRenderValue({ value: optionValue, removeOptionValue })
|
||||
) : (
|
||||
<ValueMultiItem key={optionValue} onClick={() => removeOptionValue(optionValue)}>
|
||||
{getOptionLabel(optionValue)}
|
||||
<Icon type="close" />
|
||||
</ValueMultiItem>
|
||||
),
|
||||
)}
|
||||
<AddMore>
|
||||
<Icon type="plus" />
|
||||
Add more
|
||||
@@ -128,16 +141,14 @@ const Select = ({
|
||||
className={className}
|
||||
ref={$selectRef}
|
||||
tabIndex="0"
|
||||
hasIcon={!!icon}
|
||||
onKeyDown={handleFocusedSelectKeydown}
|
||||
invalid={invalid}
|
||||
>
|
||||
<ValueContainer onClick={activateDropdown}>
|
||||
{icon && <StyledIcon type={icon} />}
|
||||
{(!isMulti || isValueEmpty) && <ChevronIcon type="chevron-down" />}
|
||||
{isValueEmpty && <Placeholder>{placeholder}</Placeholder>}
|
||||
{!isValueEmpty && !isMulti && renderSingleValue()}
|
||||
{!isValueEmpty && isMulti && renderMultiValue()}
|
||||
{(!isMulti || isValueEmpty) && <ChevronIcon type="chevron-down" top={1} />}
|
||||
</ValueContainer>
|
||||
{isDropdownOpen && (
|
||||
<Dropdown
|
||||
@@ -152,6 +163,7 @@ const Select = ({
|
||||
onChange={handleChange}
|
||||
onCreate={onCreate}
|
||||
isMulti={isMulti}
|
||||
propsRenderOption={propsRenderOption}
|
||||
/>
|
||||
)}
|
||||
</StyledSelect>
|
||||
|
||||
9
client/src/shared/components/TextEditedContent/Styles.js
Normal file
9
client/src/shared/components/TextEditedContent/Styles.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { font } from 'shared/utils/styles';
|
||||
|
||||
export const Content = styled.div`
|
||||
padding: 0 !important;
|
||||
${font.size(15)}
|
||||
${font.regular}
|
||||
`;
|
||||
21
client/src/shared/components/TextEditedContent/index.jsx
Normal file
21
client/src/shared/components/TextEditedContent/index.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable react/no-danger */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Content } from './Styles';
|
||||
|
||||
import('quill/dist/quill.snow.css');
|
||||
|
||||
const propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const TextEditedContent = ({ content, ...otherProps }) => (
|
||||
<div className="ql-snow">
|
||||
<Content className="ql-editor" dangerouslySetInnerHTML={{ __html: content }} {...otherProps} />
|
||||
</div>
|
||||
);
|
||||
|
||||
TextEditedContent.propTypes = propTypes;
|
||||
|
||||
export default TextEditedContent;
|
||||
21
client/src/shared/components/TextEditor/Styles.js
Normal file
21
client/src/shared/components/TextEditor/Styles.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
|
||||
export const EditorCont = styled.div`
|
||||
.ql-toolbar.ql-snow {
|
||||
border-radius: 4px 4px 0 0;
|
||||
border: 1px solid ${color.borderLightest};
|
||||
border-bottom: none;
|
||||
}
|
||||
.ql-container.ql-snow {
|
||||
border-radius: 0 0 4px 4px;
|
||||
border: 1px solid ${color.borderLightest};
|
||||
border-top: none;
|
||||
${font.size(15)}
|
||||
${font.regular}
|
||||
}
|
||||
.ql-editor {
|
||||
min-height: 110px;
|
||||
}
|
||||
`;
|
||||
70
client/src/shared/components/TextEditor/index.jsx
Normal file
70
client/src/shared/components/TextEditor/index.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useLayoutEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Quill from 'quill';
|
||||
|
||||
import { EditorCont } from './Styles';
|
||||
|
||||
import('quill/dist/quill.snow.css');
|
||||
|
||||
const propTypes = {
|
||||
className: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
getEditor: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
className: undefined,
|
||||
placeholder: undefined,
|
||||
defaultValue: undefined,
|
||||
};
|
||||
|
||||
const TextEditor = ({ className, placeholder, defaultValue, getEditor, ...otherProps }) => {
|
||||
const $editorContRef = useRef();
|
||||
const $editorRef = useRef();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
let editor = null;
|
||||
|
||||
const setup = async () => {
|
||||
editor = new Quill($editorRef.current, { placeholder, ...editorConfig });
|
||||
|
||||
editor.clipboard.dangerouslyPasteHTML(0, defaultValue);
|
||||
|
||||
getEditor({
|
||||
getHTML: () => $editorContRef.current.querySelector('.ql-editor').innerHTML,
|
||||
});
|
||||
};
|
||||
setup();
|
||||
|
||||
return () => {
|
||||
editor = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditorCont className={className} ref={$editorContRef}>
|
||||
<div ref={$editorRef} {...otherProps} />
|
||||
</EditorCont>
|
||||
);
|
||||
};
|
||||
|
||||
const editorConfig = {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
['blockquote', 'code-block'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||
[{ color: [] }, { background: [] }],
|
||||
['clean'],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
TextEditor.propTypes = propTypes;
|
||||
TextEditor.defaultProps = defaultProps;
|
||||
|
||||
export default TextEditor;
|
||||
@@ -25,7 +25,7 @@ const Textarea = forwardRef(({ className, invalid, onChange, ...textareaProps },
|
||||
<TextareaAutoSize
|
||||
{...textareaProps}
|
||||
onChange={event => onChange(event.target.value, event)}
|
||||
ref={ref}
|
||||
inputRef={ref}
|
||||
/>
|
||||
</StyledTextarea>
|
||||
));
|
||||
|
||||
13
client/src/shared/components/Tooltip/Styles.js
Normal file
13
client/src/shared/components/Tooltip/Styles.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { zIndexValues, mixin } from 'shared/utils/styles';
|
||||
|
||||
export const Tooltip = styled.div`
|
||||
z-index: ${zIndexValues.modal + 1};
|
||||
position: fixed;
|
||||
width: ${props => props.width}px;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
${mixin.hardwareAccelerate}
|
||||
${mixin.boxShadowDropdown}
|
||||
`;
|
||||
110
client/src/shared/components/Tooltip/index.jsx
Normal file
110
client/src/shared/components/Tooltip/index.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React, { useState, useRef, useLayoutEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||
import { Tooltip } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
className: PropTypes.string,
|
||||
placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
|
||||
offset: PropTypes.shape({
|
||||
top: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
}),
|
||||
width: PropTypes.number.isRequired,
|
||||
renderLink: PropTypes.func.isRequired,
|
||||
renderContent: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
className: undefined,
|
||||
placement: 'bottom',
|
||||
offset: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const Modal = ({ className, placement, offset, width, renderLink, renderContent }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const $linkRef = useRef();
|
||||
const $tooltipRef = useRef();
|
||||
|
||||
const openTooltip = () => setIsOpen(true);
|
||||
const closeTooltip = () => setIsOpen(false);
|
||||
|
||||
useOnOutsideClick([$tooltipRef, $linkRef], isOpen, closeTooltip);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const setTooltipPosition = () => {
|
||||
const { top, left } = calcPosition(offset, placement, $tooltipRef, $linkRef);
|
||||
$tooltipRef.current.style.top = `${top}px`;
|
||||
$tooltipRef.current.style.left = `${left}px`;
|
||||
};
|
||||
if (isOpen) {
|
||||
setTooltipPosition();
|
||||
window.addEventListener('resize', setTooltipPosition);
|
||||
window.addEventListener('scroll', setTooltipPosition);
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('resize', setTooltipPosition);
|
||||
window.removeEventListener('scroll', setTooltipPosition);
|
||||
};
|
||||
}, [isOpen, offset, placement]);
|
||||
|
||||
const renderTooltip = () => (
|
||||
<Tooltip className={className} ref={$tooltipRef} width={width}>
|
||||
{renderContent({ close: closeTooltip })}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderLink({ ref: $linkRef, onClick: isOpen ? closeTooltip : openTooltip })}
|
||||
{isOpen && ReactDOM.createPortal(renderTooltip(), $root)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const calcPosition = (offset, placement, $tooltipRef, $linkRef) => {
|
||||
const margin = 20;
|
||||
const finalOffset = { ...defaultProps.offset, ...offset };
|
||||
|
||||
const tooltipRect = $tooltipRef.current.getBoundingClientRect();
|
||||
const linkRect = $linkRef.current.getBoundingClientRect();
|
||||
|
||||
const linkCenterY = linkRect.top + linkRect.height / 2;
|
||||
const linkCenterX = linkRect.left + linkRect.width / 2;
|
||||
|
||||
const placements = {
|
||||
top: {
|
||||
top: linkRect.top - margin - tooltipRect.height,
|
||||
left: linkCenterX - tooltipRect.width / 2,
|
||||
},
|
||||
right: {
|
||||
top: linkCenterY - tooltipRect.height / 2,
|
||||
left: linkRect.right + margin,
|
||||
},
|
||||
bottom: {
|
||||
top: linkRect.bottom + margin,
|
||||
left: linkCenterX - tooltipRect.width / 2,
|
||||
},
|
||||
left: {
|
||||
top: linkCenterY - tooltipRect.height / 2,
|
||||
left: linkRect.left - margin - tooltipRect.width,
|
||||
},
|
||||
};
|
||||
return {
|
||||
top: placements[placement].top + finalOffset.top,
|
||||
left: placements[placement].left + finalOffset.left,
|
||||
};
|
||||
};
|
||||
|
||||
const $root = document.getElementById('root');
|
||||
|
||||
Modal.propTypes = propTypes;
|
||||
Modal.defaultProps = defaultProps;
|
||||
|
||||
export default Modal;
|
||||
@@ -1,9 +1,14 @@
|
||||
export { default as Avatar } from './Avatar';
|
||||
export { default as Button } from './Button';
|
||||
export { default as ConfirmModal } from './ConfirmModal';
|
||||
export { default as CopyLinkButton } from './CopyLinkButton';
|
||||
export { default as DatePicker } from './DatePicker';
|
||||
export { default as Tooltip } from './Tooltip';
|
||||
export { default as Icon } from './Icon';
|
||||
export { default as Input } from './Input';
|
||||
export { default as InputDebounced } from './InputDebounced';
|
||||
export { default as IssueTypeIcon } from './IssueTypeIcon';
|
||||
export { default as IssuePriorityIcon } from './IssuePriorityIcon';
|
||||
export { default as Logo } from './Logo';
|
||||
export { default as Modal } from './Modal';
|
||||
export { default as PageError } from './PageError';
|
||||
@@ -12,3 +17,5 @@ export { default as ProjectAvatar } from './ProjectAvatar';
|
||||
export { default as Select } from './Select';
|
||||
export { default as Spinner } from './Spinner';
|
||||
export { default as Textarea } from './Textarea';
|
||||
export { default as TextEditedContent } from './TextEditedContent';
|
||||
export { default as TextEditor } from './TextEditor';
|
||||
|
||||
Reference in New Issue
Block a user