Skip to content

Commit

Permalink
feat: custom field positions
Browse files Browse the repository at this point in the history
  • Loading branch information
axelrindle committed Mar 24, 2024
1 parent b30d288 commit 7b4e276
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "CustomFieldPosition" AS ENUM ('PUBLIC_PERSON', 'PUBLIC_ANMELDUNG', 'INTERN_PERSON', 'INTERN_ANMELDUNG', 'INTERN_VERANSTALTUNG', 'INTERN_AUSSCHREIBUNG');

-- AlterTable
ALTER TABLE "CustomField" ADD COLUMN "positions" "CustomFieldPosition"[];
11 changes: 11 additions & 0 deletions api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,16 @@ model Activity {
metadata Json @default("{}")
}

enum CustomFieldPosition {
PUBLIC_PERSON
PUBLIC_ANMELDUNG
INTERN_PERSON
INTERN_ANMELDUNG
INTERN_VERANSTALTUNG
INTERN_AUSSCHREIBUNG
}

model CustomField {
id Int @id @default(autoincrement())
name String
Expand All @@ -327,6 +337,7 @@ model CustomField {
options String[]
role Role[]
values CustomFieldValue[]
positions CustomFieldPosition[]
veranstaltungId Int?
unterveranstaltungId Int?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CustomFieldPosition } from '@prisma/client'
import { z } from 'zod'

import { CustomFieldNames } from '../../enumMappings'
Expand All @@ -16,13 +17,14 @@ export const customFieldsVeranstaltungCreate = defineProcedure({
type: z.enum(CustomFieldNames),
required: z.boolean(),
options: z.array(z.string()),
positions: z.nativeEnum(CustomFieldPosition).array(),
}),
}),
async handler({ input }) {
return await prisma.customField.create({
data: {
veranstaltungId: input.veranstaltungId,
...input.data,
veranstaltungId: input.veranstaltungId,
},
})
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CustomFieldPosition } from '@prisma/client'
import { z } from 'zod'

import { CustomFieldNames } from '../../enumMappings'
Expand All @@ -16,6 +17,7 @@ export const customFieldsVeranstaltungUpdate = defineProcedure({
type: z.enum(CustomFieldNames),
required: z.boolean(),
options: z.array(z.string()),
positions: z.nativeEnum(CustomFieldPosition).array(),
}),
}),
async handler({ input }) {
Expand Down
52 changes: 43 additions & 9 deletions frontend/src/components/BasicInputs/BasicSelect.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/vue'
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
import { CheckIcon, ChevronDownIcon } from '@heroicons/vue/24/outline'
import useValidationModel from '../../composables/useValidationModel'
Expand All @@ -9,20 +9,40 @@ import { type BasicInputDefaultProps } from './defaultProps'
export interface Option {
label: string
description?: string
value: string | number
disabled?: boolean
}
const props = defineProps<
BasicInputDefaultProps<string | number> & {
BasicInputDefaultProps<string | number | string[] | number[]> & {
options: Option[]
// eslint-disable-next-line vue/no-unused-properties
multiple?: boolean
}
>()
const emit = defineEmits<{
(event: 'update:modelValue', eventArgs: string | number | undefined): void
}>()
const { model, errorMessage } = useValidationModel(props, emit)
function formatValue(modelValue: unknown) {
if (!modelValue) {
return props.placeholder || 'Bitte wählen...'
}
if (Array.isArray(modelValue)) {
if (modelValue.length === 0) {
return props.placeholder || 'Bitte wählen...'
}
return props.options
.filter((o) => modelValue.includes(o.value))
.map((o) => o.label)
.join(', ')
}
return modelValue
}
</script>

<template>
Expand All @@ -31,20 +51,25 @@ const { model, errorMessage } = useValidationModel(props, emit)
v-if="label"
class="font-medium"
:for="id || name || label"
>{{ label }}
>
<span>{{ label }}</span>
<span
v-if="required"
class="text-danger-600"
>*</span
></label
>
>
*
</span>
</label>
<Listbox
v-model="model"
as="div"
:name="id || name || label"
:multiple="props.multiple"
>
<ListboxButton class="input-style flex items-center justify-between">
{{ options.find((option) => option.value === modelValue)?.label || placeholder || 'Bitte wählen...' }}
<span class="text-start">
{{ formatValue(modelValue) }}
</span>
<ChevronDownIcon class="h-5 text-gray-500" />
</ListboxButton>
<div class="relative mt-1">
Expand All @@ -58,18 +83,27 @@ const { model, errorMessage } = useValidationModel(props, emit)
>
<ListboxOption
v-for="option in options"
v-slot="{ active }"
v-slot="{ active, selected }"
:key="option.value"
:value="option.value"
:disabled="option.disabled"
>
<div
:class="[
active ? 'bg-primary-600 text-white' : 'text-gray-900',
selected ? 'bg-primary-200' : '',
'relative cursor-default select-none px-3 py-2',
'flex flex-row gap-x-4 items-center',
]"
>
{{ option.label }}
<CheckIcon
class="h-4"
:class="selected ? 'text-primary-600' : 'invisible'"
/>
<div class="flex flex-col">
<span>{{ option.label }}</span>
<span class="text-xs mb-1">{{ option.description }}</span>
</div>
</div>
</ListboxOption>
</ListboxOptions>
Expand Down
34 changes: 31 additions & 3 deletions frontend/src/components/CustomFieldsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { CheckIcon, CubeTransparentIcon, XMarkIcon } from '@heroicons/vue/24/out
import { useAsyncState } from '@vueuse/core'
import { useRouter } from 'vue-router'
import Loading from './UIComponents/Loading.vue'
import { apiClient } from '@/api'
import { CustomFieldPositionMapping, CustomFieldsMapping, getEnumOptions } from '@codeanker/api'
const props = defineProps<{
veranstaltungId: number
// columns?: string[]
}>()
const { state: fields } = useAsyncState(async () => {
const { state: fields, isLoading } = useAsyncState(async () => {
return apiClient.customFields.list.query({
// filter: {
// veranstaltungId: props.veranstaltungId,
Expand All @@ -22,6 +25,19 @@ const { state: fields } = useAsyncState(async () => {
}, [])
const router = useRouter()
type Field = (typeof fields.value)[number]
function formatType(field: Field) {
return CustomFieldsMapping.find((m) => m.value === field.type)?.label
}
function formatPositions(field: Field) {
return getEnumOptions(CustomFieldPositionMapping)
.filter((o) => field.positions.includes(o.value))
.map((o) => o.label)
.join(', ')
}
</script>

<template>
Expand Down Expand Up @@ -50,6 +66,12 @@ const router = useRouter()
>
Erforderlich?
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold"
>
Positionen
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-dark-primary">
Expand All @@ -70,7 +92,7 @@ const router = useRouter()
<p class="text-xs text-gray-500">{{ field.description }}</p>
</td>
<td class="whitespace-nowrap py-5 pl-4 pr-3 text-sm">
<div>{{ field.type }}</div>
<div>{{ formatType(field) }}</div>
</td>
<td class="whitespace-nowrap py-5 pl-4 pr-3 text-sm">
<CheckIcon
Expand All @@ -82,11 +104,17 @@ const router = useRouter()
class="text-red-600 size-5"
/>
</td>
<td class="whitespace-nowrap py-5 pl-4 pr-3 text-sm">
<div>{{ formatPositions(field) }}</div>
</td>
</tr>
</tbody>
</table>
<div v-if="isLoading">
<Loading />
</div>
<div
v-if="fields.length <= 0"
v-else-if="fields.length <= 0"
class="rounded-md bg-blue-50 dark:bg-blue-950 p-4 text-blue-500"
>
<div class="flex">
Expand Down
36 changes: 31 additions & 5 deletions frontend/src/components/forms/customFields/CustomFieldsForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@
import { ChevronDownIcon, ChevronUpIcon, TrashIcon } from '@heroicons/vue/24/outline'
import { computed, ref } from 'vue'
import BasicCheckbox from '@/components/BasicInputs/BasicCheckbox.vue'
import BasicInput from '@/components/BasicInputs/BasicInput.vue'
import BasicSelect, { type Option } from '@/components/BasicInputs/BasicSelect.vue'
import BasicSwitch from '@/components/BasicInputs/BasicSwitch.vue'
import Button from '@/components/UIComponents/Button.vue'
import { CustomFields, CustomFieldsMapping, type CustomFieldType } from '@codeanker/api'
import {
CustomFieldPositionMapping,
CustomFields,
CustomFieldsMapping,
getEnumOptions,
CustomFieldPosition,
type CustomFieldType,
} from '@codeanker/api'
export interface ICustomFieldData {
name: string
description?: string
type: CustomFieldType
required: boolean
options: string[]
positions: CustomFieldPosition[]
}
const props = withDefaults(
Expand All @@ -37,6 +45,8 @@ const model = computed({
})
const typeOptions = ref<Option[]>(CustomFieldsMapping)
const positionOptions = ref(getEnumOptions(CustomFieldPositionMapping))
const field = computed(() => CustomFields.find((f) => f.name === model.value.type))
function moveOptionUp(index: number) {
Expand Down Expand Up @@ -70,12 +80,27 @@ function moveOptionDown(index: number) {
label="Beschreibung"
placeholder="Eine Beschreibung oder ein Hilfstext"
/>
<div class="col-span-full">
<BasicCheckbox
<div>
<label
for="required"
class="font-medium"
>
Erforderlich?
</label>
<BasicSwitch
v-model="model.required"
label="Erforderlich?"
name="required"
label="Soll dieses Feld verpflichtend sein?"
class="mt-2"
/>
</div>
<BasicSelect
v-model="model.positions"
label="Positionen"
:options="positionOptions"
multiple
required
/>

<div
v-if="field?.hasOptions"
Expand Down Expand Up @@ -123,6 +148,7 @@ function moveOptionDown(index: number) {
:label="`Option #${index + 1}`"
placeholder="Lorem Ipsum"
class="flex-1"
required
/>
<Button
color="danger"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const form = ref<ICustomFieldData>({
required: false,
type: 'BasicInput',
options: [],
positions: [],
})
const validationErrors = ref([])
Expand All @@ -36,6 +37,7 @@ const { execute, error, isLoading } = useAsyncState(
required: data.required,
type: data.type,
options: data.options || [],
positions: data.positions || [],
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ watch(field, () => {
required: field.value?.required ?? false,
type: (field.value?.type ?? 'BasicInput') as CustomFieldType,
options: field.value?.options ?? [],
positions: field.value?.positions ?? [],
}
})
const form = ref<ICustomFieldData>({
name: '',
description: '',
required: false,
options: [],
type: 'BasicInput',
options: [],
positions: [],
})
const validationErrors = ref([])
Expand All @@ -58,6 +60,7 @@ const {
required: data.required,
type: data.type,
options: data.options,
positions: data.positions,
},
})
Expand Down

0 comments on commit 7b4e276

Please sign in to comment.