From 097167ee2ce9de180bbd3949a9fdbca2ceb71749 Mon Sep 17 00:00:00 2001 From: Jakob Date: Wed, 29 Jul 2026 17:54:42 +0200 Subject: [PATCH 1/3] Make queue rows and drawer headers toggle selection, with shift-click ranges and explicit edit buttons. Co-Authored-By: Claude --- web/src/components/MainPage.vue | 111 ++++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 5 deletions(-) diff --git a/web/src/components/MainPage.vue b/web/src/components/MainPage.vue index 97d0c08..4fba670 100644 --- a/web/src/components/MainPage.vue +++ b/web/src/components/MainPage.vue @@ -91,6 +91,96 @@ function rowTitleFull(entry: QueueEntry): string { const selectedIds = ref>(new Set()); const plateCounts = ref>(new Map()); +// Anchor for shift-click range selection: the id of the last entry row the +// user clicked, if it is still rendered. +const lastClickedId = ref(null); + +function onRowClick(entry: QueueEntry, event: MouseEvent): void { + const orderedIds = queueRows.value + .filter((row) => row.kind !== 'group') + .map((row) => row.entry.id); + const anchorIndex = + lastClickedId.value !== null ? orderedIds.indexOf(lastClickedId.value) : -1; + if (event.shiftKey && anchorIndex !== -1) { + event.preventDefault(); + const clickedIndex = orderedIds.indexOf(entry.id); + const start = Math.min(anchorIndex, clickedIndex); + const end = Math.max(anchorIndex, clickedIndex); + const selecting = !selectedIds.value.has(entry.id); + const entriesById = new Map(queue.entries.map((e) => [e.id, e])); + const nextIds = new Set(selectedIds.value); + const nextCounts = new Map(plateCounts.value); + for (let i = start; i <= end; i++) { + const id = orderedIds[i]; + if (selecting) { + nextIds.add(id); + if (!nextCounts.has(id)) { + const target = entriesById.get(id); + if (target !== undefined) nextCounts.set(id, target.quantity); + } + } else { + nextIds.delete(id); + nextCounts.delete(id); + } + } + selectedIds.value = nextIds; + plateCounts.value = nextCounts; + } else { + toggleSelected(entry); + } + lastClickedId.value = entry.id; +} + +/** + * The ids of a group's plate rows: the consecutive entry rows following its + * header in queueRows, which mirrors partitionQueue's ordering. + */ +function groupEntryIds(group: Group): string[] { + const rows = queueRows.value; + const headerIndex = rows.findIndex( + (row) => row.kind === 'group' && row.group.id === group.id, + ); + if (headerIndex === -1) return []; + const ids: string[] = []; + for (let i = headerIndex + 1; i < rows.length; i++) { + const row = rows[i]; + if (row.kind !== 'entry' || !row.grouped) break; + ids.push(row.entry.id); + } + return ids; +} + +// Group header click: select all the group's plate rows, or deselect them +// all when every one is already selected. +function onGroupClick(group: Group): void { + const ids = groupEntryIds(group); + if (ids.length === 0) return; + const allSelected = ids.every((id) => selectedIds.value.has(id)); + const entriesById = new Map(queue.entries.map((e) => [e.id, e])); + const nextIds = new Set(selectedIds.value); + const nextCounts = new Map(plateCounts.value); + for (const id of ids) { + if (allSelected) { + nextIds.delete(id); + nextCounts.delete(id); + } else { + nextIds.add(id); + if (!nextCounts.has(id)) { + const target = entriesById.get(id); + if (target !== undefined) nextCounts.set(id, target.quantity); + } + } + } + selectedIds.value = nextIds; + plateCounts.value = nextCounts; + lastClickedId.value = ids[ids.length - 1]; +} + +function isGroupSelected(group: Group): boolean { + const ids = groupEntryIds(group); + return ids.length > 0 && ids.every((id) => selectedIds.value.has(id)); +} + function toggleSelected(entry: QueueEntry): void { const nextIds = new Set(selectedIds.value); const nextCounts = new Map(plateCounts.value); @@ -248,8 +338,9 @@ function removeRow(entry: QueueEntry): void { v-if="row.kind === 'group'" :key="`g-${row.group.id}`" class="group-header" + :class="{ selected: isGroupSelected(row.group) }" role="button" - @click="app.openDrawer(row.group.id)" + @click="onGroupClick(row.group)" > @@ -259,8 +350,11 @@ function removeRow(entry: QueueEntry): void { {{ row.descriptor.counts.done }} / {{ row.descriptor.counts.total }} printed -
+ + + Edit drawer +