Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions backend/app/api/v1/routes/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def create_event_route(event: EventCreate, db: Session = Depends(get_db)):
@router.get(
"/{event_id}",
response_model=EventOut,
response_model_exclude_none=True,
summary="Get event by ID",
description="Return a single event by its ID. Includes tags and fields.",
responses={
Expand Down
7 changes: 6 additions & 1 deletion backend/app/modules/events/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@


def create_event(db: Session, event: schemas.EventCreate):
db_event = models.Event(name=event.name, description=event.description)
db_event = models.Event(
name=event.name,
description=event.description,
links=[link.model_dump() for link in event.links] if event.links else [],
)
db.add(db_event)
db.commit()
db.refresh(db_event)
Expand Down Expand Up @@ -44,6 +48,7 @@ def update_event(db: Session, event_id: int, event: schemas.EventCreate):

db_event.name = event.name
db_event.description = event.description
db_event.links = [link.model_dump() for link in event.links] if event.links else []

if event.tags is not None:
db.query(models.EventTag).filter(
Expand Down
3 changes: 3 additions & 0 deletions backend/app/modules/events/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class LinkType(str, Enum):
figma = "figma"
miro = "miro"
confluence = "confluence"
jira = "jira"
notion = "notion"
loom = "loom"
slack = "slack"
Expand All @@ -24,6 +25,8 @@ class EventLink(BaseModel):
url: str
label: Optional[str] = None

model_config = ConfigDict(exclude_none=False)


class EventBase(BaseModel):
name: str
Expand Down
10 changes: 5 additions & 5 deletions backend/app/modules/fields/crud.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from sqlalchemy import func
from sqlalchemy.orm import Session

from app.shared.models import EventField

from . import models, schemas


Expand Down Expand Up @@ -37,9 +39,7 @@ def update_field(db: Session, field_id: int, field: schemas.FieldCreate):
def delete_field(db: Session, field_id: int):
db_field = db.query(models.Field).filter(models.Field.id == field_id).first()
if db_field:
db.query(models.EventField).filter(
models.EventField.field_id == field_id
).delete()
db.query(EventField).filter(EventField.field_id == field_id).delete()
db.delete(db_field)
db.commit()
return db_field
Expand All @@ -48,8 +48,8 @@ def delete_field(db: Session, field_id: int):

def get_field_event_count(db: Session, field_id: int):
return (
db.query(func.count(models.EventField.event_id))
.filter(models.EventField.field_id == field_id)
db.query(func.count(EventField.event_id))
.filter(EventField.field_id == field_id)
.scalar()
)

Expand Down
4 changes: 3 additions & 1 deletion backend/app/modules/tags/crud.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from sqlalchemy.orm import Session

from app.shared.models import EventTag

from . import models, schemas


Expand Down Expand Up @@ -34,7 +36,7 @@ def update_tag(db: Session, tag_id: str, tag: schemas.TagCreate):
def delete_tag(db: Session, tag_id: str):
db_tag = db.query(models.Tag).filter(models.Tag.id == tag_id).first()
if db_tag:
db.query(models.EventTag).filter(models.EventTag.tag_id == tag_id).delete()
db.query(EventTag).filter(EventTag.tag_id == tag_id).delete()
db.delete(db_tag)
db.commit()
return db_tag
Expand Down
19 changes: 16 additions & 3 deletions frontend/src/modules/events/components/EventDetailsCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import DetailsCardAttribute from '@/shared/components/layout/DetailsCardAttribut
import { getEventFieldsColumns } from '@/modules/events/components/eventFieldsColumns'
import TagScrollArea from '@/modules/tags/components/TagScrollArea.vue'
import { useEventExample } from '@/modules/events/composables/useEventExample'
import EventLinks from '@/modules/events/components/EventLinks.vue'

const props = defineProps<{
event: Event
}>()

const eventExample = useEventExample(props.event)
const eventExample = useEventExample(props.event.fields)

const emit = defineEmits<{
(e: 'edit'): void
Expand Down Expand Up @@ -61,7 +62,7 @@ const columns = getEventFieldsColumns()
<!-- Tags -->
<DetailsCardAttribute v-if="event.tags.length > 0" icon="ph:tag" label="Tags">
<template #value>
<TagScrollArea class="max-w-lg">
<TagScrollArea class="-ml-2 max-w-lg">
<Badge
v-for="tag in event.tags"
:key="tag.id"
Expand All @@ -75,11 +76,23 @@ const columns = getEventFieldsColumns()
</DetailsCardAttribute>

<!-- Example -->
<DetailsCardAttribute icon="radix-icons:file-text" label="Example">
<DetailsCardAttribute class="hidden sm:flex" icon="radix-icons:file-text" label="Example">
<template #value>
<JsonPreview :value="eventExample" />
</template>
</DetailsCardAttribute>

<!-- Links -->
<DetailsCardAttribute
v-if="event.links && event.links.length > 0"
class="hidden sm:flex"
icon="radix-icons:link-2"
label="Links"
>
<template #value>
<EventLinks :links="event.links" />
</template>
</DetailsCardAttribute>
</template>

<template #content>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/modules/events/components/EventEditModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ onMounted(() => {

<template>
<Dialog :open="open" @update:open="onClose">
<DialogContent>
<DialogContent class="max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Event</DialogTitle>
<DialogDescription v-if="description">
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/modules/events/components/EventForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { computed, ref, watchEffect } from 'vue'
import { eventSchema, type EventFormValues } from '@/modules/events/validation/eventSchema'
import Skeleton from '@/shared/ui/skeleton/Skeleton.vue'
import LinkedFieldsSelector from '@/modules/fields/components/LinkedFieldsSelector.vue'
import EventLinksFormField from '@/modules/events/components/EventLinksFormField.vue'

const props = defineProps<{
event?: Event
Expand Down Expand Up @@ -71,6 +72,11 @@ watchEffect(() => {
description: props.event.description ?? '',
fields: props.event.fields?.map(f => f.id) ?? [],
tags: props.event.tags?.map(t => t.id) ?? [],
links:
props.event.links?.map(link => ({
...link,
label: link.label ?? '',
})) ?? [],
})
}
})
Expand Down Expand Up @@ -240,6 +246,20 @@ function removeTag(tagId: string) {
</FormItem>
</FormField>

<!-- Links -->
<FormField name="links" v-slot="{ componentField }">
<FormItem>
<FormLabel>External Links</FormLabel>
<FormControl>
<EventLinksFormField
:model-value="componentField.modelValue"
@update:model-value="componentField.onChange"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>

<div class="flex justify-end">
<Button type="submit" :disabled="isLoading">
{{ buttonText || 'Create Event' }}
Expand Down
47 changes: 47 additions & 0 deletions frontend/src/modules/events/components/EventLinks.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script setup lang="ts">
import type { EventLink } from '@/modules/events/types'
import { Icon } from '@iconify/vue'

defineProps<{
links: EventLink[]
}>()

const iconMap: Record<string, string> = {
figma: 'simple-icons:figma',
miro: 'simple-icons:miro',
confluence: 'simple-icons:confluence',
jira: 'simple-icons:jira',
notion: 'simple-icons:notion',
loom: 'simple-icons:loom',
slack: 'simple-icons:slack',
google: 'simple-icons:googledrive',
other: 'radix-icons:external-link',
}

function getLabel(link: EventLink): string {
if (link.label) return link.label
try {
return new URL(link.url).hostname
} catch {
return link.url
}
}
</script>

<template>
<div class="flex flex-wrap items-center gap-3">
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
<template v-for="(link, i) in links" :key="i">
<a
:href="link.url"
target="_blank"
rel="noopener noreferrer"
class="hover:text-primary flex items-center gap-1 underline underline-offset-2 transition"
:id="`event-link-${i}`"
>
<Icon :icon="iconMap[link.type]" class="h-4 w-4" />
<span>{{ getLabel(link) }}</span>
</a>
</template>
</div>
</template>
90 changes: 90 additions & 0 deletions frontend/src/modules/events/components/EventLinksFormField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<script setup lang="ts">
import { Input } from '@/shared/ui/input'
import { Button } from '@/shared/ui/button'
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/shared/ui/select'
import { EventLinkType, type EventLink } from '@/modules/events/types'
import { Icon } from '@iconify/vue'

const props = defineProps<{
modelValue: EventLink[]
}>()

const emit = defineEmits<{
(e: 'update:modelValue', val: EventLink[]): void
}>()

function update(index: number, patch: Partial<EventLink>) {
const updated = [...props.modelValue]
updated[index] = { ...updated[index], ...patch }
emit('update:modelValue', updated)
}

function addLink() {
emit('update:modelValue', [
...props.modelValue,
{ type: EventLinkType.Other, url: '', label: '' },
])
}

function removeLink(index: number) {
const updated = [...props.modelValue]
updated.splice(index, 1)
emit('update:modelValue', updated)
}
</script>

<template>
<div class="space-y-2">
<div
v-for="(link, i) in modelValue"
:key="i"
class="grid grid-cols-[auto_1fr_1fr_auto] items-center gap-2"
>
<!-- Link Type -->
<Select
:model-value="link.type"
@update:model-value="val => update(i, { type: val as EventLinkType })"
>
<SelectTrigger class="w-[90px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="type in Object.values(EventLinkType)" :key="type" :value="type">
{{ type }}
</SelectItem>
</SelectContent>
</Select>

<!-- URL -->
<Input
type="url"
placeholder="https://..."
:model-value="link.url"
@update:model-value="val => update(i, { url: String(val) })"
/>

<!-- Label -->
<Input
placeholder="Label (optional)"
:model-value="link.label"
@update:model-value="val => update(i, { label: String(val) })"
/>

<!-- Remove -->
<Button
type="button"
variant="ghost"
size="icon"
class="text-muted-foreground hover:text-destructive h-8 w-8 shrink-0"
@click="removeLink(i)"
>
<Icon icon="radix-icons:cross-2" class="h-4 w-4" />
</Button>
</div>

<Button type="button" variant="outline" size="sm" @click="addLink">
<Icon icon="radix-icons:plus" class="mr-1 h-4 w-4" />
Add link
</Button>
</div>
</template>
11 changes: 4 additions & 7 deletions frontend/src/modules/events/composables/useEventExample.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import type { Event } from '@/modules/events/types'
import type { JsonValue } from '@/modules/fields/types'
import type { Field, JsonValue } from '@/modules/fields/types'
import { FieldType } from '@/modules/fields/types'
import { computed } from 'vue'

const fallbackExamples: Record<FieldType, JsonValue> = {
[FieldType.STRING]: '',
[FieldType.INTEGER]: 0,
[FieldType.NUMBER]: 0.0,
[FieldType.BOOLEAN]: false,
[FieldType.BOOLEAN]: true,
[FieldType.ARRAY]: [],
[FieldType.OBJECT]: {},
}

export function useEventExample(event: Event) {
export function useEventExample(fields: Field[]) {
return computed(() => {
const example: Record<string, JsonValue> = {}

for (const field of event.fields) {
for (const field of fields) {
example[field.name] = field.example ?? fallbackExamples[field.field_type]
}

return example
})
}
1 change: 1 addition & 0 deletions frontend/src/modules/events/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export enum EventLinkType {
Figma = 'figma',
Miro = 'miro',
Confluence = 'confluence',
Jira = 'jira',
Notion = 'notion',
Loom = 'loom',
Slack = 'slack',
Expand Down
Loading