#14-intelligent-groups #21

Merged
jkeffects merged 15 commits from #14-intelligent-groups into main 2024-12-19 09:50:51 +00:00
7 changed files with 119 additions and 16 deletions
Showing only changes of commit 604ae30901 - Show all commits

View file

@ -64,10 +64,11 @@
</div> </div>
</template> </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 { computed, ref, watch } from "vue";
import { ChevronRightIcon, ChevronLeftIcon, XMarkIcon } from "@heroicons/vue/20/solid"; import { ChevronRightIcon, ChevronLeftIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import Spinner from "./Spinner.vue"; import Spinner from "./Spinner.vue";
import type { FieldType } from "@/types/dynamicQueries";
const props = defineProps({ const props = defineProps({
items: { type: Array<T>, default: [] }, items: { type: Array<T>, default: [] },
@ -105,8 +106,8 @@ const emit = defineEmits({
search(search: string) { search(search: string) {
return typeof search == "number"; return typeof search == "number";
}, },
clickRow(id: number) { clickRow(id: FieldType) {
return typeof id == "number"; return true;
}, },
}); });

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="flex flex-col border border-gray-300 rounded-md select-none"> <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 <div
class="p-1 border border-gray-400 bg-green-200 rounded-md" class="p-1 border border-gray-400 bg-green-200 rounded-md"
title="Abfrage starten" title="Abfrage starten"
@ -26,7 +26,10 @@
<TrashIcon class="text-gray-500 h-6 w-6 cursor-pointer" /> <TrashIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
</div> </div>
<div class="grow"></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"> <select v-model="activeQueryId" class="max-h-[34px] !py-0">
<option :value="undefined" disabled>gepeicherte Anfrage auswählen</option> <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"> <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" /> <InboxArrowDownIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
</div> </div>
</div> </div>
<div class="grow"></div> <div class="grow max-lg:hidden"></div>
<div class="flex flex-row overflow-hidden border border-gray-400 rounded-md"> <div class="flex flex-row min-w-fit overflow-hidden border border-gray-400 rounded-md">
<div <div
class="p-1" class="p-1"
:class="queryMode == 'structure' ? 'bg-gray-200' : ''" :class="queryMode == 'structure' ? 'bg-gray-200' : ''"

View file

@ -5,7 +5,7 @@
<select v-model="foreignColumn" class="w-full"> <select v-model="foreignColumn" class="w-full">
<option value="" disabled>Relation auswählen</option> <option value="" disabled>Relation auswählen</option>
<option v-for="relation in activeTable?.relations" :value="relation.column"> <option v-for="relation in activeTable?.relations" :value="relation.column">
{{ relation.column }} -> {{ relation.referencedTableName }} {{ relation.column }} -> {{ joinTableName(relation.referencedTableName) }}
</option> </option>
</select> </select>
<Table v-model="value" disable-table-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 { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
import Table from "./Table.vue"; import Table from "./Table.vue";
import { TrashIcon } from "@heroicons/vue/24/outline"; import { TrashIcon } from "@heroicons/vue/24/outline";
import { joinTableName } from "@/helpers/queryFormatter";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -79,7 +80,7 @@ export default defineComponent({
this.$emit("update:model-value", { this.$emit("update:model-value", {
...this.modelValue, ...this.modelValue,
foreignColumn: val, foreignColumn: val,
table: relTable?.referencedTableName, // TODO: use dictionary table: joinTableName(relTable?.referencedTableName ?? ""),
}); });
}, },
}, },

View 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;
}

View file

@ -1,14 +1,15 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { http } from "@/serverCom"; import { http } from "@/serverCom";
import type { TableMeta } from "../../viewmodels/admin/query.models"; 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", { export const useQueryBuilderStore = defineStore("queryBuilder", {
state: () => { state: () => {
return { return {
tableMetas: [] as Array<TableMeta>, tableMetas: [] as Array<TableMeta>,
loading: "loading" as "loading" | "fetched" | "failed", 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, totalLength: 0 as number,
loadingData: "fetched" as "loading" | "fetched" | "failed", loadingData: "fetched" as "loading" | "fetched" | "failed",
queryError: "" as string | { sql: string; code: string; msg: string }, queryError: "" as string | { sql: string; code: string; msg: string },
@ -43,7 +44,10 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
}) })
.then((result) => { .then((result) => {
if (result.data.stats == "success") { 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.totalLength = result.data.total;
this.loadingData = "fetched"; this.loadingData = "fetched";
} else { } else {
@ -62,5 +66,28 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
this.queryError = ""; this.queryError = "";
this.loadingData = "fetched"; 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);
},
}, },
}); });

View file

@ -54,6 +54,10 @@ export type OrderByStructure = {
export type OrderByType = "ASC" | "DESC"; export type OrderByType = "ASC" | "DESC";
export type QueryResult = {
[key: string]: FieldType | QueryResult | Array<QueryResult>;
};
export const whereOperationArray = [ export const whereOperationArray = [
"eq", "eq",
"neq", "neq",
@ -72,3 +76,12 @@ export const whereOperationArray = [
"endsWith", "endsWith",
"timespanEq", "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",
};

View file

@ -13,7 +13,7 @@
@query:run="sendQuery" @query:run="sendQuery"
@query:save="triggerSave" @query:save="triggerSave"
@results:clear="clearResults" @results:clear="clearResults"
@results:export="" @results:export="exportData"
/> />
<p>Ergebnisse:</p> <p>Ergebnisse:</p>
<div <div
@ -47,7 +47,7 @@
:indicateLoading="loadingData == 'loading'" :indicateLoading="loadingData == 'loading'"
@load-data="(offset, count) => sendQuery(offset, count)" @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> <p>{{ row }}</p>
</template> </template>
</Pagination> </Pagination>
@ -63,7 +63,7 @@ import MainTemplate from "@/templates/Main.vue";
import Pagination from "@/components/Pagination.vue"; import Pagination from "@/components/Pagination.vue";
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder"; import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
import BuilderHost from "../../../../components/queryBuilder/BuilderHost.vue"; 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"; import { useQueryStoreStore } from "@/stores/admin/queryStore";
</script> </script>
@ -77,7 +77,7 @@ export default defineComponent({
this.fetchTableMetas(); this.fetchTableMetas();
}, },
methods: { methods: {
...mapActions(useQueryBuilderStore, ["fetchTableMetas", "sendQuery", "clearResults"]), ...mapActions(useQueryBuilderStore, ["fetchTableMetas", "sendQuery", "clearResults", "exportData"]),
...mapActions(useQueryStoreStore, ["triggerSave"]), ...mapActions(useQueryStoreStore, ["triggerSave"]),
}, },
}); });