Skip to content

Commit 50dd499

Browse files
authored
feat: event links
* fix: examples on the cards * feat: event links in forms and cards * fix: icons * fix: hide links section if links are empty * fix: unified cross button appearance * fix: links form field improvements * fix * fix
1 parent 3fe991b commit 50dd499

15 files changed

Lines changed: 257 additions & 25 deletions

File tree

backend/app/api/v1/routes/events.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def create_event_route(event: EventCreate, db: Session = Depends(get_db)):
3939
@router.get(
4040
"/{event_id}",
4141
response_model=EventOut,
42+
response_model_exclude_none=True,
4243
summary="Get event by ID",
4344
description="Return a single event by its ID. Includes tags and fields.",
4445
responses={

backend/app/modules/events/crud.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55

66

77
def create_event(db: Session, event: schemas.EventCreate):
8-
db_event = models.Event(name=event.name, description=event.description)
8+
db_event = models.Event(
9+
name=event.name,
10+
description=event.description,
11+
links=[link.model_dump() for link in event.links] if event.links else [],
12+
)
913
db.add(db_event)
1014
db.commit()
1115
db.refresh(db_event)
@@ -44,6 +48,7 @@ def update_event(db: Session, event_id: int, event: schemas.EventCreate):
4448

4549
db_event.name = event.name
4650
db_event.description = event.description
51+
db_event.links = [link.model_dump() for link in event.links] if event.links else []
4752

4853
if event.tags is not None:
4954
db.query(models.EventTag).filter(

backend/app/modules/events/schemas.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class LinkType(str, Enum):
1212
figma = "figma"
1313
miro = "miro"
1414
confluence = "confluence"
15+
jira = "jira"
1516
notion = "notion"
1617
loom = "loom"
1718
slack = "slack"
@@ -24,6 +25,8 @@ class EventLink(BaseModel):
2425
url: str
2526
label: Optional[str] = None
2627

28+
model_config = ConfigDict(exclude_none=False)
29+
2730

2831
class EventBase(BaseModel):
2932
name: str

backend/app/modules/fields/crud.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from sqlalchemy import func
22
from sqlalchemy.orm import Session
33

4+
from app.shared.models import EventField
5+
46
from . import models, schemas
57

68

@@ -37,9 +39,7 @@ def update_field(db: Session, field_id: int, field: schemas.FieldCreate):
3739
def delete_field(db: Session, field_id: int):
3840
db_field = db.query(models.Field).filter(models.Field.id == field_id).first()
3941
if db_field:
40-
db.query(models.EventField).filter(
41-
models.EventField.field_id == field_id
42-
).delete()
42+
db.query(EventField).filter(EventField.field_id == field_id).delete()
4343
db.delete(db_field)
4444
db.commit()
4545
return db_field
@@ -48,8 +48,8 @@ def delete_field(db: Session, field_id: int):
4848

4949
def get_field_event_count(db: Session, field_id: int):
5050
return (
51-
db.query(func.count(models.EventField.event_id))
52-
.filter(models.EventField.field_id == field_id)
51+
db.query(func.count(EventField.event_id))
52+
.filter(EventField.field_id == field_id)
5353
.scalar()
5454
)
5555

backend/app/modules/tags/crud.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from sqlalchemy.orm import Session
22

3+
from app.shared.models import EventTag
4+
35
from . import models, schemas
46

57

@@ -34,7 +36,7 @@ def update_tag(db: Session, tag_id: str, tag: schemas.TagCreate):
3436
def delete_tag(db: Session, tag_id: str):
3537
db_tag = db.query(models.Tag).filter(models.Tag.id == tag_id).first()
3638
if db_tag:
37-
db.query(models.EventTag).filter(models.EventTag.tag_id == tag_id).delete()
39+
db.query(EventTag).filter(EventTag.tag_id == tag_id).delete()
3840
db.delete(db_tag)
3941
db.commit()
4042
return db_tag

frontend/src/modules/events/components/EventDetailsCard.vue

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ import DetailsCardAttribute from '@/shared/components/layout/DetailsCardAttribut
1212
import { getEventFieldsColumns } from '@/modules/events/components/eventFieldsColumns'
1313
import TagScrollArea from '@/modules/tags/components/TagScrollArea.vue'
1414
import { useEventExample } from '@/modules/events/composables/useEventExample'
15+
import EventLinks from '@/modules/events/components/EventLinks.vue'
1516
1617
const props = defineProps<{
1718
event: Event
1819
}>()
1920
20-
const eventExample = useEventExample(props.event)
21+
const eventExample = useEventExample(props.event.fields)
2122
2223
const emit = defineEmits<{
2324
(e: 'edit'): void
@@ -61,7 +62,7 @@ const columns = getEventFieldsColumns()
6162
<!-- Tags -->
6263
<DetailsCardAttribute v-if="event.tags.length > 0" icon="ph:tag" label="Tags">
6364
<template #value>
64-
<TagScrollArea class="max-w-lg">
65+
<TagScrollArea class="-ml-2 max-w-lg">
6566
<Badge
6667
v-for="tag in event.tags"
6768
:key="tag.id"
@@ -75,11 +76,23 @@ const columns = getEventFieldsColumns()
7576
</DetailsCardAttribute>
7677

7778
<!-- Example -->
78-
<DetailsCardAttribute icon="radix-icons:file-text" label="Example">
79+
<DetailsCardAttribute class="hidden sm:flex" icon="radix-icons:file-text" label="Example">
7980
<template #value>
8081
<JsonPreview :value="eventExample" />
8182
</template>
8283
</DetailsCardAttribute>
84+
85+
<!-- Links -->
86+
<DetailsCardAttribute
87+
v-if="event.links && event.links.length > 0"
88+
class="hidden sm:flex"
89+
icon="radix-icons:link-2"
90+
label="Links"
91+
>
92+
<template #value>
93+
<EventLinks :links="event.links" />
94+
</template>
95+
</DetailsCardAttribute>
8396
</template>
8497

8598
<template #content>

frontend/src/modules/events/components/EventEditModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ onMounted(() => {
4242

4343
<template>
4444
<Dialog :open="open" @update:open="onClose">
45-
<DialogContent>
45+
<DialogContent class="max-h-[80vh] overflow-y-auto">
4646
<DialogHeader>
4747
<DialogTitle>Edit Event</DialogTitle>
4848
<DialogDescription v-if="description">

frontend/src/modules/events/components/EventForm.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { computed, ref, watchEffect } from 'vue'
3636
import { eventSchema, type EventFormValues } from '@/modules/events/validation/eventSchema'
3737
import Skeleton from '@/shared/ui/skeleton/Skeleton.vue'
3838
import LinkedFieldsSelector from '@/modules/fields/components/LinkedFieldsSelector.vue'
39+
import EventLinksFormField from '@/modules/events/components/EventLinksFormField.vue'
3940
4041
const props = defineProps<{
4142
event?: Event
@@ -71,6 +72,11 @@ watchEffect(() => {
7172
description: props.event.description ?? '',
7273
fields: props.event.fields?.map(f => f.id) ?? [],
7374
tags: props.event.tags?.map(t => t.id) ?? [],
75+
links:
76+
props.event.links?.map(link => ({
77+
...link,
78+
label: link.label ?? '',
79+
})) ?? [],
7480
})
7581
}
7682
})
@@ -240,6 +246,20 @@ function removeTag(tagId: string) {
240246
</FormItem>
241247
</FormField>
242248

249+
<!-- Links -->
250+
<FormField name="links" v-slot="{ componentField }">
251+
<FormItem>
252+
<FormLabel>External Links</FormLabel>
253+
<FormControl>
254+
<EventLinksFormField
255+
:model-value="componentField.modelValue"
256+
@update:model-value="componentField.onChange"
257+
/>
258+
</FormControl>
259+
<FormMessage />
260+
</FormItem>
261+
</FormField>
262+
243263
<div class="flex justify-end">
244264
<Button type="submit" :disabled="isLoading">
245265
{{ buttonText || 'Create Event' }}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<script setup lang="ts">
2+
import type { EventLink } from '@/modules/events/types'
3+
import { Icon } from '@iconify/vue'
4+
5+
defineProps<{
6+
links: EventLink[]
7+
}>()
8+
9+
const iconMap: Record<string, string> = {
10+
figma: 'simple-icons:figma',
11+
miro: 'simple-icons:miro',
12+
confluence: 'simple-icons:confluence',
13+
jira: 'simple-icons:jira',
14+
notion: 'simple-icons:notion',
15+
loom: 'simple-icons:loom',
16+
slack: 'simple-icons:slack',
17+
google: 'simple-icons:googledrive',
18+
other: 'radix-icons:external-link',
19+
}
20+
21+
function getLabel(link: EventLink): string {
22+
if (link.label) return link.label
23+
try {
24+
return new URL(link.url).hostname
25+
} catch {
26+
return link.url
27+
}
28+
}
29+
</script>
30+
31+
<template>
32+
<div class="flex flex-wrap items-center gap-3">
33+
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
34+
<template v-for="(link, i) in links" :key="i">
35+
<a
36+
:href="link.url"
37+
target="_blank"
38+
rel="noopener noreferrer"
39+
class="hover:text-primary flex items-center gap-1 underline underline-offset-2 transition"
40+
:id="`event-link-${i}`"
41+
>
42+
<Icon :icon="iconMap[link.type]" class="h-4 w-4" />
43+
<span>{{ getLabel(link) }}</span>
44+
</a>
45+
</template>
46+
</div>
47+
</template>
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<script setup lang="ts">
2+
import { Input } from '@/shared/ui/input'
3+
import { Button } from '@/shared/ui/button'
4+
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/shared/ui/select'
5+
import { EventLinkType, type EventLink } from '@/modules/events/types'
6+
import { Icon } from '@iconify/vue'
7+
8+
const props = defineProps<{
9+
modelValue: EventLink[]
10+
}>()
11+
12+
const emit = defineEmits<{
13+
(e: 'update:modelValue', val: EventLink[]): void
14+
}>()
15+
16+
function update(index: number, patch: Partial<EventLink>) {
17+
const updated = [...props.modelValue]
18+
updated[index] = { ...updated[index], ...patch }
19+
emit('update:modelValue', updated)
20+
}
21+
22+
function addLink() {
23+
emit('update:modelValue', [
24+
...props.modelValue,
25+
{ type: EventLinkType.Other, url: '', label: '' },
26+
])
27+
}
28+
29+
function removeLink(index: number) {
30+
const updated = [...props.modelValue]
31+
updated.splice(index, 1)
32+
emit('update:modelValue', updated)
33+
}
34+
</script>
35+
36+
<template>
37+
<div class="space-y-2">
38+
<div
39+
v-for="(link, i) in modelValue"
40+
:key="i"
41+
class="grid grid-cols-[auto_1fr_1fr_auto] items-center gap-2"
42+
>
43+
<!-- Link Type -->
44+
<Select
45+
:model-value="link.type"
46+
@update:model-value="val => update(i, { type: val as EventLinkType })"
47+
>
48+
<SelectTrigger class="w-[90px]">
49+
<SelectValue />
50+
</SelectTrigger>
51+
<SelectContent>
52+
<SelectItem v-for="type in Object.values(EventLinkType)" :key="type" :value="type">
53+
{{ type }}
54+
</SelectItem>
55+
</SelectContent>
56+
</Select>
57+
58+
<!-- URL -->
59+
<Input
60+
type="url"
61+
placeholder="https://..."
62+
:model-value="link.url"
63+
@update:model-value="val => update(i, { url: String(val) })"
64+
/>
65+
66+
<!-- Label -->
67+
<Input
68+
placeholder="Label (optional)"
69+
:model-value="link.label"
70+
@update:model-value="val => update(i, { label: String(val) })"
71+
/>
72+
73+
<!-- Remove -->
74+
<Button
75+
type="button"
76+
variant="ghost"
77+
size="icon"
78+
class="text-muted-foreground hover:text-destructive h-8 w-8 shrink-0"
79+
@click="removeLink(i)"
80+
>
81+
<Icon icon="radix-icons:cross-2" class="h-4 w-4" />
82+
</Button>
83+
</div>
84+
85+
<Button type="button" variant="outline" size="sm" @click="addLink">
86+
<Icon icon="radix-icons:plus" class="mr-1 h-4 w-4" />
87+
Add link
88+
</Button>
89+
</div>
90+
</template>

0 commit comments

Comments
 (0)