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 — 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 — 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 * — 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 — 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 — 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 — 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 — solid accent fill. At most one per bar. */ 184 PRIMARY, 185 186 /** The default weight — 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 — must not be blank. */ 213 public String reason; 214 } 215}