001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.petstore.console.pages; 018 019import static org.apache.juneau.commons.utils.StringUtils.*; 020 021import java.util.*; 022 023import org.apache.juneau.petstore.dto.*; 024import org.apache.juneau.console.freemarker.*; 025import org.apache.juneau.console.views.*; 026import org.apache.juneau.console.views.RowAction.*; 027import org.apache.juneau.console.views.TableSpec.*; 028import org.apache.juneau.console.widgets.*; 029 030/** 031 * Builder twins of the Pets pages: P2 Pets ({@code pets.ftlh}), its {@code ?mixed} variant, P3 Sold 032 * ({@code pets-sold.ftlh}) and P4 one pet ({@code pet.ftlh}). 033 * 034 * <h5 class='section'>Example:</h5> 035 * <p class='bjava'> 036 * <jk>return</jk> <jsm>twin</jsm>(<jv>req</jv>, PetsPageBuilders.<jsm>pets</jsm>()); 037 * </p> 038 */ 039@SuppressWarnings({ 040 "java:S1192" // Page-definition ids and labels repeat as inline literals by design. 041}) 042public final class PetsPageBuilders { 043 044 /** 045 * The edit-pet dialog action, shared by the Pets table row menu and the Store page treemap so the two cannot drift. 046 * 047 * @param onSuccess What the page does after a saved edit; a table merges the row, a chart (which has no row) redraws. 048 * @return The action. 049 */ 050 static RowAction editPetAction(OnSuccess onSuccess) { 051 return RowAction.create("edit").label("Edit").form("/console/pets/form?id={id}").present(Present.DIALOG) 052 .endpoint("/console/pets/{id}").method(Method.PUT).onSuccess(onSuccess); 053 } 054 055 private PetsPageBuilders() {} 056 057 /** 058 * P2 Pets. 059 * 060 * @return The page spec. 061 */ 062 public static PageSpec pets() { 063 return PageSpec.create().tab("pets").toolkit("views", "datatables-buttons").table(petsTable(), "Pets"); 064 } 065 066 /** 067 * P2 Pets, half FTL: {@code pets-mixed.ftlh} lays out the page and places this spec's table with 068 * {@code <@card ref="pets"/>}. 069 * 070 * @return The page spec. 071 */ 072 public static PageSpec petsMixed() { 073 return PageSpec.create().template("pets-mixed.ftlh").table(petsTable(), "Pets"); 074 } 075 076 /** 077 * The P2 table, shared by {@link #pets()} and {@link #petsMixed()} so the two cannot drift. 078 * 079 * @return A new table spec. 080 */ 081 static TableSpec petsTable() { 082 var selection = SelectionDef.create("id").selectAll(true).scope(SelectionDef.Scope.PERSISTENT).labelField("name") 083 .selectableWhen(RowActionEnabledRule.of("status", Op.NE, "SOLD", "Already sold.")); 084 return TableSpec.create("pets").rowType("Pet").dataMode(DataMode.SERVER).dataUrl("/console/pets/query") 085 .columnConfig(true) 086 .urlState("ids") 087 .defaultOrder("id", Sort.ASC) 088 .pollIntervalMs(30_000) 089 .pausePollingWhileEditing(true) 090 .columns( 091 Column.create("id").label("Id"), 092 Column.create("name").label("Name").href("/console/pets/{id}/summary"), 093 Column.create("species").label("Species"), 094 Column.create("status").label("Status").render("pill"), 095 Column.create("price").label("Price (USD)").render(Render.of("decimal").meta("places", 2)), 096 Column.create("tags").label("Tags").defaultVisible(false)) 097 .rowClass("status", Op.EQ, "SOLD", "is-muted") 098 .ribbon( 099 RibbonItem.optionGroup("status", 100 RibbonItem.option("available").title("Available").column("status").value("$eq(AVAILABLE)"), 101 RibbonItem.option("pending").title("Pending").column("status").value("$eq(PENDING)")) 102 .title("Pet status").deselectable(true).defaultOption("available"), 103 RibbonItem.optionGroup("species", 104 RibbonItem.option("mammals").title("Mammals").column("species").value("$in(DOG,CAT,MOUSE,RABBIT)"), 105 RibbonItem.option("others").title("Others").column("species").value("$in(BIRD,FISH,SNAKE)")) 106 .title("Species").deselectable(true).defaultOption("mammals"), 107 RibbonItem.dialog("add-pet").title("+ Add pet").symbol("plus") 108 .form("/console/pets/form").endpoint("/console/pets").method(Method.POST).onSuccess(OnSuccess.REDRAW), 109 RibbonItem.export("copy", "csv").optional("excel", "pdf"), 110 RibbonItem.refresh()) 111 .rowActions( 112 editPetAction(OnSuccess.MERGE_ROW), 113 RowAction.create("delete").label("Delete").endpoint("/console/pets/{id}").method(Method.DELETE) 114 .confirm("Delete '{name}'?").confirmLabel("Delete").tone(Tone.DANGER).onSuccess(OnSuccess.REDRAW)) 115 .selection(selection) 116 .bulk(BulkMutateDef.create(WritePermit.forCapability("pets.sell"), selection).actions( 117 RowAction.create("sell").label("Mark sold").endpoint("/console/pets/{id}/sell").method(Method.POST).bulkMode(BulkMode.PER_ROW) 118 .confirmTitle("Mark pets sold").confirm("Mark the selected pets sold?").onSuccess(OnSuccess.REDRAW))); 119 } 120 121 /** 122 * P3 Sold. 123 * 124 * @return The page spec. 125 */ 126 public static PageSpec sold() { 127 return PageSpec.create().toolkit("views") 128 .table(TableSpec.create("sold").rowType("Pet").dataMode(DataMode.CLIENT).dataUrl("/console/pets/sold/rows") 129 .columns( 130 Column.create("id").label("Id"), 131 Column.create("name").label("Name"), 132 Column.create("species").label("Species"), 133 Column.create("price").label("Price (USD)").render(Render.of("decimal").meta("places", 2))) 134 .ribbon( 135 RibbonItem.optionGroup("species", 136 RibbonItem.option("dogs").title("Dogs").column("species").value("$eq(DOG)"), 137 RibbonItem.option("cats").title("Cats").column("species").value("$eq(CAT)")) 138 .title("Species"), 139 RibbonItem.refresh()), "Sold pets"); 140 } 141 142 /** 143 * P4 one pet, on one tab. 144 * 145 * @param pet The pet. 146 * @param tab The tab: {@code summary}, {@code photo}, {@code orders} or {@code history}. 147 * @param activeNav The active nav path, {@code pets/p<id>/<tab>} or {@code pets/p<id>/orders/o<orderId>}. 148 * @param order The selected order on the orders tab, or <jk>null</jk>. 149 * @param roles The viewer's roles. 150 * @param apiCsrf The API's CSRF token for the photo upload form, or <jk>null</jk> to render the form without one. 151 * @return The page spec. 152 */ 153 public static PageSpec pet(Pet pet, String tab, String activeNav, Order order, List<String> roles, String apiCsrf) { 154 var id = pet.getId(); 155 var href = "/console/pets/" + id; 156 var p = PageSpec.create().tab(activeNav).toolkit("views").title(pet.getName()) 157 .facts(Map.of("viewer", Map.of("roles", roles))) 158 .navUnder("pets", n -> { 159 var node = n.add("p" + id, pet.getName(), href + "/summary"); 160 node.add("summary", "Summary", href + "/summary"); 161 node.add("photo", "Photo", href + "/photo"); 162 var orders = node.add("orders", "Orders", href + "/orders"); 163 if (order != null) 164 orders.add("o" + order.getId(), "Order " + order.getId(), href + "/orders/" + order.getId()); 165 node.add("history", "History", href + "/history"); 166 }); 167 var name = escapeHtml(pet.getName()); 168 switch (tab) { 169 case "summary" -> p.table(TableSpec.create("pet").rowType("Pet").dataMode(DataMode.CLIENT).dataUrl(href + "/row") 170 .columns( 171 Column.create("id").label("Id"), 172 Column.create("name").label("Name").render(Render.of("edit").meta("action", "rename").meta("column", "name").meta("input", "text")), 173 Column.create("species").label("Species"), 174 Column.create("status").label("Status").render("pill"), 175 Column.create("price").label("Price (USD)").render(Render.of("edit").meta("action", "reprice").meta("column", "price").meta("input", "number"))) 176 .rowActions( 177 stage("rename", "Rename"), 178 stage("reprice", "Reprice"), 179 RowAction.create("sell").label("Mark sold").endpoint("/console/pets/{id}/sell").method(Method.POST).onSuccess(OnSuccess.REDRAW) 180 .confirm("Mark '{name}' sold?").visibleWhen(VisibilityRule.when("status").ne("SOLD"))) 181 .ribbon(RibbonItem.refresh()), pet.getName()); 182 case "photo" -> { 183 p.html("photo", "Photo", """ 184 <figure class="jc-figure"> 185 <object data="/petstore/pets/%1$s/photo" type="image/png" aria-label="Photo of %2$s"><p>No photo uploaded for %2$s yet.</p></object> 186 <figcaption>Served by <code>PetPhotoSerializer</code> from <code>GET /petstore/pets/%1$s/photo</code>.</figcaption> 187 </figure>""".formatted(id, name)); 188 if (roles.contains("admin")) { 189 var csrf = apiCsrf == null ? "" : " data-petstore-csrf=\"%s\"".formatted(escapeHtml(apiCsrf)); 190 p.html("photo-upload", "Upload photo", """ 191 <form class="jc-form" data-petstore-upload="/petstore/pets/%s/photo"%s> 192 <input type="file" name="photo" accept="image/png,image/jpeg" aria-label="Photo file"> 193 <button type="submit" class="jc-button">Upload</button> 194 </form>""".formatted(id, csrf)); 195 } 196 } 197 case "orders" -> { 198 p.table(TableSpec.create("pet-orders").rowType("Order").dataMode(DataMode.CLIENT).dataUrl(href + "/orders/rows") 199 .columns( 200 Column.create("id").label("Order").href(href + "/orders/{id}"), 201 Column.create("shipDate").label("Ship date"), 202 Column.create("status").label("Status").render("pill")) 203 .detail(RowDetail.create(href + "/orders/{id}/detail").title("Order {id}")), "Orders"); 204 if (order != null) 205 p.html("order", "Order " + order.getId(), 206 "<dl class=\"jc-details\"><dt>Status</dt><dd>%s</dd><dt>Ship date</dt><dd>%s</dd><dt>Pet</dt><dd>%s</dd></dl>" 207 .formatted(order.getStatus(), escapeHtml(order.getShipDate() == null ? "" : order.getShipDate()), name)); 208 } 209 default -> p.table(TableSpec.create("history").rowType("AuditEntry").dataMode(DataMode.CLIENT).dataUrl(href + "/history/rows") 210 .defaultOrder("at", Sort.DESC) 211 .columns( 212 Column.create("at").label("When").render("timestamp"), 213 Column.create("actor").label("Actor"), 214 Column.create("action").label("Action"), 215 Column.create("detail").label("Detail")), "History"); 216 } 217 return p; 218 } 219 220 // The pencil edit cell commits it; present=inline keeps it out of the row menu. 221 private static RowAction stage(String id, String label) { 222 return RowAction.create(id).label(label).endpoint("/console/pets/{id}/stage").method(Method.POST).noConfirm() 223 .present(Present.INLINE) 224 .visibleWhen(VisibilityRule.when("status").ne("SOLD")); 225 } 226}