Skip to content

Commit 9c57543

Browse files
committed
feat: export modal with settings
1 parent 3fe28ea commit 9c57543

4 files changed

Lines changed: 176 additions & 3 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def delete_event_route(event_id: int, db: Session = Depends(get_db)):
123123
return db_event
124124

125125

126-
@router.get("/events/{event_id}/schema.json", response_class=JSONResponse)
126+
@router.get("/{event_id}/schema.json", response_class=JSONResponse)
127127
def get_event_json_schema(
128128
event_id: int,
129129
include_descriptions: bool = Query(True),
@@ -145,7 +145,7 @@ def get_event_json_schema(
145145
return schema
146146

147147

148-
@router.get("/events/{event_id}/schema.yaml")
148+
@router.get("/{event_id}/schema.yaml")
149149
def get_event_yaml_schema(
150150
event_id: int,
151151
include_descriptions: bool = Query(True),

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const eventExample = useEventExample(props.event.fields)
2323
const emit = defineEmits<{
2424
(e: 'edit'): void
2525
(e: 'delete'): void
26+
(e: 'export'): void
2627
}>()
2728
2829
const { showCopied, showCopyError } = useEnhancedToast()
@@ -50,7 +51,7 @@ const columns = getEventFieldsColumns()
5051
</template>
5152

5253
<template #actions>
53-
<Button size="icon" variant="ghost">
54+
<Button size="icon" variant="ghost" @click="emit('export')">
5455
<Icon icon="radix-icons:share-1" class="h-4 w-4" />
5556
</Button>
5657
<Button size="icon" variant="ghost" @click="emit('edit')">
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<script setup lang="ts">
2+
import { ref, reactive, watch } from 'vue'
3+
import { useClipboard } from '@vueuse/core'
4+
import { useEnhancedToast } from '@/shared/composables/useEnhancedToast'
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogHeader,
9+
DialogTitle,
10+
DialogDescription,
11+
} from '@/shared/ui/dialog'
12+
import { Label } from '@/shared/ui/label'
13+
import { Button } from '@/shared/ui/button'
14+
import { Switch } from '@/shared/ui/switch'
15+
import { api } from '@/shared/utils/api'
16+
import { useAsyncTask } from '@/shared/composables/useAsyncTask'
17+
import { Icon } from '@iconify/vue'
18+
19+
const props = defineProps<{
20+
open: boolean
21+
eventId: number
22+
onClose: () => void
23+
}>()
24+
25+
const settings = reactive({
26+
includeDescriptions: true,
27+
includeExamples: true,
28+
additionalProperties: true,
29+
format: 'yaml' as 'yaml' | 'json',
30+
})
31+
32+
const preview = ref('')
33+
const updates = ref(0)
34+
const { run: fetchSchemaPreview, isLoading } = useAsyncTask()
35+
36+
const { copy: copyJson, isSupported } = useClipboard({ source: preview })
37+
const { showCopied, showCopyError } = useEnhancedToast()
38+
const handleCopy = async () => {
39+
try {
40+
await copyJson()
41+
showCopied('Schema')
42+
} catch {
43+
showCopyError('Schema')
44+
}
45+
}
46+
47+
function handleFetch() {
48+
const params = {
49+
include_descriptions: settings.includeDescriptions,
50+
include_examples: settings.includeExamples,
51+
additional_properties: settings.additionalProperties,
52+
}
53+
const format = settings.format
54+
55+
fetchSchemaPreview(async () => {
56+
const response = await api.get(`/events/${props.eventId}/schema.${format}`, {
57+
params,
58+
responseType: 'text',
59+
})
60+
61+
preview.value =
62+
format === 'json' ? JSON.stringify(JSON.parse(response.data), null, 2) : response.data
63+
updates.value += 1
64+
})
65+
}
66+
67+
watch(
68+
() => props.open,
69+
isOpen => {
70+
if (isOpen) {
71+
preview.value = ''
72+
handleFetch()
73+
}
74+
},
75+
{ immediate: true }
76+
)
77+
</script>
78+
79+
<template>
80+
<Dialog :open="props.open" @update:open="props.onClose">
81+
<DialogContent class="!max-w-4xl">
82+
<DialogHeader>
83+
<DialogTitle>Export Schema</DialogTitle>
84+
<DialogDescription>
85+
Generate a Swagger schema for the event. You can customize the settings below to include
86+
or exclude certain properties.
87+
</DialogDescription>
88+
</DialogHeader>
89+
90+
<div class="mt-4 grid grid-cols-1 gap-8 md:grid-cols-[1fr_2fr]">
91+
<!-- Settings Column -->
92+
<div class="space-y-6">
93+
<div class="flex items-center justify-between">
94+
<Label for="includeDescriptions">Include descriptions</Label>
95+
<Switch id="includeDescriptions" v-model="settings.includeDescriptions" />
96+
</div>
97+
98+
<div class="flex items-center justify-between">
99+
<Label for="includeExamples">Include examples</Label>
100+
<Switch id="includeExamples" v-model="settings.includeExamples" />
101+
</div>
102+
103+
<div class="flex items-center justify-between">
104+
<Label for="additionalProperties">Allow additional properties</Label>
105+
<Switch id="additionalProperties" v-model="settings.additionalProperties" />
106+
</div>
107+
108+
<div class="mt-12 flex items-center justify-between">
109+
<Label for="format">Schema format</Label>
110+
<div class="flex gap-2">
111+
<Button
112+
class="w-16 font-mono"
113+
:variant="settings.format === 'yaml' ? 'default' : 'outline'"
114+
size="sm"
115+
@click="settings.format = 'yaml'"
116+
>
117+
YAML
118+
</Button>
119+
<Button
120+
class="w-16 font-mono"
121+
:variant="settings.format === 'json' ? 'default' : 'outline'"
122+
size="sm"
123+
@click="settings.format = 'json'"
124+
>
125+
JSON
126+
</Button>
127+
</div>
128+
</div>
129+
130+
<div>
131+
<Button variant="secondary" class="w-full" @click="handleFetch" :disabled="isLoading">
132+
Update
133+
</Button>
134+
</div>
135+
</div>
136+
137+
<!-- Preview Column -->
138+
<div>
139+
<!-- <Textarea class="h-[400px] resize-none font-mono text-sm" :value="preview" readonly /> -->
140+
141+
<div
142+
side="bottom"
143+
@click.stop
144+
class="relative border-muted-foreground bg-foreground text-background h-[400px] overflow-y-auto rounded-md border p-4 text-left shadow-sm"
145+
>
146+
<button
147+
v-if="isSupported"
148+
@click="handleCopy"
149+
class="text-muted-foreground absolute top-2 right-2 cursor-pointer text-[10px]"
150+
>
151+
<Icon icon="radix-icons:copy" class="mr-1 inline h-3 w-3" />
152+
</button>
153+
<Transition name="fade-slide" mode="out-in">
154+
<div :key="updates" class="font-mono leading-snug whitespace-pre-wrap select-text">
155+
{{ preview }}
156+
</div>
157+
</Transition>
158+
</div>
159+
</div>
160+
</div>
161+
</DialogContent>
162+
</Dialog>
163+
</template>

frontend/src/modules/events/pages/EventDetailsPage.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useAsyncTask } from '@/shared/composables/useAsyncTask'
99
import type { EventFormValues } from '@/modules/events/validation/eventSchema'
1010
import { useEnhancedToast } from '@/shared/composables/useEnhancedToast'
1111
import DetailsCardSkeleton from '@/shared/components/skeletons/DetailsCardSkeleton.vue'
12+
import SwaggerExportModal from '../components/SwaggerExportModal.vue'
1213
1314
const DeleteModal = defineAsyncComponent(() => import('@/shared/components/modals/DeleteModal.vue'))
1415
const EventEditModal = defineAsyncComponent(
@@ -21,6 +22,7 @@ const event = ref<Event | null>(null)
2122
2223
const showEditModal = ref(false)
2324
const showDeleteModal = ref(false)
25+
const showSwaggerExportModal = ref(false)
2426
2527
const { run, isLoading } = useAsyncTask()
2628
const { run: runDeleteTask, isLoading: isDeleting } = useAsyncTask()
@@ -69,6 +71,7 @@ onMounted(() => {
6971
:event="event"
7072
@edit="showEditModal = true"
7173
@delete="showDeleteModal = true"
74+
@export="showSwaggerExportModal = true"
7275
/>
7376

7477
<!-- Modals -->
@@ -87,5 +90,11 @@ onMounted(() => {
8790
:isDeleting="isDeleting"
8891
description="Once deleted, this event will be removed permanently."
8992
/>
93+
<SwaggerExportModal
94+
v-if="event"
95+
:open="showSwaggerExportModal"
96+
:onClose="() => (showSwaggerExportModal = false)"
97+
:eventId="event.id"
98+
/>
9099
</div>
91100
</template>

0 commit comments

Comments
 (0)