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 — 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 — 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>} — {@link ViewTable#SELECT_ATTR}, 044 * {@link ViewTable#ROW_ID_FIELD_ATTR}, {@link ViewTable#SELECT_ALL_ATTR} — 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 — 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) — 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}