1import { Controller } from "@hotwired/stimulus" 2 3// Makes the moderation panel on a review answer the click rather than the 4// round trip. Publishing a review is a decision a curator has already made by 5// the time they press the button; waiting ~300ms to find out whether the app 6// agreed reads as a dead button, and that is exactly how this one read before 7// (the server's turbo_stream only ever replaced the index's table row, so the 8// show page changed nothing at all). 9// 10// On submit we move the panel to the state the action is about to produce. The 11// server's replace follows and overrules us â and if the request failed, we put 12// the original state back rather than leaving a lie on screen. 13export default class extends Controller { 14 static targets = ["badge", "control"] 15 static values = { 16 status: String, // pending | published | hidden â the column, as-is 17 archived: Boolean, 18 spam: Boolean 19 } 20 21 connect() { 22 this.committed = this.snapshot() 23 } 24 25 optimistic(event) { 26 switch (event.currentTarget.dataset.moderationNext) { 27 case "published": this.statusValue = "published"; break 28 case "hidden": this.statusValue = "hidden"; break 29 case "archived": this.archivedValue = true; break 30 case "unarchived": this.archivedValue = false; break 31 default: break 32 } 33 } 34 35 // A failed submit leaves the panel claiming something that didn't happen. 36 // The flash says so too, but the buttons are what the curator is looking at. 37 settle(event) { 38 if (event.detail?.success) return 39 this.restore(this.committed) 40 } 41 42 // Every setter above routes through here, so the panel is never redrawn by 43 // hand. 44 statusValueChanged() { this.render() } 45 archivedValueChanged() { this.render() } 46 spamValueChanged() { this.render() } 47 48 render() { 49 if (!this.hasBadgeTarget) return 50 51 const state = this.badgeState() 52 this.badgeTargets.forEach((badge) => { 53 badge.classList.toggle("hidden", badge.dataset.state !== state) 54 }) 55 this.controlTargets.forEach((control) => { 56 control.classList.toggle("hidden", !this.visible(control)) 57 }) 58 } 59 60 // Mirrors Ecommerce::Admin::ReviewsHelper#review_badge_state. Same order, 61 // same answers â the two are checked against each other by the panel's test. 62 badgeState() { 63 if (this.spamValue) return "spam" 64 if (this.archivedValue) return "archived" 65 if (this.published) return "published" 66 if (this.statusValue === "pending") return "pending" 67 return "hidden" 68 } 69 70 // Ecommerce::Review#published? â the status column alone is not enough. 71 get published() { 72 return this.statusValue === "published" && !this.archivedValue && !this.spamValue 73 } 74 75 visible(control) { 76 const rules = control.dataset 77 if ("showUnlessPublished" in rules) return !this.published 78 if ("showUnlessHidden" in rules) return this.statusValue !== "hidden" 79 if ("showWhenArchived" in rules) return this.archivedValue 80 if ("showUnlessArchived" in rules) return !this.archivedValue 81 return true 82 } 83 84 snapshot() { 85 return { status: this.statusValue, archived: this.archivedValue, spam: this.spamValue } 86 } 87 88 restore({ status, archived, spam }) { 89 this.statusValue = status 90 this.archivedValue = archived 91 this.spamValue = spam 92 } 93}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.