studio-engine 0.76.2 → 0.76.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/app/views/studio/_board_assets.html.erb +56 -9
- data/app/views/studio/board/_board.html.erb +10 -2
- data/lib/studio/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c352381b0778909cd1798ba2d81abf435f1c2e668198ac38fdae33310d65d8c
|
|
4
|
+
data.tar.gz: 6a80ed76a9748b246df94016572aa1b1b470360c8938390acaef92342a4d1350
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b57498e85f26bfe1ba051381c0411f0f5161513726ed9fbd6aa4769e0bd3767ec962d0f007ac09ad2326e9a97fb05d73e17e122ef5b779f6cc8e48dd923c3c24
|
|
7
|
+
data.tar.gz: 0a17dcfaa72bd771c57cacb04ee9f2f3b82baf5839bc9b739216bc1d6fc1e96b456bc68b645b0865bca9569d64b1abd5327a4e779ff74c6af4cf740776f8f14e
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,55 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.76.3 — 2026-09-23
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **A board drag that the server refuses now tells you.** The board primitive's
|
|
12
|
+
drag-reorder POSTed the new order and discarded the answer: `saveOrder` ended in
|
|
13
|
+
`.catch(function () {})` — an EMPTY handler, on a `fetch` that *resolves* on a
|
|
14
|
+
4xx and so never reached it. Two layers of silence over one request. The
|
|
15
|
+
endpoints on the other end had already written the operator a reason (turf-monster's
|
|
16
|
+
week board answers a drifted card set with *"That order does not match this week's
|
|
17
|
+
games — reload and try again"*, and `Studio::Board::Reorderable` renders
|
|
18
|
+
`{ error: … }` beside every 422 it raises) and that sentence was unreachable by
|
|
19
|
+
construction, while the board's CSS counter renumbered the cards as though the save
|
|
20
|
+
had landed. A refused save now toasts the server's own words — or `window.alert`s
|
|
21
|
+
them on a `toasts: false` board — and resolves `false`.
|
|
22
|
+
- **Fixed at the seam, so all five boards get it.** The check lives in the
|
|
23
|
+
factory's `request()`, which now REJECTS on a refusal rather than handing back a
|
|
24
|
+
resolved 4xx. That is the semantics both callers already assumed by writing
|
|
25
|
+
`.catch(…)`, and it is what neither underlying fetcher provides. Every board
|
|
26
|
+
riding this factory is covered by the one change: turf-monster's NFL week order,
|
|
27
|
+
and mcritchie-studio's tasks (the DevOps board itself), depth charts, content and
|
|
28
|
+
news. No consumer change is needed beyond the gem bump.
|
|
29
|
+
- **An expired session is no longer a silently lost drag either.** A host that
|
|
30
|
+
defines `window.authedFetch` (turf-monster does) gets `null` back from it on a
|
|
31
|
+
401 or a rate-limited 429. `saveOrder` never checked for that, and `applyMove`
|
|
32
|
+
would have thrown a `TypeError` off `resp.ok`. `request()` now names it:
|
|
33
|
+
*"Session expired — please sign in again."*
|
|
34
|
+
- **A refused reorder is not auto-reverted, deliberately.** Unlike a single-card
|
|
35
|
+
move there is no one card to snap back: the whole column re-ranked, and SortableJS
|
|
36
|
+
hands the drop over with the pre-drop order already gone. Every reorder endpoint's
|
|
37
|
+
refusal tells the operator to reload, which restores the stored order exactly; the
|
|
38
|
+
board's job is to make sure that instruction is read. `optimistic:` governs moves
|
|
39
|
+
only, and `studio/board/_board` now says so.
|
|
40
|
+
- **`saveOrder` returns a promise** (resolving `true`/`false`, never rejecting), so
|
|
41
|
+
a caller can wait for a save to land instead of assuming it did.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **The board factory is now executed under test, not grepped.**
|
|
46
|
+
`test/views/board_factory_save_behavior_test.rb` extracts
|
|
47
|
+
`studio/_board_assets`'s `<script>` and runs it under node with stub
|
|
48
|
+
`window`/`document`/`fetch`, asserting `saveOrder`, `applyMove` and `request` by
|
|
49
|
+
CALLING them — nine scenarios covering the refusal text, the `null` session, an
|
|
50
|
+
unparseable error body, the silent success, the `toasts: false` fallback, the
|
|
51
|
+
unchanged event/hook seam and both move outcomes. The bug above survived every
|
|
52
|
+
source-substring assertion in `board_primitive_test.rb`, because the broken build
|
|
53
|
+
contained the word `catch` too; that section now says where behavioral coverage
|
|
54
|
+
belongs.
|
|
55
|
+
|
|
7
56
|
## 0.76.2 — 2026-09-22
|
|
8
57
|
|
|
9
58
|
## 0.76.1 — 2026-09-19
|
|
@@ -187,12 +187,11 @@
|
|
|
187
187
|
body[this.moveParam.resource] = {};
|
|
188
188
|
body[this.moveParam.resource][this.moveParam.attr] = newZone;
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
190
|
+
// request() rejects on a refusal (and reads the server's message), so this
|
|
191
|
+
// `then` runs only on a real success and the `catch` below is the ONE
|
|
192
|
+
// failure path. It used to unwrap the 4xx itself; that check now lives at
|
|
193
|
+
// the seam, where saveOrder gets it too.
|
|
194
|
+
return this.request(url, "PATCH", body).then(function () {
|
|
196
195
|
self.setCardZone(card, newZone);
|
|
197
196
|
self.toast("Moved to " + self.label(newZone), "success");
|
|
198
197
|
self.emit("board-moved", id, fromKey, newZone);
|
|
@@ -208,6 +207,23 @@
|
|
|
208
207
|
|
|
209
208
|
// Save the destination column's order. Reads the ordered id list and POSTs
|
|
210
209
|
// it under the neutral payload key (`slugs` | `ids`) plus the advisory zone.
|
|
210
|
+
//
|
|
211
|
+
// A REFUSAL IS SHOWN, NOT SWALLOWED. Through 0.76.2 this was a bare
|
|
212
|
+
// `.catch(){}` on a `fetch` that resolves on 4xx: two layers of silence over
|
|
213
|
+
// a server that had already written the operator its reason ("That order does
|
|
214
|
+
// not match this week's games — reload and try again"). The board renumbered
|
|
215
|
+
// the cards as though the save had landed. request() now rejects on a refusal
|
|
216
|
+
// and the catch below toasts what the server said.
|
|
217
|
+
//
|
|
218
|
+
// NO AUTO-REVERT, deliberately. Unlike a single-card move there is no one
|
|
219
|
+
// card to snap back — the whole column re-ranked, and SortableJS hands us the
|
|
220
|
+
// drop with the pre-drop order already gone. Every reorder endpoint's refusal
|
|
221
|
+
// tells the operator to RELOAD, which restores the stored truth exactly; the
|
|
222
|
+
// board's job is to make sure that instruction is read.
|
|
223
|
+
//
|
|
224
|
+
// RETURNS A PROMISE (resolving true/false, never rejecting) so a caller can
|
|
225
|
+
// wait for the save to land. It resolves rather than rejects so the
|
|
226
|
+
// fire-and-forget call in handleSortEnd cannot raise an unhandled rejection.
|
|
211
227
|
saveOrder: function (toZone) {
|
|
212
228
|
var self = this;
|
|
213
229
|
var ids = Array.prototype.slice
|
|
@@ -216,12 +232,15 @@
|
|
|
216
232
|
var zone = this.zoneKey(toZone);
|
|
217
233
|
this.emit("board-reordered", null, zone, zone, { ids: ids, zone: zone });
|
|
218
234
|
this.hook(this.onDropHook, { ids: ids, zone: zone });
|
|
219
|
-
if (this.demo || !this.reorderUrl) return;
|
|
235
|
+
if (this.demo || !this.reorderUrl) return Promise.resolve(true);
|
|
220
236
|
var payload = {};
|
|
221
237
|
payload[this.reorderPayload] = ids;
|
|
222
238
|
payload.zone = zone;
|
|
223
|
-
this.request(this.reorderUrl, "POST", payload).
|
|
224
|
-
|
|
239
|
+
return this.request(this.reorderUrl, "POST", payload).then(function () {
|
|
240
|
+
return true;
|
|
241
|
+
}).catch(function (e) {
|
|
242
|
+
self.toast((e && e.message) || "Order save failed — reload and try again.", "error");
|
|
243
|
+
return false;
|
|
225
244
|
});
|
|
226
245
|
},
|
|
227
246
|
|
|
@@ -377,6 +396,22 @@
|
|
|
377
396
|
},
|
|
378
397
|
|
|
379
398
|
// --- helpers -------------------------------------------------------
|
|
399
|
+
// The board's ONE HTTP seam, and it REJECTS ON A SERVER REFUSAL. That is
|
|
400
|
+
// the semantics every caller in this factory already assumed by writing
|
|
401
|
+
// `.catch(…)`, and it is the semantics NEITHER underlying fetcher has:
|
|
402
|
+
//
|
|
403
|
+
// * `window.fetch` RESOLVES on 4xx/5xx. A 422 is an ordinary resolution,
|
|
404
|
+
// so a bare `.catch()` sees network-level failure and nothing else.
|
|
405
|
+
// That is how saveOrder discarded an operator-readable 422 in silence
|
|
406
|
+
// on all five boards that ride this factory.
|
|
407
|
+
// * `window.authedFetch` (the turf-monster fetcher this picks up when the
|
|
408
|
+
// host defines it) returns the response for anything that is not a 401
|
|
409
|
+
// — so a 422 resolves there too — and resolves to NULL on an expired
|
|
410
|
+
// session or a rate-limited tier, which `resp.ok` cannot be read from.
|
|
411
|
+
//
|
|
412
|
+
// Normalising both HERE is the point: a caller can no longer re-introduce
|
|
413
|
+
// the silence by forgetting a check. To ignore a refusal it must now say so
|
|
414
|
+
// out loud, in its own `.catch`.
|
|
380
415
|
request: function (url, method, body) {
|
|
381
416
|
var csrf = (document.querySelector('meta[name="csrf-token"]') || {}).content || "";
|
|
382
417
|
var fetcher = window.authedFetch || window.fetch;
|
|
@@ -384,6 +419,18 @@
|
|
|
384
419
|
method: method,
|
|
385
420
|
headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf, "Accept": "application/json" },
|
|
386
421
|
body: JSON.stringify(body || {})
|
|
422
|
+
}).then(function (resp) {
|
|
423
|
+
// authedFetch's null — the session went away under the board. Name it,
|
|
424
|
+
// rather than throwing a TypeError off resp.ok on the next line.
|
|
425
|
+
if (!resp) throw new Error("Session expired — please sign in again.");
|
|
426
|
+
if (resp.ok) return resp;
|
|
427
|
+
// Prefer the SERVER'S OWN words. Every board endpoint this factory talks
|
|
428
|
+
// to renders `{ error: "…" }` beside its 4xx — Studio::Board::Reorderable
|
|
429
|
+
// here in the engine, and the hand-rolled siblings in the apps — and that
|
|
430
|
+
// text is written to be read by an operator.
|
|
431
|
+
return resp.json().catch(function () { return {}; }).then(function (err) {
|
|
432
|
+
throw new Error(err.error || ("Failed (" + resp.status + ")"));
|
|
433
|
+
});
|
|
387
434
|
});
|
|
388
435
|
},
|
|
389
436
|
|
|
@@ -59,7 +59,13 @@
|
|
|
59
59
|
card_partial: (req) partial rendered per card (e.g. "tasks/task_card").
|
|
60
60
|
card_as: local the record binds to in card_partial. Default :card.
|
|
61
61
|
card_locals: per-card extra locals — a Proc ->(record){ Hash } or a Hash.
|
|
62
|
-
reorder_url: POST endpoint the drag-reorder saves the new order to.
|
|
62
|
+
reorder_url: POST endpoint the drag-reorder saves the new order to. It is
|
|
63
|
+
expected to answer a refusal with a 4xx carrying
|
|
64
|
+
{ error: "…" } (Studio::Board::Reorderable already does);
|
|
65
|
+
the factory TOASTS that sentence. A refused reorder is NOT
|
|
66
|
+
reverted — the whole column moved and the pre-drop order is
|
|
67
|
+
gone by then — so write that message as the instruction the
|
|
68
|
+
operator should follow, e.g. "… reload and try again".
|
|
63
69
|
reorder_payload: :slugs | :ids — the key the ordered id list POSTs under.
|
|
64
70
|
Default :slugs.
|
|
65
71
|
move_url: PATCH template for a cross-column move, ":id" → the card's id
|
|
@@ -78,8 +84,10 @@
|
|
|
78
84
|
empty_selector: the empty-state selector (revert anchor + count toggle).
|
|
79
85
|
Default ".kanban-empty"; nil for a board with no empty state.
|
|
80
86
|
live_channel: Turbo Stream channel for live updates (nil = static board).
|
|
81
|
-
optimistic: revert + red-ring flash a failed
|
|
87
|
+
optimistic: revert + red-ring flash a failed MOVE. Default true. It does
|
|
88
|
+
not govern reorders, which never revert (see reorder_url).
|
|
82
89
|
toasts: show the toast host (false = window.alert on error). Default true.
|
|
90
|
+
Either way a refused move or save reaches the operator.
|
|
83
91
|
empty_label: empty-zone text. Default "Drop here".
|
|
84
92
|
header_slot: raw HTML rendered above the columns (pass html_safe content).
|
|
85
93
|
above_board_slot: raw HTML rendered between the header and the columns.
|
data/lib/studio/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.76.
|
|
4
|
+
version: 0.76.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|