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}