PluginProbe
The Innovative Form Builder – IvyForms / 1.1
The Innovative Form Builder – IvyForms v1.1
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 / useUnsavedChangesStore.ts

useUnsavedChangesStore.ts in The Innovative Form Builder – IvyForms 1.1, at frontend/src/stores/useUnsavedChangesStore.ts

221 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { ref, computed } from 'vue'
2 import type { ComputedRef } from 'vue'
3 import { defineStore } from 'pinia'
4 import { useActionEntityStore } from '@/stores/actionEntityStore'
5
6 /**
7 * Base entity types that can track unsaved changes in Lite
8 */
9 export const BASE_ENTITY_KEYS = [
10 'formBuilder',
11 'confirmationBuilder',
12 'notificationBuilder',
13 ] as const
14
15 /**
16 * Design principles:
17 * 1. Uses a dirty flag approach
18 * 2. Each entity store is responsible for calling markDirty() when changes occur
19 * 3. Changes are marked clean after successful save operations
20 *
21 * How to use in entity stores:
22 * 1. Call startEditing() when loading data or starting to edit
23 * 2. Call markDirty() whenever user makes any change
24 * 3. Call markClean() after successful save
25 * 4. Call stopEditing() when leaving the editor
26 */
27 export const useUnsavedChangesStore = defineStore('unsavedChanges', () => {
28 // Track dirty state for each entity type
29 const dirtyFlags = ref<Record<string, boolean>>({
30 formBuilder: false,
31 confirmationBuilder: false,
32 notificationBuilder: false,
33 })
34
35 // Track which entities are currently being edited (loaded from server)
36 const editingFlags = ref<Record<string, boolean>>({
37 formBuilder: false,
38 confirmationBuilder: false,
39 notificationBuilder: false,
40 })
41
42 // Action entity store for showing the dialog
43 const actionEntityStore = useActionEntityStore()
44
45 /**
46 * Register a new entity type for tracking (used by Pro plans)
47 * @param entityKey - The entity key to register (e.g., 'webhookBuilder')
48 */
49 const registerEntity = (entityKey: string) => {
50 if (!(entityKey in dirtyFlags.value)) {
51 dirtyFlags.value[entityKey] = false
52 editingFlags.value[entityKey] = false
53 }
54 }
55
56 /**
57 * Mark an entity as dirty (has unsaved changes)
58 * Call this whenever user makes a change to form fields, settings, etc.
59 */
60 const markDirty = (entityKey: string) => {
61 // Only mark dirty if the entity is being edited
62 if (editingFlags.value[entityKey]) {
63 dirtyFlags.value[entityKey] = true
64 }
65 }
66
67 /**
68 * Mark an entity as clean (no unsaved changes)
69 * Call this after successful save operations
70 */
71 const markClean = (entityKey: string) => {
72 dirtyFlags.value[entityKey] = false
73 }
74
75 /**
76 * Mark an entity as being edited (loaded from server or new form started)
77 * This enables dirty tracking for the entity
78 */
79 const startEditing = (entityKey: string) => {
80 editingFlags.value[entityKey] = true
81 dirtyFlags.value[entityKey] = false
82 }
83
84 /**
85 * Mark an entity as no longer being edited
86 * This disables dirty tracking and clears the dirty flag
87 */
88 const stopEditing = (entityKey: string) => {
89 editingFlags.value[entityKey] = false
90 dirtyFlags.value[entityKey] = false
91 }
92
93 /**
94 * Check if a specific entity has unsaved changes
95 */
96 const isDirty = (entityKey: string): boolean => {
97 return dirtyFlags.value[entityKey]
98 }
99
100 /**
101 * Get a computed ref for dirty state (useful for reactive bindings)
102 */
103 const isDirtyRef = (entityKey: string): ComputedRef<boolean> => {
104 return computed(() => dirtyFlags.value[entityKey])
105 }
106
107 /**
108 * Check if a specific entity is being edited
109 */
110 const isEntityEditing = (entityKey: string): boolean => {
111 return editingFlags.value[entityKey]
112 }
113
114 /**
115 * Check if any of the specified entities have unsaved changes
116 * @param entities - Array of entity keys to check (defaults to all)
117 */
118 const hasAnyUnsavedChanges = (entities: string[] = [...BASE_ENTITY_KEYS]): boolean => {
119 return entities.some((entity) => dirtyFlags.value[entity])
120 }
121
122 /**
123 * Get a computed ref for any unsaved changes (useful for reactive bindings)
124 */
125 const hasAnyUnsavedChangesRef = (
126 entities: string[] = [...BASE_ENTITY_KEYS],
127 ): ComputedRef<boolean> => {
128 return computed(() => entities.some((entity) => dirtyFlags.value[entity]))
129 }
130
131 /**
132 * Clear all dirty flags
133 * Use when navigating away after user confirms, or after successful save of all entities
134 */
135 const clearAllDirtyFlags = () => {
136 Object.keys(dirtyFlags.value).forEach((key) => {
137 dirtyFlags.value[key] = false
138 })
139 }
140
141 /**
142 * Reset all state (dirty flags and editing flags)
143 * Use when completely leaving the form builder context
144 */
145 const resetAllState = () => {
146 Object.keys(dirtyFlags.value).forEach((key) => {
147 dirtyFlags.value[key] = false
148 editingFlags.value[key] = false
149 })
150 }
151
152 /**
153 * Show confirmation dialog if there are unsaved changes, then proceed
154 * @param onProceed - Callback to execute if user confirms or no changes exist
155 * @param entities - Which entities to check (defaults to all)
156 */
157 const confirmIfDirty = (onProceed: () => void, entities: string[] = [...BASE_ENTITY_KEYS]) => {
158 if (!hasAnyUnsavedChanges(entities)) {
159 onProceed()
160 return
161 }
162
163 actionEntityStore.showUnsavedChangesDialog(() => {
164 // Clear dirty flags before proceeding since user confirmed they want to leave
165 entities.forEach((entity) => markClean(entity))
166 onProceed()
167 })
168 }
169
170 /**
171 * Handle admin menu click with unsaved changes check
172 * Used to intercept WordPress admin menu clicks
173 * @param event - Click event
174 * @param entities - Which entities to check
175 */
176 const handleAdminMenuClick = (event: Event, entities: string[] = [...BASE_ENTITY_KEYS]) => {
177 const mouseEvent = event as MouseEvent
178 const target = mouseEvent.target as HTMLElement
179 const link = target.closest('a') as HTMLAnchorElement | null
180
181 if (!link) return
182
183 // Prevent default navigation first, before checking for unsaved changes
184 mouseEvent.preventDefault()
185 mouseEvent.stopPropagation()
186
187 // If no unsaved changes, proceed with navigation immediately
188 if (!hasAnyUnsavedChanges(entities)) {
189 window.location.href = link.href
190 return
191 }
192
193 confirmIfDirty(() => {
194 window.location.href = link.href
195 }, entities)
196 }
197 return {
198 // Entity registration
199 registerEntity,
200
201 // State tracking
202 markDirty,
203 markClean,
204 startEditing,
205 stopEditing,
206 isDirty,
207 isDirtyRef,
208 isEntityEditing,
209
210 // Aggregated state checks
211 hasAnyUnsavedChanges,
212 hasAnyUnsavedChangesRef,
213 clearAllDirtyFlags,
214 resetAllState,
215
216 // Navigation helpers
217 confirmIfDirty,
218 handleAdminMenuClick,
219 }
220 })
221