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

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