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.console.views;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023/**
024 * Builds the standard table ribbon.
025 *
026 * <p>
027 * {@code [search] [create] [filters] [export: copy/csv/link + extras] [view: collapse / columns / reset] [refresh]}
028 *
029 * <p>
030 * Each slot has a canonical group, so the ribbon sorts the same way everywhere:
031 *
032 * <table class='styled'>
033 *    <caption>Ribbon slots</caption>
034 *    <tr><th>Slot</th><th>Group</th><th>Item ids</th></tr>
035 *    <tr><td>search</td><td>{@code search}</td><td>{@code search}</td></tr>
036 *    <tr><td>create</td><td>{@code entity}</td><td>your {@link Standard#create(RibbonItem...)} items</td></tr>
037 *    <tr><td>filters</td><td>{@code filter}</td><td>your {@link Standard#filter(RibbonItem...)} items</td></tr>
038 *    <tr><td>export</td><td>{@code export}</td><td>{@code export} (buttons {@code copy}, {@code csv} plus extras), {@code link}</td></tr>
039 *    <tr><td>view</td><td>{@code viewing}</td><td>{@code collapse}, {@code columns}, {@code reset}</td></tr>
040 *    <tr><td>refresh</td><td>{@code page}</td><td>{@code refresh}</td></tr>
041 * </table>
042 *
043 * <p>
044 * Every item carries an explicit id, so the output describes itself.  The ids {@code collapse} and {@code reset}
045 * are explicit overrides of the stock defaults {@code collapseAll} and {@code resetDefaults}.  {@code build()}
046 * replaces the group of every item you pass to {@code create} or {@code filter} with the slot's group.
047 *
048 * <p>
049 * The {@code copy} and {@code csv} buttons need the DataTables Buttons extension, so a page using the preset must
050 * list the {@code datatables-buttons} toolkit pack.  Without it the export cluster is omitted at runtime and the link
051 * button still renders.  The collapse button is hidden at runtime on a table with no row detail, and the columns and
052 * reset buttons on a table without {@code columnConfig}.
053 *
054 * <h2 class='section'>Example:</h2>
055 * <p class='bjava'>
056 *    TableSpec.<jsm>create</jsm>(<js>"pets"</js>)
057 *       .ribbon(Ribbons.<jsm>standard</jsm>()
058 *          .create(RibbonItem.<jsm>dialog</jsm>(<js>"add-pet"</js>).title(<js>"Add pet"</js>))
059 *          .export(<js>"excel"</js>, <js>"pdf"</js>)
060 *          .build());
061 * </p>
062 */
063@SuppressWarnings({
064   "java:S1192" // Ribbon item ids read more clearly inline than as constants.
065})
066public final class Ribbons {
067
068   private static final List<String> VALID_IDS = List.of(
069      "search", "export", "copy", "csv", "excel", "pdf", "link", "collapse", "columns", "reset", "refresh");
070
071   private Ribbons() {}
072
073   /**
074    * Starts the standard preset.
075    *
076    * @return A new builder.
077    */
078   public static Standard standard() {
079      return new Standard();
080   }
081
082   /** Builder for the standard table ribbon. */
083   public static final class Standard {
084
085      private final List<RibbonItem> create = new ArrayList<>();
086      private final List<RibbonItem> filter = new ArrayList<>();
087      private final List<String> extras = new ArrayList<>();
088      private final Set<String> without = new LinkedHashSet<>();
089      private String refreshId = "refresh";
090
091      private Standard() {}
092
093      /**
094       * Adds items to the create slot (group {@code entity}); repeatable.
095       *
096       * <p>
097       * Ids of items added here are not checked for duplicates by the builder; build-time id validation applies
098       * to the stock items only, so keep the ids you pass unique among themselves and against the stock ids.
099       *
100       * @param items The items.
101       * @return This object.
102       */
103      public Standard create(RibbonItem...items) {
104         create.addAll(List.of(items));
105         return this;
106      }
107
108      /**
109       * Adds items to the filter slot (group {@code filter}); repeatable.
110       *
111       * @param items The items.
112       * @return This object.
113       */
114      public Standard filter(RibbonItem...items) {
115         filter.addAll(List.of(items));
116         return this;
117      }
118
119      /**
120       * Adds optional export buttons (for example {@code "excel"}, {@code "pdf"}) after copy and csv.
121       *
122       * <p>
123       * An optional button is omitted at runtime when its library is missing.
124       *
125       * @param buttons The buttons, each {@code excel} or {@code pdf}.
126       * @return This object.
127       */
128      public Standard export(String...buttons) {
129         extras.addAll(List.of(buttons));
130         return this;
131      }
132
133      /**
134       * Drops a preset item or export button by id.
135       *
136       * <p>
137       * Valid ids are {@code search}, {@code export}, {@code copy}, {@code csv}, {@code excel}, {@code pdf},
138       * {@code link}, {@code collapse}, {@code columns}, {@code reset}, {@code refresh}, and the id given to
139       * {@link #refreshId(String)}.  An unknown id is rejected by {@link #build()}.
140       *
141       * @param id The id.
142       * @return This object.
143       */
144      public Standard without(String id) {
145         without.add(id);
146         return this;
147      }
148
149      /**
150       * Renames the refresh item (default {@code "refresh"}).
151       *
152       * @param id The id.  Must not be <jk>null</jk> or blank.
153       * @return This object.
154       */
155      public Standard refreshId(String id) {
156         if (ib(id))
157            throw new IllegalArgumentException("Ribbons refresh id must not be null or blank.");
158         refreshId = id;
159         return this;
160      }
161
162      /**
163       * Builds the items in slot order, each with its canonical group.
164       *
165       * @return An immutable list, for {@link TableSpec#ribbon(List)}.
166       * @throws IllegalArgumentException If {@link #without(String)} named an id that is not a preset id.
167       */
168      @SuppressWarnings({
169         "java:S3776" // Branching is inherent to build(), which assembles the ribbon from many optional items.
170      })
171      public List<RibbonItem> build() {
172         for (var w : without)
173            if (! VALID_IDS.contains(w) && ! w.equals(refreshId))
174               throw new IllegalArgumentException("Unknown ribbon preset id '" + w + "'. Valid ids: " + VALID_IDS + " and the refresh id '" + refreshId + "'.");
175         var out = new ArrayList<RibbonItem>();
176         if (! without.contains("search"))
177            out.add(RibbonItem.search().id("search").group("search"));
178         create.forEach(i -> out.add(i.group("entity")));
179         filter.forEach(i -> out.add(i.group("filter")));
180         if (! without.contains("export")) {
181            var buttons = new ArrayList<String>();
182            if (! without.contains("copy"))
183               buttons.add("copy");
184            if (! without.contains("csv"))
185               buttons.add("csv");
186            var optional = extras.stream().filter(x -> ! without.contains(x)).toList();
187            if (! buttons.isEmpty() || ! optional.isEmpty()) {
188               var e = RibbonItem.export(buttons.toArray(String[]::new)).id("export").group("export");
189               if (! optional.isEmpty())
190                  e.optional(optional.toArray(String[]::new));
191               out.add(e);
192            }
193         }
194         if (! without.contains("link"))
195            out.add(RibbonItem.linkButton().id("link").group("export"));
196         if (! without.contains("collapse"))
197            out.add(RibbonItem.collapseAll().id("collapse").group("viewing"));
198         if (! without.contains("columns"))
199            out.add(RibbonItem.columnSettings().id("columns").group("viewing"));
200         if (! without.contains("reset"))
201            out.add(RibbonItem.resetDefaults().id("reset").group("viewing"));
202         if (! without.contains(refreshId))
203            out.add(RibbonItem.refresh().id(refreshId).group("page"));
204         return List.copyOf(out);
205      }
206   }
207}