Skip to content

Commit 03990f6

Browse files
authored
Merge pull request #10 from devforth/import-loader-dialog
feat: enhance CSV import process with loading indicators and improved state management
2 parents 3a03b26 + d67b6e7 commit 03990f6

File tree

2 files changed

+48
-27
lines changed

2 files changed

+48
-27
lines changed

custom/ImportCsv.vue

+34-16
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
<template>
2-
<div @click="importCsv" class="cursor-pointer flex gap-2 items-center">
2+
<div @click="handleImportClick"
3+
:class="[
4+
'cursor-pointer flex gap-2 items-center',
5+
checkProgress ? 'opacity-50 pointer-events-none' : ''
6+
]">
37
{{$t('Import from CSV')}}
4-
5-
<svg v-if="inProgress"
6-
aria-hidden="true" class="w-4 h-4 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/><path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/></svg>
8+
</div>
9+
<div v-if="checkProgress" class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-20">
10+
<div class="flex flex-col items-center bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg">
11+
<p class="text-gray-700 dark:text-gray-300 text-sm">{{ $t('Checking data...') }}</p>
12+
<svg aria-hidden="true" class="w-4 h-4 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/><path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/></svg>
13+
</div>
714
</div>
815
<Dialog ref="confirmDialog" :header="t('Import Confirmation')" :buttons="computedButtons">
916
<div v-if="importStats">
@@ -29,11 +36,17 @@
2936
<p>{{ $t('Would you like to proceed?') }}</p>
3037
</template>
3138
</div>
39+
<div v-if="importProgress" class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-20">
40+
<div class="flex flex-col items-center bg-white dark:bg-gray-800 p-6 rounded-lg shadow-lg">
41+
<p class="text-gray-700 dark:text-gray-300 text-sm">{{ $t('Importing...') }}</p>
42+
<svg aria-hidden="true" class="w-4 h-4 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/><path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/></svg>
43+
</div>
44+
</div>
3245
</Dialog>
3346
</template>
3447

3548
<script setup lang="ts">
36-
import { ref, Ref, computed } from 'vue';
49+
import { ref, Ref, computed, watch } from 'vue';
3750
import { callAdminForthApi } from '@/utils';
3851
import adminforth from '@/adminforth';
3952
import Papa from 'papaparse';
@@ -42,7 +55,8 @@ import { useI18n } from 'vue-i18n';
4255
4356
const { t } = useI18n();
4457
45-
const inProgress: Ref<boolean> = ref(false);
58+
const importProgress: Ref<boolean> = ref(false);
59+
const checkProgress: Ref<boolean> = ref(false);
4660
const confirmDialog = ref(null);
4761
const importStats = ref(null);
4862
const pendingData = ref(null);
@@ -64,33 +78,35 @@ const computedButtons = computed(() => {
6478
return buttons.filter(button => button.visible !== false);
6579
});
6680
async function confirmImport(dialog) {
67-
dialog.hide();
6881
await postData(pendingData.value);
82+
dialog.hide();
6983
}
7084
7185
async function checkRecords(data: Record<string, string[]>) {
86+
checkProgress.value = true;
7287
const resp = await callAdminForthApi({
7388
path: `/plugin/${props.meta.pluginInstanceId}/check-records`,
7489
method: 'POST',
7590
body: { data }
7691
});
77-
92+
checkProgress.value = false;
7893
return resp;
7994
}
8095
8196
async function confirmImportNewOnly(dialog) {
82-
dialog.hide();
8397
await postDataNewOnly(pendingData.value);
98+
dialog.hide();
8499
}
85100
86101
async function postData(data: Record<string, string[]>, skipDuplicates: boolean = false) {
102+
importProgress.value = true;
87103
const resp = await callAdminForthApi({
88104
path: `/plugin/${props.meta.pluginInstanceId}/import-csv`,
89105
method: 'POST',
90106
body: { data }
91107
});
92108
93-
inProgress.value = false;
109+
importProgress.value = false;
94110
95111
if (resp.importedCount > 0 || resp.updatedCount > 0) {
96112
adminforth.list.refresh();
@@ -106,13 +122,14 @@ async function postData(data: Record<string, string[]>, skipDuplicates: boolean
106122
}
107123
108124
async function postDataNewOnly(data: Record<string, string[]>) {
125+
importProgress.value = true;
109126
const resp = await callAdminForthApi({
110127
path: `/plugin/${props.meta.pluginInstanceId}/import-csv-new-only`,
111128
method: 'POST',
112129
body: { data }
113130
});
114131
115-
inProgress.value = false;
132+
importProgress.value = false;
116133
if (resp.importedCount > 0) {
117134
adminforth.list.refresh();
118135
}
@@ -125,18 +142,15 @@ async function postDataNewOnly(data: Record<string, string[]>) {
125142
}
126143
127144
async function importCsv() {
128-
inProgress.value = false;
129145
const fileInput = document.createElement('input');
130146
131147
fileInput.type = 'file';
132148
fileInput.accept = '.csv';
133149
fileInput.click();
134150
fileInput.onchange = async (e) => {
135-
inProgress.value = true;
136151
137152
const file = (e.target as HTMLInputElement).files?.[0];
138153
if (!file) {
139-
inProgress.value = false;
140154
return;
141155
}
142156
const reader = new FileReader();
@@ -177,7 +191,6 @@ async function importCsv() {
177191
const stats = await checkRecords(data);
178192
importStats.value = stats;
179193
confirmDialog.value?.open();
180-
inProgress.value = false;
181194
},
182195
error: (error) => {
183196
adminforth.alert({
@@ -188,7 +201,6 @@ async function importCsv() {
188201
}
189202
});
190203
} catch (error) {
191-
inProgress.value = false;
192204
adminforth.alert({
193205
message: `Error processing CSV: ${error.message}`,
194206
variant: 'danger'
@@ -198,4 +210,10 @@ async function importCsv() {
198210
reader.readAsText(file);
199211
};
200212
}
213+
function handleImportClick() {
214+
console.log('handleImportClick', checkProgress.value);
215+
if (!checkProgress.value) {
216+
importCsv();
217+
}
218+
}
201219
</script>

index.ts

+14-11
Original file line numberDiff line numberDiff line change
@@ -212,35 +212,38 @@ export default class ImportExport extends AdminForthPlugin {
212212
path: `/plugin/${this.pluginInstanceId}/check-records`,
213213
noAuth: true,
214214
handler: async ({ body }) => {
215-
const { data } = body;
216-
215+
const { data } = body as { data: Record<string, unknown[]> };
217216
const primaryKeyColumn = this.resourceConfig.columns.find(col => col.primaryKey);
218-
219-
const rows = [];
220217
const columns = Object.keys(data);
221218
const columnValues = Object.values(data);
222-
for (let i = 0; i < (columnValues[0] as any[]).length; i++) {
219+
220+
const rows = Array.from({ length: columnValues[0].length }, (_, i) => {
223221
const row = {};
224222
for (let j = 0; j < columns.length; j++) {
225223
row[columns[j]] = columnValues[j][i];
226224
}
227-
rows.push(row);
228-
}
225+
return row;
226+
});
229227

230228
const primaryKeys = rows
231229
.map(row => row[primaryKeyColumn.name])
232230
.filter(key => key !== undefined && key !== null && key !== '');
233231

234-
const records = await this.adminforth.resource(this.resourceConfig.resourceId).list([])
235-
236-
const existingRecords = records.filter((record) => primaryKeys.includes(record[primaryKeyColumn.name]));
232+
const existingRecords = await this.adminforth
233+
.resource(this.resourceConfig.resourceId)
234+
.list([{
235+
field: primaryKeyColumn.name,
236+
operator: AdminForthFilterOperators.IN,
237+
value: primaryKeys,
238+
}]);
237239

238240
return {
239241
ok: true,
240242
total: rows.length,
241243
existingCount: existingRecords.length,
242-
newCount: rows.length - existingRecords.length
244+
newCount: rows.length - existingRecords.length,
243245
};
246+
244247
}
245248
});
246249
}

0 commit comments

Comments
 (0)