@wishbone-media/spark 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +994 -916
- package/formkit.theme.mjs +24 -4
- package/package.json +1 -1
- package/src/composables/index.js +2 -1
- package/src/composables/useFormSubmission.js +160 -0
- package/src/utils/formErrors.js +35 -0
- package/src/utils/index.js +1 -0
package/formkit.theme.mjs
CHANGED
|
@@ -216,6 +216,9 @@ const classes = {
|
|
|
216
216
|
"group-data-[invalid]:border-red-500": true,
|
|
217
217
|
"group-data-[invalid]:ring-1": true,
|
|
218
218
|
"group-data-[invalid]:ring-red-500": true,
|
|
219
|
+
"group-data-[errors]:border-red-500": true,
|
|
220
|
+
"group-data-[errors]:ring-1": true,
|
|
221
|
+
"group-data-[errors]:ring-red-500": true,
|
|
219
222
|
"group-data-[disabled]:bg-gray-100": true,
|
|
220
223
|
"group-data-[disabled]:!cursor-not-allowed": true,
|
|
221
224
|
"shadow-xs": true,
|
|
@@ -225,7 +228,9 @@ const classes = {
|
|
|
225
228
|
"dark:border-gray-700": true,
|
|
226
229
|
"dark:group-data-[disabled]:bg-gray-800/5": true,
|
|
227
230
|
"dark:group-data-[invalid]:border-red-500": true,
|
|
228
|
-
"dark:group-data-[invalid]:ring-red-500": true
|
|
231
|
+
"dark:group-data-[invalid]:ring-red-500": true,
|
|
232
|
+
"dark:group-data-[errors]:border-red-500": true,
|
|
233
|
+
"dark:group-data-[errors]:ring-red-500": true
|
|
229
234
|
},
|
|
230
235
|
"family:text__input": {
|
|
231
236
|
"appearance-none": true,
|
|
@@ -293,6 +298,9 @@ const classes = {
|
|
|
293
298
|
"group-data-[invalid]:border-red-500": true,
|
|
294
299
|
"group-data-[invalid]:ring-1": true,
|
|
295
300
|
"group-data-[invalid]:ring-red-500": true,
|
|
301
|
+
"group-data-[errors]:border-red-500": true,
|
|
302
|
+
"group-data-[errors]:ring-1": true,
|
|
303
|
+
"group-data-[errors]:ring-red-500": true,
|
|
296
304
|
"group-data-[disabled]:bg-gray-100": true,
|
|
297
305
|
"group-data-[disabled]:!cursor-not-allowed": true,
|
|
298
306
|
"shadow-xs": true,
|
|
@@ -302,7 +310,9 @@ const classes = {
|
|
|
302
310
|
"dark:border-gray-700": true,
|
|
303
311
|
"dark:group-data-[disabled]:bg-gray-700/40": true,
|
|
304
312
|
"dark:group-data-[invalid]:border-red-500": true,
|
|
305
|
-
"dark:group-data-[invalid]:ring-red-500": true
|
|
313
|
+
"dark:group-data-[invalid]:ring-red-500": true,
|
|
314
|
+
"dark:group-data-[errors]:border-red-500": true,
|
|
315
|
+
"dark:group-data-[errors]:ring-red-500": true
|
|
306
316
|
},
|
|
307
317
|
"family:dropdown__input": {
|
|
308
318
|
"appearance-none": true,
|
|
@@ -791,7 +801,7 @@ const classes = {
|
|
|
791
801
|
"dark:text-gray-300": true
|
|
792
802
|
},
|
|
793
803
|
"form__messages": {
|
|
794
|
-
"": true
|
|
804
|
+
"hidden": true,
|
|
795
805
|
},
|
|
796
806
|
"form__message": {
|
|
797
807
|
"text-red-600": true,
|
|
@@ -895,6 +905,9 @@ const classes = {
|
|
|
895
905
|
"group-data-[invalid]:border-red-500": true,
|
|
896
906
|
"group-data-[invalid]:ring-1": true,
|
|
897
907
|
"group-data-[invalid]:ring-red-500": true,
|
|
908
|
+
"group-data-[errors]:border-red-500": true,
|
|
909
|
+
"group-data-[errors]:ring-1": true,
|
|
910
|
+
"group-data-[errors]:ring-red-500": true,
|
|
898
911
|
"group-data-[disabled]:bg-gray-100": true,
|
|
899
912
|
"group-data-[disabled]:!cursor-not-allowed": true,
|
|
900
913
|
"shadow-xs": true,
|
|
@@ -905,7 +918,9 @@ const classes = {
|
|
|
905
918
|
"dark:border-gray-700": true,
|
|
906
919
|
"dark:group-data-[disabled]:bg-gray-800/5": true,
|
|
907
920
|
"dark:group-data-[invalid]:border-red-500": true,
|
|
908
|
-
"dark:group-data-[invalid]:ring-red-500": true
|
|
921
|
+
"dark:group-data-[invalid]:ring-red-500": true,
|
|
922
|
+
"dark:group-data-[errors]:border-red-500": true,
|
|
923
|
+
"dark:group-data-[errors]:ring-red-500": true
|
|
909
924
|
},
|
|
910
925
|
"select__input": {
|
|
911
926
|
"appearance-none": true,
|
|
@@ -1084,6 +1099,9 @@ const classes = {
|
|
|
1084
1099
|
"group-data-[invalid]:border-red-500": true,
|
|
1085
1100
|
"group-data-[invalid]:ring-1": true,
|
|
1086
1101
|
"group-data-[invalid]:ring-red-500": true,
|
|
1102
|
+
"group-data-[errors]:border-red-500": true,
|
|
1103
|
+
"group-data-[errors]:ring-1": true,
|
|
1104
|
+
"group-data-[errors]:ring-red-500": true,
|
|
1087
1105
|
"group-data-[disabled]:bg-gray-100": true,
|
|
1088
1106
|
"shadow-xs": true,
|
|
1089
1107
|
"group-[&]/repeater:shadow-none": true,
|
|
@@ -1092,6 +1110,8 @@ const classes = {
|
|
|
1092
1110
|
"dark:group-data-[disabled]:bg-gray-800/5": true,
|
|
1093
1111
|
"dark:group-data-[invalid]:border-red-500": true,
|
|
1094
1112
|
"dark:group-data-[invalid]:ring-red-500": true,
|
|
1113
|
+
"dark:group-data-[errors]:border-red-500": true,
|
|
1114
|
+
"dark:group-data-[errors]:ring-red-500": true,
|
|
1095
1115
|
"dark:bg-transparent": true
|
|
1096
1116
|
},
|
|
1097
1117
|
"textarea__input": {
|
package/package.json
CHANGED
package/src/composables/index.js
CHANGED
|
@@ -2,4 +2,5 @@ export { sparkModalService } from './sparkModalService.js'
|
|
|
2
2
|
export { sparkNotificationService } from './sparkNotificationService.js'
|
|
3
3
|
export { sparkOverlayService } from './sparkOverlayService.js'
|
|
4
4
|
export { useSparkOverlay } from './useSparkOverlay.js'
|
|
5
|
-
export { useSparkTableRouteSync } from './useSparkTableRouteSync.js'
|
|
5
|
+
export { useSparkTableRouteSync } from './useSparkTableRouteSync.js'
|
|
6
|
+
export { useFormSubmission } from './useFormSubmission.js'
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import { useRouter } from 'vue-router'
|
|
3
|
+
import { sparkNotificationService } from './sparkNotificationService.js'
|
|
4
|
+
import { getAxiosInstance } from '../plugins/axios.js'
|
|
5
|
+
import { parseLaravelErrors, getFormLevelMessage, isValidationError } from '../utils/index.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Composable for handling form submissions with Laravel backend
|
|
9
|
+
* Automatically handles loading states, notifications, validation errors, and redirects
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} options - Configuration options
|
|
12
|
+
* @param {string} options.successMessage - Message to show on success (default: 'Saved successfully!')
|
|
13
|
+
* @param {string} options.createMessage - Message to show on create (default: 'Created successfully!')
|
|
14
|
+
* @param {string} options.updateMessage - Message to show on update (default: 'Updated successfully!')
|
|
15
|
+
* @param {string|Object|Function} options.redirectTo - Vue Router location to redirect after create (optional)
|
|
16
|
+
* Can be a function that receives response data
|
|
17
|
+
* @param {Function} options.onSuccess - Callback after successful submission (receives response data)
|
|
18
|
+
* @param {Function} options.onError - Callback after error (receives error, can return true to skip default handling)
|
|
19
|
+
* @param {boolean} options.showNotification - Whether to show success notification (default: true)
|
|
20
|
+
* @param {boolean} options.setFieldErrors - Whether to set field-level errors on 422 (default: true)
|
|
21
|
+
* @param {boolean} options.setFormErrors - Whether to show form-level error notification on 422 (default: false)
|
|
22
|
+
*
|
|
23
|
+
* @returns {Object} Form submission helpers
|
|
24
|
+
*/
|
|
25
|
+
export function useFormSubmission(options = {}) {
|
|
26
|
+
const {
|
|
27
|
+
successMessage = 'Saved successfully!',
|
|
28
|
+
createMessage = 'Created successfully!',
|
|
29
|
+
updateMessage = 'Updated successfully!',
|
|
30
|
+
redirectTo = null,
|
|
31
|
+
onSuccess = null,
|
|
32
|
+
onError = null,
|
|
33
|
+
showNotification = true,
|
|
34
|
+
setFieldErrors = true,
|
|
35
|
+
setFormErrors = false,
|
|
36
|
+
} = options
|
|
37
|
+
|
|
38
|
+
const router = useRouter()
|
|
39
|
+
const submitting = ref(false)
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Execute the form submission
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} config - Submission configuration
|
|
45
|
+
* @param {Function} config.submitFn - Async function that makes the API call
|
|
46
|
+
* @param {string} config.method - 'create' | 'update' (determines success message)
|
|
47
|
+
* @param {Object} config.node - FormKit node (second param from @submit handler)
|
|
48
|
+
* @param {boolean} config.isEditMode - Whether this is an update operation
|
|
49
|
+
*
|
|
50
|
+
* @returns {Promise<{success: boolean, data: any, error: any}>}
|
|
51
|
+
*/
|
|
52
|
+
async function submit(config) {
|
|
53
|
+
const { submitFn, method = 'create', node = null, isEditMode = false } = config
|
|
54
|
+
|
|
55
|
+
submitting.value = true
|
|
56
|
+
|
|
57
|
+
// Clear previous FormKit errors
|
|
58
|
+
if (node) {
|
|
59
|
+
node.clearErrors()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = await submitFn()
|
|
64
|
+
const data = response.data
|
|
65
|
+
|
|
66
|
+
// Show success notification
|
|
67
|
+
if (showNotification) {
|
|
68
|
+
const message = isEditMode
|
|
69
|
+
? updateMessage
|
|
70
|
+
: method === 'create'
|
|
71
|
+
? createMessage
|
|
72
|
+
: successMessage
|
|
73
|
+
|
|
74
|
+
sparkNotificationService.show({
|
|
75
|
+
type: 'success',
|
|
76
|
+
message,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Execute success callback
|
|
81
|
+
if (onSuccess) {
|
|
82
|
+
await onSuccess(data)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Redirect after create (if specified and not in edit mode)
|
|
86
|
+
if (!isEditMode && redirectTo) {
|
|
87
|
+
const location = typeof redirectTo === 'function'
|
|
88
|
+
? redirectTo(data)
|
|
89
|
+
: redirectTo
|
|
90
|
+
await router.push(location)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { success: true, data, error: null }
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const response = error.response
|
|
96
|
+
|
|
97
|
+
// Allow custom error handling to intercept
|
|
98
|
+
if (onError) {
|
|
99
|
+
const handled = await onError(error)
|
|
100
|
+
if (handled === true) {
|
|
101
|
+
return { success: false, data: null, error }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Handle validation errors (422)
|
|
106
|
+
if (isValidationError(response)) {
|
|
107
|
+
const fieldErrors = parseLaravelErrors(response)
|
|
108
|
+
|
|
109
|
+
if (fieldErrors && node && setFieldErrors) {
|
|
110
|
+
node.setErrors([], fieldErrors)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (setFormErrors) {
|
|
114
|
+
sparkNotificationService.show({
|
|
115
|
+
type: 'danger',
|
|
116
|
+
message: getFormLevelMessage(response),
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
// Handle non-validation errors
|
|
121
|
+
sparkNotificationService.show({
|
|
122
|
+
type: 'danger',
|
|
123
|
+
message: getFormLevelMessage(response),
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { success: false, data: null, error }
|
|
128
|
+
} finally {
|
|
129
|
+
submitting.value = false
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Convenience method for simple POST/PUT submissions
|
|
135
|
+
*
|
|
136
|
+
* @param {Object} config - Submission configuration
|
|
137
|
+
* @param {string} config.url - API endpoint
|
|
138
|
+
* @param {Object} config.payload - Request payload
|
|
139
|
+
* @param {string} config.method - 'post' | 'put' (default: 'post')
|
|
140
|
+
* @param {Object} config.node - FormKit node
|
|
141
|
+
* @param {boolean} config.isEditMode - Whether this is an update
|
|
142
|
+
*/
|
|
143
|
+
async function submitToApi(config) {
|
|
144
|
+
const { url, payload, method = 'post', node, isEditMode } = config
|
|
145
|
+
const axios = getAxiosInstance()
|
|
146
|
+
|
|
147
|
+
return submit({
|
|
148
|
+
submitFn: () => axios[method](url, payload),
|
|
149
|
+
method: isEditMode ? 'update' : 'create',
|
|
150
|
+
node,
|
|
151
|
+
isEditMode,
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
submitting,
|
|
157
|
+
submit,
|
|
158
|
+
submitToApi,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse Laravel validation errors from API response
|
|
3
|
+
* Laravel returns: { errors: { field: ["error1", "error2"] } }
|
|
4
|
+
* FormKit accepts: { field: ["error1", "error2"] } (compatible)
|
|
5
|
+
*
|
|
6
|
+
* @param {Object} response - Axios error.response object
|
|
7
|
+
* @returns {Object|null} - Field errors object or null if not validation error
|
|
8
|
+
*/
|
|
9
|
+
export function parseLaravelErrors(response) {
|
|
10
|
+
if (response?.status !== 422) return null
|
|
11
|
+
return response.data?.errors || null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get a form-level error message from Laravel response
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} response - Axios error.response object
|
|
18
|
+
* @returns {string} - Error message for display
|
|
19
|
+
*/
|
|
20
|
+
export function getFormLevelMessage(response) {
|
|
21
|
+
if (response?.status === 422) {
|
|
22
|
+
return response.data?.message || 'Please fix the validation errors.'
|
|
23
|
+
}
|
|
24
|
+
return response?.data?.message || 'An error occurred.'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if the error response is a validation error (422)
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} response - Axios error.response object
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isValidationError(response) {
|
|
34
|
+
return response?.status === 422
|
|
35
|
+
}
|
package/src/utils/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { customiseHeader } from './sparkTable/header.js'
|
|
|
3
3
|
export { renderHeaderTitle } from './sparkTable/header-title.js'
|
|
4
4
|
export { updateRow } from './sparkTable/update-row.js'
|
|
5
5
|
export { formatTemporal, parseDatetime } from './formatTemporal.js'
|
|
6
|
+
export { parseLaravelErrors, getFormLevelMessage, isValidationError } from './formErrors.js'
|