Skip to content

Commit

Permalink
Checklist (#1207)
Browse files Browse the repository at this point in the history
* Add checklist

* Cleanup

* Remove padding

* Add menu

* Cleanup

* Fixes and cleanup
  • Loading branch information
timmo001 committed Jun 27, 2020
1 parent 4311ce6 commit a382463
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 12 deletions.
3 changes: 3 additions & 0 deletions frontend/src/Components/Cards/Base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
entitySizes,
} from '../HomeAssistant/HomeAssistant';
import { CommandType } from '../Utils/Command';
import Checklist from './Checklist/Checklist';
import Entity from '../HomeAssistant/Cards/Entity';
import Frame from './Frame';
import Image from './Image';
Expand Down Expand Up @@ -241,6 +242,8 @@ function Base(props: BaseProps): ReactElement {
<News {...props} />
) : props.card.type === 'rss' ? (
<RSS {...props} />
) : props.card.type === 'checklist' ? (
<Checklist {...props} />
) : (
<Fragment />
);
Expand Down
115 changes: 115 additions & 0 deletions frontend/src/Components/Cards/Checklist/Checklist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, { Fragment, ReactElement, useCallback, useMemo } from 'react';
import arrayMove from 'array-move';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Divider from '@material-ui/core/Divider';
import Grid from '@material-ui/core/Grid';
import AddIcon from '@material-ui/icons/Add';

import { BaseProps } from '../Base';
import { ChecklistItem } from '../../Configuration/Config';
import clone from '../../../utils/clone';
import Item from './Item';
import makeKey from '../../../utils/makeKey';

const useStyles = makeStyles(() => ({
root: {
height: '100%',
width: '100%',
overflowY: 'auto',
},
button: {
width: '100%',
},
inputChecked: {
textDecoration: 'line-through',
},
}));

function Checklist(props: BaseProps): ReactElement | null {
const handleDeleteItem = useCallback(
(key: number) => () => {
const items = props.card.checklist_items || [];
items.splice(key, 1);
props.handleUpdate({ ...props.card, checklist_items: items });
},
[props]
);

const handleMoveItem = useCallback(
(key: number) => (amount: number) => {
const items = clone(props.card.checklist_items) || [];
props.handleUpdate({
...props.card,
checklist_items: arrayMove(items, key, key + amount),
});
},
[props]
);

const handleUpdateItem = useCallback(
(key: number) => (item: ChecklistItem) => {
const items = props.card.checklist_items || [];
items[key] = item;
props.handleUpdate({ ...props.card, checklist_items: items });
},
[props]
);

const handleAddItem = useCallback(() => {
const items = props.card.checklist_items || [];
items.push({ key: makeKey(32), text: '', checked: false });
props.handleUpdate({ ...props.card, checklist_items: items });
}, [props]);

const classes = useStyles();
return (
<Grid
className={classes.root}
container
direction="row"
justify="center"
alignContent="flex-start"
alignItems="center">
{useMemo(() => {
if (
props.card.checklist_items !== undefined &&
Array.isArray(props.card.checklist_items)
) {
const itemLength = props.card.checklist_items.length - 1;
return props.card.checklist_items.map(
(item: ChecklistItem, key: number) => (
<Fragment key={key}>
<Item
item={item}
maxPosition={itemLength}
position={key}
handleDeleteItem={handleDeleteItem(key)}
handleMoveItem={handleMoveItem(key)}
handleUpdateItem={handleUpdateItem(key)}
/>
{key !== itemLength && (
<Grid item xs={12}>
<Divider light variant="middle" />
</Grid>
)}
</Fragment>
)
);
}
}, [
props.card.checklist_items,
handleDeleteItem,
handleMoveItem,
handleUpdateItem,
])}
<Grid item xs={12}>
<Button className={classes.button} onClick={handleAddItem}>
<AddIcon />
</Button>
</Grid>
</Grid>
);
}

export default Checklist;
160 changes: 160 additions & 0 deletions frontend/src/Components/Cards/Checklist/Item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import React, {
ChangeEvent,
Fragment,
ReactElement,
useCallback,
useState,
useEffect,
} from 'react';
import clsx from 'clsx';
import { makeStyles } from '@material-ui/core/styles';
import Checkbox from '@material-ui/core/Checkbox';
import Grid from '@material-ui/core/Grid';
import IconButton from '@material-ui/core/IconButton';
import InputBase from '@material-ui/core/InputBase';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import MoreVertIcon from '@material-ui/icons/MoreVert';

import { ChecklistItem } from '../../Configuration/Config';

const useStyles = makeStyles(() => ({
input: {
padding: 0,
},
inputChecked: {
textDecoration: 'line-through',
},
}));

interface ItemProps {
item: ChecklistItem;
maxPosition: number;
position: number;
handleDeleteItem: () => void;
handleMoveItem: (amount: number) => void;
handleUpdateItem: (item: ChecklistItem) => void;
}

let updateTimeout: NodeJS.Timeout;
function Checklist(props: ItemProps): ReactElement | null {
const [item, setItem] = useState<ChecklistItem>(props.item);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);

const handleOpenMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};

const handleCloseMenu = () => {
setAnchorEl(null);
};

function handleDeleteItem(): void {
handleCloseMenu();
props.handleDeleteItem();
}

function handleMoveUp(): void {
handleCloseMenu();
props.handleMoveItem(-1);
}

function handleMoveDown(): void {
handleCloseMenu();
props.handleMoveItem(+1);
}

const handleUpdateItem = useCallback(
async (newItem: ChecklistItem) => {
setItem(newItem);
if (updateTimeout) clearTimeout(updateTimeout);
updateTimeout = setTimeout(() => props.handleUpdateItem(newItem), 500);
},
[props]
);

const handleCheckedChange = useCallback(
async (_event: ChangeEvent<HTMLInputElement>, checked: boolean) => {
handleUpdateItem({ ...item, checked: checked });
},
[item, handleUpdateItem]
);

const handleTextChange = useCallback(
async (event: ChangeEvent<HTMLInputElement>) => {
handleUpdateItem({ ...item, text: event.target.value });
},
[item, handleUpdateItem]
);

useEffect(() => {
if (props.item.key !== item.key) setItem(props.item);
}, [props.item, item]);

const classes = useStyles();
return (
<Fragment>
<Grid
component="form"
item
xs={12}
container
direction="row"
justify="center"
alignContent="flex-start"
alignItems="center">
<Grid item>
<Checkbox
checked={item.checked}
inputProps={{ 'aria-label': 'checked' }}
onChange={handleCheckedChange}
/>
</Grid>
<Grid item xs>
<InputBase
className={clsx(
classes.input,
item.checked && classes.inputChecked
)}
disabled={item.checked}
inputProps={{ 'aria-label': 'text' }}
multiline
value={item.text}
onChange={handleTextChange}
/>
</Grid>
<Grid item>
<IconButton
aria-label="more"
aria-controls="long-menu"
aria-haspopup="true"
onClick={handleOpenMenu}>
<MoreVertIcon />
</IconButton>
</Grid>
</Grid>
<Menu
id="long-menu"
anchorEl={anchorEl}
keepMounted
open={open}
onClose={handleCloseMenu}
PaperProps={{
style: {
minWidth: '20ch',
},
}}>
{props.position > 0 && (
<MenuItem onClick={handleMoveUp}>Move Up</MenuItem>
)}
{props.position < props.maxPosition && (
<MenuItem onClick={handleMoveDown}>Move Down</MenuItem>
)}
<MenuItem onClick={handleDeleteItem}>Delete</MenuItem>
</Menu>
</Fragment>
);
}

export default Checklist;
21 changes: 21 additions & 0 deletions frontend/src/Components/Configuration/Config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,15 @@ export type CardProps = {
chart_detail?: number;
chart_from?: number;
chart_labels?: boolean;
checklist_items?: ChecklistItem[];
};

export interface ChecklistItem {
key: string;
checked: boolean;
text: string;
}

export type NewsProps = {
news_api_key: string;
};
Expand Down Expand Up @@ -195,6 +202,7 @@ export const cardTypes: CardType[] = [
{ name: 'markdown', title: 'Markdown' },
{ name: 'news', title: 'News Feed' },
{ name: 'rss', title: 'RSS Feed' },
{ name: 'checklist', title: 'Checklist' },
];

export const cardTypeDefaults: { [type: string]: CardProps } = {
Expand Down Expand Up @@ -276,6 +284,19 @@ export const cardTypeDefaults: { [type: string]: CardProps } = {
height: 3,
url: '',
},
checklist: {
key: '',
group: '',
title: cardTypes[6].title,
type: 'checklist',
elevation: 1,
background: '',
padding: '',
square: false,
width: 2,
height: 3,
checklist_items: [],
},
};

export const colorItems: string[] = [
Expand Down
25 changes: 13 additions & 12 deletions frontend/src/Components/Configuration/EditCard/Base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import Switch from '@material-ui/core/Switch';
import TextField from '@material-ui/core/TextField';
import { ColorResult } from 'react-color';

import { CardProps, cardTypes, CardType, ConfigurationProps } from '../Config';
import { CommandType } from '../../Utils/Command';
import { HomeAssistantChangeProps } from '../../HomeAssistant/HomeAssistant';
// import ColorAdornment from '../../Utils/ColorAdornment';
import ColorAdornment from '../../Utils/ColorAdornment';
import Entity from './Entity';
import Frame from './Frame';
import Image from './Image';
Expand Down Expand Up @@ -67,9 +68,9 @@ function Base(props: BaseExtendedProps): ReactElement | null {
}
}

// const handleColorChange = (name: string) => (color: ColorResult): void => {
// props.handleManualChange?.(name, color.hex);
// };
const handleColorChange = (name: string) => (color: ColorResult): void => {
props.handleManualChange?.(name, color.hex);
};

const classes = useStyles();

Expand Down Expand Up @@ -148,14 +149,14 @@ function Base(props: BaseExtendedProps): ReactElement | null {
InputLabelProps={{ shrink: true }}
label="Background"
placeholder="default"
// InputProps={{
// endAdornment: (
// <ColorAdornment
// color={props.card.background}
// handleColorChange={handleColorChange('background')}
// />
// ),
// }}
InputProps={{
endAdornment: (
<ColorAdornment
color={props.card.background}
handleColorChange={handleColorChange('background')}
/>
),
}}
value={props.card.background || 'default'}
onChange={props.handleChange && props.handleChange('background')}
/>
Expand Down

0 comments on commit a382463

Please sign in to comment.