PluginProbe
The Innovative Form Builder – IvyForms / 0.8.2
The Innovative Form Builder – IvyForms v0.8.2
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / stores / actionEntityStore.ts

actionEntityStore.ts in The Innovative Form Builder – IvyForms 0.8.2, at frontend/src/stores/actionEntityStore.ts

529 lines 14.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import IvyMessage from '@/views/_components/message/ivyMessage.ts'
2 import { defineStore } from 'pinia'
3 import { ref, watch } from 'vue'
4 import type { Ref } from 'vue'
5 import { useLabels } from '@/composables/useLabels'
6 import { useAllForms } from '@/stores/useAllForms'
7 import { useAllEntries } from '@/stores/useAllEntries'
8 import { useNotificationSettingBuilder } from '@/stores/useNotificationSettingBuilder.ts'
9 import { useRoute } from 'vue-router'
10 import { useFormBuilder } from '@/stores/useFormBuilder'
11 import { useApiClient } from '@/composables/useApiClient'
12
13 type DeleteEntityOptions = {
14 entityType: string
15 ids: number[]
16 successMessage?: string
17 failedMessage?: string
18 errorMessage?: string
19 }
20
21 type DeleteSingleEntityOptions = {
22 entityType: string
23 id: number
24 successMessage?: string
25 failedMessage?: string
26 errorMessage?: string
27 }
28
29 type DuplicateEntityOptions = {
30 entityType: string
31 id: number
32 successMessage?: string
33 failedMessage?: string
34 errorMessage?: string
35 }
36
37 /* TODO: Redesign - Internationalization */
38 export const useActionEntityStore = defineStore('actionEntity', () => {
39 type EntityType = 'form' | 'forms' | 'notification' | 'entry' | 'entries'
40 type ActionType = 'delete' | 'cancel' | 'duplicate' | 'unsaved_changes'
41 type ButtonType = 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'tertiary'
42
43 type DialogData = {
44 title: string
45 subtitle: string
46 dialogType?: 'info' | 'success' | 'warning' | 'error' | 'default' | 'pro' | 'upcoming'
47 buttons?: {
48 close: { type?: ButtonType; text: string; function?: () => void }
49 confirm: {
50 type: ButtonType
51 text: string
52 function?: () => void
53 }
54 } | null
55 }
56
57 type LoadingCallbacks = {
58 setLoading?: (isLoading: boolean) => void
59 onSuccess?: () => void
60 }
61
62 const defaultDialogData: DialogData = {
63 title: '',
64 subtitle: '',
65 dialogType: 'default',
66 }
67
68 const dialogVisible: Ref<boolean> = ref(false)
69 const dialogLoading: Ref<boolean> = ref(false)
70 const actionAllowed: Ref<boolean> = ref(true)
71 const dialogData: Ref<DialogData> = ref({ ...defaultDialogData })
72 const entityId: Ref<number | number[]> = ref(0)
73 const entityType: Ref<EntityType> = ref('forms')
74 const actionType: Ref<ActionType> = ref('delete')
75 const successCallback: Ref<(() => void) | null> = ref(null)
76 const loadingCallbacks: Ref<LoadingCallbacks> = ref({})
77
78 // For unsaved changes dialog
79 const pendingNavigationCallback: Ref<(() => void) | null> = ref(null)
80
81 watch(dialogVisible, (value) => {
82 if (!value) {
83 setTimeout(onClose, 150)
84 }
85 })
86
87 const handleActionClick = async (
88 id: number | null,
89 ids: number[] | null = null,
90 type: EntityType,
91 action: ActionType,
92 callbacks: LoadingCallbacks = {},
93 navigationCallback?: () => void,
94 ) => {
95 entityId.value = ids || id
96 entityType.value = type
97 actionType.value = action
98 loadingCallbacks.value = callbacks
99
100 // Store navigation callback for unsaved changes
101 if (action === 'unsaved_changes' && navigationCallback) {
102 pendingNavigationCallback.value = navigationCallback
103 }
104
105 generateDialogMessage()
106 dialogVisible.value = true
107 }
108
109 const generateDialogMessage = () => {
110 dialogData.value.title = getDialogText()['title']
111 dialogData.value.subtitle = getDialogText()['subtitle']
112
113 dialogData.value.buttons = dialogData.value.buttons
114 ? dialogData.value.buttons
115 : {
116 close: { text: getLabel('close') },
117 confirm: { type: 'primary', text: getDialogText()['confirmText'] },
118 }
119 if (actionType.value === 'delete') {
120 dialogData.value.buttons.confirm.type = 'danger'
121 dialogData.value.dialogType = 'error'
122 }
123
124 if (actionType.value === 'duplicate') {
125 dialogData.value.buttons.confirm.type = 'primary'
126 dialogData.value.dialogType = 'info'
127 }
128
129 if (actionType.value === 'unsaved_changes') {
130 dialogData.value.buttons.confirm.type = 'warning'
131 dialogData.value.dialogType = 'warning'
132 }
133 }
134
135 const getDialogText = () => {
136 const titles = {
137 delete: {
138 forms: getLabel('are_you_sure_delete_forms'),
139 form: getLabel('are_you_sure_delete_form'),
140 notification: getLabel('are_you_sure_delete_notification'),
141 entry: getLabel('are_you_sure_delete_entry'),
142 entries: getLabel('are_you_sure_delete_entries'),
143 },
144 duplicate: {
145 form: getLabel('duplicate_form'),
146 notification: getLabel('duplicate_notification'),
147 },
148 unsaved_changes: {
149 forms: getLabel('unsaved_changes'),
150 form: getLabel('unsaved_changes'),
151 notification: getLabel('unsaved_changes'),
152 },
153 }
154
155 const subtitles = {
156 delete: {
157 forms: getLabel('action_cannot_be_undone'),
158 form: getLabel('action_cannot_be_undone'),
159 notification: getLabel('action_cannot_be_undone'),
160 entry: getLabel('action_cannot_be_undone'),
161 entries: getLabel('action_cannot_be_undone'),
162 },
163 duplicate: {
164 forms: getLabel('duplicate_form_subtitle'),
165 form: getLabel('duplicate_form_subtitle'),
166 notification: getLabel('duplicate_notification_subtitle'),
167 },
168 }
169
170 let titleKey: string = entityType.value
171 if (
172 entityType.value === 'forms' &&
173 Array.isArray(entityId.value) &&
174 entityId.value.length === 1
175 ) {
176 titleKey = 'form'
177 }
178 if (
179 entityType.value === 'entries' &&
180 Array.isArray(entityId.value) &&
181 entityId.value.length === 1
182 ) {
183 titleKey = 'entry'
184 }
185
186 const text = {
187 delete: {
188 title: titles.delete[titleKey],
189 subtitle: subtitles.delete[titleKey],
190 confirmText: getLabel('delete'),
191 },
192 duplicate: {
193 title: titles.duplicate[titleKey],
194 subtitle: subtitles.duplicate[titleKey],
195 confirmText: getLabel('duplicate'),
196 },
197 unsaved_changes: {
198 title: titles.unsaved_changes[titleKey],
199 confirmText: getLabel('leave'),
200 },
201 }
202
203 type MessageActions = keyof typeof text
204 return text[actionType.value as MessageActions]
205 }
206
207 const refreshData = async (entityType: string, id?: number) => {
208 const stores = {
209 forms: useAllForms,
210 notification: useNotificationSettingBuilder,
211 entry: useAllEntries,
212 entries: useAllEntries,
213 }
214
215 const store = stores[entityType]?.()
216
217 if (!store) {
218 IvyMessage({ message: `Invalid entity type: ${entityType}`, type: 'error' })
219 return
220 }
221
222 try {
223 // Special handling for entries to preserve any selected form filter
224 if (entityType === 'entries' || entityType === 'entry') {
225 const formBuilderStore = useFormBuilder()
226 const route = useRoute()
227
228 let routeFormId: number | undefined = undefined
229 const paramVal =
230 (route?.params as Record<string, unknown>)?.formId ??
231 (route?.params as Record<string, unknown>)?.id
232 if (paramVal !== undefined && paramVal !== null) {
233 routeFormId = Array.isArray(paramVal)
234 ? Number(paramVal[0])
235 : Number(paramVal as string | number)
236 }
237
238 const formIdToPass =
239 id ?? (formBuilderStore.formId ? Number(formBuilderStore.formId) : routeFormId)
240
241 await store.fetchData?.({ formId: formIdToPass, shouldGetCount: true })
242 } else {
243 if (id !== undefined) {
244 await store.fetchData?.(id)
245 } else {
246 await store.fetchData?.()
247 }
248 }
249
250 if (entityType === 'forms') {
251 const allFormsStore = useAllForms()
252 allFormsStore.tableData = [...store.tableData]
253 }
254 if (entityType === 'entries' || entityType === 'entry') {
255 const allEntriesStore = useAllEntries()
256 allEntriesStore.tableData = [...store.tableData]
257 }
258 } catch (error) {
259 IvyMessage({
260 message: `${getLabel('error_refreshing')} ${entityType}: ${error}`,
261 type: 'error',
262 })
263 }
264 }
265
266 const { getLabel } = useLabels()
267
268 const deleteEntities = async (options: DeleteEntityOptions): Promise<boolean> => {
269 const { entityType, ids } = options
270
271 const messageEntityType =
272 entityType === 'forms' && ids.length === 1
273 ? 'form'
274 : entityType === 'entries' && ids.length === 1
275 ? 'entry'
276 : entityType
277
278 if (loadingCallbacks.value.setLoading) {
279 loadingCallbacks.value.setLoading(true)
280 }
281
282 try {
283 dialogVisible.value = false
284
285 const { request } = useApiClient()
286 const { error } = await request(`${entityType}/delete`, {
287 method: 'POST',
288 data: { ids },
289 })
290
291 if (!error) {
292 IvyMessage({
293 message: options.successMessage || getLabel(`${messageEntityType}_deleted`),
294 type: 'success',
295 })
296
297 await refreshData(entityType)
298
299 // Call onSuccess callback if provided
300 if (loadingCallbacks.value.onSuccess) {
301 loadingCallbacks.value.onSuccess()
302 }
303
304 return true
305 } else {
306 IvyMessage({
307 message:
308 options.failedMessage ||
309 `${getLabel(`failed_to_delete_${messageEntityType}`)} ${error.message || ''}`,
310 type: 'error',
311 })
312 return false
313 }
314 } catch (error) {
315 IvyMessage({
316 message:
317 options.errorMessage || `${getLabel(`error_deleting_${messageEntityType}`)} ${error}`,
318 type: 'error',
319 })
320 return false
321 } finally {
322 if (loadingCallbacks.value.setLoading) {
323 loadingCallbacks.value.setLoading(false)
324 }
325 loadingCallbacks.value = {}
326 }
327 }
328
329 const route = useRoute()
330 const deleteSingleEntity = async (options: DeleteSingleEntityOptions): Promise<boolean> => {
331 const { entityType, id } = options
332
333 if (loadingCallbacks.value.setLoading) {
334 loadingCallbacks.value.setLoading(true)
335 }
336
337 try {
338 dialogVisible.value = false
339
340 const { request } = useApiClient()
341 const { error } = await request(`${entityType}/delete/${id}/`, {
342 method: 'POST',
343 data: { id: id },
344 })
345
346 if (!error) {
347 IvyMessage({
348 message: options.successMessage || getLabel(`${entityType}_deleted`),
349 type: 'success',
350 })
351
352 if (entityType === 'notification') {
353 if (route.params.id) {
354 const formIdParam = Array.isArray(route.params.id)
355 ? route.params.id[0]
356 : route.params.id
357
358 const formId = parseInt(formIdParam, 10)
359
360 await refreshData(entityType, formId)
361 }
362 } else {
363 await refreshData(entityType)
364 }
365
366 // Call onSuccess callback if provided
367 if (loadingCallbacks.value.onSuccess) {
368 loadingCallbacks.value.onSuccess()
369 }
370
371 return true
372 } else {
373 IvyMessage({
374 message:
375 options.failedMessage ||
376 `${getLabel(`failed_to_delete_${entityType}`)} ${error.message || ''}`,
377 type: 'error',
378 })
379 return false
380 }
381 } catch (error) {
382 IvyMessage({
383 message: options.errorMessage || `${getLabel(`error_deleting_${entityType}`)} ${error}`,
384 type: 'error',
385 })
386 dialogVisible.value = false
387 return false
388 } finally {
389 if (loadingCallbacks.value.setLoading) {
390 loadingCallbacks.value.setLoading(false)
391 }
392 loadingCallbacks.value = {}
393 }
394 }
395
396 const duplicateSingleEntity = async (options: DuplicateEntityOptions): Promise<boolean> => {
397 const { entityType, id } = options
398
399 if (loadingCallbacks.value.setLoading) {
400 loadingCallbacks.value.setLoading(true)
401 }
402
403 try {
404 dialogVisible.value = false
405
406 const { request } = useApiClient()
407 const { error } = await request(`${entityType}/duplicate/${id}/`, {
408 method: 'POST',
409 })
410
411 if (!error) {
412 IvyMessage({
413 message: options.successMessage || getLabel(`${entityType}_duplicated`),
414 type: 'success',
415 })
416
417 const refreshEntityType =
418 entityType === 'form' ? 'forms' : entityType === 'entry' ? 'entries' : entityType
419
420 if (entityType === 'notification') {
421 if (route.params.id) {
422 const formIdParam = Array.isArray(route.params.id)
423 ? route.params.id[0]
424 : route.params.id
425
426 const formId = parseInt(formIdParam, 10)
427
428 await refreshData(refreshEntityType, formId)
429 }
430 } else {
431 await refreshData(refreshEntityType)
432 }
433
434 if (successCallback.value) {
435 successCallback.value()
436 }
437
438 return true
439 } else {
440 IvyMessage({
441 message:
442 options.failedMessage ||
443 `${getLabel(`failed_to_duplicate_${entityType}`)} ${error.message || ''}`,
444 type: 'error',
445 })
446 return false
447 }
448 } catch (error) {
449 IvyMessage({
450 message: options.errorMessage || `${getLabel(`error_duplicating_${entityType}`)} ${error}`,
451 type: 'error',
452 })
453 dialogVisible.value = false
454 return false
455 } finally {
456 if (loadingCallbacks.value.setLoading) {
457 loadingCallbacks.value.setLoading(false)
458 }
459 loadingCallbacks.value = {}
460 }
461 }
462
463 const confirmAction = async () => {
464 switch (actionType.value) {
465 case 'delete':
466 if (Array.isArray(entityId.value)) {
467 await deleteEntities({
468 entityType: entityType.value,
469 ids: entityId.value,
470 })
471 } else {
472 await deleteSingleEntity({
473 entityType: entityType.value,
474 id: entityId.value as number,
475 })
476 }
477 break
478 case 'duplicate':
479 await duplicateSingleEntity({
480 entityType: entityType.value,
481 id: entityId.value as number,
482 })
483 break
484 case 'unsaved_changes':
485 if (pendingNavigationCallback.value) {
486 pendingNavigationCallback.value()
487 pendingNavigationCallback.value = null
488 }
489 dialogVisible.value = false
490 break
491 }
492 }
493
494 const onClose = () => {
495 actionAllowed.value = true
496 dialogData.value = { ...defaultDialogData }
497 pendingNavigationCallback.value = null
498 }
499
500 const showUnsavedChangesDialog = (navigationCallback: () => void) => {
501 handleActionClick(null, null, 'form', 'unsaved_changes', {}, navigationCallback)
502 }
503
504 /**
505 * Show a custom dialog with specific configuration
506 * Used for Pro feature upgrade dialogs and other custom dialogs
507 */
508 const showDialog = (config: DialogData) => {
509 dialogData.value = {
510 ...defaultDialogData,
511 ...config,
512 }
513 dialogVisible.value = true
514 }
515
516 return {
517 dialogVisible,
518 dialogLoading,
519 actionAllowed,
520 dialogData,
521 actionType,
522 handleActionClick,
523 confirmAction,
524 refreshData,
525 showUnsavedChangesDialog,
526 showDialog,
527 }
528 })
529