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]
name = "sigl"
version = "0.1.1"
version = "0.1.2"
description = "Simple Grocery List"
authors = ["Jonathan Krauss <jkrauss@asymworks.com>"]
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 sqlalchemy import func
from sqlalchemy.orm import Session
from sigl.exc import DomainError, NotFoundError
@@ -44,6 +45,8 @@ def list_addItem(
if not productName:
raise DomainError('Product Name cannot be empty')
product = product_by_name(session, productName)
if not product:
product = Product(name=productName, category=productCategory)
if remember is not None:
product.remember = remember
@@ -104,10 +107,10 @@ def list_deleteCrossedOff(session: Session, id: int) -> ShoppingList:
raise NotFoundError(f'List {id} does not exist')
for entry in sList.entries:
if not entry.product.remember:
session.delete(entry.product)
if entry.crossedOff:
session.delete(entry)
if not entry.product.remember:
session.delete(entry.product)
session.commit()
@@ -230,10 +233,15 @@ def products_all(session: Session) -> List[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()
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(
session: Session,
name: str,

View File

@@ -5,27 +5,31 @@
<div class="text-sm font-bold text-gray-800">Add Item to {{ list.name }}</div>
</div>
{% 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 %}
<form method="post">
<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>
<div class="flex flex-col pb-4">
<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'>
<option value="" disabled selected>Select a Product</option>
<option value="new">Create a New Product</option>
{% for p in products|sort(attribute='category')|sort(attribute='name') %}
<option value="{{ p.id }}">{{ p.name }}{% if p.category %} (in {{ p.category }}){% endif %}</option>
{% endfor %}
</select>
<jt-autocomplete clearable>
<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)" />
</jt-autocomplete>
<span class="text-xs text-blue-300" x-show="newProduct">New Product</span>
</div>
<div class="flex flex-col sm:flex-row sm:justify-between" x-show="productId == 'new'">
<div class="w-full sm:mr-1 flex flex-col pb-4">
<label for="productName" class="py-1 text-xs text-gray-700 font-semibold">Product Name:</label>
<input type="text" id="productName" name="productName" class="p-1 text-sm border border-gray-200 rounded" />
<div class="flex flex-row align-center justify-start gap-2 pb-4" x-show="newProduct">
<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="rememberProduct" class="text-xs text-gray-700 font-semibold">Remember Product</label>
</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>
<input type="text" id="productCategory" name="productCategory" class="p-1 text-sm border border-gray-200 rounded" />
</div>
@@ -49,4 +53,19 @@
</div>
</div>
</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 %}

View File

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

View File

@@ -6,7 +6,12 @@ Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
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
pytestmark = pytest.mark.usefixtures('app')
@@ -60,3 +65,11 @@ def test_product_all_items_skips_non_remembered(session):
assert p1 in products
assert p3 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_entry_set_crossedOff,
product_by_id,
product_create,
)
# Always use 'app' fixture so ORM gets initialized
@@ -45,6 +46,42 @@ def test_list_add_product_defaults(session):
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
def test_list_add_product_no_remember(session):
"""Test adding a Product to a List without remembering it."""