Merge 5f35bf3544 into 89c1ed71e1
commit
aae1a21c2c
@ -0,0 +1,5 @@
|
||||
import { withPopup } from '../lib/popup';
|
||||
|
||||
import CardCopyStep from './CardCopyStep';
|
||||
|
||||
export default withPopup(CardCopyStep);
|
||||
@ -0,0 +1,184 @@
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Dropdown, Form } from 'semantic-ui-react';
|
||||
import { Popup } from '../../lib/custom-ui';
|
||||
|
||||
import { useForm } from '../../hooks';
|
||||
|
||||
import styles from './CardCopyStep.module.scss';
|
||||
import store from '../../store';
|
||||
|
||||
const CardCopyStep = React.memo(
|
||||
({ projectsToLists, defaultPath, onBoardFetch, onBack, onClose, onCopyCard }) => {
|
||||
const [t] = useTranslation();
|
||||
// Get store to get value for description string
|
||||
const st = store.getState();
|
||||
|
||||
const keys = Object.keys(st.orm.Card.itemsById);
|
||||
if (defaultPath.description === undefined) {
|
||||
keys.forEach((key) => {
|
||||
if (key === defaultPath.id) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
defaultPath.description = st.orm.Card.itemsById[key].description;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (defaultPath.dueDate === null || defaultPath.dueDate === undefined) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
delete defaultPath.dueDate;
|
||||
}
|
||||
|
||||
const [path, handleFieldChange] = useForm(() => ({
|
||||
projectId: null,
|
||||
boardId: null,
|
||||
listId: null,
|
||||
name: defaultPath.name,
|
||||
description: defaultPath.description,
|
||||
tasks: defaultPath.tasks,
|
||||
attachments: defaultPath.attachments,
|
||||
labels: defaultPath.labels,
|
||||
users: defaultPath.users,
|
||||
...defaultPath,
|
||||
}));
|
||||
|
||||
const selectedProject = useMemo(
|
||||
() => projectsToLists.find((project) => project.id === path.projectId) || null,
|
||||
[projectsToLists, path.projectId],
|
||||
);
|
||||
|
||||
const selectedBoard = useMemo(
|
||||
() =>
|
||||
(selectedProject && selectedProject.boards.find((board) => board.id === path.boardId)) ||
|
||||
null,
|
||||
[selectedProject, path.boardId],
|
||||
);
|
||||
|
||||
const selectedList = useMemo(
|
||||
() => (selectedBoard && selectedBoard.lists.find((list) => list.id === path.listId)) || null,
|
||||
[selectedBoard, path.listId],
|
||||
);
|
||||
|
||||
const handleBoardIdChange = useCallback(
|
||||
(event, data) => {
|
||||
if (selectedProject.boards.find((board) => board.id === data.value).isFetching === null) {
|
||||
onBoardFetch(data.value);
|
||||
}
|
||||
|
||||
handleFieldChange(event, data);
|
||||
},
|
||||
[onBoardFetch, handleFieldChange, selectedProject],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
onCopyCard(selectedList.id, path);
|
||||
onClose();
|
||||
}, [onCopyCard, selectedList.id, path, onClose]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t('action.copyCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div className={styles.text}>{t('common.project')}</div>
|
||||
<Dropdown
|
||||
fluid
|
||||
selection
|
||||
name="projectId"
|
||||
options={projectsToLists.map((project) => ({
|
||||
text: project.name,
|
||||
value: project.id,
|
||||
}))}
|
||||
value={selectedProject && selectedProject.id}
|
||||
placeholder={
|
||||
projectsToLists.length === 0 ? t('common.noProjects') : t('common.selectProject')
|
||||
}
|
||||
disabled={projectsToLists.length === 0}
|
||||
className={styles.field}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
{selectedProject && (
|
||||
<>
|
||||
<div className={styles.text}>{t('common.board')}</div>
|
||||
<Dropdown
|
||||
fluid
|
||||
selection
|
||||
name="boardId"
|
||||
options={selectedProject.boards.map((board) => ({
|
||||
text: board.name,
|
||||
value: board.id,
|
||||
}))}
|
||||
value={selectedBoard && selectedBoard.id}
|
||||
placeholder={
|
||||
selectedProject.boards.length === 0
|
||||
? t('common.noBoards')
|
||||
: t('common.selectBoard')
|
||||
}
|
||||
disabled={selectedProject.boards.length === 0}
|
||||
className={styles.field}
|
||||
onChange={handleBoardIdChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{selectedBoard && (
|
||||
<>
|
||||
<div className={styles.text}>{t('common.list')}</div>
|
||||
<Dropdown
|
||||
fluid
|
||||
selection
|
||||
name="listId"
|
||||
options={selectedBoard.lists.map((list) => ({
|
||||
text: list.name,
|
||||
value: list.id,
|
||||
}))}
|
||||
value={selectedList && selectedList.id}
|
||||
placeholder={
|
||||
selectedBoard.isFetching === false && selectedBoard.lists.length === 0
|
||||
? t('common.noLists')
|
||||
: t('common.selectList')
|
||||
}
|
||||
loading={selectedBoard.isFetching !== false}
|
||||
disabled={selectedBoard.isFetching !== false || selectedBoard.lists.length === 0}
|
||||
className={styles.field}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
positive
|
||||
content={t('action.copy')} // change this action.copy
|
||||
disabled={(selectedBoard && selectedBoard.isFetching !== false) || !selectedList}
|
||||
/>
|
||||
</Form>
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CardCopyStep.propTypes = {
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
description: PropTypes.string,
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
projectsToLists: PropTypes.array.isRequired,
|
||||
defaultPath: PropTypes.object.isRequired,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
// onMove: PropTypes.func.isRequired,
|
||||
// onTransfer: PropTypes.func.isRequired,
|
||||
onBoardFetch: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onCopyCard: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
CardCopyStep.defaultProps = {
|
||||
onBack: undefined,
|
||||
description: undefined,
|
||||
};
|
||||
|
||||
export default CardCopyStep;
|
||||
@ -0,0 +1,12 @@
|
||||
:global(#app) {
|
||||
.field {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #444444;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import CardCopyStep from './CardCopyStep';
|
||||
|
||||
export default CardCopyStep;
|
||||
@ -0,0 +1,164 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
import { Popup } from '../../lib/custom-ui';
|
||||
|
||||
import styles from './SortStep.module.scss';
|
||||
|
||||
const SortStep = React.memo(
|
||||
({ title, content, buttonContent, onBack, onConfirm, selectedOption, setSelectedOption }) => {
|
||||
const [t] = useTranslation();
|
||||
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
function handleChange(event) {
|
||||
setSelectedOption(event.target.id);
|
||||
}
|
||||
|
||||
const updateState = () => {
|
||||
if (checked) {
|
||||
setChecked(false);
|
||||
} else {
|
||||
setChecked(true);
|
||||
}
|
||||
};
|
||||
|
||||
function changeEvent(event) {
|
||||
handleChange(event);
|
||||
updateState();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header onBack={onBack}>
|
||||
{t(title, {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<div className={styles.content}>{t(content)}</div>
|
||||
<div>
|
||||
<label htmlFor="name">
|
||||
<input
|
||||
type="radio"
|
||||
id="name"
|
||||
name="sort"
|
||||
value="name"
|
||||
onChange={changeEvent}
|
||||
checked={selectedOption === 'name'}
|
||||
/>
|
||||
{t('action.sortByTitleList', {
|
||||
context: 'title',
|
||||
})}
|
||||
</label>
|
||||
<br />
|
||||
|
||||
<label htmlFor="id">
|
||||
<input
|
||||
type="radio"
|
||||
id="id"
|
||||
name="sort"
|
||||
value="id"
|
||||
onChange={changeEvent}
|
||||
checked={selectedOption === 'id'}
|
||||
/>
|
||||
{t('action.sortByIdList', {
|
||||
context: 'title',
|
||||
})}
|
||||
</label>
|
||||
<br />
|
||||
<label htmlFor="position">
|
||||
<input
|
||||
type="radio"
|
||||
id="position"
|
||||
name="sort"
|
||||
value="position"
|
||||
onChange={changeEvent}
|
||||
checked={selectedOption === 'position'}
|
||||
/>
|
||||
{t('action.sortByPositionList', {
|
||||
context: 'title',
|
||||
})}
|
||||
</label>
|
||||
<br />
|
||||
<label htmlFor="createdAt">
|
||||
<input
|
||||
type="radio"
|
||||
id="createdAt"
|
||||
name="sort"
|
||||
value="createdAt"
|
||||
onChange={changeEvent}
|
||||
checked={selectedOption === 'createdAt'}
|
||||
/>
|
||||
{t('action.sortByCreatedAt', {
|
||||
context: 'title',
|
||||
})}
|
||||
</label>
|
||||
<br />
|
||||
<label htmlFor="updatedAt">
|
||||
<input
|
||||
type="radio"
|
||||
id="updatedAt"
|
||||
name="sort"
|
||||
value="updatedAt"
|
||||
onChange={changeEvent}
|
||||
checked={selectedOption === 'updatedAt'}
|
||||
/>
|
||||
{t('action.sortByUpdatedAtList', {
|
||||
context: 'title',
|
||||
})}
|
||||
</label>
|
||||
<br />
|
||||
<label htmlFor="dueDate">
|
||||
<input
|
||||
type="radio"
|
||||
id="dueDate"
|
||||
name="sort"
|
||||
value="dueDate"
|
||||
onChange={changeEvent}
|
||||
checked={selectedOption === 'dueDate'}
|
||||
/>
|
||||
{t('action.sortByDueDateList', {
|
||||
context: 'title',
|
||||
})}
|
||||
</label>
|
||||
<br />
|
||||
<label htmlFor="creatorUserId">
|
||||
<input
|
||||
type="radio"
|
||||
id="creatorUserId"
|
||||
name="sort"
|
||||
value="creatorUserId"
|
||||
onChange={changeEvent}
|
||||
checked={selectedOption === 'creatorUserId'}
|
||||
/>
|
||||
{t('action.sortByCreatorUserIdList', {
|
||||
context: 'title',
|
||||
})}
|
||||
</label>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<Button fluid negative content={t(buttonContent)} onClick={onConfirm} />
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
SortStep.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
buttonContent: PropTypes.string.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onBack: PropTypes.func,
|
||||
selectedOption: PropTypes.string.isRequired,
|
||||
setSelectedOption: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SortStep.defaultProps = {
|
||||
onBack: undefined,
|
||||
};
|
||||
|
||||
export default SortStep;
|
||||
@ -0,0 +1,7 @@
|
||||
:global(#app) {
|
||||
.content {
|
||||
color: #212121;
|
||||
padding-bottom: 6px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import SortStep from './SortStep';
|
||||
|
||||
export default SortStep;
|
||||
@ -1,4 +1,5 @@
|
||||
import usePopup from './use-popup';
|
||||
import closePopup from './close-popup';
|
||||
import withPopup from './with-popup';
|
||||
|
||||
export { usePopup, closePopup };
|
||||
export { usePopup, closePopup, withPopup };
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
import { ResizeObserver } from '@juggle/resize-observer';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Popup as SemanticUIPopup } from 'semantic-ui-react';
|
||||
|
||||
import styles from './Popup.module.css';
|
||||
|
||||
export default (WrappedComponent, defaultProps) => {
|
||||
const Popup = React.memo(({ children, ...props }) => {
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
|
||||
const wrapper = useRef(null);
|
||||
const resizeObserver = useRef(null);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
setIsOpened(true);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsOpened(false);
|
||||
}, []);
|
||||
|
||||
const handleMouseDown = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const handleClick = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const handleTriggerClick = useCallback(
|
||||
(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
const { onClick } = children;
|
||||
|
||||
if (onClick) {
|
||||
onClick(event);
|
||||
}
|
||||
},
|
||||
[children],
|
||||
);
|
||||
|
||||
const handleContentRef = useCallback((element) => {
|
||||
if (resizeObserver.current) {
|
||||
resizeObserver.current.disconnect();
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
resizeObserver.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
resizeObserver.current = new ResizeObserver(() => {
|
||||
if (resizeObserver.current.isInitial) {
|
||||
resizeObserver.current.isInitial = false;
|
||||
return;
|
||||
}
|
||||
|
||||
wrapper.current.positionUpdate();
|
||||
});
|
||||
|
||||
resizeObserver.current.isInitial = true;
|
||||
resizeObserver.current.observe(element);
|
||||
}, []);
|
||||
|
||||
const tigger = React.cloneElement(children, {
|
||||
onClick: handleTriggerClick,
|
||||
});
|
||||
|
||||
return (
|
||||
<SemanticUIPopup
|
||||
basic
|
||||
wide
|
||||
ref={wrapper}
|
||||
trigger={tigger}
|
||||
on="click"
|
||||
open={isOpened}
|
||||
position="bottom left"
|
||||
popperModifiers={[
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
boundariesElement: 'window',
|
||||
},
|
||||
},
|
||||
]}
|
||||
className={styles.wrapper}
|
||||
onOpen={handleOpen}
|
||||
onClose={handleClose}
|
||||
onMouseDown={handleMouseDown}
|
||||
onClick={handleClick}
|
||||
{...defaultProps} // eslint-disable-line react/jsx-props-no-spreading
|
||||
>
|
||||
<div ref={handleContentRef}>
|
||||
<Button icon="close" onClick={handleClose} className={styles.closeButton} />
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<WrappedComponent {...props} onClose={handleClose} />
|
||||
</div>
|
||||
</SemanticUIPopup>
|
||||
);
|
||||
});
|
||||
|
||||
Popup.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
return Popup;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue