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.widgets;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023/**
024 * An opaque id naming a write action on the enclosing view.
025 *
026 * <p>
027 * This type deliberately does <b>not</b> import any views-module type.  Existence of the named action on the
028 * enclosing view's action catalog is checked in views ({@code RegionDef} / serving-path validation), never
029 * here.  The bar does not grow its own write protocol (no endpoint / method / csrf fields).
030 *
031 * @since 10.0.0
032 */
033@SuppressWarnings({
034   "java:S1845" // Fluent-builder setters intentionally mirror field names (Juneau DSL convention).
035})
036public class ActionRef implements ActionBarItem {
037
038   /** The opaque action id.  Must not be blank. */
039   public String id;
040
041   /** The visual weight this bar paints for this action.  Defaults to {@link Emphasis#SECONDARY}. */
042   public Emphasis emphasis = Emphasis.SECONDARY;
043
044   /**
045    * The row-state rules that must all match for this action to be offered, in the author's declared order.
046    *
047    * <p>
048    * {@code null} or empty means the action is never gated on row state.  See
049    * {@link #enabledWhen(String,Op,Object,String)} for the semantics, including the deliberate absence of a
050    * hide/show mode.
051    */
052   public List<EnabledRule> enabledWhen;
053
054   /**
055    * Creates an action reference with the specified id.
056    *
057    * @param id The action id.  Must not be <jk>null</jk> or blank.
058    * @return A new {@link ActionRef}.
059    */
060   public static ActionRef of(String id) {
061      if (ib(id))
062         throw iaex("ActionRef id must not be null or blank.");
063      var a = new ActionRef();
064      a.id = id;
065      return a;
066   }
067
068   /**
069    * Sets the visual weight this bar paints for this action.
070    *
071    * <p>
072    * This is a per-bar rendering property, not a property of the action itself &mdash; the same action can be
073    * the primary button in one bar (e.g. the Detail View header) and an ordinary item in another.  Java-only:
074    * never marshalled into {@code VIEW_META}, so it does not move a bar-contract version.
075    *
076    * @param value The emphasis.  Must not be <jk>null</jk>.
077    * @return This object.
078    */
079   public ActionRef emphasis(Emphasis value) {
080      this.emphasis = value;
081      return this;
082   }
083
084   /**
085    * Gates this action on the row's own state, comparing one field against one value.
086    *
087    * <p>
088    * A row whose field does not satisfy every declared rule is offered the action <b>present but disabled</b>,
089    * carrying {@code reason} as its explanation.  Call this more than once to declare more than one rule; all of
090    * them must match.
091    *
092    * <h5 class='section'>This is presentation only &mdash; never the authorization gate:</h5>
093    * <p>
094    * The rule exists so an operator is not offered a button that cannot possibly work.  The server still re-reads
095    * the row's state and refuses on its own authority; a client-side rule that quietly became the only check would
096    * be a regression, not an optimization.  Nothing here is a substitute for the server's own refusal.
097    *
098    * <h5 class='section'>Disabled, never hidden:</h5>
099    * <p>
100    * Prefer disabled; hide only when the action is irrelevant.  A detail-panel action has no
101    * {@code visibleWhen(...)}: removing a momentarily-impossible action makes the bar's contents jump between rows
102    * and hides the fact that the action exists at all.  When an
103    * action is invalid for an <b>entire</b> view rather than per row, omit it from that view's action list
104    * &mdash; that is the right tool, and it is why per-row hiding is not offered.
105    *
106    * <h5 class='section'>Which reason an operator sees:</h5>
107    * <p>
108    * When several rules on one action fail, the <b>first-declared</b> one wins and evaluation stops there.  The
109    * author's ordering is therefore the priority mechanism: no severity field, no concatenation, no
110    * most-specific-rule heuristic.  The runtime surfaces that one reason through both a native tooltip and
111    * {@code aria-describedby}, and clears both again when the rules pass.
112    *
113    * <h5 class='section'>A field the row does not carry fails closed:</h5>
114    * <p>
115    * A rule keyed on a field the enclosing panel never declares is rejected at startup.  If the field is
116    * nonetheless missing from a row's payload at runtime, the rule <b>fails</b> and the action disables &mdash;
117    * including for {@link Op#ABSENT}, which tests a field that is present and empty rather than one that was
118    * never returned.
119    *
120    * <p>
121    * Java-only, like {@link #emphasis}: the bar is painted into server-emitted markup and is never marshalled into
122    * the view's JSON sidecar, so this does not move a bar-contract version.
123    *
124    * @param field The row field to test.  Must not be <jk>null</jk> or blank.
125    * @param op The comparison operator.  Must be {@link Op#EQ} or {@link Op#NE}.
126    * @param value The comparison value.  Must not be <jk>null</jk>.
127    * @param reason Why the action is unavailable when this rule does not match.  Must not be <jk>null</jk> or
128    *    blank &mdash; with the action disabled rather than removed, this string is the only thing that explains an
129    *    inert button.
130    * @return This object.
131    */
132   public ActionRef enabledWhen(String field, Op op, Object value, String reason) {
133      if (op != null && op.isCollectionOp())
134         throw iaex("enabledWhen op '%s' is a visibility-rule operator; use eq or ne.", op.wire());
135      if (op == null || ! op.requiresValue())
136         throw iaex("enabledWhen op '%s' does not take a value; use the (field, op, reason) form.",
137            op == null ? "null" : op.wire());
138      if (value == null)
139         throw iaex("enabledWhen op '%s' requires a non-null value.", op.wire());
140      return addEnabledRule(field, op, value, reason);
141   }
142
143   /**
144    * Gates this action on the presence or absence of a row field.
145    *
146    * <p>
147    * The presence-based counterpart of {@link #enabledWhen(String,Op,Object,String)}, which carries the full
148    * semantics &mdash; presentation only, disabled rather than hidden, first-declared reason wins, and a field the
149    * row does not carry fails closed.
150    *
151    * @param field The row field to test.  Must not be <jk>null</jk> or blank.
152    * @param op The comparison operator.  Must be {@link Op#PRESENT} or {@link Op#ABSENT}.
153    * @param reason Why the action is unavailable when this rule does not match.  Must not be <jk>null</jk> or
154    *    blank.
155    * @return This object.
156    */
157   public ActionRef enabledWhen(String field, Op op, String reason) {
158      if (op == null || op.requiresValue())
159         throw iaex("enabledWhen op '%s' requires a value; use the (field, op, value, reason) form.",
160            op == null ? "null" : op.wire());
161      return addEnabledRule(field, op, null, reason);
162   }
163
164   private ActionRef addEnabledRule(String field, Op op, Object value, String reason) {
165      if (ib(field))
166         throw iaex("enabledWhen field must not be null or blank.");
167      if (ib(reason))
168         throw iaex("enabledWhen reason must not be null or blank.");
169      var r = new EnabledRule();
170      r.field = field;
171      r.op = op;
172      r.value = value;
173      r.reason = reason;
174      if (enabledWhen == null)
175         enabledWhen = new ArrayList<>();
176      enabledWhen.add(r);
177      return this;
178   }
179
180   /** The visual weight an {@link ActionRef} button is painted with. */
181   public enum Emphasis {
182
183      /** The bar's single most prominent action &mdash; solid accent fill.  At most one per bar. */
184      PRIMARY,
185
186      /** The default weight &mdash; neutral fill, accent-coloured label. */
187      SECONDARY
188   }
189
190   /**
191    * One row-state rule gating an {@link ActionRef}.
192    *
193    * <p>
194    * Built by {@link ActionRef#enabledWhen(String,Op,Object,String)}, which is the only shape that can produce a
195    * well-formed one; a hand-assembled instance is checked again at serve time, so neither route
196    * can smuggle a rule with no {@link #reason} past the bar.
197    */
198   public static class EnabledRule {
199
200      /** The row field this rule tests.  Must not be blank. */
201      public String field;
202
203      /** The comparison operator.  Must not be <jk>null</jk>. */
204      public Op op;
205
206      /**
207       * The comparison value for {@link Op#EQ}/{@link Op#NE}; must be <jk>null</jk> for
208       * {@link Op#PRESENT}/{@link Op#ABSENT}.
209       */
210      public Object value;
211
212      /** Why the action is unavailable when this rule does not match.  Required &mdash; must not be blank. */
213      public String reason;
214   }
215}