// Copyright (c) 2019 Shellyl_N and Authors // license: ISC // https://github.com/shellyln import React from 'react'; import { connect } from 'react-redux'; import { RouteComponentProps } from 'react-router-dom'; import { makeStyles, useTheme } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import IconButton from '@material-ui/core/IconButton'; import AddBoxIcon from '@material-ui/icons/AddBox'; import clsx from 'clsx'; import marked from 'marked'; import createDOMPurify from 'dompurify'; import { Qr } from 'red-agate-barcode/modules/barcode/Qr'; import { LaneDef, StatusLaneDef, KanbanRecord, KanbanBoardState, KanbanBoardRecord } from '../types'; import gensym from '../lib/gensym'; import { parseISODate } from '../lib/datetime'; import { isDark } from '../lib/theme'; import { mapDispatchToProps, mapStateToProps, KanbanBoardActions } from '../dispatchers/KanbanBoardDispatcher'; import KanbanDialog from '../components/KanbanDialog'; import TextInputDialog from '../components/TextInputDialog'; import { getConstructedAppStore } from '../store'; import './KanbanBoardView.css'; type StickysProps = KanbanBoardActions & { records: KanbanRecord[], taskStatus: StatusLaneDef, teamOrStory: LaneDef, taskStatuses: StatusLaneDef[], teamOrStories: LaneDef[], board: KanbanBoardRecord, }; type StickyProps = KanbanBoardActions & { record: KanbanRecord, taskStatus: StatusLaneDef, teamOrStory: LaneDef, taskStatuses: StatusLaneDef[], teamOrStories: LaneDef[], board: KanbanBoardRecord, }; type KanbanBoardViewProps = KanbanBoardState & KanbanBoardActions & RouteComponentProps<{id: string}> & { }; const DOMPurify = createDOMPurify(window); const useStyles = makeStyles(theme => ({ root: {}, smallIcon: { width: '20px', height: '20px', }, })); const mapNeverStateToProps = () => ({}); const agent = window.navigator.userAgent.toLowerCase(); const firefox = (agent.indexOf('firefox') !== -1); const Sticky_: React.FC = (props) => { const [open, setOpen] = React.useState(false); const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const dueDate = props.record.dueDate ? parseISODate(props.record.dueDate) : null; const expired = (! props.taskStatus.completed) && (dueDate ? dueDate < today : false); function handleOnDragStart(ev: React.DragEvent) { ev.dataTransfer.setData('elId', (ev.target as any).id); } function handleEditApply(rec: KanbanRecord) { props.updateSticky(rec); setOpen(false); } function handleArchive(id: string) { props.archiveSticky(id); setOpen(false); } function handleUnarchive(id: string) { props.unarchiveSticky(id); setOpen(false); } function handleDelete(id: string) { props.deleteSticky(id); setOpen(false); } function handleEditCancel() { setOpen(false); } return ( <> {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} setOpen(true)} onDragStart={handleOnDragStart}>
{props.board.displayTags && props.record.tags.length ?
    { props.record.tags.map((x, index) => { const tags = props.board.tags || []; const matched = tags.find(q => q.value === x); return (
  • {x}
  • ); }) }
: <> }
{props.board.displayBarcode && props.record.barcode ?
: <> } {props.record.flags.includes('Marked') ?
{'📍'}
: <> } {props.record.dueDate ?
{(expired ? '🔥' : '⏳' ) + props.record.dueDate}
: <> }
{open ? : <> } ); } const Sticky = connect(mapNeverStateToProps, mapDispatchToProps)(Sticky_); const Stickys_: React.FC = (props) => { function handleOnDragOver(ev: React.DragEvent) { ev.preventDefault(); } function handleOnDrop(ev: React.DragEvent) { try { const elId = ev.dataTransfer.getData('elId'); const el = document.getElementById(elId); props.updateStickyLanes({ kanbanId: (el as any).dataset.recordId, taskStatusValue: props.taskStatus.value, teamOrStoryValue: props.teamOrStory.value, }) } catch (e) { alert(e.message); } ev.preventDefault(); } const filtered = props.records.filter(x => !x.flags || !x.flags.includes('Archived')); return (
{filtered.map(record => ( ))} {(firefox && filtered.length === 0) ? // NOTE: hack for the height of div becomes 0 in Firefox
 
: <> }
); } const Stickys = connect(mapNeverStateToProps, mapDispatchToProps)(Stickys_); const KanbanBoardView: React.FC = (props) => { const classes = useStyles(); const theme = useTheme(); const [textInputOpen, setTextInputOpen] = React.useState({ open: false, title: '', message: '', fieldLabel: '', value: '', validator: (value: string) => value.trim().length <= 0, onClose: handleCloseDialogEditBoardName, }); function handleClickEditBoardName() { const currentState = getConstructedAppStore().getState(); setTextInputOpen(Object.assign({}, textInputOpen, { open: true, title: 'Edit board name', message: '', fieldLabel: 'Board name', value: currentState.kanbanBoard.activeBoard.name, })); } function handleCloseDialogEditBoardName(apply: boolean, value?: string) { setTextInputOpen(Object.assign({}, textInputOpen, { open: false })); if (apply && value) { const currentState = getConstructedAppStore().getState(); props.updateBoardName({ boardId: currentState.kanbanBoard.activeBoardId, boardName: value }); } } if (props.match.params.id) { if (props.activeBoard._id !== props.match.params.id) { const index = props.boards.findIndex(x => x._id === props.match.params.id); props.changeActiveBoard(props.match.params.id); return (
{index < 0 ? <> No boards found. {props.history.push('/')}} > Click here to show main board. : <> }
); } } return (
{props.activeBoard.name} {props.activeBoard.taskStatuses.map(taskStatus => ( ))} {props.activeBoard.teamOrStories.map(teamOrStory => ( {props.activeBoard.taskStatuses.map(taskStatus => ( ))} ))}
props.addSticky()}> {taskStatus.caption || taskStatus.value}
{teamOrStory.caption || teamOrStory.value} x.teamOrStory === teamOrStory.value && x.taskStatus === taskStatus.value)} />
{props.activeBoard.boardNote ?
: <> } {textInputOpen.open ? : <> }
); } export default connect(mapStateToProps, mapDispatchToProps)(KanbanBoardView);