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 * The row-selection opt-in for a {@link ViewTable} — the first of the view table's two INDEPENDENT
025 * opt-ins.
026 *
027 * <h5 class='section'>Independent from bulk mutation, by construction</h5>
028 * <p>
029 * Declaring a {@link SelectionDef} adds a per-row checkbox column (and, when {@link #selectAll()} is set, a
030 * select-all header checkbox) to a view table &mdash; nothing else. It does <b>not</b>, by any default or
031 * convenience, surface a bulk-mutate control: {@link ViewTable}'s selection markers
032 * ({@link ViewTable#SELECT_ATTR} and siblings) have no code
033 * path that can render one. A view can therefore declare "selectable, for export" while categorically excluding
034 * bulk mutation &mdash; e.g. a PagerDuty-style incident table that wants selection for a client-side export/copy
035 * action but must never expose a multi-row mutate. A {@link BulkMutateDef} is a separate, independent type that
036 * <b>consumes</b> a {@link SelectionDef} (it requires one to be constructed at all) but a {@link SelectionDef}
037 * never requires, implies, or references one back.
038 *
039 * <h5 class='section'>Non-wire: never touches {@code VIEW_META}</h5>
040 * <p>
041 * A {@link SelectionDef} is deliberately <b>not</b> part of {@code VIEW_META} and never reaches the
042 * JSON sidecar. {@link ViewTable} instead stamps it as
043 * plain HTML attributes directly on the emitted {@code <table>} &mdash; {@link ViewTable#SELECT_ATTR},
044 * {@link ViewTable#ROW_ID_FIELD_ATTR}, {@link ViewTable#SELECT_ALL_ATTR} &mdash; mirroring the existing
045 * {@link ViewTable#CSRF_ATTR} auto-embed pattern. The {@code juneau-views.js} runtime reads them straight off the
046 * DOM, so a table with no {@link SelectionDef} carries none of these attributes and renders no selection UI at
047 * all; there is no shared, independently-versioned wire contract to keep in lockstep (contrast
048 * {@link BulkMutateDef}, whose actual bulk-action list <i>is</i> wire data and therefore carries its own,
049 * separate {@link BulkMutateDef#CONTRACT_VERSION}).
050 *
051 * <h5 class='section'>Selection identity is a stable row id, never a DOM/table index</h5>
052 * <p>
053 * {@link #rowIdField()} names the row-data key {@code juneau-views.js} reads to stamp a stable
054 * {@code data-juneau-row-id} attribute on each rendered {@code <tr>} (see {@code createdRow} in that runtime).
055 * Selection state is keyed by that value, never by a row's position in the DOM or in the DataTables draw &mdash;
056 * so a poll refresh, a sort, or a page change can never retarget a selection (or a bulk mutate driven
057 * from one) onto whichever rows now occupy those same positions. The runtime's persistence rule drops any
058 * previously-selected id that is no longer present in the newly drawn row set, so a selection whose rows have
059 * scrolled off screen or off the current page is never kept as a live, actionable target.
060 *
061 * <h5 class='section'>Example:</h5>
062 * <p class='bjava'>
063 *    <jc>// Selectable for export, with NO bulk-mutate capability - satisfies the separability condition by construction.</jc>
064 *    SelectionDef <jv>selection</jv> = SelectionDef.<jsm>create</jsm>(<js>"id"</js>)
065 *       .selectAll(<jk>true</jk>)
066 *       .selectableWhen(RowActionEnabledRule.<jsm>of</jsm>(<js>"state"</js>, Op.<jsf>EQ</jsf>, <js>"PENDING"</js>,
067 *          <js>"Only pending changes can be aborted."</js>))
068 *       .labelField(<js>"name"</js>);
069 * </p>
070 *
071 * <h5 class='section'>See Also:</h5>
072 * <ul>
073 *    <li class='jc'>{@link BulkMutateDef}
074 *    <li class='jc'>{@link ViewTable}
075 * </ul>
076 *
077 * @since 10.0.0
078 */
079public final class SelectionDef {
080
081   /**
082    * Where a {@link SelectionDef}'s row selection lives across a view's lifetime.
083    *
084    * @since 10.0.0
085    */
086   public enum Scope {
087
088      /** Selection survives paging, sorting, searching and refresh. */
089      PERSISTENT("persistent"),
090
091      /** Selection is dropped for any row not in the current draw. */
092      PAGE("page");
093
094      private final String wire;
095
096      Scope(String wire) {
097         this.wire = wire;
098      }
099
100      /**
101       * The wire-form string for this scope.
102       *
103       * @return The wire string ({@code "persistent"} or {@code "page"}).
104       */
105      public String wire() {
106         return wire;
107      }
108   }
109
110   private final String rowIdField;
111   private boolean selectAll = true;
112   private Scope scope = Scope.PERSISTENT;
113   private List<RowActionEnabledRule> selectableWhen;
114   private String labelField;
115
116   private SelectionDef(String rowIdField) {
117      this.rowIdField = rowIdField;
118   }
119
120   /**
121    * Starts a new {@link SelectionDef} bound to the specified stable row-id field.
122    *
123    * @param rowIdField The row-data key whose value is this row's stable identity (e.g. {@code "id"} or
124    *    {@code "incidentId"}). Must not be <jk>null</jk> or blank.
125    * @return A new {@link SelectionDef}, with {@link #selectAll()} defaulting to <jk>true</jk>.
126    * @throws IllegalArgumentException If {@code rowIdField} is <jk>null</jk> or blank.
127    */
128   public static SelectionDef create(String rowIdField) {
129      if (ib(rowIdField))
130         throw iaex("SelectionDef rowIdField must not be null or blank.");
131      return new SelectionDef(rowIdField);
132   }
133
134   /**
135    * Sets whether a select-all header checkbox is rendered alongside the per-row checkboxes.
136    *
137    * <p>
138    * Select-all always applies only to the rows currently on screen (the current page's draw) &mdash; it can
139    * never reach into rows on another page, which would be indistinguishable from the off-screen-retarget risk
140    * this whole opt-in exists to close.
141    *
142    * @param value The new value. Defaults to <jk>true</jk>.
143    * @return This object.
144    */
145   public SelectionDef selectAll(boolean value) {
146      selectAll = value;
147      return this;
148   }
149
150   /**
151    * The row-data key used as this table's stable row identity.
152    *
153    * @return The row-id field name.
154    */
155   public String rowIdField() {
156      return rowIdField;
157   }
158
159   /**
160    * Whether a select-all header checkbox is rendered.
161    *
162    * @return <jk>true</jk> if select-all is enabled.
163    */
164   public boolean selectAll() {
165      return selectAll;
166   }
167
168   /**
169    * Sets whether selection persists across paging/sorting/searching/refresh ({@link Scope#PERSISTENT}, the
170    * default) or is dropped for any row not in the current draw ({@link Scope#PAGE}).
171    *
172    * @param value The new scope. A <jk>null</jk> value resets to {@link Scope#PERSISTENT}.
173    * @return This object.
174    */
175   public SelectionDef scope(Scope value) {
176      scope = value == null ? Scope.PERSISTENT : value;
177      return this;
178   }
179
180   /**
181    * Sets the rules that gate which rows can be selected at all.
182    *
183    * <p>
184    * A row failing any rule renders its checkbox disabled, carrying the failing rule's reason as both a tooltip
185    * and an accessible description; select-all skips such rows entirely.
186    *
187    * @param value The gating rules. An empty or <jk>null</jk> array clears this to <jk>null</jk> (no gating).
188    * @return This object.
189    */
190   public SelectionDef selectableWhen(RowActionEnabledRule...value) {
191      selectableWhen = value == null || value.length == 0 ? null : l(value);
192      return this;
193   }
194
195   /**
196    * Sets the row-data field used as this row's human-readable label in a confirm dialog's target list.
197    *
198    * @param value The field name. May be <jk>null</jk> (falls back to the row id).
199    * @return This object.
200    */
201   public SelectionDef labelField(String value) {
202      labelField = value;
203      return this;
204   }
205
206   /**
207    * This selection's scope.
208    *
209    * @return The scope. Never <jk>null</jk>.
210    */
211   public Scope scope() {
212      return scope;
213   }
214
215   /**
216    * This selection's gating rules.
217    *
218    * @return The rules, or <jk>null</jk> if none were set.
219    */
220   public List<RowActionEnabledRule> selectableWhen() {
221      return selectableWhen;
222   }
223
224   /**
225    * This selection's label field.
226    *
227    * @return The field name, or <jk>null</jk> if none was set.
228    */
229   public String labelField() {
230      return labelField;
231   }
232}