Editable shopping category

This commit is contained in:
Tim Holloway 2022-01-05 11:02:42 -05:00
parent e4da9a6a71
commit ce0f354f9a
11 changed files with 659 additions and 340 deletions

View File

@ -88,6 +88,11 @@
<version>2.3.3</version> <version>2.3.3</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.apache.tomcat.embed</groupId> <groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId> <artifactId>tomcat-embed-jasper</artifactId>

View File

@ -0,0 +1,106 @@
package com.mousetech.gourmetj;
import java.io.Serializable;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.faces.event.AjaxBehaviorEvent;
import javax.faces.view.ViewScoped;
import javax.inject.Inject;
import javax.inject.Named;
import org.primefaces.event.SelectEvent;
import com.mousetech.gourmetj.persistence.dao.ShopcatRepository;
import com.mousetech.gourmetj.persistence.model.Shopcat;
@Named
@ViewScoped
public class EditShopcatBean implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private String ingkey;
private String shopcatName;
private String shopcatSuggestion;
private List<String> shopcatSuggestionList;
@Inject
ShopcatRepository shopcatRepository;
/**
* Default Constructor.
*/
public EditShopcatBean() {
}
@PostConstruct
public void init() {
this.shopcatSuggestionList = this.shopcatRepository
.findDistinctCategoryNative();
// // Get ingkey from Flash scope
// ingkey = (String) JSFUtils.getFlash("ingkey");
// Shopcat scat = this.shopcatRepository
// .findShopcatByIngkey(ingkey);
// if (scat != null) {
// this.setShopcatName(scat.getShopcategory());
// }
}
/**
* @return the shopcatName
*/
public String getShopcatName() {
return shopcatName;
}
/**
* @param shopcatName the shopcatName to set
*/
public void setShopcatName(String shopcatName) {
this.shopcatName = shopcatName;
}
/**
* @return the shopcatSuggestion
*/
public String getShopcatSuggestion() {
return shopcatSuggestion;
}
/**
* @param shopcatSuggestion the shopcatSuggestion to set
*/
public void setShopcatSuggestion(String shopcatSuggestion) {
this.shopcatSuggestion = shopcatSuggestion;
}
/**
* @return the ingkey
*/
public String getIngkey() {
return ingkey;
}
public void setIngkey(String ingkey) {
this.ingkey = ingkey;
}
/**
* @return the shopcatSuggestionList
*/
public List<String> getShopcatSuggestionList() {
return shopcatSuggestionList;
}
public void ajaxShopcatSuggest(AjaxBehaviorEvent event) {
this.shopcatName = this.shopcatSuggestion;
}
}

View File

@ -100,7 +100,6 @@ public class IngredientUI implements IngredientIF {
* @see #getAmount * @see #getAmount
*/ */
public String getDisplayAmount() { public String getDisplayAmount() {
// TODO
Double amt = ingredient.getAmount(); Double amt = ingredient.getAmount();
if (amt == null) { if (amt == null) {
return ""; return "";
@ -120,8 +119,7 @@ public class IngredientUI implements IngredientIF {
if (amount.isBlank()) { if (amount.isBlank()) {
ingredient.setAmount(null); ingredient.setAmount(null);
} }
IngredientDigester digester = new IngredientDigester(); Double[] amt = IngredientDigester.digestAmount(amount);
Double[] amt = digester.digestAmount(amount);
ingredient.setAmount(amt[0]); ingredient.setAmount(amt[0]);
ingredient.setRangeamount(amt[1]); ingredient.setRangeamount(amt[1]);
} }

View File

@ -83,4 +83,12 @@ public class JSFUtils {
return (FacesContext.getCurrentInstance() return (FacesContext.getCurrentInstance()
.getExternalContext().getFlash()); .getExternalContext().getFlash());
} }
public static Object getFlash(String key) {
return flashScope().get(key);
}
public static void putFlash(String key, Object value) {
flashScope().put(key, value);
}
} }

View File

@ -2,8 +2,11 @@ package com.mousetech.gourmetj;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@ -17,6 +20,8 @@ import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.servlet.http.Part; import javax.servlet.http.Part;
import org.apache.commons.lang3.StringUtils;
import org.primefaces.PrimeFaces;
import org.primefaces.event.FileUploadEvent; import org.primefaces.event.FileUploadEvent;
import org.primefaces.model.UploadedFile; import org.primefaces.model.UploadedFile;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -30,7 +35,6 @@ import com.mousetech.gourmetj.persistence.model.Recipe;
import com.mousetech.gourmetj.persistence.model.Shopcat; import com.mousetech.gourmetj.persistence.model.Shopcat;
import com.mousetech.gourmetj.persistence.service.RecipeService; import com.mousetech.gourmetj.persistence.service.RecipeService;
import com.mousetech.gourmetj.springweb.PictureController; import com.mousetech.gourmetj.springweb.PictureController;
import com.mousetech.gourmetj.IngredientDigester;
/** /**
* Backing bean for display/edit recipe detail * Backing bean for display/edit recipe detail
@ -218,8 +222,7 @@ public class RecipeDetailBean implements Serializable {
private void init() { private void init() {
this.recipe = userSession.getRecipe(); this.recipe = userSession.getRecipe();
log.info("Using recipe: " + this.recipe);
log.info("Using recipe: " + this.recipe );
/** /**
* For "create new, this recipe is a blank constructed * For "create new, this recipe is a blank constructed
* and passed from main page. For Detail display, it's * and passed from main page. For Detail display, it's
@ -243,7 +246,7 @@ public class RecipeDetailBean implements Serializable {
getIngredients().setWrappedData( getIngredients().setWrappedData(
buildIngredientFacade(recipe.getIngredientHash())); buildIngredientFacade(recipe.getIngredientHash()));
log.info("Set recipe: " + this.recipe ); log.info("Set recipe: " + this.recipe);
} }
/** /**
@ -438,6 +441,64 @@ public class RecipeDetailBean implements Serializable {
} }
} }
// =====
@Inject
EditShopcatBean editShopcatBean;
public void ajaxEditShopcat(IngredientUI item) {
// Map<String, Object> options = new HashMap<>();
// options.put("resizable", false);
// TODO: Reject if ingkey is empty/null
editShopcatBean.setIngkey(item.getIngkey());
editShopcatBean.setShopcatName(item.getShopCat());
// if ( ! StringUtils.isBlank(key) ) {
// PrimeFaces.current().dialog().openDynamic("editShopcat");
// }
}
public void doUpdateShopcat() {
final String key = editShopcatBean.getIngkey();
if ( StringUtils.isBlank(key)) {
return; // Do not set category if no ingKey
}
final String catname = editShopcatBean.getShopcatName();
Shopcat sc = this.recipeService
.findShopcatForIngredientKey(catname);
if (sc == null) {
sc = new Shopcat();
sc.setIngkey(key);
sc.setShopcategory(catname);
} else {
if (catname.equals(sc.getShopcategory())) {
// No change. Do nothing.
return;
}
}
if ( StringUtils.isBlank(catname)) {
this.recipeService.deleteShopcat(sc);
} else {
this.recipeService.saveShopcat(sc);
}
updateDisplayedShopcats(key, sc);
}
/**
* When Shopcat name changes, update the Ingredients.
* In detailEdit, an AJAX "render" will then update
* the display. In recipeDetails, nothing actually shows.
*/
private void updateDisplayedShopcats(String key, Shopcat sc) {
List<IngredientUI> ingList = this.getWrappedIngredients();
for (IngredientUI ingUI: ingList ) {
if ( key.equals(ingUI.getIngkey())) {
ingUI.setShopCat(sc);
}
}
}
/** /**
* Bulk add for ingredients. Looks for long input and if * Bulk add for ingredients. Looks for long input and if
* found, tries to split it up and add as multiple * found, tries to split it up and add as multiple
@ -581,7 +642,7 @@ public class RecipeDetailBean implements Serializable {
* it. * it.
*/ */
private boolean saveIngredients() { private boolean saveIngredients() {
if ( ! updateIngredientList()) { if (!updateIngredientList()) {
return false; return false;
} }
updateRecipeCategories(recipe, category); updateRecipeCategories(recipe, category);
@ -597,7 +658,7 @@ public class RecipeDetailBean implements Serializable {
for (IngredientUI iui : saveIng) { for (IngredientUI iui : saveIng) {
Ingredient ing = iui.getIngredient(); Ingredient ing = iui.getIngredient();
ing.setRecipe(recipe); ing.setRecipe(recipe);
if ( ! updateShopcat(iui) ) { if (!updateShopcat(iui)) {
return false; return false;
} }
iList.add(ing); iList.add(ing);
@ -623,7 +684,6 @@ public class RecipeDetailBean implements Serializable {
} }
} }
/** /**
* Update shopcat for Ingredient. * Update shopcat for Ingredient.
* *
@ -633,15 +693,17 @@ public class RecipeDetailBean implements Serializable {
private boolean updateShopcat(IngredientUI ingUI) { private boolean updateShopcat(IngredientUI ingUI) {
final Ingredient ing = ingUI.getIngredient(); final Ingredient ing = ingUI.getIngredient();
final String ingKey = ing.getIngkey(); final String ingKey = ing.getIngkey();
if ( (ingKey == null) || (ingKey.isBlank())) { if ((ingKey == null) || (ingKey.isBlank())) {
ing.setIngkey(null); ing.setIngkey(null);
ing.setShopCat(null); ing.setShopCat(null);
return true; return true;
} }
Shopcat scat = recipeService.findShopcatForIngredientKey(ingKey); Shopcat scat = recipeService
if ( scat == null ) { .findShopcatForIngredientKey(ingKey);
if (scat == null) {
JSFUtils.addErrorMessage( JSFUtils.addErrorMessage(
"No Shopping Category is defined for Ingredient Key "+ ingKey); "No Shopping Category is defined for Ingredient Key "
+ ingKey);
return false; return false;
} }
@ -808,7 +870,8 @@ public class RecipeDetailBean implements Serializable {
public void ajaxUpdateShopcat(IngredientUI item) { public void ajaxUpdateShopcat(IngredientUI item) {
log.warn("SHOPCAT2 "); log.warn("SHOPCAT2 ");
updateShopcat(item);; updateShopcat(item);
;
} }
// *** // ***

View File

@ -1,23 +1,28 @@
package com.mousetech.gourmetj; package com.mousetech.gourmetj;
import javax.inject.Qualifier;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.web.filter.HiddenHttpMethodFilter; import org.springframework.http.HttpStatus;
@SpringBootApplication @SpringBootApplication
@EntityScan(value = {"com.mousetech.gourmetj.persistence.model"}) @EntityScan(value = {
"com.mousetech.gourmetj.persistence.model" })
public class SpringPrimeFacesApplication { public class SpringPrimeFacesApplication {
final String errorPage = "/error/error.html";
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(SpringPrimeFacesApplication.class, args); SpringApplication.run(SpringPrimeFacesApplication.class,
args);
} }
@Bean @Bean
@ -39,4 +44,20 @@ public class SpringPrimeFacesApplication {
} }
}; };
} }
@Bean
public ErrorPageRegistrar errorPageRegistrar() {
return new ErrorPageRegistrar() {
@Override
public void registerErrorPages(
ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage(
HttpStatus.NOT_FOUND, errorPage));
registry.addErrorPages(new ErrorPage(
HttpStatus.INTERNAL_SERVER_ERROR,
errorPage));
}
};
}
} }

View File

@ -89,6 +89,15 @@ public class RecipeService implements Serializable {
return shopcatRepository.findShopcatByIngkey(ingkey); return shopcatRepository.findShopcatByIngkey(ingkey);
} }
public void saveShopcat(Shopcat sc) {
shopcatRepository.save(sc);
}
public void deleteShopcat(Shopcat sc) {
shopcatRepository.delete(sc);
}
/** /**
* CategoryService as a sub-function of RecipeService * CategoryService as a sub-function of RecipeService
*/ */

View File

@ -1,16 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ui:composition xmlns:f="http://xmlns.jcp.org/jsf/core" <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html" xmlns:p="http://primefaces.org/ui"
xmlns:tc="http://myfaces.apache.org/tobago/component"
> >
<head></head>
<body>
<ui:composition>
<f:view> <f:view>
<h:head> <h:head>
<title><ui:insert name="title">Gourmet Recipe Manager (web version)</ui:insert></title> <title><ui:insert name="title">Gourmet Recipe Manager (web version)</ui:insert></title>
<link rel="icon" type="image/vnd.microsoft.icon" <link rel="icon" type="image/vnd.microsoft.icon"
href="#{pageContext.contextPath}/favicon.ico" href="#{pageContext.contextPath}/favicon.ico"
/> />
<h:outputStylesheet name="css/style.css" /> <h:outputStylesheet name="css/style.css" />
</h:head> </h:head>
<h:body> <h:body>
@ -27,4 +31,6 @@
<p>Based on Gourmet Recipe Manager by T. Hinkle</p> <p>Based on Gourmet Recipe Manager by T. Hinkle</p>
</h:body> </h:body>
</f:view> </f:view>
</ui:composition> </ui:composition>
</body>
</html>

View File

@ -1,11 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ui:composition template="/WEB-INF/layout/layout.xhtml" <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
xmlns:c="http://xmlns.jcp.org/jstl" xmlns:c="http://xmlns.jcp.org/jstl"
> >
<head></head>
<body>
<ui:composition template="/WEB-INF/layout/layout.xhtml">
<ui:define name="title">Gourmet Recipe Manager</ui:define> <ui:define name="title">Gourmet Recipe Manager</ui:define>
<ui:define name="content"> <ui:define name="content">
<style> <style>
@ -29,17 +33,21 @@
header="#{recipeDetailBean.recipe.title}" header="#{recipeDetailBean.recipe.title}"
> >
<h:form id="form1" enctype="multipart/form-data"> <h:form id="form1" enctype="multipart/form-data">
<p:tabView id="tabGroupClient" orientation="left" <p:tabView id="tabGroupClient"
orientation="left"
activeIndex="#{userSession.detailTab}" activeIndex="#{userSession.detailTab}"
> >
<p:tab id="overviewTab" title="Description"> <p:tab id="overviewTab"
title="Description"
>
<p:panelGrid columns="2"> <p:panelGrid columns="2">
<f:facet name="header">Description</f:facet> <f:facet name="header">Description</f:facet>
<p:outputLabel for="@next" <p:outputLabel for="@next"
value="Title" value="Title"
/> />
<p:inputText id="rtitle" size="45" <p:inputText id="rtitle"
required="true" focus="true" size="45" required="true"
focus="true"
placeholder="A recipe title is required." placeholder="A recipe title is required."
value="#{recipeDetailBean.recipe.title}" value="#{recipeDetailBean.recipe.title}"
> >
@ -58,7 +66,8 @@
<p:commandButton <p:commandButton
value="&lt;- Suggest" value="&lt;- Suggest"
> >
<f:ajax execute="rcategory bxCat" <f:ajax
execute="rcategory bxCat"
listener="#{recipeDetailBean.ajaxSuggestCategory}" listener="#{recipeDetailBean.ajaxSuggestCategory}"
render="rcategory" render="rcategory"
/> />
@ -87,7 +96,8 @@
<p:outputLabel for="@next" <p:outputLabel for="@next"
value="Source" value="Source"
/> />
<p:inputText id="rsource" size="45" <p:inputText id="rsource"
size="45"
value="#{recipeDetailBean.recipe.source}" value="#{recipeDetailBean.recipe.source}"
/> />
<p:outputLabel for="@next" <p:outputLabel for="@next"
@ -114,7 +124,8 @@
global="true" mode="advanced" global="true" mode="advanced"
multiple="false" multiple="false"
update=":messages picPanel" update=":messages picPanel"
auto="true" sizeLimit="1000000" auto="true"
sizeLimit="1000000"
allowTypes="/(\.|\/)(gif|jpe?g|png)$/" allowTypes="/(\.|\/)(gif|jpe?g|png)$/"
/> />
<p:commandButton id="ctlDelImg" <p:commandButton id="ctlDelImg"
@ -166,7 +177,8 @@
disabled="true" disabled="true"
/> />
<p:button value="Paste" /> <p:button value="Paste" />
<p:commandButton value="Delete" <p:commandButton
value="Delete"
id="ctlDelete" id="ctlDelete"
disabled="not #{recipeDetailBean.selectionActive}" disabled="not #{recipeDetailBean.selectionActive}"
> >
@ -191,12 +203,15 @@
id="selected" id="selected"
value="#{item.selected}" value="#{item.selected}"
> >
<f:ajax immediate="true" <f:ajax
immediate="true"
render="pnlIngredients" render="pnlIngredients"
/> />
</p:selectBooleanCheckbox> </p:selectBooleanCheckbox>
</p:column> </p:column>
<p:column style="width: 3.6em"> <p:column
style="width: 3.6em"
>
<f:facet name="header"> <f:facet name="header">
Amt. Amt.
</f:facet> </f:facet>
@ -251,7 +266,8 @@
size="20" size="20"
rendered="#{not item.ingGroup}" rendered="#{not item.ingGroup}"
> >
<f:ajax event="change" <f:ajax
event="change"
listener="#{recipeDetailBean.ajaxUpdateShopcat(item)}" listener="#{recipeDetailBean.ajaxUpdateShopcat(item)}"
render="shopCat" render="shopCat"
/> />
@ -261,14 +277,24 @@
<f:facet name="header"> <f:facet name="header">
Shop. Cat. Shop. Cat.
</f:facet> </f:facet>
<h:outputText id="shopCat" <h:outputText
id="shopCat"
value="#{item.shopCat}" value="#{item.shopCat}"
rendered="#{not item.ingGroup}" rendered="#{not item.ingGroup}"
/> />
<p:commandButton
id="eShopcat"
value="E"
action="#{recipeDetailBean.ajaxEditShopcat(item)}"
update="editShopcatDlg"
oncomplete="PF('editShopcatDlg').show()"
>
</p:commandButton>
</p:column> </p:column>
</p:dataTable> </p:dataTable>
<h:panelGroup id="pnlIng"> <h:panelGroup id="pnlIng">
<p:remoteCommand name="ingButton" <p:remoteCommand
name="ingButton"
action="#{recipeDetailBean.doAddIngredient}" action="#{recipeDetailBean.doAddIngredient}"
process="@this ctlAddIngTxt" process="@this ctlAddIngTxt"
update="pnlIngredients" update="pnlIngredients"
@ -284,7 +310,8 @@
title="You can paste in multiple ingredients here!" title="You can paste in multiple ingredients here!"
onkeydown="if (event.keyCode === 13) { ingButton(); this.focus(); return false; }" onkeydown="if (event.keyCode === 13) { ingButton(); this.focus(); return false; }"
/> />
<p:commandButton id="ctlAddIng" <p:commandButton
id="ctlAddIng"
value="+ Add" value="+ Add"
onclick="ingButton(); return false;" onclick="ingButton(); return false;"
> >
@ -299,7 +326,8 @@
<div id="insection"> <div id="insection">
<p:textEditor <p:textEditor
id="ctlInstructions" id="ctlInstructions"
height="320" escape="false" height="320"
escape="false"
value="#{recipeDetailBean.recipe.instructions}" value="#{recipeDetailBean.recipe.instructions}"
/> />
</div> </div>
@ -323,5 +351,10 @@
/> />
</h:form> </h:form>
</p:panel> </p:panel>
<p:dialog id="editShopcatDlg" widgetVar="editShopcatDlg">
<ui:include src="editShopcat.xhtml" />
</p:dialog>
</ui:define> </ui:define>
</ui:composition> </ui:composition>
</body>
</html>

View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
>
<h:head>
<title>Shopping Category</title>
<style type="text/css">
html {
font-size: 14px;
}
</style>
</h:head>
<h:body>
<ui:component>
<h:form id="frmShopcat">
<p:panelGrid columns="1">
<div>Ingredient key:
#{editShopcatBean.ingkey}
</div>
<p:outputLabel for="@next"
value="Category Name" />
<p:inputText id="ctlShopcat"
value="#{editShopcatBean.shopcatName}"
>
</p:inputText>
<h:outputText
value="suggestion:" />
<p:selectOneMenu
id="ctlShopcatMenu"
value="#{editShopcatBean.shopcatSuggestion}"
>
<f:selectItems
value="#{editShopcatBean.shopcatSuggestionList}" />
<p:ajax event="change"
listener="#{editShopcatBean.ajaxShopcatSuggest}"
update="ctlShopcat" />
</p:selectOneMenu>
<p:panelGrid columns="2"
style="width: 100%"
>
<p:commandButton
id="scDlgOK" value="OK"
style="width: 6em"
action="#{recipeDetailBean.doUpdateShopcat}"
update="form1:tabGroupClient:ingredientTable"
oncomplete="PF('editShopcatDlg').hide()" />
<p:commandButton
id="scDlgCan" value="Cancel"
style="width: 6em"
onclick="PF('editShopcatDlg').hide()" />
</p:panelGrid>
</p:panelGrid>
</h:form>
</ui:component>
</h:body>
</html>

View File

@ -0,0 +1,9 @@
<html>
<head>
<title>ERROR</title>
</head>
<body>
<h1>Uh-oh!</h1>
An error happened.
</body>
</html>