mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Add generation button for saved exports and improve UI flow
Introduce a Vue.js component to handle the generation of saved exports directly via a new button. Adjust related API endpoints, improve status handling, and remove redundant backend logic. Minor UI enhancements and translation updates included.
This commit is contained in:
parent
0c2508d26d
commit
180437f637
@ -53,10 +53,6 @@ final readonly class ExportGenerationController
|
|||||||
throw new AccessDeniedHttpException('Only users can download an export');
|
throw new AccessDeniedHttpException('Only users can download an export');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StoredObject::STATUS_PENDING === $exportGeneration->getStoredObject()->getStatus()) {
|
|
||||||
return new JsonResponse(['status' => 'pending']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
$this->serializer->serialize(
|
$this->serializer->serialize(
|
||||||
$exportGeneration,
|
$exportGeneration,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
|
||||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||||
import { ExportGeneration } from "ChillMainAssets/types";
|
import { ExportGeneration } from "ChillMainAssets/types";
|
||||||
|
|
||||||
@ -15,5 +14,5 @@ export const generateFromSavedExport = async (
|
|||||||
): Promise<ExportGeneration> =>
|
): Promise<ExportGeneration> =>
|
||||||
makeFetch(
|
makeFetch(
|
||||||
"POST",
|
"POST",
|
||||||
`/api/1.0/main/export/export-generation/${savedExportUuid}`
|
`/api/1.0/main/export/export-generation/create-from-saved-export/${savedExportUuid}`,
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
export function buildReturnPath(location: Location): string {
|
||||||
|
return location.pathname + location.search;
|
||||||
|
}
|
@ -22,7 +22,9 @@ const props = defineProps<AppProps>();
|
|||||||
|
|
||||||
const exportGeneration = ref<ExportGeneration | null>(null);
|
const exportGeneration = ref<ExportGeneration | null>(null);
|
||||||
|
|
||||||
const status = computed<StoredObjectStatus>(() => exportGeneration.value?.status ?? 'pending');
|
const status = computed<StoredObjectStatus>(
|
||||||
|
() => exportGeneration.value?.status ?? "pending",
|
||||||
|
);
|
||||||
const storedObject = computed<null | StoredObject>(() => {
|
const storedObject = computed<null | StoredObject>(() => {
|
||||||
if (exportGeneration.value === null) {
|
if (exportGeneration.value === null) {
|
||||||
return null;
|
return null;
|
||||||
@ -69,7 +71,7 @@ const onObjectNewStatusCallback = async function (): Promise<void> {
|
|||||||
props.exportGenerationId,
|
props.exportGenerationId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending.value) {
|
||||||
checkForReady();
|
checkForReady();
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import GenerateButton from "ChillMainAssets/vuejs/SavedExportButtons/Component/GenerateButton.vue";
|
||||||
|
|
||||||
|
interface SavedExportButtonsConfig {
|
||||||
|
savedExportUuid: string;
|
||||||
|
savedExportAlias: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<SavedExportButtonsConfig>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<generate-button
|
||||||
|
:saved-export-uuid="props.savedExportUuid"
|
||||||
|
></generate-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
@ -0,0 +1,186 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
trans,
|
||||||
|
SAVED_EXPORT_EXECUTE,
|
||||||
|
EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING_SHORT,
|
||||||
|
EXPORT_GENERATION_TOO_MANY_RETRIES,
|
||||||
|
EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT,
|
||||||
|
EXPORT_GENERATION_EXPORT_READY,
|
||||||
|
} from "translator";
|
||||||
|
import {
|
||||||
|
fetchExportGenerationStatus,
|
||||||
|
generateFromSavedExport,
|
||||||
|
} from "ChillMainAssets/lib/api/export";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { ExportGeneration } from "ChillMainAssets/types";
|
||||||
|
import { StoredObject, StoredObjectStatus } from "ChillDocStoreAssets/types";
|
||||||
|
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
||||||
|
import { useToast } from "vue-toast-notification";
|
||||||
|
import "vue-toast-notification/dist/theme-sugar.css";
|
||||||
|
|
||||||
|
interface SavedExportButtonGenerateConfig {
|
||||||
|
savedExportUuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<SavedExportButtonGenerateConfig>();
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e: "generate");
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const exportGeneration = ref<ExportGeneration | null>(null);
|
||||||
|
|
||||||
|
const status = computed<StoredObjectStatus | "inactive">(
|
||||||
|
() => exportGeneration.value?.status ?? "inactive",
|
||||||
|
);
|
||||||
|
const storedObject = computed<null | StoredObject>(() => {
|
||||||
|
if (exportGeneration.value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exportGeneration.value?.storedObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isInactive = computed<boolean>(() => status.value === "inactive");
|
||||||
|
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");
|
||||||
|
const filename = computed<string>(() => {
|
||||||
|
if (null === exportGeneration.value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${exportGeneration.value?.storedObject.title}-${exportGeneration.value?.createdAt?.datetime8601}`;
|
||||||
|
});
|
||||||
|
const externalDownloadLink = computed<string>(
|
||||||
|
() => `/fr/main/export-generation/${exportGeneration.value?.id}/wait`,
|
||||||
|
);
|
||||||
|
const classes = computed<Record<string, boolean>>(() => {
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
const buttonClasses = computed<Record<string, boolean>>(() => {
|
||||||
|
return { btn: true, "btn-outline-primary": true };
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
tryiesForReady.value < 10 ? 1500 : 5000,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExportGenerationSuccess = function (): void {
|
||||||
|
toast.success(trans(EXPORT_GENERATION_EXPORT_READY));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onObjectNewStatusCallback = async function (): Promise<void> {
|
||||||
|
if (null === exportGeneration.value) {
|
||||||
|
checkForReady();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const newExportGeneration = await fetchExportGenerationStatus(
|
||||||
|
exportGeneration.value?.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newExportGeneration.status !== exportGeneration.value) {
|
||||||
|
if (newExportGeneration.status === "ready") {
|
||||||
|
onExportGenerationSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exportGeneration.value = newExportGeneration;
|
||||||
|
|
||||||
|
if (isPending.value) {
|
||||||
|
checkForReady();
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickGenerate = async (): Promise<void> => {
|
||||||
|
emits("generate");
|
||||||
|
exportGeneration.value = await generateFromSavedExport(
|
||||||
|
props.savedExportUuid,
|
||||||
|
);
|
||||||
|
|
||||||
|
onObjectNewStatusCallback();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
v-if="isInactive"
|
||||||
|
:class="buttonClasses"
|
||||||
|
type="button"
|
||||||
|
@click="onClickGenerate"
|
||||||
|
>
|
||||||
|
<i class="fa fa-cog"></i> {{ trans(SAVED_EXPORT_EXECUTE) }}
|
||||||
|
</button>
|
||||||
|
<template v-if="isPending && isFetching">
|
||||||
|
<span class="btn">
|
||||||
|
<i class="fa fa-cog fa-spin fa-fw"></i>
|
||||||
|
<span class="pending-message">{{
|
||||||
|
trans(EXPORT_GENERATION_EXPORT_GENERATION_IS_PENDING_SHORT)
|
||||||
|
}}</span>
|
||||||
|
<a :href="externalDownloadLink" class="externalDownloadLink">
|
||||||
|
<i class="bi bi-box-arrow-up-right"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div v-if="isPending && !isFetching" :class="buttonClasses">
|
||||||
|
<span>{{ trans(EXPORT_GENERATION_TOO_MANY_RETRIES) }}</span>
|
||||||
|
</div>
|
||||||
|
<download-button
|
||||||
|
v-else-if="isReady && storedObject?.currentVersion !== null"
|
||||||
|
:classes="buttonClasses"
|
||||||
|
:stored-object="storedObject"
|
||||||
|
:at-version="storedObject?.currentVersion"
|
||||||
|
:filename="filename"
|
||||||
|
></download-button>
|
||||||
|
<div v-else-if="isFailure" :class="classes">
|
||||||
|
<span class="btn">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i>
|
||||||
|
<span class="pending-message">{{
|
||||||
|
trans(EXPORT_GENERATION_ERROR_WHILE_GENERATING_EXPORT)
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.pending-message {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.externalDownloadLink {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,13 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
|
||||||
|
import App from "./App.vue";
|
||||||
|
|
||||||
|
const buttons = document.querySelectorAll<HTMLDivElement>(
|
||||||
|
"[data-generate-export-button]",
|
||||||
|
);
|
||||||
|
|
||||||
|
buttons.forEach((button) => {
|
||||||
|
const savedExportUuid = button.dataset.savedExportUuid as string;
|
||||||
|
|
||||||
|
createApp(App, { savedExportUuid, savedExportAlias: "" }).mount(button);
|
||||||
|
});
|
@ -1,5 +1,13 @@
|
|||||||
{% extends "@ChillMain/layout.html.twig" %}
|
{% extends "@ChillMain/layout.html.twig" %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ encore_entry_link_tags('mod_saved_export_button') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ encore_entry_script_tags('mod_saved_export_button') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{{ 'saved_export.My saved exports'|trans }}{% endblock %}
|
{% block title %}{{ 'saved_export.My saved exports'|trans }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -31,6 +39,9 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
|
<li>
|
||||||
|
<div class="" data-generate-export-button data-saved-export-uuid="{{ s.saved.id|escape('html_attr') }}"></div>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
@ -39,7 +50,6 @@
|
|||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_title_and_description'|trans }}</a></li>
|
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_title_and_description'|trans }}</a></li>
|
||||||
<li><a href="{{ chill_path_add_return_path('chill_main_export_new', {'alias': s.saved.exportAlias,'from_saved': s.saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_filters_aggregators_and_execute'|trans }}</a></li>
|
<li><a href="{{ chill_path_add_return_path('chill_main_export_new', {'alias': s.saved.exportAlias,'from_saved': s.saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_filters_aggregators_and_execute'|trans }}</a></li>
|
||||||
<li><a href="{{ path('chill_main_export_generate_from_saved', { id: s.saved.id }) }}" class="dropdown-item"><i class="fa fa-cog"></i> {{ 'saved_export.execute'|trans }}</a></li>
|
|
||||||
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="dropdown-item"><i class="fa fa-trash"></i> {{ 'Delete'|trans }}</a></li>
|
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="dropdown-item"><i class="fa fa-trash"></i> {{ 'Delete'|trans }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -110,6 +110,10 @@ module.exports = function (encore, entries) {
|
|||||||
"mod_workflow_attachment",
|
"mod_workflow_attachment",
|
||||||
__dirname + "/Resources/public/vuejs/WorkflowAttachment/index",
|
__dirname + "/Resources/public/vuejs/WorkflowAttachment/index",
|
||||||
);
|
);
|
||||||
|
encore.addEntry(
|
||||||
|
"mod_saved_export_button",
|
||||||
|
__dirname + "/Resources/public/vuejs/SavedExportButtons/index.ts",
|
||||||
|
);
|
||||||
|
|
||||||
// Vue entrypoints
|
// Vue entrypoints
|
||||||
encore.addEntry(
|
encore.addEntry(
|
||||||
|
@ -718,6 +718,7 @@ notification:
|
|||||||
export:
|
export:
|
||||||
generation:
|
generation:
|
||||||
Export generation is pending: La génération de l'export est en cours
|
Export generation is pending: La génération de l'export est en cours
|
||||||
|
Export generation is pending_short: En cours
|
||||||
Come back later: Retour à l'index
|
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.
|
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
|
Error while generating export: Erreur interne lors de la génération de l'export
|
||||||
@ -791,7 +792,7 @@ saved_export:
|
|||||||
Created on %date%: Créé le %date%
|
Created on %date%: Créé le %date%
|
||||||
update_title_and_description: Modifier le titre et la description
|
update_title_and_description: Modifier le titre et la description
|
||||||
update_filters_aggregators_and_execute: Modifier les filtres et regroupements et télécharger
|
update_filters_aggregators_and_execute: Modifier les filtres et regroupements et télécharger
|
||||||
execute: Télécharger
|
execute: Générer
|
||||||
Update existing: Mettre à jour le rapport enregistré existant
|
Update existing: Mettre à jour le rapport enregistré existant
|
||||||
|
|
||||||
absence:
|
absence:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user