Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/fix: FORMS-1483 queued file handling #1515

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
40 changes: 31 additions & 9 deletions app/frontend/src/components/base/BaseNotificationBar.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup>
import { storeToRefs } from 'pinia';
import { onBeforeMount, onBeforeUnmount } from 'vue';
import { computed, onBeforeMount, onBeforeUnmount } from 'vue';
import { useI18n } from 'vue-i18n';

import { useFormStore } from '~/store/form';
Expand All @@ -19,6 +19,18 @@ let timeout = null;

const { isRTL } = storeToRefs(useFormStore());

const TITLE = computed(() =>
properties.notification?.translate
? t(properties.notification.title)
: properties.notification.title
);

const TEXT = computed(() =>
properties.notification?.translate
? t(properties.notification.text)
: properties.notification.text
);

function alertClosed() {
const notificationStore = useNotificationStore();
notificationStore.deleteNotification(properties.notification);
Expand Down Expand Up @@ -58,12 +70,14 @@ onBeforeMount(() => {
);
}
const notificationStore = useNotificationStore();
timeout = setTimeout(
() => notificationStore.deleteNotification(properties.notification),
properties.notification.timeout
? properties.notification.timeout * 1000
: 10000
);
if (!properties.notification.retain) {
timeout = setTimeout(
() => notificationStore.deleteNotification(properties.notification),
properties.notification.timeout
? properties.notification.timeout * 1000
: 10000
);
}
});

onBeforeUnmount(() => {
Expand All @@ -84,8 +98,8 @@ onBeforeUnmount(() => {
prominent
closable
class="mb-3"
:title="notification.title"
:text="$t(notification.text)"
:title="TITLE"
:text="TEXT"
@update:model-value="alertClosed"
></v-alert>
</template>
Expand All @@ -94,4 +108,12 @@ onBeforeUnmount(() => {
.target-notification :deep(.v-alert__icon.v-icon):after {
display: none;
}

.v-alert {
color: white !important;
}

.v-alert-title {
font-weight: bold !important;
}
</style>
126 changes: 106 additions & 20 deletions app/frontend/src/components/designer/FormViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ const json_csv = ref({
});
const loadingSubmission = ref(false);
const permissions = ref([]);
const queuedDeleteFiles = ref([]);
const queuedUploadFiles = ref([]);
const reRenderFormIo = ref(0);
const saveDraftDialog = ref(false);
const saveDraftState = ref(0);
Expand Down Expand Up @@ -798,7 +800,7 @@ async function saveDraft() {
try {
saving.value = true;

const response = await sendSubmission(true, submission.value);
const response = await sendSubmission(true);
if (properties.submissionId && properties.submissionId !== null) {
// Editing an existing draft
// Update this route with saved flag
Expand Down Expand Up @@ -833,15 +835,20 @@ async function saveDraft() {
}
}

async function sendSubmission(isDraft, sub) {
sub.data.lateEntry =
async function sendSubmission(isDraft) {
const uploadError = await uploadQueuedFiles();
const deleteError = await deleteQueuedFiles();

if (uploadError || deleteError) return;

submission.value.data.lateEntry =
form.value?.schedule?.expire !== undefined &&
form.value.schedule.expire === true
? form.value.schedule.allowLateSubmissions
: false;
const body = {
draft: isDraft,
submission: sub,
submission: submission.value,
};

let response;
Expand Down Expand Up @@ -933,14 +940,14 @@ async function onBeforeSubmit(submission, next) {

// FormIO submit event
// eslint-disable-next-line no-unused-vars
async function onSubmit(sub) {
async function onSubmit() {
if (properties.preview) {
alert(t('trans.formViewer.submissionsPreviewAlert'));
confirmSubmit.value = false;
return;
}

const errors = await doSubmit(sub);
const errors = await doSubmit();

// if we are here, the submission has been saved to our db
// the passed in submission is the formio submission, not our db persisted submission record...
Expand All @@ -958,12 +965,12 @@ async function onSubmit(sub) {
}

// Not a formIO event, our saving routine to POST the submission to our API
async function doSubmit(sub) {
async function doSubmit() {
// since we are not using formio api
// we should do the actual submit here, and return any error that occurrs to handle in the submit event
let errMsg = undefined;
try {
const response = await sendSubmission(false, sub);
const response = await sendSubmission(false);

if ([200, 201].includes(response.status)) {
// all is good, flag no errors and carry on...
Expand Down Expand Up @@ -1059,18 +1066,18 @@ function leaveThisPage() {
}
}

function yes() {
saveDraftFromModal(true);
async function yes() {
await saveDraftFromModal(true);
}

function no() {
saveDraftFromModal(false);
async function no() {
await saveDraftFromModal(false);
}

function saveDraftFromModal(event) {
async function saveDraftFromModal(event) {
doYouWantToSaveTheDraft.value = false;
if (event) {
saveDraftFromModalNow();
await saveDraftFromModalNow();
} else {
leaveThisPage();
}
Expand All @@ -1080,7 +1087,7 @@ function saveDraftFromModal(event) {
async function saveDraftFromModalNow() {
try {
saving.value = true;
await sendSubmission(true, submission.value);
await sendSubmission(true);
saving.value = false;
// Creating a new submission in draft state
// Go to the user form draft page
Expand Down Expand Up @@ -1108,9 +1115,11 @@ function beforeWindowUnload(e) {
}
}

async function deleteFile(file) {
const fileId = file?.data?.id ? file.data.id : file?.id ? file.id : undefined;
return fileService.deleteFile(fileId);
function deleteFile(file, cfg) {
queuedDeleteFiles.value.push({
file: file,
onSuccess: cfg.onSuccess,
});
}

async function getFile(fileId, options = {}) {
Expand Down Expand Up @@ -1150,8 +1159,85 @@ async function getFile(fileId, options = {}) {
}
}

async function uploadFile(file, config = {}) {
return fileService.uploadFile(file, config);
function uploadFile(file, cfg = {}) {
queuedUploadFiles.value.push({
file: file,
config: {
onUploadProgress: cfg.onUploadProgress,
headers: cfg.headers,
},
onUploaded: cfg.onUploaded,
onError: cfg.onError,
});

notificationStore.addNotification({
...NotificationTypes.WARNING,
title: 'trans.alert.warning',
text: 'trans.formViewer.fileUploadWarning',
retain: true,
unique: true,
translate: true,
});
}

async function uploadQueuedFiles() {
let err = false;
for (let i = queuedUploadFiles.value.length - 1; i >= 0; i--) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious: is there a reason to upload them in reverse order? If I add file 0, 1, and 2, my submission shows file 2, 1, and 0. Might the users find this to be unexpected behaviour?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reasoning for this was because it was uploading files from the array, and when successful, would remove those files from the array. That way when running the upload file function again, it would just run through the array until it is empty.

let response;
try {
response = await fileService.uploadFile(
queuedUploadFiles.value[i].file,
queuedUploadFiles.value[i].config
);
queuedUploadFiles.value[i].onUploaded(response);
queuedUploadFiles.value.splice(i, 1);
} catch (error) {
err = true;
queuedUploadFiles.value[i].onError({
detail: error?.message ? error.message : error,
});
notificationStore.addNotification({
text: t('trans.formViewer.errorSavingFile', {
fileName: queuedUploadFiles.value[i].file.originalName,
error: error,
}),
consoleError: t('trans.formViewer.errorSavingFile', {
fileName: queuedUploadFiles.value[i].file.originalName,
error: error,
}),
});
}
}
return err;
}

async function deleteQueuedFiles() {
let err = false;
for (let i = queuedDeleteFiles.value.length - 1; i >= 0; i--) {
const fileId = queuedDeleteFiles.value[i].file?.data?.id
? queuedDeleteFiles.value[i].file.data.id
: queuedDeleteFiles.value[i].file?.id
? queuedDeleteFiles.value[i].file.id
: undefined;
try {
await fileService.deleteFile(fileId);
queuedDeleteFiles.value[i].onSuccess();
queuedDeleteFiles.value.splice(i, 1);
} catch (error) {
err = true;
notificationStore.addNotification({
text: t('trans.formViewer.errorDeletingFile', {
fileName: queuedDeleteFiles.value[i].file.originalName,
error: error,
}),
consoleError: t('trans.formViewer.errorDeletingFile', {
fileName: queuedDeleteFiles.value[i].file.originalName,
error: error,
}),
});
}
}
return err;
}
</script>

Expand Down
18 changes: 3 additions & 15 deletions app/frontend/src/components/designer/FormViewerActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -159,23 +159,11 @@ function toggleWideLayout() {

<!-- Save a draft -->
<span v-if="canSaveDraft && draftEnabled && !bulkFile" class="ml-2">
<v-tooltip location="bottom">
<template #activator="{ props }">
<v-btn
color="primary"
icon
v-bind="props"
size="x-small"
:title="$t('trans.formViewerActions.saveAsADraft')"
@click="$emit('save-draft')"
>
<v-icon icon="mdi:mdi-content-save"></v-icon>
</v-btn>
</template>
<v-btn color="primary" variant="outlined" @click="$emit('save-draft')">
<span :lang="locale">{{
$t('trans.formViewerActions.saveAsADraft')
$t('trans.formViewerActions.saveAsDraft')
}}</span>
</v-tooltip>
</v-btn>
</span>

<!-- Go to draft edit -->
Expand Down
9 changes: 7 additions & 2 deletions app/frontend/src/internationalization/trans/chefs/ar/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -832,10 +832,12 @@
"failedResSubmissn": "فشل الرد من نقطة نهاية الإرسال. رمز الاستجابة: {status}",
"errSubmittingForm": "حدث خطأ أثناء إرسال هذا النموذج",
"errorSavingFile": "خطأ في حفظ الملفات. اسم الملف: {fileName}. خطأ: {error}",
"errorDeletingFile": "خطأ في حذف الملفات. اسم الملف: {fileName}. الخطأ: {error}",
"submittingDraftErrMsg": "حدث خطأ أثناء حفظ المسودة",
"submittingDraftConsErrMsg": "خطأ في حفظ المسودة. معرف التقديم: {submitId}. خطأ: {error}",
"formDraftAccessErrMsg": "تم إرسال طلب التقديم بالفعل ، مع إعادة التوجيه إلى صفحة العرض",
"formUnauthorizedMessage": "غير مخول لك مشاهدة هذا النموذج، يرجى الاتصال بمالك النموذج للحصول على الوصول."
"formUnauthorizedMessage": "غير مخول لك مشاهدة هذا النموذج، يرجى الاتصال بمالك النموذج للحصول على الوصول.",
"fileUploadWarning": "لتحميل المرفقات، يجب عليك إما النقر على زر \"حفظ كمسودة\" أو إرسال النموذج. إذا انتقلت بعيدًا عن هذه الشاشة، فستفقد مرفقاتك."
},
"bCGovFooter": {
"home": "بيت",
Expand Down Expand Up @@ -911,7 +913,7 @@
},
"formViewerActions": {
"viewMyDraftOrSubmissions": "عرض المسودة / التقديمات الخاصة بي",
"saveAsADraft": "احفظ كمسودة",
"saveAsDraft": "احفظ كمسودة",
"editThisDraft": "قم بتحرير هذه المسودة",
"switchSingleSubmssn": "التبديل إلى الإرسال الفردي",
"switchMultiSubmssn": "التبديل إلى تقديمات متعددة",
Expand Down Expand Up @@ -1083,5 +1085,8 @@
"history": "تاريخ",
"user": "مستخدم"
}
},
"alert": {
"warning": "تحذير"
}
}
9 changes: 7 additions & 2 deletions app/frontend/src/internationalization/trans/chefs/de/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -832,10 +832,12 @@
"failedResSubmissn": "Fehlgeschlagene Antwort vom Übermittlungsendpunkt. Antwortcode: {status}",
"errSubmittingForm": "Beim Absenden dieses Formulars ist ein Fehler aufgetreten",
"errorSavingFile": "Fehler beim Speichern der Dateien. Dateiname: {fileName}. Fehler: {error}",
"errorDeletingFile": "Fehler beim Löschen der Dateien. Dateiname: {fileName}. Fehler: {error}",
"submittingDraftErrMsg": "Beim Speichern eines Entwurfs ist ein Fehler aufgetreten",
"submittingDraftConsErrMsg": "Fehler beim Speichern des Entwurfs. Einreichungs-ID: {submissionId}. Fehler: {error}",
"formDraftAccessErrMsg": "Die angeforderte Übermittlung wurde bereits übermittelt und wird zur Ansichtsseite weitergeleitet",
"formUnauthorizedMessage": "Sie sind nicht berechtigt, dieses Formular anzusehen. Bitte kontaktieren Sie den Formulareigentümer für den Zugang."
"formUnauthorizedMessage": "Sie sind nicht berechtigt, dieses Formular anzusehen. Bitte kontaktieren Sie den Formulareigentümer für den Zugang.",
"fileUploadWarning": "Um Ihre Anhänge hochzuladen, müssen Sie entweder auf die Schaltfläche „Als Entwurf speichern“ klicken oder Ihr Formular absenden. Wenn Sie diesen Bildschirm verlassen, gehen Ihre Anhänge verloren."
},
"bCGovFooter": {
"home": "Heim",
Expand Down Expand Up @@ -911,7 +913,7 @@
},
"formViewerActions": {
"viewMyDraftOrSubmissions": "Meine Entwürfe/Einreichungen ansehen",
"saveAsADraft": "Als einen Entwurf sichern",
"saveAsDraft": "Als einen Entwurf sichern",
"editThisDraft": "Bearbeiten Sie diesen Entwurf",
"switchSingleSubmssn": "Wechseln Sie zur Einzeleinreichung",
"switchMultiSubmssn": "Wechseln Sie zu mehreren Einreichungen",
Expand Down Expand Up @@ -1083,5 +1085,8 @@
"history": "GESCHICHTE",
"user": "Benutzer"
}
},
"alert": {
"warning": "Warnung"
}
}
Loading