Migrate export generation flow from plain JS to Vue

Replaced the old JavaScript-based export generation logic with a Vue.js implementation to improve maintainability and modularity. Introduced a new API endpoint to fetch export status, updated the Webpack config, and integrated translations and Twig templates for the new flow. The Vue-based solution enhances user feedback and error handling during the export process.
This commit is contained in:
Julien Fastré 2025-03-13 17:03:14 +01:00
parent 80ce7f0bf1
commit bd61eedfbb
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
8 changed files with 208 additions and 15 deletions

View File

@ -1,14 +0,0 @@
import { download_report } from "../../lib/download-report/download-report";
window.addEventListener("DOMContentLoaded", function (e) {
const export_generate_url = window.export_generate_url;
if (typeof export_generate_url === "undefined") {
console.error("Alias not found!");
throw new Error("Alias not found!");
}
const query = window.location.search,
container = document.querySelector("#download_container");
download_report(export_generate_url + query.toString(), container);
});

View File

@ -0,0 +1,129 @@
<script setup lang="ts">
import {
trans,
EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING,
EXPORT_GENERATION_TOO_MANY_RETRIES,
EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT,
EXPORT_GENERATION_EXPORT_READY,
} from "translator";
import { computed, onMounted, ref } from "vue";
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
import { fetchExportGenerationStatus } from "ChillMainAssets/vuejs/DownloadExport/api";
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
interface AppProps {
exportGenerationId: string;
}
const props = defineProps<AppProps>();
const status = ref<StoredObjectStatus>("pending");
const storedObject = ref<null | StoredObject>(null);
const isPending = computed<boolean>(() => status.value === "pending");
const isFetching = computed<boolean>(
() => tryiesForReady.value < maxTryiesForReady,
);
const isReady = computed<boolean>(() => status.value === "ready");
const isFailure = computed<boolean>(() => status.value === "failure");
/**
* counter for the number of times that we check for a new status
*/
let tryiesForReady = ref<number>(0);
/**
* how many times we may check for a new status, once loaded
*/
const maxTryiesForReady = 120;
const checkForReady = function (): void {
if (
"ready" === status.value ||
"empty" === status.value ||
"failure" === status.value ||
// stop reloading if the page stays opened for a long time
tryiesForReady.value > maxTryiesForReady
) {
return;
}
tryiesForReady.value = tryiesForReady.value + 1;
setTimeout(onObjectNewStatusCallback, 5000);
};
const onObjectNewStatusCallback = async function (): Promise<void> {
let status_response = await fetchExportGenerationStatus(
props.exportGenerationId,
);
if (status_response.status === "pending") {
checkForReady();
return Promise.resolve();
}
status.value = status_response.status;
storedObject.value = status_response.stored_object;
return Promise.resolve();
};
onMounted(() => {
onObjectNewStatusCallback();
});
</script>
<template>
<div id="waiting-screen">
<div
v-if="isPending && isFetching"
class="alert alert-danger text-center"
>
<div>
<p>
{{ trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING) }}
</p>
</div>
<div>
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
<span class="sr-only">Loading...</span>
</div>
</div>
<div v-if="isPending && !isFetching" class="alert alert-info">
<div>
<p>
{{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}
</p>
</div>
</div>
<div v-if="isFailure" class="alert alert-danger text-center">
<div>
<p>
{{ trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT) }}
</p>
</div>
</div>
<div v-if="isReady" class="alert alert-success text-center">
<div>
<p>
{{ trans(EXPORT_GENERATION_EXPORT_READY) }}
</p>
<p v-if="storedObject !== null">
<document-action-buttons-group
:stored-object="storedObject"
></document-action-buttons-group>
</p>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
#waiting-screen {
> .alert {
min-height: 350px;
}
}
</style>

View File

@ -0,0 +1,14 @@
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
export const fetchExportGenerationStatus = async (
exportGenerationId: string,
): Promise<
| { status: "pending" }
| { status: StoredObjectStatus; stored_object: StoredObject }
> => {
return makeFetch(
"GET",
`/api/1.0/main/export-generation/${exportGenerationId}/object`,
);
};

View File

@ -0,0 +1,12 @@
import { createApp } from "vue";
import App from "./App.vue";
const el = document.getElementById("app");
if (null === el) {
console.error("div element app was not found");
throw new Error("div element app was not found");
}
const exportGenerationId = el?.dataset.exportGenerationId as string;
createApp(App, { exportGenerationId }).mount(el);

View File

@ -0,0 +1,23 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('page_download_exports') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('page_download_exports') }}
{% endblock %}
{% block content %}
<div id="app" data-export-generation-id="{{ exportGeneration.id | escape('html_attr') }}"></div>
<ul class="sticky-form-buttons record_actions">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_main_export_index') }}" class="btn btn-cancel">
{{ 'export.generation.Come back later'|trans|chill_return_path_label }}
</a>
</li>
</ul>
{% endblock content %}

View File

@ -1101,3 +1101,25 @@ paths:
204:
description: "resource was deleted successfully"
/1.0/main/export-generation/{id}/object:
get:
tags:
- export
summary: get the object status and details of an export-generation
parameters:
- name: id
in: path
required: true
description: The entity export generation id
schema:
type: string
format: uuid
responses:
403:
description: Access denied
200:
description: "ok"
content:
application/json:
schema:
type: object

View File

@ -34,7 +34,7 @@ module.exports = function (encore, entries) {
);
encore.addEntry(
"page_download_exports",
__dirname + "/Resources/public/page/export/download-export.js",
__dirname + "/Resources/public/vuejs/DownloadExport/index.ts",
);
// Modules entrypoints
@ -120,4 +120,5 @@ module.exports = function (encore, entries) {
"vue_onthefly",
__dirname + "/Resources/public/vuejs/OnTheFly/index.js",
);
};

View File

@ -716,6 +716,12 @@ notification:
export:
generation:
Export generation is pending: La génération de l'export est en cours
Come back later: Retour à l'index
Too many retries: Le nombre de vérification de la disponibilité de l'export a échoué. Essayez de recharger la page.
Error while generating export: Erreur interne lors de la génération de l'export
Export ready: L'export est prêt à être téléchargé
address_helper:
id: Identifiant de l'adresse
street: Voie