Skip to content

Commit

Permalink
refactor Item state. clear buttons working. closes #24
Browse files Browse the repository at this point in the history
  • Loading branch information
collinmurd committed Sep 8, 2024
1 parent 43e9c81 commit f837ca3
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 36 deletions.
12 changes: 6 additions & 6 deletions app/src/components/Item/Item.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ const mockData: IItem = {

describe('Render', () => {
it('should mark checked', () => {
render(<Item item={mockData} removeItem={jest.fn()} edit={false} />)
render(<Item item={mockData} removeItem={jest.fn()} updateItemState={jest.fn()} edit={false} />)
expect(screen.getByRole('checkbox')).toHaveAttribute('checked');
});

it('should not mark unchecked', () => {
mockData.checked = false;
render(<Item item={mockData} removeItem={jest.fn()} edit={false} />)
render(<Item item={mockData} removeItem={jest.fn()} updateItemState={jest.fn()} edit={false} />)
expect(screen.getByRole('checkbox')).not.toHaveAttribute('checked');
});
})
Expand All @@ -33,7 +33,7 @@ describe('Check an item', () => {
});

it('should call the api when the box is checked', async () => {
render(<Item item={mockData} removeItem={jest.fn()} edit={false} />);
render(<Item item={mockData} removeItem={jest.fn()} updateItemState={jest.fn()} edit={false} />);
(updateItem as jest.Mock).mockReturnValue(Promise.resolve({
id: "1",
description: "Apples",
Expand All @@ -48,7 +48,7 @@ describe('Check an item', () => {

it('should call the api when the box is unchecked', async () => {
mockData.checked = true;
render(<Item item={mockData} removeItem={jest.fn()} edit={false} />);
render(<Item item={mockData} removeItem={jest.fn()} updateItemState={jest.fn()} edit={false} />);
(updateItem as jest.Mock).mockReturnValue(Promise.resolve({
id: "1",
description: "Apples",
Expand All @@ -67,7 +67,7 @@ describe('Delete an item', () => {
it('should call removeItem when delete button is clicked', async () => {
const removeItem = jest.fn();
const user = userEvent.setup();
render(<Item item={mockData} removeItem={removeItem} edit={false} />)
render(<Item item={mockData} removeItem={removeItem} updateItemState={jest.fn()} edit={false} />)

await user.click(screen.getByLabelText('Delete Apples'));
expect(removeItem).toHaveBeenCalled();
Expand All @@ -78,7 +78,7 @@ describe('Edit an item', () => {
it('should open the text box when the edit button is clicked', async () => {
const removeItem = jest.fn();
const user = userEvent.setup();
render(<Item item={mockData} removeItem={removeItem} edit={false} />)
render(<Item item={mockData} removeItem={removeItem} updateItemState={jest.fn()} edit={false} />)

await user.click(await screen.findByLabelText('Edit Apples'));

Expand Down
58 changes: 34 additions & 24 deletions app/src/components/Item/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ import classes from './Item.module.css'
import { IconPencil, IconTrash } from "@tabler/icons-react";
import { useExitOnEscape } from "../../hooks";

export function Item(props: {item: IItem, removeItem: Function, edit: boolean}) {
const [itemDescription, setItemDescription] = useState(props.item.description);
const [checked, setChecked] = useState(props.item.checked);
export interface IItemProps {
item: IItem,
edit: boolean,
removeItem: (item: IItem) => void,
updateItemState: (item: IItem) => void
}

export function Item(props: IItemProps) {
const [editing, setEditing] = useState(props.edit);

useExitOnEscape(handleEditFinished);

function handleChecked(event: React.ChangeEvent<HTMLInputElement>) {
updateItem({
checked: event.target.checked,
description: itemDescription,
id: props.item.id,
section: props.item.section
});
setChecked(!checked)
props.item.checked = event.target.checked;
updateItem(props.item);
props.updateItemState(props.item);
}

function handleDelete() {
Expand All @@ -31,24 +32,33 @@ export function Item(props: {item: IItem, removeItem: Function, edit: boolean})
setEditing(true);
}

function handleDescriptionChanged(event: React.ChangeEvent<HTMLInputElement>) {
props.item.description = event.target.value;
props.updateItemState(props.item);
}

function handleEditFinished() {
if (!itemDescription) {
// empty input, delete the item
handleDelete();
} else {
props.item.description = itemDescription;
setEditing(false);
updateItem(props.item);
if (editing) {
if (!props.item.description) {
// empty input, delete the item
handleDelete();
} else {
props.item.description = props.item.description;
setEditing(false);
console.log('here?')
console.log(props.item.id)
updateItem(props.item);
}
}
}

var descriptionContent = <span className={classes.itemDescription}>{itemDescription}</span>;
var descriptionContent = <span className={classes.itemDescription}>{props.item.description}</span>;
var editButtons = (
<div>
<ActionIcon variant="subtle" aria-label={'Edit ' + itemDescription} onClick={handleEditClicked}>
<ActionIcon variant="subtle" aria-label={'Edit ' + props.item.description} onClick={handleEditClicked}>
<IconPencil />
</ActionIcon>
<ActionIcon variant="subtle" aria-label={'Delete ' + itemDescription} onClick={handleDelete}>
<ActionIcon variant="subtle" aria-label={'Delete ' + props.item.description} onClick={handleDelete}>
<IconTrash />
</ActionIcon>
</div>
Expand All @@ -58,15 +68,15 @@ export function Item(props: {item: IItem, removeItem: Function, edit: boolean})
descriptionContent = (
<TextInput
className={classes.itemDescription}
value={itemDescription}
value={props.item.description}
autoFocus
onBlur={handleEditFinished}
onChange={e => setItemDescription(e.target.value)} />
onChange={handleDescriptionChanged} />
);

editButtons = (
<div>
<ActionIcon variant="subtle" aria-label={'Delete ' + itemDescription} onClick={handleDelete}>
<ActionIcon variant="subtle" aria-label={'Delete ' + props.item.description} onClick={handleDelete}>
<IconTrash />
</ActionIcon>
</div>
Expand All @@ -78,7 +88,7 @@ export function Item(props: {item: IItem, removeItem: Function, edit: boolean})
<Flex align="center">
<Checkbox
type="checkbox"
checked={checked}
checked={props.item.checked}
onChange={handleChecked} />
{descriptionContent}
</Flex>
Expand Down
5 changes: 4 additions & 1 deletion app/src/components/List/List.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { render, screen, waitFor, userEvent, UserEvent} from '../../testing-utils';
import { IItem } from '@groceries/shared';
import { List } from './List';
import { deleteItem, getItems } from '../../services/api';
import { batchDeleteItems, deleteItem, getItems } from '../../services/api';

jest.mock('../../services/api')

Expand Down Expand Up @@ -136,6 +136,7 @@ describe('Clearing items', () => {
var user: UserEvent;
beforeEach(() => {
(getItems as jest.Mock).mockReturnValue(Promise.resolve(mockData));
(batchDeleteItems as jest.Mock).mockReturnValue(Promise.resolve(""));
user = userEvent.setup();
});

Expand All @@ -155,6 +156,7 @@ describe('Clearing items', () => {
render(<List setError={jest.fn()}/>)
await user.click(await screen.findByRole('button', {name: 'Clear'}));
await user.click(await screen.findByRole('menuitem', {name: 'Clear All'}));
expect(batchDeleteItems).toHaveBeenCalled();
expect(screen.queryByText('Chicken')).toBeNull();
expect(screen.queryByText('Steak')).toBeNull();
expect(screen.queryByText('Apples')).toBeNull();
Expand All @@ -164,6 +166,7 @@ describe('Clearing items', () => {
render(<List setError={jest.fn()}/>)
await user.click(await screen.findByRole('button', {name: 'Clear'}));
await user.click(await screen.findByRole('menuitem', {name: 'Clear Checked'}));
expect(batchDeleteItems).toHaveBeenCalled();
expect(screen.queryByText('Chicken')).toBeInTheDocument();
expect(screen.queryByText('Steak')).toBeNull();
expect(screen.queryByText('Apples')).toBeInTheDocument();
Expand Down
27 changes: 23 additions & 4 deletions app/src/components/List/List.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { createItem, deleteItem, getItems } from '../../services/api';
import { batchDeleteItems, createItem, deleteItem, getItems } from '../../services/api';
import { Section } from '../Section/Section';
import { Button, Loader, Menu, TextInput } from '@mantine/core';
import { useExitOnEscape } from '../../hooks';
Expand Down Expand Up @@ -57,6 +57,15 @@ export function List(props: {setError: Function}) {
});
}

function updateItem(item: IItem) {
var itemToUpdate = items.find(i => i.id === item.id);
if (itemToUpdate) {
itemToUpdate = {...item}
}

setItems([...items]);
}

function removeItem(item: IItem) {
deleteItem(item)
.then(_ => {
Expand All @@ -65,11 +74,21 @@ export function List(props: {setError: Function}) {
}

function clearAllItems() {
setItems([]);
batchDeleteItems(items
.filter(i => i.id)
.map(i => i.id!))
.then(() => {
setItems([]);
});
}

function clearCheckedItems() {
setItems([...items.filter(i => !i.checked)]);
batchDeleteItems(items
.filter(i => i.checked)
.map(i => i.id!))
.then(() => {
setItems([...items.filter(i => !i.checked)]);
});
}

// create section components
Expand All @@ -80,7 +99,7 @@ export function List(props: {setError: Function}) {
addNewItem={addNewItem}>
{items
.filter(i => i.section === section)
.map(i => <Item key={i.id} item={i} removeItem={removeItem} edit={false} />)}
.map(i => <Item key={i.id} item={i} removeItem={removeItem} updateItemState={updateItem} edit={false} />)}
</Section>
);

Expand Down
2 changes: 1 addition & 1 deletion app/src/components/Section/Section.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const mockData: IItem[] = [
describe('Render', () => {
it('should render', () => {
render(<Section name={mockName} addNewItem={jest.fn()}>
{mockData.map(i => <Item key={i.id} item={i} removeItem={jest.fn()} edit={false} />)}
{mockData.map(i => <Item key={i.id} item={i} removeItem={jest.fn()} updateItemState={jest.fn()} edit={false} />)}
</Section>)
});
});
Expand Down
4 changes: 4 additions & 0 deletions app/src/services/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,8 @@ export async function updateItem(item: IItem): Promise<IItem> {

export async function deleteItem(item: IItem) {
return call('DELETE', `/items/${item.id}`);
}

export async function batchDeleteItems(ids: string[]) {
return call('POST', '/items:batchDelete', ids);
}

0 comments on commit f837ca3

Please sign in to comment.