#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>
</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;
},
});

View file

@ -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' : ''"

View file

@ -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 ?? ""),
});
},
},

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 { 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);
},
},
});

View file

@ -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",
};

View file

@ -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"]),
},
});