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

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