max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_013vDpLixQiknHhfHiGxFWo7
6 files changed,
+155 insertions,
-115 deletions
| @@ -6636,7 +6636,7 @@ | |||
| 6636 | 6636 | ||
| 6637 | 6637 | [[package]] | |
| 6638 | 6638 | name = "quasi-axum" | |
| 6639 | - | version = "0.94.0" | |
| 6639 | + | version = "0.96.0" | |
| 6640 | 6640 | dependencies = [ | |
| 6641 | 6641 | "axum", | |
| 6642 | 6642 | "http 1.4.2", | |
| @@ -6647,7 +6647,7 @@ | |||
| 6647 | 6647 | ||
| 6648 | 6648 | [[package]] | |
| 6649 | 6649 | name = "quasi-basics" | |
| 6650 | - | version = "0.94.0" | |
| 6650 | + | version = "0.96.0" | |
| 6651 | 6651 | dependencies = [ | |
| 6652 | 6652 | "makeover-layout", | |
| 6653 | 6653 | "quasi-router", | |
| @@ -6655,7 +6655,7 @@ | |||
| 6655 | 6655 | ||
| 6656 | 6656 | [[package]] | |
| 6657 | 6657 | name = "quasi-http" | |
| 6658 | - | version = "0.94.0" | |
| 6658 | + | version = "0.96.0" | |
| 6659 | 6659 | dependencies = [ | |
| 6660 | 6660 | "form_urlencoded", | |
| 6661 | 6661 | "http 1.4.2", | |
| @@ -6664,7 +6664,7 @@ | |||
| 6664 | 6664 | ||
| 6665 | 6665 | [[package]] | |
| 6666 | 6666 | name = "quasi-router" | |
| 6667 | - | version = "0.94.0" | |
| 6667 | + | version = "0.96.0" | |
| 6668 | 6668 | dependencies = [ | |
| 6669 | 6669 | "makeover-layout", | |
| 6670 | 6670 | ] | |
| @@ -6687,7 +6687,7 @@ | |||
| 6687 | 6687 | ||
| 6688 | 6688 | [[package]] | |
| 6689 | 6689 | name = "quasi-webview" | |
| 6690 | - | version = "0.94.0" | |
| 6690 | + | version = "0.96.0" | |
| 6691 | 6691 | dependencies = [ | |
| 6692 | 6692 | "docengine", | |
| 6693 | 6693 | "makeover-layout", | |
| @@ -10761,11 +10761,11 @@ | |||
| 10761 | 10761 | ||
| 10762 | 10762 | [[patch.unused]] | |
| 10763 | 10763 | name = "quasi-immediate" | |
| 10764 | - | version = "0.94.0" | |
| 10764 | + | version = "0.96.0" | |
| 10765 | 10765 | ||
| 10766 | 10766 | [[patch.unused]] | |
| 10767 | 10767 | name = "quasi-notifs" | |
| 10768 | - | version = "0.94.0" | |
| 10768 | + | version = "0.96.0" | |
| 10769 | 10769 | ||
| 10770 | 10770 | [[patch.unused]] | |
| 10771 | 10771 | name = "quasi-store" | |
| @@ -10773,7 +10773,15 @@ | |||
| 10773 | 10773 | ||
| 10774 | 10774 | [[patch.unused]] | |
| 10775 | 10775 | name = "quasi-tauri" | |
| 10776 | - | version = "0.94.0" | |
| 10776 | + | version = "0.96.0" | |
| 10777 | + | ||
| 10778 | + | [[patch.unused]] | |
| 10779 | + | name = "synckit-client" | |
| 10780 | + | version = "0.10.0" | |
| 10781 | + | ||
| 10782 | + | [[patch.unused]] | |
| 10783 | + | name = "synckit-config" | |
| 10784 | + | version = "0.2.0" | |
| 10777 | 10785 | ||
| 10778 | 10786 | [[patch.unused]] | |
| 10779 | 10787 | name = "kberg" | |
| @@ -10786,11 +10794,3 @@ | |||
| 10786 | 10794 | [[patch.unused]] | |
| 10787 | 10795 | name = "painhours" | |
| 10788 | 10796 | version = "0.1.0" | |
| 10789 | - | ||
| 10790 | - | [[patch.unused]] | |
| 10791 | - | name = "synckit-client" | |
| 10792 | - | version = "0.10.0" | |
| 10793 | - | ||
| 10794 | - | [[patch.unused]] | |
| 10795 | - | name = "synckit-config" | |
| 10796 | - | version = "0.2.0" |
| @@ -163,7 +163,7 @@ | |||
| 163 | 163 | # through quasi beside its Askama equivalent, so the cost of describing a screen | |
| 164 | 164 | # here is measured rather than argued. Not load-bearing for any shipped route. | |
| 165 | 165 | # See wiki look-wave-2, tier G. | |
| 166 | - | quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.95" } | |
| 166 | + | quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.96" } | |
| 167 | 167 | # The description vocabulary quasi's screen types are built from. Pinned here | |
| 168 | 168 | # rather than reached through quasi-router's re-export because a described | |
| 169 | 169 | # screen names FieldKind and Tone directly; it has to track what quasi-router | |
| @@ -179,13 +179,13 @@ | |||
| 179 | 179 | # For the request head the per-viewer state factory reads. axum re-exports it, | |
| 180 | 180 | # but the factory's signature is quasi-axum's and names `http::request::Parts`. | |
| 181 | 181 | http = "1.3.1" | |
| 182 | - | quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.95" } | |
| 183 | - | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.95" } | |
| 182 | + | quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.96" } | |
| 183 | + | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.96" } | |
| 184 | 184 | # The first-party widget set. The carousel was proved here against three pages | |
| 185 | 185 | # and then moved there; what is left in `quasi/widgets/carousel.rs` is the | |
| 186 | 186 | # Askama glue. Not `makeover-basics`: a widget is an assembly of `Node`s, so it | |
| 187 | 187 | # sits above quasi-router rather than inside the description suite. | |
| 188 | - | quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.95" } | |
| 188 | + | quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.96" } | |
| 189 | 189 | ||
| 190 | 190 | # Git source browser | |
| 191 | 191 | gix = { version = "0.86", default-features = false, features = ["blame", "mailmap", "revision", "sha1", "max-performance-safe"] } | |
| @@ -277,7 +277,7 @@ | |||
| 277 | 277 | # Written from the crate rather than copied into the repo for the reason the | |
| 278 | 278 | # constants exist: a stale copy in a static directory is a silently dead bulk | |
| 279 | 279 | # bar, and the version has to be the one the emitter agrees with. | |
| 280 | - | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.95" } | |
| 280 | + | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.96" } | |
| 281 | 281 | ||
| 282 | 282 | [dev-dependencies] | |
| 283 | 283 | tower = { version = "0.5.3", features = ["util"] } |
| @@ -61,18 +61,45 @@ | |||
| 61 | 61 | speedEl: document.getElementById('version-upload-speed'), | |
| 62 | 62 | }); | |
| 63 | 63 | ||
| 64 | - | // The picker is the described field's own input: what it takes and how | |
| 65 | - | // many is `upload_field::version_queue`, and the rows below it are this | |
| 66 | - | // file's, because a queue with a label per row is not one file going to | |
| 67 | - | // one place. | |
| 68 | - | var versionFileInput = document.getElementById('version-files'); | |
| 69 | - | var fileRows = document.getElementById('version-file-rows'); | |
| 64 | + | // The picker and the slots it fills are both described now | |
| 65 | + | // (`upload_field::version_queue` and `version_file_queue`), and | |
| 66 | + | // `quasi-repeat.js` makes a slot per picked file. What it cannot know | |
| 67 | + | // is what a File is called or what to guess for its label, so it | |
| 68 | + | // announces each slot and that is filled here. | |
| 69 | + | var fileQueue = document.getElementById('version-file'); | |
| 70 | 70 | ||
| 71 | - | if (versionFileInput) { | |
| 72 | - | versionFileInput.addEventListener('change', function() { | |
| 73 | - | for (var i = 0; i < this.files.length; i++) addFileRow(this.files[i]); | |
| 74 | - | this.value = ''; | |
| 75 | - | }); | |
| 71 | + | document.addEventListener('quasi:repeat:took', function(event) { | |
| 72 | + | var slot = event.detail.slot; | |
| 73 | + | var file = event.detail.file; | |
| 74 | + | if (!fileQueue || !fileQueue.contains(slot)) return; | |
| 75 | + | ||
| 76 | + | // The File itself rides on the slot. Nothing else holds the queue: | |
| 77 | + | // the slots are the queue, so removing one removes its file and | |
| 78 | + | // the renumbering `quasi-repeat.js` does is not this file's | |
| 79 | + | // problem any more. | |
| 80 | + | slot.file = file; | |
| 81 | + | ||
| 82 | + | var named = slot.querySelector('label[for]'); | |
| 83 | + | if (named) named.textContent = file.name; | |
| 84 | + | slot.setAttribute('data-repeat-named', ''); | |
| 85 | + | ||
| 86 | + | var label = slot.querySelector('input[type="text"]'); | |
| 87 | + | if (label && !label.value) label.value = guessLabel(file.name); | |
| 88 | + | }); | |
| 89 | + | ||
| 90 | + | /** The slots standing now, in order, each with its file and label. */ | |
| 91 | + | function pickedFiles() { | |
| 92 | + | if (!fileQueue) return []; | |
| 93 | + | return [].slice.call(fileQueue.querySelectorAll('[data-repeat-at]')) | |
| 94 | + | .filter(function(slot) { return slot.file; }) | |
| 95 | + | .map(function(slot) { | |
| 96 | + | var input = slot.querySelector('input[type="text"]'); | |
| 97 | + | return { | |
| 98 | + | file: slot.file, | |
| 99 | + | slot: slot, | |
| 100 | + | label: input ? input.value.trim() : '' | |
| 101 | + | }; | |
| 102 | + | }); | |
| 76 | 103 | } | |
| 77 | 104 | ||
| 78 | 105 | function guessLabel(name) { | |
| @@ -85,50 +112,26 @@ | |||
| 85 | 112 | return ''; | |
| 86 | 113 | } | |
| 87 | 114 | ||
| 88 | - | function addFileRow(file) { | |
| 89 | - | var idx = fileQueue.length; | |
| 90 | - | fileQueue.push({ file: file, idx: idx }); | |
| 91 | - | var tr = document.createElement('tr'); | |
| 92 | - | tr.dataset.idx = idx; | |
| 93 | - | tr.style.borderBottom = '1px solid var(--border)'; | |
| 94 | - | tr.innerHTML = | |
| 95 | - | '<td style="padding: 0.4rem 0.5rem 0.4rem 0; font-size: 0.85rem; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="' + escapeHtml(file.name) + '">' + escapeHtml(file.name) + '</td>' + | |
| 96 | - | '<td style="padding: 0.4rem 0.5rem;"><input type="text" class="version-label-input" data-idx="' + idx + '" value="' + escapeHtml(guessLabel(file.name)) + '" placeholder="e.g., macOS (arm)" style="width: 100%; padding: 0.25rem 0.4rem; font-size: 0.85rem;"></td>' + | |
| 97 | - | '<td style="padding: 0.4rem 0.5rem;"><button type="button" class="btn-secondary version-remove-file" data-idx="' + idx + '" style="padding: 0.2rem 0.5rem; font-size: 0.75rem;">Remove</button></td>'; | |
| 98 | - | fileRows.appendChild(tr); | |
| 99 | - | ||
| 100 | - | tr.querySelector('.version-remove-file').addEventListener('click', function() { | |
| 101 | - | fileQueue[idx] = null; | |
| 102 | - | tr.remove(); | |
| 103 | - | }); | |
| 104 | - | } | |
| 105 | - | ||
| 106 | 115 | // Upload all button | |
| 107 | 116 | createBtn.addEventListener('click', function() { | |
| 108 | 117 | var versionNumber = document.getElementById('new-version-number').value.trim(); | |
| 109 | 118 | var changelog = document.getElementById('version-changelog').value.trim(); | |
| 110 | - | var entries = fileQueue.filter(function(e) { return e !== null; }); | |
| 119 | + | var entries = pickedFiles(); | |
| 111 | 120 | ||
| 112 | 121 | if (!versionNumber) { showToast('Please enter a version number.'); return; } | |
| 113 | 122 | if (entries.length === 0) { showToast('Please add at least one file.'); return; } | |
| 114 | 123 | ||
| 115 | 124 | this.disabled = true; | |
| 116 | 125 | this.textContent = 'Uploading...'; | |
| 117 | - | document.getElementById('new-version-form').classList.add('hidden'); | |
| 118 | 126 | document.getElementById('version-upload-progress').classList.remove('hidden'); | |
| 119 | 127 | ||
| 120 | - | // Build queue display | |
| 121 | - | var queueEl = document.getElementById('version-upload-queue'); | |
| 122 | - | queueEl.innerHTML = ''; | |
| 128 | + | // The queue stays on screen and says its own progress, one slot at | |
| 129 | + | // a time, which is what `Progress` is for. The second copy of this | |
| 130 | + | // list that used to live inside the progress panel is gone with it. | |
| 123 | 131 | for (var q = 0; q < entries.length; q++) { | |
| 124 | - | var li = document.createElement('div'); | |
| 125 | - | li.id = 'queue-item-' + entries[q].idx; | |
| 126 | - | li.style.cssText = 'display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0; font-size: 0.85rem;'; | |
| 127 | - | var labelInput = document.querySelector('.version-label-input[data-idx="' + entries[q].idx + '"]'); | |
| 128 | - | var labelText = labelInput ? labelInput.value.trim() : ''; | |
| 129 | - | var displayName = escapeHtml(entries[q].file.name) + (labelText ? ' (' + escapeHtml(labelText) + ')' : ''); | |
| 130 | - | li.innerHTML = '<span class="queue-status" style="width: 1.5em; text-align: center; opacity: 0.5;">-</span><span style="flex: 1;">' + displayName + '</span><span class="queue-size" style="opacity: 0.5;">' + formatSize(entries[q].file.size) + '</span>'; | |
| 131 | - | queueEl.appendChild(li); | |
| 132 | + | entries[q].slot.querySelectorAll('input, button').forEach(function(control) { | |
| 133 | + | control.disabled = true; | |
| 134 | + | }); | |
| 132 | 135 | } | |
| 133 | 136 | ||
| 134 | 137 | uploadSequentially(entries, 0, versionNumber, changelog); | |
| @@ -140,13 +143,14 @@ | |||
| 140 | 143 | return bytes + ' B'; | |
| 141 | 144 | } | |
| 142 | 145 | ||
| 143 | - | function updateQueueStatus(idx, status) { | |
| 144 | - | var el = document.getElementById('queue-item-' + idx); | |
| 145 | - | if (!el) return; | |
| 146 | - | var s = el.querySelector('.queue-status'); | |
| 147 | - | if (status === 'uploading') { s.textContent = '...'; s.style.opacity = '1'; } | |
| 148 | - | else if (status === 'done') { s.textContent = 'OK'; s.style.opacity = '0.7'; el.style.opacity = '0.6'; } | |
| 149 | - | else if (status === 'error') { s.textContent = '!'; s.style.color = 'var(--error, #c0392b)'; s.style.opacity = '1'; } | |
| 146 | + | /** | |
| 147 | + | * Where a slot's own work has got to, in the renderer's own word for | |
| 148 | + | * it. `working`, `done` and `failed` are what `Progress` emits, so the | |
| 149 | + | * stylesheet has one thing to read whether the server described the | |
| 150 | + | * state or this set it. | |
| 151 | + | */ | |
| 152 | + | function slotProgress(entry, progress) { | |
| 153 | + | if (entry.slot) entry.slot.setAttribute('data-repeat-progress', progress); | |
| 150 | 154 | } | |
| 151 | 155 | ||
| 152 | 156 | function uploadSequentially(entries, i, versionNumber, changelog) { | |
| @@ -163,10 +167,9 @@ | |||
| 163 | 167 | } | |
| 164 | 168 | ||
| 165 | 169 | var entry = entries[i]; | |
| 166 | - | var labelInput = document.querySelector('.version-label-input[data-idx="' + entry.idx + '"]'); | |
| 167 | - | var label = labelInput ? labelInput.value.trim() : ''; | |
| 170 | + | var label = entry.label; | |
| 168 | 171 | ||
| 169 | - | updateQueueStatus(entry.idx, 'uploading'); | |
| 172 | + | slotProgress(entry, 'working'); | |
| 170 | 173 | uploader.filenameEl.textContent = entry.file.name + (entries.length > 1 ? ' (' + (i + 1) + '/' + entries.length + ')' : ''); | |
| 171 | 174 | ||
| 172 | 175 | fetch(createRoute, { | |
| @@ -224,11 +227,11 @@ | |||
| 224 | 227 | if (confirmData && confirmData.pending_review) { | |
| 225 | 228 | showToast('Version upload held for review: our scanner flagged it.', 'warning'); | |
| 226 | 229 | } | |
| 227 | - | updateQueueStatus(entry.idx, 'done'); | |
| 230 | + | slotProgress(entry, 'done'); | |
| 228 | 231 | uploadSequentially(entries, i + 1, versionNumber, changelog); | |
| 229 | 232 | }) | |
| 230 | 233 | .catch(function(err) { | |
| 231 | - | updateQueueStatus(entry.idx, 'error'); | |
| 234 | + | slotProgress(entry, 'failed'); | |
| 232 | 235 | showVersionError(err.message || 'Upload failed'); | |
| 233 | 236 | }); | |
| 234 | 237 | } |
| @@ -9210,34 +9210,21 @@ | |||
| 9210 | 9210 | gap: var(--gap-section); | |
| 9211 | 9211 | } | |
| 9212 | 9212 | ||
| 9213 | - | .item-version-upload-file-table { | |
| 9214 | - | width: 100%; | |
| 9215 | - | border-collapse: collapse; | |
| 9216 | - | margin-top: var(--gap-peer); | |
| 9213 | + | /* Where one slot's own work has got to, in the three words `Progress` emits. | |
| 9214 | + | Here rather than in makeover because the hook is a data attribute | |
| 9215 | + | `quasi-webview` writes rather than a class makeover names, and this is its | |
| 9216 | + | only consumer. A second host wanting it is the reason to move it. */ | |
| 9217 | + | .field-repeat-slot[data-repeat-progress="working"] { | |
| 9218 | + | opacity: 0.75; | |
| 9217 | 9219 | } | |
| 9218 | 9220 | ||
| 9219 | - | .item-version-upload-file-table thead tr { | |
| 9220 | - | text-align: left; | |
| 9221 | - | font-size: var(--text-fine); | |
| 9222 | - | opacity: 0.7; | |
| 9223 | - | text-transform: uppercase; | |
| 9224 | - | letter-spacing: 0.03em; | |
| 9221 | + | .field-repeat-slot[data-repeat-progress="done"] { | |
| 9222 | + | opacity: 0.55; | |
| 9225 | 9223 | } | |
| 9226 | 9224 | ||
| 9227 | - | .item-version-upload-file-table th:first-child { | |
| 9228 | - | padding: var(--gap-peer) var(--gap-peer) var(--gap-peer) 0; | |
| 9229 | - | } | |
| 9230 | - | ||
| 9231 | - | .item-version-upload-file-table th + th { | |
| 9232 | - | padding: var(--gap-peer); | |
| 9233 | - | } | |
| 9234 | - | ||
| 9235 | - | .item-version-upload-file-table th:last-child { | |
| 9236 | - | width: 40px; | |
| 9237 | - | } | |
| 9238 | - | ||
| 9239 | - | .item-version-upload-queue { | |
| 9240 | - | margin-bottom: var(--gap-section); | |
| 9225 | + | .field-repeat-slot[data-repeat-progress="failed"] { | |
| 9226 | + | border-left: 2px solid var(--danger); | |
| 9227 | + | padding-left: var(--gap-peer); | |
| 9241 | 9228 | } | |
| 9242 | 9229 | ||
| 9243 | 9230 | .item-version-upload-speed { |
| @@ -38,7 +38,7 @@ | |||
| 38 | 38 | //! live here once and each surface is named for what it is. | |
| 39 | 39 | ||
| 40 | 40 | use makeover_layout::{Family, FieldKind}; | |
| 41 | - | use quasi_router::{Accepted, Action, Field, Node}; | |
| 41 | + | use quasi_router::{Accepted, Action, Field, Node, Repeat}; | |
| 42 | 42 | ||
| 43 | 43 | /// Every suffix a version file may carry. | |
| 44 | 44 | /// | |
| @@ -227,6 +227,35 @@ | |||
| 227 | 227 | html | |
| 228 | 228 | } | |
| 229 | 229 | ||
| 230 | + | /// The files the reader has picked, one slot each, with the label that names | |
| 231 | + | /// the platform it is for. | |
| 232 | + | /// | |
| 233 | + | /// The shape `7f04f751` counted and could not say, until quasi 0.96 said it. | |
| 234 | + | /// Three members carry it and each answers a different half of what the | |
| 235 | + | /// hand-written table was doing: | |
| 236 | + | /// | |
| 237 | + | /// - [`Repeat::added_by`] names [`version_queue`]'s picker as where the slots | |
| 238 | + | /// come from, so no renderer offers an add control. A blank row here is | |
| 239 | + | /// nothing a reader can fill: there is no way to type a file. | |
| 240 | + | /// - [`Instance::called`] names each slot by its file, set by the host as it | |
| 241 | + | /// takes them, because only the picker knows what a `File` is called. The | |
| 242 | + | /// ordinal would have put "File 2" where "track-arm.dmg" goes. | |
| 243 | + | /// - [`Progress`] is the per-slot status the run sets, which is what the | |
| 244 | + | /// second copy of this list inside the progress panel used to draw. | |
| 245 | + | /// | |
| 246 | + | /// The question itself is the label, and it is the only thing here that is | |
| 247 | + | /// answered: the file name is what the slot *is*, and the bytes go to S3 | |
| 248 | + | /// through a presigned PUT rather than through this form. | |
| 249 | + | #[must_use] | |
| 250 | + | pub fn version_file_queue() -> String { | |
| 251 | + | use quasi_axum::Serves as _; | |
| 252 | + | ||
| 253 | + | let mut label = Field::new(FieldKind::Text, "version-file", "File"); | |
| 254 | + | label.placeholder = Some("e.g., macOS (arm)".to_owned()); | |
| 255 | + | let field = label.repeating(Repeat::new().added_by("version-files").removing("Remove")); | |
| 256 | + | quasi_webview::Webview::new().fragment(&Node::field(field)) | |
| 257 | + | } | |
| 258 | + | ||
| 230 | 259 | /// The control that starts the run, and the address it starts at. | |
| 231 | 260 | /// | |
| 232 | 261 | /// [`Action::by_host`] for the reason every other upload here is: the host | |
| @@ -250,6 +279,30 @@ | |||
| 250 | 279 | ||
| 251 | 280 | #[cfg(test)] | |
| 252 | 281 | mod tests { | |
| 282 | + | /// The three members `7f04f751` waited on, on the surface that wanted all | |
| 283 | + | /// of them. Read together they are the conversion: before this the rows | |
| 284 | + | /// were built by `addFileRow` with inline styles, the label input was | |
| 285 | + | /// found by a hand-written class, and nothing said a slot could fail. | |
| 286 | + | #[test] | |
| 287 | + | fn the_picked_file_queue_says_where_its_slots_come_from_and_names_them_by_file() { | |
| 288 | + | let html = super::version_file_queue(); | |
| 289 | + | ||
| 290 | + | // The slots come from the picker above, so no renderer offers an add | |
| 291 | + | // control: there is no blank a reader could fill. | |
| 292 | + | assert!( | |
| 293 | + | html.contains(r#"data-repeat-add-from="version-files""#), | |
| 294 | + | "{html}" | |
| 295 | + | ); | |
| 296 | + | // The add control itself, which is a different attribute from the one | |
| 297 | + | // above and would otherwise match it as a prefix. | |
| 298 | + | assert!(!html.contains("field-repeat-add"), "{html}"); | |
| 299 | + | // The question that is actually answered is the label. | |
| 300 | + | assert!(html.contains(r#"data-repeat="version-file""#), "{html}"); | |
| 301 | + | assert!(html.contains("data-repeat-slots"), "{html}"); | |
| 302 | + | // And the blank the host clones per picked file. | |
| 303 | + | assert!(html.contains("<template"), "{html}"); | |
| 304 | + | } | |
| 305 | + | ||
| 253 | 306 | /// The four axes `f7261a5a` settled, on the surface that has all of them. | |
| 254 | 307 | /// Read together they are the whole point of the conversion: before this | |
| 255 | 308 | /// the accept list was an attribute in Askama, the destination was a | |
| @@ -652,7 +705,13 @@ | |||
| 652 | 705 | .render() | |
| 653 | 706 | .expect("render the uploader"); | |
| 654 | 707 | ||
| 655 | - | assert_eq!(html.matches("<button").count(), 3, "{html}"); | |
| 708 | + | // Three on the surface, plus the Remove inside the queue's blank | |
| 709 | + | // `<template>`: inert markup until the picker clones it into a slot, | |
| 710 | + | // and the described queue's own rather than a fourth control anyone | |
| 711 | + | // wrote here. The queue offers no add control at all, which is the | |
| 712 | + | // point of `Adds::Elsewhere`. | |
| 713 | + | assert_eq!(html.matches("<button").count(), 4, "{html}"); | |
| 714 | + | assert!(!html.contains("field-repeat-add"), "{html}"); | |
| 656 | 715 | assert!( | |
| 657 | 716 | html.contains( | |
| 658 | 717 | r#"data-upload-goes="/dashboard/item/11111111-1111-1111-1111-111111111111?tab=files""# |
| @@ -40,19 +40,11 @@ | |||
| 40 | 40 | which is also the order the described field's own label reads in. -#} | |
| 41 | 41 | {{ crate::quasi::upload_field::version_queue()|safe }} | |
| 42 | 42 | ||
| 43 | - | {#- The one part of the form still written out: a row per picked file | |
| 44 | - | with a label field in it. That shape has no member yet and the | |
| 45 | - | counting behind that is in `crate::quasi::item_files`. -#} | |
| 46 | - | <table class="item-version-upload-file-table" id="version-file-table"> | |
| 47 | - | <thead> | |
| 48 | - | <tr> | |
| 49 | - | <th>File</th> | |
| 50 | - | <th>Label</th> | |
| 51 | - | <th></th> | |
| 52 | - | </tr> | |
| 53 | - | </thead> | |
| 54 | - | <tbody id="version-file-rows"></tbody> | |
| 55 | - | </table> | |
| 43 | + | {#- A slot per picked file, each named by its file and asking for the | |
| 44 | + | label that names its platform: | |
| 45 | + | `crate::quasi::upload_field::version_file_queue`. The slots come | |
| 46 | + | from the picker above, which is why no add control is drawn. -#} | |
| 47 | + | {{ crate::quasi::upload_field::version_file_queue()|safe }} | |
| 56 | 48 | ||
| 57 | 49 | {#- Where the run begins and that it waits: | |
| 58 | 50 | `crate::quasi::upload_field::version_upload_all`. The host makes | |
| @@ -83,7 +75,6 @@ | |||
| 83 | 75 | ||
| 84 | 76 | <!-- Shared upload progress/status (outside both forms so always visible) --> | |
| 85 | 77 | <div class="upload-progress hidden" id="version-upload-progress"> | |
| 86 | - | <div class="item-version-upload-queue" id="version-upload-queue"></div> | |
| 87 | 78 | <div class="progress-info"> | |
| 88 | 79 | <span id="version-upload-filename">filename.zip</span> | |
| 89 | 80 | <span id="version-upload-percent">0%</span> |