Use Autocomplete instead of Select in Add to List screen

This commit is contained in:
2023-01-05 10:07:01 -08:00
parent 4d0b9b015c
commit 7b236b1c2f
6 changed files with 105 additions and 28 deletions

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "sigl" name = "sigl"
version = "0.1.1" version = "0.1.2"
description = "Simple Grocery List" description = "Simple Grocery List"
authors = ["Jonathan Krauss <jkrauss@asymworks.com>"] authors = ["Jonathan Krauss <jkrauss@asymworks.com>"]
license = "BSD-3-Clause" license = "BSD-3-Clause"

View File

@@ -6,6 +6,7 @@ Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
from typing import List, Optional, Union from typing import List, Optional, Union
from sqlalchemy import func
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sigl.exc import DomainError, NotFoundError from sigl.exc import DomainError, NotFoundError
@@ -44,6 +45,8 @@ def list_addItem(
if not productName: if not productName:
raise DomainError('Product Name cannot be empty') raise DomainError('Product Name cannot be empty')
product = product_by_name(session, productName)
if not product:
product = Product(name=productName, category=productCategory) product = Product(name=productName, category=productCategory)
if remember is not None: if remember is not None:
product.remember = remember product.remember = remember
@@ -104,10 +107,10 @@ def list_deleteCrossedOff(session: Session, id: int) -> ShoppingList:
raise NotFoundError(f'List {id} does not exist') raise NotFoundError(f'List {id} does not exist')
for entry in sList.entries: for entry in sList.entries:
if not entry.product.remember:
session.delete(entry.product)
if entry.crossedOff: if entry.crossedOff:
session.delete(entry) session.delete(entry)
if not entry.product.remember:
session.delete(entry.product)
session.commit() session.commit()
@@ -230,10 +233,15 @@ def products_all(session: Session) -> List[Product]:
def product_by_id(session: Session, id: int) -> Optional[Product]: def product_by_id(session: Session, id: int) -> Optional[Product]:
"""Load a specific Product.""" """Load a specific Product by Id."""
return session.query(Product).filter(Product.id == id).one_or_none() return session.query(Product).filter(Product.id == id).one_or_none()
def product_by_name(session: Session, name: str) -> Optional[Product]:
"""Load a specific Product by Name."""
return session.query(Product).filter(func.lower(Product.name) == func.lower(name)).one_or_none()
def product_create( def product_create(
session: Session, session: Session,
name: str, name: str,

View File

@@ -5,27 +5,31 @@
<div class="text-sm font-bold text-gray-800">Add Item to {{ list.name }}</div> <div class="text-sm font-bold text-gray-800">Add Item to {{ list.name }}</div>
</div> </div>
{% endblock %} {% endblock %}
{% block head_scripts %}
<script type="module">
import JtAutocomplete from "https://unpkg.com/@jadetree/ui/dist/components/autocomplete.js";
JtAutocomplete.register();
</script>
<link rel="stylesheet" href="https://unpkg.com/@jadetree/ui/css/index.css" />
{% endblock %}
{% block main %} {% block main %}
<form method="post"> <form method="post">
<div class="py-2 px-4 flex flex-col"> <div class="py-2 px-4 flex flex-col">
<fieldset class="flex flex-col" x-data="{productId:''}"> <fieldset class="flex flex-col" x-data="{productName:'',newProduct:false}">
<legend class="sr-only">Select Product to Add</legend> <legend class="sr-only">Select Product to Add</legend>
<div class="flex flex-col pb-4"> <div class="flex flex-col pb-4">
<label for="product" class="py-1 text-xs text-gray-700 font-semibold">Product:</label> <label for="product" class="py-1 text-xs text-gray-700 font-semibold">Product:</label>
<select id="product" name="product" class="flex-grow p-1 text-sm bg-white border rounded" x-model='productId'> <jt-autocomplete clearable>
<option value="" disabled selected>Select a Product</option> <input id="product" name="productName" class="p-1 text-sm border border-gray-200 rounded" list="product-list" x-model="productName" @blur="newProduct=!isExistingProduct(productName)" />
<option value="new">Create a New Product</option> </jt-autocomplete>
{% for p in products|sort(attribute='category')|sort(attribute='name') %} <span class="text-xs text-blue-300" x-show="newProduct">New Product</span>
<option value="{{ p.id }}">{{ p.name }}{% if p.category %} (in {{ p.category }}){% endif %}</option>
{% endfor %}
</select>
</div> </div>
<div class="flex flex-col sm:flex-row sm:justify-between" x-show="productId == 'new'"> <div class="flex flex-row align-center justify-start gap-2 pb-4" x-show="newProduct">
<div class="w-full sm:mr-1 flex flex-col pb-4"> <input type="checkbox" id="rememberProduct" name="remember" class="appearance-none h-4 w-4 border border-gray-300 rounded-sm bg-white checked:bg-blue-600 checked:border-blue-600 focus:outline-none transition duration-200 bg-no-repeat bg-center bg-contain cursor-pointer" />
<label for="productName" class="py-1 text-xs text-gray-700 font-semibold">Product Name:</label> <label for="rememberProduct" class="text-xs text-gray-700 font-semibold">Remember Product</label>
<input type="text" id="productName" name="productName" class="p-1 text-sm border border-gray-200 rounded" />
</div> </div>
<div class="w-full sm:ml-1 flex flex-col pb-4"> <div class="flex flex-col sm:flex-row sm:justify-between" x-show="newProduct">
<div class="w-full flex flex-col pb-4">
<label for="productCategory" class="py-1 text-xs text-gray-700 font-semibold">Category:</label> <label for="productCategory" class="py-1 text-xs text-gray-700 font-semibold">Category:</label>
<input type="text" id="productCategory" name="productCategory" class="p-1 text-sm border border-gray-200 rounded" /> <input type="text" id="productCategory" name="productCategory" class="p-1 text-sm border border-gray-200 rounded" />
</div> </div>
@@ -49,4 +53,19 @@
</div> </div>
</div> </div>
</form> </form>
<datalist id="product-list">
{% for p in products|sort(attribute='category')|sort(attribute='name') %}
<option{% if p.category %} data-category="{{ p.category }}"{% endif %}>{{ p.name }}</option>
{% endfor %}
</datalist>
{% endblock %}
{% block body_scripts %}
<script language="javascript">
function isExistingProduct(product) {
if (!product) return true;
const products = Array.from(document.querySelectorAll('#product-list option'))
.map((opt) => opt.textContent.toLowerCase().trim());
return products.includes(product.toLowerCase().trim());
}
</script>
{% endblock %} {% endblock %}

View File

@@ -6,6 +6,7 @@ Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
from flask import ( from flask import (
Blueprint, Blueprint,
current_app,
flash, flash,
jsonify, jsonify,
make_response, make_response,
@@ -225,7 +226,7 @@ def addItem(id):
sList = list_by_id(db.session, id) sList = list_by_id(db.session, id)
products = products_all(db.session) products = products_all(db.session)
if request.method == 'POST': if request.method == 'POST':
if 'product' not in request.form: if 'productName' not in request.form:
flash( flash(
'An internal error occurred. Please reload the page and try again', 'An internal error occurred. Please reload the page and try again',
'error' 'error'
@@ -236,21 +237,20 @@ def addItem(id):
products=products, products=products,
) )
productId = request.form['product']
productName = request.form.get('productName', '').strip() productName = request.form.get('productName', '').strip()
productCategory = request.form.get('productCategory', '').strip() productCategory = request.form.get('productCategory', '').strip()
remember = request.form.get('remember', 'off') == 'on'
quantity = request.form.get('quantity', '').strip() quantity = request.form.get('quantity', '').strip()
notes = request.form.get('notes', '').strip() notes = request.form.get('notes', '').strip()
if productId == 'new' or productId == '': current_app.logger.info(f'Remember Value: {remember}')
productId = None
list_addItem( list_addItem(
db.session, db.session,
id, id,
productId=productId,
productName=productName, productName=productName,
productCategory=productCategory, productCategory=productCategory,
remember=remember,
quantity=quantity, quantity=quantity,
notes=notes, notes=notes,
) )

View File

@@ -6,7 +6,12 @@ Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
import pytest import pytest
from sigl.domain.service import product_by_id, product_create, products_all from sigl.domain.service import (
product_by_id,
product_by_name,
product_create,
products_all,
)
# Always use 'app' fixture so ORM gets initialized # Always use 'app' fixture so ORM gets initialized
pytestmark = pytest.mark.usefixtures('app') pytestmark = pytest.mark.usefixtures('app')
@@ -60,3 +65,11 @@ def test_product_all_items_skips_non_remembered(session):
assert p1 in products assert p1 in products
assert p3 in products assert p3 in products
assert p2 not in products assert p2 not in products
@pytest.mark.unit
def test_product_lookup_by_name(session):
"""Test that a Product can be looked up by Name (case-insensitive)."""
p1 = product_create(session, 'Apples')
product = product_by_name(session, 'apples')
assert product == p1

View File

@@ -13,6 +13,7 @@ from sigl.domain.service import (
list_deleteCrossedOff, list_deleteCrossedOff,
list_entry_set_crossedOff, list_entry_set_crossedOff,
product_by_id, product_by_id,
product_create,
) )
# Always use 'app' fixture so ORM gets initialized # Always use 'app' fixture so ORM gets initialized
@@ -45,6 +46,42 @@ def test_list_add_product_defaults(session):
assert list.entries[0] == entry assert list.entries[0] == entry
@pytest.mark.unit
def test_list_add_product_by_id(session):
"""Test adding an existing Product to a List by Id."""
p1 = product_create(session, 'Eggs', category='Dairy')
list = list_create(session, 'Test')
entry = list_addItem(session, list.id, productId=p1.id)
assert entry.id is not None
assert entry.product is not None
assert entry.product.name == 'Eggs'
assert entry.product.category == 'Dairy'
assert entry.product.remember is True
assert len(list.entries) == 1
assert list.entries[0] == entry
@pytest.mark.unit
def test_list_add_product_by_name(session):
"""Test adding an existing Product to a List by Name."""
product_create(session, 'Eggs', category='Dairy')
list = list_create(session, 'Test')
entry = list_addItem(session, list.id, productName='eggs')
assert entry.id is not None
assert entry.product is not None
assert entry.product.name == 'Eggs'
assert entry.product.category == 'Dairy'
assert entry.product.remember is True
assert len(list.entries) == 1
assert list.entries[0] == entry
@pytest.mark.unit @pytest.mark.unit
def test_list_add_product_no_remember(session): def test_list_add_product_no_remember(session):
"""Test adding a Product to a List without remembering it.""" """Test adding a Product to a List without remembering it."""