Skip to content

Commit 9755775

Browse files
committed
WIP: allow setting macro in new conversations along with attachments
- new composable useFileUpload.js
1 parent f43acb7 commit 9755775

File tree

14 files changed

+697
-428
lines changed

14 files changed

+697
-428
lines changed

frontend/src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
<Command />
107107

108108
<!-- Create conversation dialog -->
109-
<CreateConversation v-model="openCreateConversationDialog" />
109+
<CreateConversation v-model="openCreateConversationDialog" v-if="openCreateConversationDialog" />
110110
</template>
111111

112112
<script setup>
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { ref, readonly } from 'vue'
2+
import { useEmitter } from '@/composables/useEmitter'
3+
import { EMITTER_EVENTS } from '@/constants/emitterEvents.js'
4+
import { handleHTTPError } from '@/utils/http'
5+
import api from '@/api'
6+
7+
/**
8+
* Composable for handling file uploads
9+
* @param {Object} options - Configuration options
10+
* @param {Function} options.onFileUploadSuccess - Callback when file upload succeeds (uploadedFile)
11+
* @param {Function} options.onUploadError - Optional callback when file upload fails (file, error)
12+
* @param {string} options.linkedModel - The linked model for the upload
13+
* @param {Array} options.mediaFiles - Optional external array to manage files (if not provided, internal array is used)
14+
*/
15+
export function useFileUpload (options = {}) {
16+
const {
17+
onFileUploadSuccess,
18+
onUploadError,
19+
linkedModel,
20+
mediaFiles: externalMediaFiles
21+
} = options
22+
23+
const emitter = useEmitter()
24+
const uploadingFiles = ref([])
25+
const isUploading = ref(false)
26+
const internalMediaFiles = ref([])
27+
28+
// Use external mediaFiles if provided, otherwise use internal
29+
const mediaFiles = externalMediaFiles || internalMediaFiles
30+
31+
/**
32+
* Handles the file upload process when files are selected.
33+
* Uploads each file to the server and adds them to the mediaFiles array.
34+
* @param {Event} event - The file input change event containing selected files
35+
*/
36+
const handleFileUpload = (event) => {
37+
const files = Array.from(event.target.files)
38+
uploadingFiles.value = files
39+
isUploading.value = true
40+
41+
for (const file of files) {
42+
api
43+
.uploadMedia({
44+
files: file,
45+
inline: false,
46+
linked_model: linkedModel
47+
})
48+
.then((resp) => {
49+
const uploadedFile = resp.data.data
50+
51+
// Add to media files array
52+
if (Array.isArray(mediaFiles.value)) {
53+
mediaFiles.value.push(uploadedFile)
54+
} else {
55+
mediaFiles.push(uploadedFile)
56+
}
57+
58+
// Remove from uploading list
59+
uploadingFiles.value = uploadingFiles.value.filter((f) => f.name !== file.name)
60+
61+
// Call success callback
62+
if (onFileUploadSuccess) {
63+
onFileUploadSuccess(uploadedFile)
64+
}
65+
66+
// Update uploading state
67+
if (uploadingFiles.value.length === 0) {
68+
isUploading.value = false
69+
}
70+
})
71+
.catch((error) => {
72+
uploadingFiles.value = uploadingFiles.value.filter((f) => f.name !== file.name)
73+
74+
// Call error callback or show default toast
75+
if (onUploadError) {
76+
onUploadError(file, error)
77+
} else {
78+
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
79+
variant: 'destructive',
80+
description: handleHTTPError(error).message
81+
})
82+
}
83+
84+
// Update uploading state
85+
if (uploadingFiles.value.length === 0) {
86+
isUploading.value = false
87+
}
88+
})
89+
}
90+
}
91+
92+
/**
93+
* Handles the file delete event.
94+
* Removes the file from the mediaFiles array.
95+
* @param {String} uuid - The UUID of the file to delete
96+
*/
97+
const handleFileDelete = (uuid) => {
98+
if (Array.isArray(mediaFiles.value)) {
99+
mediaFiles.value = [
100+
...mediaFiles.value.filter((item) => item.uuid !== uuid)
101+
]
102+
} else {
103+
const index = mediaFiles.findIndex((item) => item.uuid === uuid)
104+
if (index > -1) {
105+
mediaFiles.splice(index, 1)
106+
}
107+
}
108+
}
109+
110+
/**
111+
* Upload files programmatically (without event)
112+
* @param {File[]} files - Array of files to upload
113+
*/
114+
const uploadFiles = (files) => {
115+
const mockEvent = { target: { files } }
116+
handleFileUpload(mockEvent)
117+
}
118+
119+
/**
120+
* Clear all media files
121+
*/
122+
const clearMediaFiles = () => {
123+
if (Array.isArray(mediaFiles.value)) {
124+
mediaFiles.value = []
125+
} else {
126+
mediaFiles.length = 0
127+
}
128+
}
129+
130+
return {
131+
// State
132+
uploadingFiles: readonly(uploadingFiles),
133+
isUploading: readonly(isUploading),
134+
mediaFiles: externalMediaFiles ? readonly(mediaFiles) : readonly(internalMediaFiles),
135+
136+
// Methods
137+
handleFileUpload,
138+
handleFileDelete,
139+
uploadFiles,
140+
clearMediaFiles
141+
}
142+
}

frontend/src/features/admin/macros/MacroForm.vue

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,26 +41,53 @@
4141
</FormItem>
4242
</FormField>
4343

44-
<FormField v-slot="{ componentField }" name="visibility">
45-
<FormItem>
46-
<FormLabel>{{ t('admin.macro.visibility') }}</FormLabel>
47-
<FormControl>
48-
<Select v-bind="componentField">
49-
<SelectTrigger>
50-
<SelectValue placeholder="Select visibility" />
51-
</SelectTrigger>
52-
<SelectContent>
53-
<SelectGroup>
54-
<SelectItem value="all">{{ t('admin.macro.visibility.all') }}</SelectItem>
55-
<SelectItem value="team">{{ t('globals.terms.team') }}</SelectItem>
56-
<SelectItem value="user">{{ t('globals.terms.user') }}</SelectItem>
57-
</SelectGroup>
58-
</SelectContent>
59-
</Select>
60-
</FormControl>
61-
<FormMessage />
62-
</FormItem>
63-
</FormField>
44+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
45+
<FormField v-slot="{ componentField }" name="visible_when">
46+
<FormItem>
47+
<FormLabel>{{ t('admin.macro.visibleWhen') }}</FormLabel>
48+
<FormControl>
49+
<Select v-bind="componentField">
50+
<SelectTrigger>
51+
<SelectValue />
52+
</SelectTrigger>
53+
<SelectContent>
54+
<SelectGroup>
55+
<SelectItem value="replying">{{ t('admin.macro.replying') }}</SelectItem>
56+
<SelectItem value="starting_conversation">{{
57+
t('admin.macro.startingConversation')
58+
}}</SelectItem>
59+
<SelectItem value="adding_private_note">{{
60+
t('admin.macro.addingPrivateNote')
61+
}}</SelectItem>
62+
</SelectGroup>
63+
</SelectContent>
64+
</Select>
65+
</FormControl>
66+
<FormMessage />
67+
</FormItem>
68+
</FormField>
69+
70+
<FormField v-slot="{ componentField }" name="visibility">
71+
<FormItem>
72+
<FormLabel>{{ t('admin.macro.visibility') }}</FormLabel>
73+
<FormControl>
74+
<Select v-bind="componentField">
75+
<SelectTrigger>
76+
<SelectValue placeholder="Select visibility" />
77+
</SelectTrigger>
78+
<SelectContent>
79+
<SelectGroup>
80+
<SelectItem value="all">{{ t('admin.macro.visibility.all') }}</SelectItem>
81+
<SelectItem value="team">{{ t('globals.terms.team') }}</SelectItem>
82+
<SelectItem value="user">{{ t('globals.terms.user') }}</SelectItem>
83+
</SelectGroup>
84+
</SelectContent>
85+
</Select>
86+
</FormControl>
87+
<FormMessage />
88+
</FormItem>
89+
</FormField>
90+
</div>
6491

6592
<FormField v-if="form.values.visibility === 'team'" v-slot="{ componentField }" name="team_id">
6693
<FormItem>
@@ -114,7 +141,10 @@
114141
<div class="flex items-center gap-2">
115142
<div v-if="selected" class="flex items-center gap-2">
116143
<Avatar class="w-7 h-7">
117-
<AvatarImage :src="selected.avatar_url || ''" :alt="selected.label.slice(0, 2)" />
144+
<AvatarImage
145+
:src="selected.avatar_url || ''"
146+
:alt="selected.label.slice(0, 2)"
147+
/>
118148
<AvatarFallback>{{ selected.label.slice(0, 2).toUpperCase() }}</AvatarFallback>
119149
</Avatar>
120150
<span>{{ selected.label }}</span>
@@ -189,7 +219,11 @@ const submitLabel = computed(() => {
189219
)
190220
})
191221
const form = useForm({
192-
validationSchema: toTypedSchema(createFormSchema(t))
222+
validationSchema: toTypedSchema(createFormSchema(t)),
223+
initialValues: {
224+
visible_when: props.initialValues.visible_when || 'replying',
225+
visibility: props.initialValues.visibility || 'all'
226+
}
193227
})
194228
195229
const actionConfig = ref({

frontend/src/features/admin/macros/formSchema.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const createFormSchema = (t) => z.object({
1313
message_content: z.string().optional(),
1414
actions: actionSchema(t).optional().default([]),
1515
visibility: z.enum(['all', 'team', 'user']),
16+
visible_when: z.enum(['replying', 'starting_conversation', 'adding_private_note']).optional().default('replying'),
1617
team_id: z.string().nullable().optional(),
1718
user_id: z.string().nullable().optional(),
1819
})

0 commit comments

Comments
 (0)