fix(submissions): broadcast crop delete + fix for crop admin

This commit is contained in:
2026-10-16 02:19:18 +02:00
parent a110575e0a
commit 1daf2757b8
7 changed files with 194 additions and 56 deletions
+39 -21
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from "vue";
import { ref, onMounted, onUnmounted, computed, watch } from "vue";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { useTitle } from "@vueuse/core";
@@ -47,7 +47,8 @@ const userSubmissions = ref<SubmissionOut[]>([]);
const isLoadingSubmissions = ref(false);
const selectedSubmissionFile = ref<any>(null);
const isSubmissionFileModalOpen = ref(false);
const copyFeedback = ref<string>("");
// Feedback shown on the copy button that was clicked, keyed by "file" or crop id
const copyFeedback = ref<{ key: string; message: string } | null>(null);
const isDrawingCrop = ref(false);
const cropStartX = ref(0);
const cropStartY = ref(0);
@@ -141,23 +142,23 @@ const closeSubmissionFileModal = () => {
selectedSubmissionFile.value = null;
};
const copyOCRText = async () => {
if (selectedSubmissionFile.value?.ocr_raw_data) {
try {
await navigator.clipboard.writeText(selectedSubmissionFile.value.ocr_raw_data);
copyFeedback.value = t("common.copied");
setTimeout(() => {
copyFeedback.value = "";
}, 2000);
} catch (err) {
copyFeedback.value = t("submissions.copyFailed");
setTimeout(() => {
copyFeedback.value = "";
}, 2000);
}
const copyOCRText = async (text: string | undefined, key: string) => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
copyFeedback.value = { key, message: t("common.copied") };
} catch (err) {
copyFeedback.value = { key, message: t("submissions.copyFailed") };
}
setTimeout(() => {
if (copyFeedback.value?.key === key) copyFeedback.value = null;
}, 2000);
};
const copyLabel = (key: string) =>
copyFeedback.value?.key === key ? copyFeedback.value.message : t("common.copy");
const getCanvasCoordinates = (canvas: HTMLCanvasElement, clientX: number, clientY: number) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
@@ -246,6 +247,12 @@ const redrawCropCanvas = () => {
img.src = selectedSubmissionFile.value?.file_url;
};
// Crops can also arrive over the websocket (e.g. drawn by another admin)
watch(
() => selectedSubmissionFile.value?.crops?.length,
() => redrawCropCanvas(),
);
const submitCrop = async () => {
if (!currentCrop.value || !selectedSubmissionFile.value) return;
@@ -262,10 +269,12 @@ const submitCrop = async () => {
},
});
// Add the new crop to the list immediately
// Add the new crop to the list immediately, unless the websocket beat us to it
if (newCrop) {
selectedSubmissionFile.value.crops = selectedSubmissionFile.value.crops || [];
selectedSubmissionFile.value.crops.push(newCrop);
if (!selectedSubmissionFile.value.crops.some((c: any) => c.id === newCrop.id)) {
selectedSubmissionFile.value.crops.push(newCrop);
}
}
currentCrop.value = null;
@@ -480,6 +489,7 @@ const loadUserSubmissions = async () => {
// the next match when there's only one, so players follow along without a refresh.
useSubmissionsWebSocket({
userSubmissions,
scopeId: props.scopeId,
onScopeUpdate: (scope) => {
if (scope.id !== props.scopeId || (scope.is_active && scope.assigned)) return;
@@ -909,9 +919,10 @@ onUnmounted(() => {
<div>
<div class="flex items-center justify-between mb-2">
<p class="text-xs font-semibold text-base-content/70">{{ t("submissions.extractedText") }}</p>
<button @click="copyOCRText" class="btn btn-xs btn-ghost gap-1">
<button @click="copyOCRText(selectedSubmissionFile.ocr_raw_data, 'file')"
class="btn btn-xs btn-ghost gap-1">
<i class="mdi mdi-content-copy"></i>
{{ copyFeedback || t("common.copy") }}
{{ copyLabel("file") }}
</button>
</div>
<div
@@ -948,7 +959,14 @@ onUnmounted(() => {
<div v-for="crop in selectedSubmissionFile.crops" :key="crop.id" class="space-y-2">
<div class="flex items-center justify-between">
<p class="text-xs font-medium">{{ crop.label || t("submissions.cropLabelFallback", { x: crop.x1, y: crop.y1 }) }}</p>
<span :class="getOCRStatusColor(crop.ocr_status)">{{ crop.ocr_status }}</span>
<div class="flex items-center gap-1">
<button v-if="crop.ocr_status === 'processed' && crop.ocr_raw_data"
@click="copyOCRText(crop.ocr_raw_data, crop.id)" class="btn btn-xs btn-ghost gap-1">
<i class="mdi mdi-content-copy"></i>
{{ copyLabel(crop.id) }}
</button>
<span :class="getOCRStatusColor(crop.ocr_status)">{{ crop.ocr_status }}</span>
</div>
</div>
<div v-if="crop.ocr_status === 'processed' && crop.ocr_raw_data"
class="bg-base-100 p-2 rounded text-xs whitespace-pre-wrap break-words font-mono max-h-32 overflow-y-auto">