Join Table Name & formatted results with export
This commit is contained in:
parent
9206a657c1
commit
604ae30901
7 changed files with 119 additions and 16 deletions
|
@ -64,10 +64,11 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T extends { id: number }">
|
||||
<script setup lang="ts" generic="T extends { id: FieldType }">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { ChevronRightIcon, ChevronLeftIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||
import Spinner from "./Spinner.vue";
|
||||
import type { FieldType } from "@/types/dynamicQueries";
|
||||
|
||||
const props = defineProps({
|
||||
items: { type: Array<T>, default: [] },
|
||||
|
@ -105,8 +106,8 @@ const emit = defineEmits({
|
|||
search(search: string) {
|
||||
return typeof search == "number";
|
||||
},
|
||||
clickRow(id: number) {
|
||||
return typeof id == "number";
|
||||
clickRow(id: FieldType) {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="flex flex-col border border-gray-300 rounded-md select-none">
|
||||
<div class="flex flex-row gap-2 border-b border-gray-300 p-2">
|
||||
<div class="flex flex-row max-lg:flex-wrap gap-2 border-b border-gray-300 p-2">
|
||||
<div
|
||||
class="p-1 border border-gray-400 bg-green-200 rounded-md"
|
||||
title="Abfrage starten"
|
||||
|
@ -26,7 +26,10 @@
|
|||
<TrashIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<div v-if="allowPredefinedSelect && can('read', 'settings', 'query_store')" class="flex flex-row gap-2">
|
||||
<div
|
||||
v-if="allowPredefinedSelect && can('read', 'settings', 'query_store')"
|
||||
class="flex flex-row gap-2 max-lg:w-full max-lg:order-10"
|
||||
>
|
||||
<select v-model="activeQueryId" class="max-h-[34px] !py-0">
|
||||
<option :value="undefined" disabled>gepeicherte Anfrage auswählen</option>
|
||||
<option v-for="query in queries" :key="query.id" :value="query.id" @click="value = query.query">
|
||||
|
@ -42,8 +45,8 @@
|
|||
<InboxArrowDownIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<div class="flex flex-row overflow-hidden border border-gray-400 rounded-md">
|
||||
<div class="grow max-lg:hidden"></div>
|
||||
<div class="flex flex-row min-w-fit overflow-hidden border border-gray-400 rounded-md">
|
||||
<div
|
||||
class="p-1"
|
||||
:class="queryMode == 'structure' ? 'bg-gray-200' : ''"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<select v-model="foreignColumn" class="w-full">
|
||||
<option value="" disabled>Relation auswählen</option>
|
||||
<option v-for="relation in activeTable?.relations" :value="relation.column">
|
||||
{{ relation.column }} -> {{ relation.referencedTableName }}
|
||||
{{ relation.column }} -> {{ joinTableName(relation.referencedTableName) }}
|
||||
</option>
|
||||
</select>
|
||||
<Table v-model="value" disable-table-select />
|
||||
|
@ -24,6 +24,7 @@ import type { DynamicQueryStructure } from "../../types/dynamicQueries";
|
|||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
import Table from "./Table.vue";
|
||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import { joinTableName } from "@/helpers/queryFormatter";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -79,7 +80,7 @@ export default defineComponent({
|
|||
this.$emit("update:model-value", {
|
||||
...this.modelValue,
|
||||
foreignColumn: val,
|
||||
table: relTable?.referencedTableName, // TODO: use dictionary
|
||||
table: joinTableName(relTable?.referencedTableName ?? ""),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
58
src/helpers/queryFormatter.ts
Normal file
58
src/helpers/queryFormatter.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { joinTableFormatter, type FieldType, type QueryResult } from "../types/dynamicQueries";
|
||||
|
||||
export function joinTableName(name: string): string {
|
||||
let normalized = joinTableFormatter[name];
|
||||
return normalized ?? name;
|
||||
}
|
||||
|
||||
export function flattenQueryResult(result: Array<QueryResult>): Array<{ [key: string]: FieldType }> {
|
||||
function flatten(row: QueryResult, prefix: string = ""): Array<{ [key: string]: FieldType }> {
|
||||
let results: Array<{ [key: string]: FieldType }> = [{}];
|
||||
|
||||
for (const key in row) {
|
||||
const value = row[key];
|
||||
const newKey = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null)) {
|
||||
console.log(value, newKey);
|
||||
const arrayResults: Array<{ [key: string]: FieldType }> = [];
|
||||
value.forEach((item) => {
|
||||
const flattenedItems = flatten(item, newKey);
|
||||
arrayResults.push(...flattenedItems);
|
||||
});
|
||||
|
||||
const tempResults: Array<{ [key: string]: FieldType }> = [];
|
||||
results.forEach((res) => {
|
||||
arrayResults.forEach((arrRes) => {
|
||||
tempResults.push({ ...res, ...arrRes });
|
||||
});
|
||||
});
|
||||
results = tempResults;
|
||||
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
console.log(value, newKey);
|
||||
const objResults = flatten(value as QueryResult, newKey);
|
||||
const tempResults: Array<{ [key: string]: FieldType }> = [];
|
||||
results.forEach((res) => {
|
||||
objResults.forEach((objRes) => {
|
||||
tempResults.push({ ...res, ...objRes });
|
||||
});
|
||||
});
|
||||
results = tempResults;
|
||||
} else {
|
||||
results.forEach((res) => {
|
||||
res[newKey] = String(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
const flattenedResults: Array<{ [key: string]: FieldType }> = [];
|
||||
|
||||
result.forEach((item) => {
|
||||
const flattenedItems = flatten(item);
|
||||
flattenedResults.push(...flattenedItems);
|
||||
});
|
||||
|
||||
return flattenedResults;
|
||||
}
|
|
@ -1,14 +1,15 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "@/serverCom";
|
||||
import type { TableMeta } from "../../viewmodels/admin/query.models";
|
||||
import type { DynamicQueryStructure } from "../../types/dynamicQueries";
|
||||
import type { DynamicQueryStructure, FieldType } from "../../types/dynamicQueries";
|
||||
import { flattenQueryResult } from "../../helpers/queryFormatter";
|
||||
|
||||
export const useQueryBuilderStore = defineStore("queryBuilder", {
|
||||
state: () => {
|
||||
return {
|
||||
tableMetas: [] as Array<TableMeta>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
data: [] as Array<{ id: number; [key: string]: any }>,
|
||||
data: [] as Array<{ id: FieldType; [key: string]: FieldType }>,
|
||||
totalLength: 0 as number,
|
||||
loadingData: "fetched" as "loading" | "fetched" | "failed",
|
||||
queryError: "" as string | { sql: string; code: string; msg: string },
|
||||
|
@ -43,7 +44,10 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
|
|||
})
|
||||
.then((result) => {
|
||||
if (result.data.stats == "success") {
|
||||
this.data = result.data.rows;
|
||||
this.data = flattenQueryResult(result.data.rows).map((row) => ({
|
||||
id: row.id ?? "", // Ensure id is present
|
||||
...row,
|
||||
}));
|
||||
this.totalLength = result.data.total;
|
||||
this.loadingData = "fetched";
|
||||
} else {
|
||||
|
@ -62,5 +66,28 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
|
|||
this.queryError = "";
|
||||
this.loadingData = "fetched";
|
||||
},
|
||||
exportData() {
|
||||
if (this.data.length == 0) return;
|
||||
const csvString = [Object.keys(this.data[0]), ...this.data.map((d) => Object.values(d))]
|
||||
.map((e) => e.join(";"))
|
||||
.join("\n");
|
||||
|
||||
// Create a Blob from the CSV string
|
||||
const blob = new Blob([csvString], { type: "text/csv" });
|
||||
|
||||
// Create a download link
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "items.csv";
|
||||
|
||||
// Append the link to the document and trigger the download
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Clean up
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -54,6 +54,10 @@ export type OrderByStructure = {
|
|||
|
||||
export type OrderByType = "ASC" | "DESC";
|
||||
|
||||
export type QueryResult = {
|
||||
[key: string]: FieldType | QueryResult | Array<QueryResult>;
|
||||
};
|
||||
|
||||
export const whereOperationArray = [
|
||||
"eq",
|
||||
"neq",
|
||||
|
@ -72,3 +76,12 @@ export const whereOperationArray = [
|
|||
"endsWith",
|
||||
"timespanEq",
|
||||
];
|
||||
|
||||
export const joinTableFormatter: { [key: string]: string } = {
|
||||
member_awards: "memberAwards",
|
||||
membership_status: "membershipStatus",
|
||||
member_qualifications: "memberQualifications",
|
||||
member_executive_positions: "memberExecutivePositions",
|
||||
communication_type: "communicationType",
|
||||
executive_position: "executivePosition",
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
@query:run="sendQuery"
|
||||
@query:save="triggerSave"
|
||||
@results:clear="clearResults"
|
||||
@results:export=""
|
||||
@results:export="exportData"
|
||||
/>
|
||||
<p>Ergebnisse:</p>
|
||||
<div
|
||||
|
@ -47,7 +47,7 @@
|
|||
:indicateLoading="loadingData == 'loading'"
|
||||
@load-data="(offset, count) => sendQuery(offset, count)"
|
||||
>
|
||||
<template #pageRow="{ row }: { row: { id: number; [key: string]: any } }">
|
||||
<template #pageRow="{ row }: { row: { id: FieldType; [key: string]: FieldType } }">
|
||||
<p>{{ row }}</p>
|
||||
</template>
|
||||
</Pagination>
|
||||
|
@ -63,7 +63,7 @@ import MainTemplate from "@/templates/Main.vue";
|
|||
import Pagination from "@/components/Pagination.vue";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import BuilderHost from "../../../../components/queryBuilder/BuilderHost.vue";
|
||||
import type { DynamicQueryStructure } from "@/types/dynamicQueries";
|
||||
import type { DynamicQueryStructure, FieldType } from "@/types/dynamicQueries";
|
||||
import { useQueryStoreStore } from "@/stores/admin/queryStore";
|
||||
</script>
|
||||
|
||||
|
@ -77,7 +77,7 @@ export default defineComponent({
|
|||
this.fetchTableMetas();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useQueryBuilderStore, ["fetchTableMetas", "sendQuery", "clearResults"]),
|
||||
...mapActions(useQueryBuilderStore, ["fetchTableMetas", "sendQuery", "clearResults", "exportData"]),
|
||||
...mapActions(useQueryStoreStore, ["triggerSave"]),
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue