PluginProbe
The Innovative Form Builder – IvyForms / 0.8
The Innovative Form Builder – IvyForms v0.8
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, at frontend/src/stores/actionEntityStore.ts

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