Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions custom/ExportCsv.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const open = ref(false);
const rootRef = ref<HTMLElement | null>(null);
const allCount = ref<number | null>(null);
const filteredCount = ref<number | null>(null);
let countsPromise: Promise<void> | null = null;

defineExpose({
click: () => { toggle(); },
Expand Down Expand Up @@ -91,7 +92,7 @@ onUnmounted(() => document.removeEventListener('mousedown', handleClickOutside))
function toggle() {
open.value = !open.value;
if (open.value) {
fetchCounts();
countsPromise = fetchCounts();
}
}

Expand Down Expand Up @@ -119,7 +120,55 @@ async function fetchCounts() {

function run(select: string) {
open.value = false;
exportCsv(select);
if (props.meta?.exportBigDataset) {
startExportJob(select);
} else {
exportCsv(select);
}
}

/**
* Total is taken from counts already fetched for the dropdown labels, so the backend does not have
* to run an extra count query just to render export progress.
*/
async function getTotalForSelection(select: string): Promise<number | undefined> {
if (select === 'selected') {
return props.checkboxes?.length || 0;
}
await countsPromise;
const count = select === 'filtered' ? filteredCount.value : allCount.value;
return count ?? undefined;
}

async function startExportJob(select: string) {
inProgress.value = true;
try {
const resp = await callAdminForthApi({
path: `/plugin/${props.meta?.pluginInstanceId}/start-export-job`,
method: 'POST',
body: {
filters: select === 'filtered' ? filtersStore.getFilters() : [],
sort: filtersStore.getSort(),
selectedIds: select === 'selected' ? props.checkboxes : undefined,
totalRows: await getTotalForSelection(select),
},
});

if (!resp?.ok) {
throw new Error(resp?.error || t('Failed to start export'));
}

// registered globally by the background jobs plugin
(window as any).OpenJobInfoPopup?.(resp.jobId);
} catch (error) {
adminforth.alert({
message: error instanceof Error ? error.message : t('Export failed'),
variant: 'danger',
});
} finally {
inProgress.value = false;
adminforth.list.closeThreeDotsDropdown();
}
}

function downloadFile(data: string, filename: string) {
Expand Down
100 changes: 100 additions & 0 deletions custom/ExportCsvJobViewComponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<template>
<div class="flex flex-col gap-3 w-full">
<div class="flex items-center justify-between gap-4 text-sm text-gray-700 dark:text-gray-200">
<span>
{{ $t('Exported rows') }}:
<span class="font-semibold">{{ exportedRows.toLocaleString() }}</span>
<template v-if="totalRows">
/ {{ totalRows.toLocaleString() }}
</template>
</span>
<span v-if="fileName" class="truncate text-xs text-gray-500 dark:text-gray-400" :title="fileName">
{{ fileName }}
</span>
</div>

<p v-if="job.status === 'CANCELLED'" class="text-sm text-gray-500 dark:text-gray-400">
{{ $t('Export was cancelled, the file was not saved.') }}
</p>
<p v-else-if="job.state?.error" class="text-sm text-red-600 dark:text-red-400">
{{ job.state.error }}
</p>

<Button
v-if="fileReady && job.status !== 'CANCELLED'"
class="w-fit mx-auto my-2"
:loader="downloading"
@click="download"
>
{{ $t('Download CSV') }}
</Button>
</div>
</template>

<script setup lang="ts">
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import { Button } from '@/afcl';
import { callAdminForthApi } from '@/utils';
import adminforth from '@/adminforth';
import { useI18n } from 'vue-i18n';

const { t } = useI18n();

const props = defineProps<{
job: {
id: string;
status: string;
state?: Record<string, any>;
};
meta: Record<string, any>;
subscribeToJobStateFields: (fieldNames: string[]) => () => void;
subscribeToJobTaskFields: (taskNames: string[], fieldNames: string[]) => () => void;
getJobTasks: () => Promise<Record<string, any>[]>;

}>();

const downloading = ref(false);
let unsubscribe: (() => void) | undefined;

// component declaration is passed as `meta`, plugin meta sits one level deeper
const pluginInstanceId = computed(() => props.meta?.meta?.pluginInstanceId ?? props.meta?.pluginInstanceId);

const exportedRows = computed(() => props.job.state?.exportedRows ?? 0);
const totalRows = computed(() => props.job.state?.totalRows ?? 0);
const fileName = computed(() => props.job.state?.fileName ?? '');
const fileReady = computed(() => !!props.job.state?.fileReady);

async function download() {
downloading.value = true;
try {
const res = await callAdminForthApi({
path: `/plugin/${pluginInstanceId.value}/export-job-download-url`,
method: 'POST',
body: { jobId: props.job.id },
});
if (!res?.ok) {
throw new Error(res?.error || t('Failed to get download link'));
}
const link = document.createElement('a');
link.href = res.url;
link.download = res.fileName || 'export.csv';
link.rel = 'noopener';
link.click();
} catch (e) {
adminforth.alert({
message: e instanceof Error ? e.message : t('Failed to get download link'),
variant: 'danger',
});
} finally {
downloading.value = false;
}
}

onMounted(() => {
unsubscribe = props.subscribeToJobStateFields(['exportedRows', 'totalRows', 'fileReady', 'fileName']);
});

onBeforeUnmount(() => {
unsubscribe?.();
});
</script>
Loading