Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "faq_categories" ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0;

-- AlterTable
ALTER TABLE "faqs" ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0;
12 changes: 7 additions & 5 deletions apps/api/prisma/schema/Faq.prisma
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
model FaqCategory {
id String @id @default(uuid(7))
name String
id String @id @default(uuid(7))
name String
sortOrder Int @default(0)
Comment thread
supermomme marked this conversation as resolved.
Outdated

unterveranstaltungId String
unterveranstaltung Unterveranstaltung @relation(fields: [unterveranstaltungId], references: [id])
Expand All @@ -11,9 +12,10 @@ model FaqCategory {
}

model Faq {
id String @id @default(uuid(7))
question String
answer String
id String @id @default(uuid(7))
question String
answer String
sortOrder Int @default(0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich würde den default auf NULL ändern um den Nutzer dazu zu bewegen, eine Reihenfolge festzulegen. Die Datenbank hat intern auch eine eigene Standard Reihenfolge meine ich.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vorher wurde beim list kein orderBy genutzt. Dadurch gab es keine garantierte Reihenfolge. (man würde denken, der nutzt ID oderso, aber dem ist nicht so. Kein orderBy = keine garantierte Reihenfolge)

Alsoo entweder wir setzen für die existierenden Daten die order. Was über eine Migration glaub ich schwer wird.
Oder wir lassen das so, da beim nächsten bearbeiten der Reihenfolge soweiso von jeder betroffenen category und faq die order entsprechend gesetzt wird.
Vom Prinzip her, sollte bei beiden Fällen egal sein, ob order im default null oder 0


categoryId String
category FaqCategory @relation(fields: [categoryId], references: [id])
Expand Down
13 changes: 13 additions & 0 deletions apps/api/src/services/faqs/faqCreateProcedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,22 @@ export const faqCreateProcedure = defineProtectedMutateProcedure({
faq: faqSchema,
}),
handler: async ({ input: { faq, unterveranstaltungId } }) => {
const [maxCategoryOrder, maxFaqOrder] = await Promise.all([

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich habe ein Reordering schon für Custom Fields gebaut, da kannst du dir das meiste abschauen. Sollte einheitlich sein.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ich seh, dass dort eine weitere Tabelle hinter "Reihenfolge ändern" steckt, wo man die via buttons nach oben und unten bewegen kann.
Ich denke das ist hier eher weniger sinnvoll, da die FAQs und Kategorien nicht in einer Tabellarisch Form dargestellt werden.

Man könnte überlegen das Drag-n-Drop feature via ein "Reihenfolge ändern" button/toggle ein/ausschalten.

prisma.faqCategory.aggregate({
where: { unterveranstaltungId },
_max: { sortOrder: true },
}),
prisma.faq.aggregate({
where: { category: { unterveranstaltungId } },
_max: { sortOrder: true },
}),
])

await prisma.faq.create({
data: {
question: faq.question,
answer: faq.answer,
sortOrder: (maxFaqOrder._max.sortOrder ?? -1) + 1,
unterveranstaltung: {
connect: {
id: unterveranstaltungId,
Expand All @@ -26,6 +38,7 @@ export const faqCreateProcedure = defineProtectedMutateProcedure({
create: {
name: faq.category,
unterveranstaltungId,
sortOrder: (maxCategoryOrder._max.sortOrder ?? -1) + 1,
},
where: {
name_unterveranstaltungId: {
Expand Down
17 changes: 15 additions & 2 deletions apps/api/src/services/faqs/faqListProcedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,37 @@ export async function listFaqs(unterveranstaltungId: string) {
},
},
},
orderBy: {
sortOrder: 'asc',
},
Comment thread
supermomme marked this conversation as resolved.
select: {
id: true,
question: true,
answer: true,
sortOrder: true,
category: {
select: {
id: true,
name: true,
sortOrder: true,
},
},
},
})

const groups = groupBy(
list.map((v) => ({ ...v, category: v.category.name })),
list.map((v) => ({
...v,
category: v.category.name,
categoryId: v.category.id,
categorySortOrder: v.category.sortOrder,
})),
({ category }) => category
)

return Object.fromEntries(Object.entries(groups).sort(([a], [b]) => a.localeCompare(b)))
return Object.fromEntries(
Object.entries(groups).sort(([, a], [, b]) => (a[0]?.categorySortOrder ?? 0) - (b[0]?.categorySortOrder ?? 0))
Comment thread
supermomme marked this conversation as resolved.
Outdated
)
}

export const faqListProcedure = defineProtectedQueryProcedure({
Expand Down
40 changes: 40 additions & 0 deletions apps/api/src/services/faqs/faqReorderProcedure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import z from 'zod'

import prisma from '../../prisma.js'
import { defineProtectedMutateProcedure } from '../../types/defineProcedure.js'

export const faqReorderProcedure = defineProtectedMutateProcedure({
key: 'reorder',
roleIds: ['ADMIN', 'GLIEDERUNG_ADMIN'],
inputSchema: z.strictObject({
faqOrder: z.array(
z.strictObject({
id: z.string().uuid(),
sortOrder: z.number().int().min(0),
categoryId: z.string().uuid(),
})
),
categoryOrder: z.array(
z.strictObject({
id: z.string().uuid(),
sortOrder: z.number().int().min(0),
})
),
}),
handler: async ({ input: { faqOrder, categoryOrder } }) => {
await prisma.$transaction([
...faqOrder.map(({ id, sortOrder, categoryId }) =>
prisma.faq.update({
where: { id },
data: { sortOrder, categoryId },
})
),
...categoryOrder.map(({ id, sortOrder }) =>
prisma.faqCategory.update({
where: { id },
data: { sortOrder },
})
),
])
Comment thread
supermomme marked this conversation as resolved.
Outdated
},
})
4 changes: 3 additions & 1 deletion apps/api/src/services/faqs/faqs.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { faqCreateProcedure } from './faqCreateProcedure.js'
import { faqDeleteProcedure } from './faqDeleteProcecure.js'
import { faqCategorySearchProcedure, faqListProcedure } from './faqListProcedure.js'
import { faqUpdateProcedure } from './faqUpdateProcedure.js'
import { faqReorderProcedure } from './faqReorderProcedure.js'

// Import Routes here - do not delete this line

Expand All @@ -12,6 +13,7 @@ export const faqsRouter = mergeRouters(
faqCategorySearchProcedure,
faqCreateProcedure,
faqUpdateProcedure,
faqDeleteProcedure
faqDeleteProcedure,
faqReorderProcedure
// Add Routes here - do not delete this line
)
3 changes: 2 additions & 1 deletion apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"vaul-vue": "^0.4.1",
"vue": "catalog:",
"vue-router": "^4.2.5",
"vue-sonner": "^1.3.0"
"vue-sonner": "^1.3.0",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@codeanker/eslint-config": "workspace:*",
Expand Down
18 changes: 17 additions & 1 deletion apps/frontend/src/views/FAQs/FAQFormModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ async function onSubmit() {
emit('success')
}

async function onDelete() {
if (!props.faq) return
await apiClient.faq.delete.mutate(props.faq.id)
modal.value?.hide()
emit('success')
}

defineExpose<ModalApi>({
show() {
modal.value?.show()
Expand Down Expand Up @@ -127,7 +134,16 @@ defineExpose<ModalApi>({
/>
</div>

<div class="flex justify-end col-span-2 mt-8">
<div class="flex justify-between col-span-2 mt-8">
<Button
v-if="isEdit"
type="button"
color="danger"
@click="onDelete"
>
Entfernen
</Button>
<span v-else />
<Button type="submit"> Speichern </Button>
</div>
</ValidateForm>
Expand Down
106 changes: 81 additions & 25 deletions apps/frontend/src/views/FAQs/FAQList.vue
Original file line number Diff line number Diff line change
@@ -1,29 +1,62 @@
<script setup lang="ts">
import { apiClient } from '@/api'
import Loading from '@/components/UIComponents/Loading.vue'
import { Bars3Icon } from '@heroicons/vue/24/outline'
import { useAsyncState } from '@vueuse/core'
import { computed, ref, useTemplateRef } from 'vue'
import { computed, ref, useTemplateRef, watch } from 'vue'
import draggable from 'vuedraggable'
import FAQFormModal, { type FAQ } from './FAQFormModal.vue'
import { toast } from 'vue-sonner'

type Props = {
unterveranstaltungId: string
}

const { unterveranstaltungId } = defineProps<Props>()

const { state, isLoading, execute } = useAsyncState(() => apiClient.faq.list.query({ unterveranstaltungId }), {})
const hasKeys = computed(() => Object.keys(state.value).length > 0)
const { state, isLoading, execute } = useAsyncState(
async () => apiClient.faq.list.query({ unterveranstaltungId }),
{},
{ resetOnExecute: false }
)

type Category = {
name: string
categoryId: string
faqs: FAQ[]
}
const categories = ref<Category[]>([])
watch(state, () => {
categories.value = Object.entries(state.value).map(([name, faqs]) => ({
name,
categoryId: faqs[0]?.categoryId ?? '',
faqs: [...faqs],
}))
})

const hasKeys = computed(() => categories.value.length > 0)

const formModal = useTemplateRef('formModal')
const editFaq = ref<FAQ>()

function openFormModal(faq?: FAQ) {
if (faq !== undefined) {
editFaq.value = faq
}
editFaq.value = faq
formModal.value?.show()
}

async function saveOrder() {
const faqOrder = categories.value.flatMap((cat) =>
cat.faqs.map((faq, index) => ({ id: faq.id, sortOrder: index, categoryId: cat.categoryId }))
)
const categoryOrder = categories.value.map((cat, index) => ({
id: cat.categoryId,
sortOrder: index,
}))
await apiClient.faq.reorder.mutate({ faqOrder, categoryOrder })
await execute()
toast.success('FAQ-Reihenfolge gespeichert')
}

defineExpose({
openFormModal,
})
Expand All @@ -37,27 +70,50 @@ defineExpose({
@success="execute"
/>

<Loading v-if="isLoading" />
<Loading v-if="isLoading && !hasKeys" />
<p v-else-if="!hasKeys">Hier wurden noch keine FAQs angelegt.</p>

<div
v-for="(list, category) in state"
<draggable
v-else
:key="category"
class="space-y-4"
v-model="categories"
item-key="categoryId"
handle=".category-handle"
class="grid grid-cols-3 gap-8 items-start"
@end="saveOrder"
>
<p class="text-gray-500 font-normal">{{ category }}</p>
<div
v-for="(faq, index) in list"
:key="index"
class="transition-all rounded-lg shadow hover:shadow-lg bg-primary-5 dark:bg-primary-950 p-2 select-none cursor-pointer"
@click="() => openFormModal(faq)"
>
<span class="font-bold">{{ faq.question }}</span>
<br />
<!-- eslint-disable vue/no-v-html -->
<div v-html="faq.answer" />
<!-- eslint-enable vue/no-v-html -->
</div>
</div>
<template #item="{ element: category }">
<div class="space-y-4 mb-6">
<div class="flex items-center gap-2">
<Bars3Icon class="category-handle w-5 h-5 text-gray-400 cursor-grab active:cursor-grabbing" />
<p class="text-gray-500 font-normal">{{ category.name }}</p>
</div>
<draggable
v-model="category.faqs"
item-key="id"
handle=".faq-handle"
group="faqs"
class="space-y-4"
@end="saveOrder"
Comment thread
supermomme marked this conversation as resolved.
>
<template #item="{ element: faq }">
<div
class="transition-all rounded-lg shadow hover:shadow-lg bg-primary-5 dark:bg-primary-950 p-2 select-none flex items-start gap-2"
>
<Bars3Icon class="faq-handle w-5 h-5 mt-0.5 text-gray-400 cursor-grab active:cursor-grabbing shrink-0" />
<div
class="cursor-pointer flex-1"
@click="() => openFormModal(faq)"
>
<span class="font-bold">{{ faq.question }}</span>
<br />
<!-- eslint-disable vue/no-v-html -->
<div v-html="faq.answer" />
<!-- eslint-enable vue/no-v-html -->
</div>
</div>
</template>
</draggable>
</div>
</template>
</draggable>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -374,16 +374,16 @@ const anmeldeLinkCreateModal = useTemplateRef('anmeldeLinkCreateModal')
</p>
</div>
<div class="flex items-center gap-x-4">
<RouterLink
<RouterLink
class="text-primary-600 flex items-center gap-x-1"
:to="{
name: 'Unterveranstaltung Custom Field erstellen',
params: { veranstaltungId: route.params.veranstaltungId },
}"
>
:to="{
name: 'Unterveranstaltung Custom Field erstellen',
params: { veranstaltungId: route.params.veranstaltungId },
}"
>
<PlusIcon class="size-4" />
<span>Neues Feld</span>
</RouterLink>
</RouterLink>
<RouterLink
class="text-primary-600 flex items-center gap-x-1"
:to="{
Expand Down Expand Up @@ -416,13 +416,11 @@ const anmeldeLinkCreateModal = useTemplateRef('anmeldeLinkCreateModal')
<div class="flex-1"></div>
<Button @click="() => faqList?.openFormModal()"> Frage anlegen </Button>
</div>
<div class="grid grid-cols-3 gap-8">
<FAQList
v-if="unterveranstaltung"
ref="faqList"
:unterveranstaltung-id="unterveranstaltung.id"
/>
</div>
<FAQList
v-if="unterveranstaltung"
ref="faqList"
:unterveranstaltung-id="unterveranstaltung.id"
/>
<hr class="my-10" />
</Tab>

Expand Down
Loading
Loading