Skip to content

Commit ec0f25c

Browse files
authored
feat: textarea field type for stat blocks
* feat: textarea in stats * chore: add changeset for stat textarea
1 parent 7609bd1 commit ec0f25c

10 files changed

Lines changed: 128 additions & 60 deletions

File tree

.changeset/feat-stat-textarea.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@dm-hero/app": minor
3+
---
4+
5+
Add textarea field type to stat blocks

packages/app/app/components/locations/LocationEditDialog.vue

Lines changed: 61 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
:label="$t('locations.name')"
7272
:rules="[(v: string) => !!v || $t('locations.nameRequired')]"
7373
variant="outlined"
74-
class="mb-4"
74+
class="mt-2 mb-4"
7575
/>
7676

7777
<v-textarea
@@ -354,8 +354,8 @@ const props = defineProps<{
354354
355355
const emit = defineEmits<{
356356
'update:show': [value: boolean]
357-
saved: [location: Location]
358-
created: [location: Location]
357+
'saved': [location: Location]
358+
'created': [location: Location]
359359
}>()
360360
361361
const { t } = useI18n()
@@ -368,7 +368,7 @@ const { hasDirtyTabs, dirtyTabLabels } = useDialogDirtyStateProvider()
368368
// Internal state
369369
const internalShow = computed({
370370
get: () => props.show,
371-
set: (value) => emit('update:show', value),
371+
set: value => emit('update:show', value),
372372
})
373373
374374
const loading = ref(false)
@@ -408,48 +408,48 @@ const linkedLore = ref<LinkedLore[]>([])
408408
// Available entities for dropdowns (from store, sorted alphabetically)
409409
// Use entity_id to filter out already linked entities
410410
const availableNpcs = computed(() => {
411-
const linkedEntityIds = new Set(linkedNpcs.value.map((n) => n.entity_id))
411+
const linkedEntityIds = new Set(linkedNpcs.value.map(n => n.entity_id))
412412
return (entitiesStore.npcs || [])
413-
.filter((npc) => !linkedEntityIds.has(npc.id))
414-
.map((npc) => ({ id: npc.id, name: npc.name }))
413+
.filter(npc => !linkedEntityIds.has(npc.id))
414+
.map(npc => ({ id: npc.id, name: npc.name }))
415415
.sort((a, b) => a.name.localeCompare(b.name))
416416
})
417417
418418
const availableItems = computed(() => {
419-
const linkedEntityIds = new Set(linkedItems.value.map((i) => i.entity_id))
419+
const linkedEntityIds = new Set(linkedItems.value.map(i => i.entity_id))
420420
return (entitiesStore.items || [])
421-
.filter((item) => !linkedEntityIds.has(item.id))
422-
.map((item) => ({ id: item.id, name: item.name }))
421+
.filter(item => !linkedEntityIds.has(item.id))
422+
.map(item => ({ id: item.id, name: item.name }))
423423
.sort((a, b) => a.name.localeCompare(b.name))
424424
})
425425
426426
const availableLore = computed(() => {
427-
const linkedEntityIds = new Set(linkedLore.value.map((l) => l.entity_id))
427+
const linkedEntityIds = new Set(linkedLore.value.map(l => l.entity_id))
428428
return (entitiesStore.lore || [])
429-
.filter((lore) => !linkedEntityIds.has(lore.id))
430-
.map((lore) => ({ id: lore.id, name: lore.name }))
429+
.filter(lore => !linkedEntityIds.has(lore.id))
430+
.map(lore => ({ id: lore.id, name: lore.name }))
431431
.sort((a, b) => a.name.localeCompare(b.name))
432432
})
433433
434434
const availableParentLocations = computed(() => {
435435
if (!entitiesStore.locations) return []
436436
// Exclude current location to prevent circular reference
437437
const filtered = location.value
438-
? entitiesStore.locations.filter((loc) => loc.id !== location.value?.id)
438+
? entitiesStore.locations.filter(loc => loc.id !== location.value?.id)
439439
: entitiesStore.locations
440440
return [...filtered].sort((a, b) => a.name.localeCompare(b.name))
441441
})
442442
443443
// Item relation type suggestions from TypeScript types
444444
const itemRelationTypeSuggestions = computed(() =>
445-
LOCATION_ITEM_RELATION_TYPES.map((type) => ({
445+
LOCATION_ITEM_RELATION_TYPES.map(type => ({
446446
value: type,
447447
title: t(`locations.itemRelationTypes.${type}`),
448448
})).sort((a, b) => a.title.localeCompare(b.title)),
449449
)
450450
451451
const locationTypes = computed(() =>
452-
LOCATION_TYPES.map((type) => ({
452+
LOCATION_TYPES.map(type => ({
453453
value: type,
454454
title: t(`locations.types.${type}`, type),
455455
})).sort((a, b) => a.title.localeCompare(b.title)),
@@ -466,13 +466,13 @@ const originalImageData = ref({
466466
// Check if image-critical fields have unsaved changes
467467
const hasUnsavedImageChanges = computed(() => {
468468
const currentType = getComboboxValue(
469-
form.value.metadata.type as string | { value: string; title: string } | undefined,
469+
form.value.metadata.type as string | { value: string, title: string } | undefined,
470470
)
471471
return (
472-
form.value.name !== originalImageData.value.name ||
473-
form.value.description !== originalImageData.value.description ||
474-
currentType !== originalImageData.value.type ||
475-
form.value.metadata.region !== originalImageData.value.region
472+
form.value.name !== originalImageData.value.name
473+
|| form.value.description !== originalImageData.value.description
474+
|| currentType !== originalImageData.value.type
475+
|| form.value.metadata.region !== originalImageData.value.region
476476
)
477477
})
478478
@@ -503,11 +503,13 @@ async function loadData(locationId: number | null | undefined) {
503503
await loadLocation(locationId)
504504
await loadRelations(locationId)
505505
await loadCounts(locationId)
506-
} else {
506+
}
507+
else {
507508
// Create mode: reset form
508509
resetForm()
509510
}
510-
} finally {
511+
}
512+
finally {
511513
loading.value = false
512514
}
513515
}
@@ -533,7 +535,7 @@ async function loadLocation(locationId: number) {
533535
// Find the matching location type object for the combobox
534536
// This ensures the translated title is displayed, not the raw key
535537
const typeKey = data.metadata?.type || ''
536-
const typeObject = typeKey ? locationTypes.value.find((lt) => lt.value === typeKey) : undefined
538+
const typeObject = typeKey ? locationTypes.value.find(lt => lt.value === typeKey) : undefined
537539
538540
form.value = {
539541
name: data.name,
@@ -553,7 +555,8 @@ async function loadLocation(locationId: number) {
553555
type: typeKey || '',
554556
region: data.metadata?.region || '',
555557
}
556-
} catch (e) {
558+
}
559+
catch (e) {
557560
console.error('[LocationEditDialog] Failed to load location:', e)
558561
}
559562
}
@@ -570,7 +573,8 @@ async function loadCounts(locationId: number) {
570573
images: number
571574
}>(`/api/locations/${locationId}/counts`)
572575
counts.value = data
573-
} catch (e) {
576+
}
577+
catch (e) {
574578
console.error('[LocationEditDialog] Failed to load counts:', e)
575579
}
576580
}
@@ -602,7 +606,7 @@ async function loadRelations(locationId: number) {
602606
])
603607
604608
// Map NPCs: determine actual entity_id based on direction
605-
linkedNpcs.value = npcsRaw.map((npc) => ({
609+
linkedNpcs.value = npcsRaw.map(npc => ({
606610
id: npc.id, // relation_id
607611
entity_id: npc.direction === 'outgoing' ? npc.to_entity_id : npc.from_entity_id,
608612
name: npc.name,
@@ -611,7 +615,7 @@ async function loadRelations(locationId: number) {
611615
}))
612616
613617
// Map Items: determine actual entity_id based on direction
614-
linkedItems.value = itemsRaw.map((item) => ({
618+
linkedItems.value = itemsRaw.map(item => ({
615619
id: item.id, // relation_id
616620
entity_id: item.direction === 'outgoing' ? item.to_entity_id : item.from_entity_id,
617621
name: item.name,
@@ -621,14 +625,15 @@ async function loadRelations(locationId: number) {
621625
}))
622626
623627
// Map Lore: determine actual entity_id based on direction
624-
linkedLore.value = loreRaw.map((l) => ({
628+
linkedLore.value = loreRaw.map(l => ({
625629
id: l.id, // relation_id
626630
entity_id: l.direction === 'outgoing' ? l.to_entity_id : l.from_entity_id,
627631
name: l.name,
628632
description: l.description,
629633
image_url: l.image_url,
630634
}))
631-
} catch (e) {
635+
}
636+
catch (e) {
632637
console.error('[LocationEditDialog] Failed to load relations:', e)
633638
}
634639
}
@@ -662,7 +667,7 @@ function resetForm() {
662667
// ============================================================================
663668
// Helper: Extract value from combobox selection (can be string or {value, title} object)
664669
// ============================================================================
665-
function getComboboxValue(val: string | { value: string; title: string } | undefined): string {
670+
function getComboboxValue(val: string | { value: string, title: string } | undefined): string {
666671
if (!val) return ''
667672
if (typeof val === 'string') return val
668673
if (typeof val === 'object' && 'value' in val) return val.value
@@ -684,7 +689,7 @@ async function save() {
684689
// Extract actual values from combobox selections
685690
const metadata = {
686691
...form.value.metadata,
687-
type: getComboboxValue(form.value.metadata.type as string | { value: string; title: string } | undefined),
692+
type: getComboboxValue(form.value.metadata.type as string | { value: string, title: string } | undefined),
688693
}
689694
690695
if (location.value) {
@@ -700,13 +705,14 @@ async function save() {
700705
})
701706
702707
// Update store
703-
const index = entitiesStore.locations?.findIndex((l) => l.id === location.value!.id)
708+
const index = entitiesStore.locations?.findIndex(l => l.id === location.value!.id)
704709
if (index !== undefined && index !== -1 && entitiesStore.locations) {
705710
entitiesStore.locations[index] = { ...entitiesStore.locations[index], ...updated }
706711
}
707712
708713
emit('saved', updated)
709-
} else {
714+
}
715+
else {
710716
// Create new location
711717
const created = await $fetch<Location>('/api/locations', {
712718
method: 'POST',
@@ -726,9 +732,11 @@ async function save() {
726732
}
727733
728734
close()
729-
} catch (e) {
735+
}
736+
catch (e) {
730737
console.error('[LocationEditDialog] Failed to save:', e)
731-
} finally {
738+
}
739+
finally {
732740
saving.value = false
733741
}
734742
}
@@ -753,7 +761,7 @@ async function addNpcRelation(payload: { npcId: number }) {
753761
},
754762
})
755763
756-
const npc = entitiesStore.npcs?.find((n) => n.id === payload.npcId)
764+
const npc = entitiesStore.npcs?.find(n => n.id === payload.npcId)
757765
if (npc) {
758766
linkedNpcs.value.push({
759767
id: createdRelation.id, // relation_id from API
@@ -765,7 +773,8 @@ async function addNpcRelation(payload: { npcId: number }) {
765773
}
766774
767775
await loadCounts(location.value.id)
768-
} catch (e) {
776+
}
777+
catch (e) {
769778
console.error('[LocationEditDialog] Failed to add NPC relation:', e)
770779
}
771780
}
@@ -779,14 +788,15 @@ async function removeNpcRelation(relationId: number) {
779788
780789
try {
781790
await $fetch(`/api/entity-relations/${relationId}`, { method: 'DELETE' })
782-
linkedNpcs.value = linkedNpcs.value.filter((n) => n.id !== relationId)
791+
linkedNpcs.value = linkedNpcs.value.filter(n => n.id !== relationId)
783792
await loadCounts(location.value.id)
784-
} catch (e) {
793+
}
794+
catch (e) {
785795
console.error('[LocationEditDialog] Failed to remove NPC relation:', e)
786796
}
787797
}
788798
789-
async function addItemRelation(payload: { itemId: number; relationType?: string }) {
799+
async function addItemRelation(payload: { itemId: number, relationType?: string }) {
790800
if (!location.value || !payload.itemId) return
791801
792802
try {
@@ -801,7 +811,8 @@ async function addItemRelation(payload: { itemId: number; relationType?: string
801811
802812
await loadRelations(location.value.id)
803813
await loadCounts(location.value.id)
804-
} catch (e) {
814+
}
815+
catch (e) {
805816
console.error('[LocationEditDialog] Failed to add item relation:', e)
806817
}
807818
}
@@ -813,7 +824,8 @@ async function removeItemRelation(relationId: number) {
813824
await $fetch(`/api/entity-relations/${relationId}`, { method: 'DELETE' })
814825
await loadRelations(location.value.id)
815826
await loadCounts(location.value.id)
816-
} catch (e) {
827+
}
828+
catch (e) {
817829
console.error('[LocationEditDialog] Failed to remove item relation:', e)
818830
}
819831
}
@@ -831,7 +843,7 @@ async function addLoreRelation(loreId: number) {
831843
},
832844
})
833845
834-
const lore = entitiesStore.lore?.find((l) => l.id === loreId)
846+
const lore = entitiesStore.lore?.find(l => l.id === loreId)
835847
if (lore) {
836848
linkedLore.value.push({
837849
id: createdRelation.id, // relation_id from API
@@ -843,7 +855,8 @@ async function addLoreRelation(loreId: number) {
843855
}
844856
845857
await loadCounts(location.value.id)
846-
} catch (e) {
858+
}
859+
catch (e) {
847860
console.error('[LocationEditDialog] Failed to add lore relation:', e)
848861
}
849862
}
@@ -853,9 +866,10 @@ async function removeLoreRelation(relationId: number) {
853866
854867
try {
855868
await $fetch(`/api/entity-relations/${relationId}`, { method: 'DELETE' })
856-
linkedLore.value = linkedLore.value.filter((l) => l.id !== relationId)
869+
linkedLore.value = linkedLore.value.filter(l => l.id !== relationId)
857870
await loadCounts(location.value.id)
858-
} catch (e) {
871+
}
872+
catch (e) {
859873
console.error('[LocationEditDialog] Failed to remove lore relation:', e)
860874
}
861875
}

0 commit comments

Comments
 (0)