Fourth section of #153's change. The product edit reads the ladders the product is a tier of. For a tier product the Entitlement set select no longer offers "None"; it opens with the disabled placeholder "Choose an entitlement set", and the field's one line reads "This product is a tier of <ladders>, and a tier needs an entitlement set." in place of the off-ladder sentence, which moves from the field's declared hint to the same line slot (the create page keeps it). A submitted "None" is refused with that sentence; so is a clear refused by section 2's trigger when the product became a tier after the page was read, after the ladders are read again. A failed read of the ladders is logged and saves nothing, instead of reading as "on no ladder". The edit's write now runs in its own READ COMMITTED transaction (db.InTx with db.ReadCommitted, item 4.6), so a tier add that commits first is seen by the clear whatever the server's default isolation; at REPEATABLE READ the edit's snapshot predated the tier and the clear committed. UpdateProduct is split into helpers, each answer and log line kept. Tests: TestTheEntitlementSetFieldFollowsTheProductsTiers, TestTheCreatePageCarriesTheOffLadderLine, TestTierNeedsSetSentence, TestProductEditOfATierProductOffersNoNone, TestProductEditRefusesNoneForAProductThatBecameATier, TestProductEditWithUnreadLaddersSavesNothing, TestProductEditClearingASetIsRefusedWhenATierAddCommitsFirstAtAnyDefaultIsolation (on a connection whose default is REPEATABLE READ). The forms golden operator.product.html moves the hint to the line. TestUpdateProductEntitlementSetSentinel builds its product on committed rows, since the edit's own transaction cannot see an uncommitted fixture; its assertions are unchanged. productTierOf takes no context: it logs through the handler's logger and unparam refuses the unused parameter. Breaks: 20 of 20 caught. The parse's error left in place; None offered whatever the tiers (T6, T6c, T6d); the placeholder left out (T6d, T6); the ladders out of name order; the line kept beside the error; the off-ladder line blanked beside an error; the create page's line lost; the failed read treated as no tier; the save going ahead when the read failed; the parse's field error left when the ladders are unread; L3 not logged; the second-read refusal not logged; the database's refusal unmapped; the refusal named over a failed second read; the join reduced to the first name; the write at the server's default isolation; the empty-value condition dropped. Review: round 1, no blocker or major; four minor and three nits, applied. #153's sequence (item 5.4), app rebuilt from this tree, from the demo snapshot, the whole walkthrough package three times without a reset: ok each run. The plan-ladders walkthrough's ladder now has tiers with sets, the org-types walkthrough committed a default change to it on runs 2 and 3 and put the default back, and the grant revoke passed on all three runs; the app log has no "has no entitlement set". Item 5.4 is reworded to what ran: the three-walkthrough filter it named finds no revoke subject on the demo snapshot, which another walkthrough's grant supplies. Full suite on reset databases: 4731 pass, 14 skip. make screens: no change beyond section 1's; the captured product is not a tier.
681 lines
35 KiB
HTML
681 lines
35 KiB
HTML
|
|
===== operator.product unbound
|
|
|
|
|
|
<div class="card app-form-box mb-3"><div class="card-body">
|
|
<form id="form-operator.product" data-form="operator.product" data-form-kind="create" class="app-form" novalidate hx-post="/partials/operator/products" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
|
|
<div id="form-operator.product-error" class="app-form-error" role="alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-form-fields">
|
|
<div id="form-operator.product-name" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-name-control">Name</label>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-name-control" name="name" value="" required maxlength="200" autocomplete="off" aria-describedby="form-operator.product-name-warning" hx-get="/partials/operator/products/name-check" hx-include="closest form" hx-swap="innerHTML" hx-target="#form-operator\.product-name-warning" hx-trigger="input changed delay:300ms">
|
|
|
|
<div id="form-operator.product-name-warning"></div>
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-display_category" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-display_category-control">Display category <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Display category" data-bs-content="Grouping for the storefront and operator lists; never drives behavior. Blank is fine." aria-label="Help: Display category"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-display_category-control" name="display_category" value="" placeholder="e.g. addon" maxlength="100" autocomplete="off">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-entitlement_set_id" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-entitlement_set_id-control">Entitlement set</label>
|
|
|
|
<select class="form-select" id="form-operator.product-entitlement_set_id-control" name="entitlement_set_id" required aria-describedby="form-operator.product-entitlement_set_id-line">
|
|
<option value="" disabled selected>Choose one</option>
|
|
<option value="a">Alpha</option>
|
|
<option value="b">Beta</option>
|
|
|
|
</select>
|
|
|
|
<div id="form-operator.product-entitlement_set_id-line"></div>
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-visibility" class="mb-3 app-form-field">
|
|
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-visibility-control">Listed</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Listed" data-bs-content="Shown in the member catalog." aria-label="Help: Listed"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-visibility-control" name="visibility" value="public" ></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-description" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-description-control">Description <span class="text-muted">(optional)</span></label>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-description-control" name="description" value="" maxlength="500" autocomplete="off">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-features" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-features-control">Additional features <span class="text-muted">(optional)</span></label>
|
|
|
|
<textarea class="form-control" id="form-operator.product-features-control" name="features" rows="3" maxlength="2000" aria-describedby="form-operator.product-features-hint"></textarea>
|
|
|
|
|
|
|
|
<div id="form-operator.product-features-hint" class="form-text">Shown on the Products page, one per line; supplements entitlement-derived limits.</div>
|
|
|
|
|
|
</div><div id="form-operator.product-is_active" class="mb-3 app-form-field">
|
|
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-is_active-control">Active</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Active" data-bs-content="An inactive product is not offered when new grants are created. Grants that already reference it keep working." aria-label="Help: Active"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-is_active-control" name="is_active" value="true" ></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><input type="hidden" name="exclude" value="" >
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2 app-form-commit">
|
|
|
|
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Create product</button>
|
|
|
|
|
|
<a href="/operator/products" class="btn btn-outline-secondary">Cancel</a>
|
|
|
|
|
|
</div>
|
|
|
|
</form>
|
|
</div></div>
|
|
|
|
|
|
===== operator.product unbound edit side
|
|
|
|
|
|
<div class="card app-form-box mb-3"><div class="card-body">
|
|
<form id="form-operator.product" data-form="operator.product" data-form-kind="edit" class="app-form" novalidate hx-put="/partials/operator/products/{productID}" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
|
|
<div id="form-operator.product-error" class="app-form-error" role="alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-form-fields">
|
|
<div id="form-operator.product-name" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-name-control">Name</label>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-name-control" name="name" value="" required maxlength="200" autocomplete="off" aria-describedby="form-operator.product-name-warning" hx-get="/partials/operator/products/name-check" hx-include="closest form" hx-swap="innerHTML" hx-target="#form-operator\.product-name-warning" hx-trigger="input changed delay:300ms">
|
|
|
|
<div id="form-operator.product-name-warning"></div>
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-display_category" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-display_category-control">Display category <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Display category" data-bs-content="Grouping for the storefront and operator lists; never drives behavior. Blank is fine." aria-label="Help: Display category"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-display_category-control" name="display_category" value="" placeholder="e.g. addon" maxlength="100" autocomplete="off">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-entitlement_set_id" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-entitlement_set_id-control">Entitlement set</label>
|
|
|
|
<select class="form-select" id="form-operator.product-entitlement_set_id-control" name="entitlement_set_id" required aria-describedby="form-operator.product-entitlement_set_id-line">
|
|
<option value="" disabled selected>Choose one</option>
|
|
<option value="a">Alpha</option>
|
|
<option value="b">Beta</option>
|
|
|
|
</select>
|
|
|
|
<div id="form-operator.product-entitlement_set_id-line"></div>
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-visibility" class="mb-3 app-form-field">
|
|
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-visibility-control">Listed</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Listed" data-bs-content="Shown in the member catalog." aria-label="Help: Listed"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-visibility-control" name="visibility" value="public" ></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-description" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-description-control">Description <span class="text-muted">(optional)</span></label>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-description-control" name="description" value="" maxlength="500" autocomplete="off">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-features" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-features-control">Additional features <span class="text-muted">(optional)</span></label>
|
|
|
|
<textarea class="form-control" id="form-operator.product-features-control" name="features" rows="3" maxlength="2000" aria-describedby="form-operator.product-features-hint"></textarea>
|
|
|
|
|
|
|
|
<div id="form-operator.product-features-hint" class="form-text">Shown on the Products page, one per line; supplements entitlement-derived limits.</div>
|
|
|
|
|
|
</div><div id="form-operator.product-is_active" class="mb-3 app-form-field">
|
|
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-is_active-control">Active</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Active" data-bs-content="An inactive product is not offered when new grants are created. Grants that already reference it keep working." aria-label="Help: Active"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-is_active-control" name="is_active" value="true" ></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><input type="hidden" name="exclude" value="" >
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2 app-form-commit">
|
|
|
|
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Save changes</button>
|
|
|
|
|
|
</div>
|
|
|
|
</form>
|
|
</div></div>
|
|
|
|
|
|
===== operator.product create side
|
|
|
|
|
|
<div class="card app-form-box mb-3"><div class="card-body">
|
|
<form id="form-operator.product" data-form="operator.product" data-form-kind="create" class="app-form" novalidate hx-post="/partials/operator/products" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
|
|
<div id="form-operator.product-error" class="app-form-error" role="alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-form-fields">
|
|
<div id="form-operator.product-name" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-name-control">Name</label>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-name-control" name="name" value="" required maxlength="200" autocomplete="off" aria-describedby="form-operator.product-name-warning" hx-get="/partials/operator/products/name-check" hx-include="closest form" hx-swap="innerHTML" hx-target="#form-operator\.product-name-warning" hx-trigger="input changed delay:300ms">
|
|
|
|
<div id="form-operator.product-name-warning"></div>
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-display_category" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-display_category-control">Display category <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Display category" data-bs-content="Grouping for the storefront and operator lists; never drives behavior. Blank is fine." aria-label="Help: Display category"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-display_category-control" name="display_category" value="" placeholder="e.g. addon" maxlength="100" autocomplete="off">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-entitlement_set_id" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-entitlement_set_id-control">Entitlement set</label>
|
|
|
|
<select class="form-select" id="form-operator.product-entitlement_set_id-control" name="entitlement_set_id" required aria-describedby="form-operator.product-entitlement_set_id-line">
|
|
<option value="" disabled selected>Choose one</option>
|
|
<option value="a">Alpha</option>
|
|
<option value="b">Beta</option>
|
|
|
|
</select>
|
|
|
|
<div id="form-operator.product-entitlement_set_id-line"></div>
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-visibility" class="mb-3 app-form-field">
|
|
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-visibility-control">Listed</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Listed" data-bs-content="Shown in the member catalog." aria-label="Help: Listed"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-visibility-control" name="visibility" value="public" ></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2 app-form-commit">
|
|
|
|
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Create product</button>
|
|
|
|
|
|
<a href="/operator/products" class="btn btn-outline-secondary">Cancel</a>
|
|
|
|
|
|
</div>
|
|
|
|
</form>
|
|
</div></div>
|
|
|
|
|
|
===== operator.product unbound with values
|
|
|
|
|
|
<div class="card app-form-box mb-3"><div class="card-body">
|
|
<form id="form-operator.product" data-form="operator.product" data-form-kind="create" class="app-form" novalidate hx-post="/partials/operator/products" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
|
|
<div id="form-operator.product-error" class="app-form-error" role="alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-form-fields">
|
|
<div id="form-operator.product-name" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-name-control">Name</label>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-name-control" name="name" value="" required maxlength="200" autocomplete="off" aria-describedby="form-operator.product-name-warning" hx-get="/partials/operator/products/name-check" hx-include="closest form" hx-swap="innerHTML" hx-target="#form-operator\.product-name-warning" hx-trigger="input changed delay:300ms">
|
|
|
|
<div id="form-operator.product-name-warning"></div>
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-display_category" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-display_category-control">Display category <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Display category" data-bs-content="Grouping for the storefront and operator lists; never drives behavior. Blank is fine." aria-label="Help: Display category"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-display_category-control" name="display_category" value="" placeholder="e.g. addon" maxlength="100" autocomplete="off">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-entitlement_set_id" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-entitlement_set_id-control">Entitlement set</label>
|
|
|
|
<select class="form-select" id="form-operator.product-entitlement_set_id-control" name="entitlement_set_id" required aria-describedby="form-operator.product-entitlement_set_id-line">
|
|
<option value="" disabled selected>Choose one</option>
|
|
<option value="a">Alpha</option>
|
|
<option value="b">Beta</option>
|
|
|
|
</select>
|
|
|
|
<div id="form-operator.product-entitlement_set_id-line"></div>
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-visibility" class="mb-3 app-form-field">
|
|
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-visibility-control">Listed</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Listed" data-bs-content="Shown in the member catalog." aria-label="Help: Listed"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-visibility-control" name="visibility" value="public" ></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-description" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-description-control">Description <span class="text-muted">(optional)</span></label>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-description-control" name="description" value="" maxlength="500" autocomplete="off">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-features" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-features-control">Additional features <span class="text-muted">(optional)</span></label>
|
|
|
|
<textarea class="form-control" id="form-operator.product-features-control" name="features" rows="3" maxlength="2000" aria-describedby="form-operator.product-features-hint"></textarea>
|
|
|
|
|
|
|
|
<div id="form-operator.product-features-hint" class="form-text">Shown on the Products page, one per line; supplements entitlement-derived limits.</div>
|
|
|
|
|
|
</div><div id="form-operator.product-is_active" class="mb-3 app-form-field">
|
|
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-is_active-control">Active</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Active" data-bs-content="An inactive product is not offered when new grants are created. Grants that already reference it keep working." aria-label="Help: Active"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-is_active-control" name="is_active" value="true" ></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><input type="hidden" name="exclude" value="sample" >
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2 app-form-commit">
|
|
|
|
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Create product</button>
|
|
|
|
|
|
<a href="/operator/products" class="btn btn-outline-secondary">Cancel</a>
|
|
|
|
|
|
</div>
|
|
|
|
</form>
|
|
</div></div>
|
|
|
|
|
|
===== operator.product record
|
|
|
|
|
|
<div class="card app-form-box mb-3"><div class="card-body">
|
|
<form id="form-operator.product-i1" data-form="operator.product" data-form-kind="create" class="app-form" novalidate hx-post="/partials/operator/products" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
|
|
<div id="form-operator.product-i1-error" class="app-form-error" role="alert"></div>
|
|
|
|
<div class="app-form-message mb-2"><p>msg</p></div>
|
|
|
|
|
|
|
|
<div class="app-form-fields">
|
|
<div id="form-operator.product-i1-name" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-i1-name-control">Name</label>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-i1-name-control" name="name" value="sample" required maxlength="200" autocomplete="off" aria-describedby="form-operator.product-i1-name-warning" hx-get="/partials/operator/products/name-check" hx-include="closest form" hx-swap="innerHTML" hx-target="#form-operator\.product-name-warning" hx-trigger="input changed delay:300ms">
|
|
|
|
<div id="form-operator.product-i1-name-warning"></div>
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-i1-display_category" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-i1-display_category-control">Display category <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Display category" data-bs-content="Grouping for the storefront and operator lists; never drives behavior. Blank is fine." aria-label="Help: Display category"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-i1-display_category-control" name="display_category" value="sample" placeholder="e.g. addon" maxlength="100" autocomplete="off">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-i1-entitlement_set_id" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-i1-entitlement_set_id-control">Entitlement set</label>
|
|
|
|
<select class="form-select" id="form-operator.product-i1-entitlement_set_id-control" name="entitlement_set_id" required aria-describedby="form-operator.product-i1-entitlement_set_id-line">
|
|
<option value="" disabled>Choose one</option>
|
|
<option value="a" selected>Alpha</option>
|
|
<option value="b">Beta</option>
|
|
|
|
</select>
|
|
|
|
<div id="form-operator.product-i1-entitlement_set_id-line"></div>
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-i1-visibility" class="mb-3 app-form-field">
|
|
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-i1-visibility-control">Listed</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Listed" data-bs-content="Shown in the member catalog." aria-label="Help: Listed"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-i1-visibility-control" name="visibility" value="public" checked ></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-i1-description" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-i1-description-control">Description <span class="text-muted">(optional)</span></label>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-i1-description-control" name="description" value="sample" maxlength="500" autocomplete="off">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-i1-features" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-i1-features-control">Additional features <span class="text-muted">(optional)</span></label>
|
|
|
|
<textarea class="form-control" id="form-operator.product-i1-features-control" name="features" rows="3" maxlength="2000" aria-describedby="form-operator.product-i1-features-hint">sample</textarea>
|
|
|
|
|
|
|
|
<div id="form-operator.product-i1-features-hint" class="form-text">Shown on the Products page, one per line; supplements entitlement-derived limits.</div>
|
|
|
|
|
|
</div><div id="form-operator.product-i1-is_active" class="mb-3 app-form-field">
|
|
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-i1-is_active-control">Active</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Active" data-bs-content="An inactive product is not offered when new grants are created. Grants that already reference it keep working." aria-label="Help: Active"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-i1-is_active-control" name="is_active" value="true" checked ></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><input type="hidden" name="exclude" value="sample" >
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2 app-form-commit">
|
|
|
|
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Create product</button>
|
|
|
|
|
|
<a href="/operator/products" class="btn btn-outline-secondary">Cancel</a>
|
|
|
|
|
|
</div>
|
|
|
|
</form>
|
|
</div></div>
|
|
|
|
|
|
===== operator.product refused [entitlement_set_id name]
|
|
|
|
|
|
<div class="card app-form-box mb-3"><div class="card-body">
|
|
<form id="form-operator.product" data-form="operator.product" data-form-kind="create" class="app-form" novalidate hx-post="/partials/operator/products" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
|
|
<div id="form-operator.product-error" class="app-form-error" role="alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-form-fields">
|
|
<div id="form-operator.product-name" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-name-control">Name</label>
|
|
|
|
<input type="text" class="form-control is-invalid" id="form-operator.product-name-control" name="name" value="" required maxlength="200" autocomplete="off" aria-describedby="form-operator.product-name-error form-operator.product-name-warning" aria-invalid="true" hx-get="/partials/operator/products/name-check" hx-include="closest form" hx-swap="innerHTML" hx-target="#form-operator\.product-name-warning" hx-trigger="input changed delay:300ms" autofocus>
|
|
|
|
<div id="form-operator.product-name-warning"></div>
|
|
|
|
|
|
|
|
<div id="form-operator.product-name-error" class="invalid-feedback d-block">Enter a name.</div>
|
|
</div><div id="form-operator.product-display_category" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-display_category-control">Display category <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Display category" data-bs-content="Grouping for the storefront and operator lists; never drives behavior. Blank is fine." aria-label="Help: Display category"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-display_category-control" name="display_category" value="" placeholder="e.g. addon" maxlength="100" autocomplete="off">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-entitlement_set_id" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-entitlement_set_id-control">Entitlement set</label>
|
|
|
|
<select class="form-select is-invalid" id="form-operator.product-entitlement_set_id-control" name="entitlement_set_id" required aria-describedby="form-operator.product-entitlement_set_id-error form-operator.product-entitlement_set_id-line" aria-invalid="true">
|
|
<option value="" disabled selected>Choose one</option>
|
|
<option value="a">Alpha</option>
|
|
<option value="b">Beta</option>
|
|
|
|
</select>
|
|
|
|
<div id="form-operator.product-entitlement_set_id-line"></div>
|
|
|
|
|
|
|
|
<div id="form-operator.product-entitlement_set_id-error" class="invalid-feedback d-block">Choose an entitlement set.</div>
|
|
</div><div id="form-operator.product-visibility" class="mb-3 app-form-field">
|
|
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-visibility-control">Listed</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Listed" data-bs-content="Shown in the member catalog." aria-label="Help: Listed"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-visibility-control" name="visibility" value="public" ></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-description" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-description-control">Description <span class="text-muted">(optional)</span></label>
|
|
|
|
<input type="text" class="form-control" id="form-operator.product-description-control" name="description" value="" maxlength="500" autocomplete="off">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><div id="form-operator.product-features" class="mb-3 app-form-field">
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-features-control">Additional features <span class="text-muted">(optional)</span></label>
|
|
|
|
<textarea class="form-control" id="form-operator.product-features-control" name="features" rows="3" maxlength="2000" aria-describedby="form-operator.product-features-hint"></textarea>
|
|
|
|
|
|
|
|
<div id="form-operator.product-features-hint" class="form-text">Shown on the Products page, one per line; supplements entitlement-derived limits.</div>
|
|
|
|
|
|
</div><div id="form-operator.product-is_active" class="mb-3 app-form-field">
|
|
|
|
|
|
|
|
<label class="form-label" for="form-operator.product-is_active-control">Active</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Active" data-bs-content="An inactive product is not offered when new grants are created. Grants that already reference it keep working." aria-label="Help: Active"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
|
|
|
|
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-is_active-control" name="is_active" value="true" ></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div><input type="hidden" name="exclude" value="" >
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2 app-form-commit">
|
|
|
|
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Create product</button>
|
|
|
|
|
|
<a href="/operator/products" class="btn btn-outline-secondary">Cancel</a>
|
|
|
|
|
|
</div>
|
|
|
|
</form>
|
|
</div></div>
|
|
|
|
|
|
===== operator.product bad-values refusals
|
|
description: Description must be 500 characters or fewer.
|
|
display_category: Display category must be 100 characters or fewer.
|
|
entitlement_set_id: Choose an entitlement set from the list.
|
|
name: Name must be 200 characters or fewer.
|