@wishbone-media/spark 0.26.0 → 0.28.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 +1797 -1542
- package/formkit.theme.mjs +24 -4
- package/package.json +1 -1
- package/src/components/SparkImageUpload.vue +202 -0
- package/src/components/index.js +1 -0
- 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
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<label v-if="label" class="block text-sm font-medium text-gray-700 mb-2">{{ label }}</label>
|
|
4
|
+
<div v-if="modelValue" class="mb-2 relative inline-block">
|
|
5
|
+
<a :href="modelValue" target="_blank" rel="noopener noreferrer">
|
|
6
|
+
<img
|
|
7
|
+
:src="modelValue"
|
|
8
|
+
:alt="label || 'Image preview'"
|
|
9
|
+
:class="[height, 'w-auto object-contain border border-gray-300 rounded-md cursor-pointer hover:opacity-80 transition-opacity']"
|
|
10
|
+
/>
|
|
11
|
+
</a>
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
class="absolute -top-2 -right-2 w-5 h-5 bg-white/80 hover:bg-white text-gray-500 hover:text-gray-700 rounded-full flex items-center justify-center shadow-sm border border-gray-200"
|
|
15
|
+
:disabled="disabled"
|
|
16
|
+
@click="clearImage"
|
|
17
|
+
>
|
|
18
|
+
<font-awesome-icon :icon="Icons.farXmark" class="text-xs" />
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
<div
|
|
22
|
+
v-if="!modelValue"
|
|
23
|
+
class="relative rounded-md transition-colors"
|
|
24
|
+
:class="{
|
|
25
|
+
'ring-2 ring-blue-500 ring-offset-2 bg-blue-50': isDragging,
|
|
26
|
+
}"
|
|
27
|
+
@dragover.prevent="onDragOver"
|
|
28
|
+
@dragenter.prevent="onDragOver"
|
|
29
|
+
@dragleave.prevent="onDragLeave"
|
|
30
|
+
@drop.prevent="onDrop"
|
|
31
|
+
>
|
|
32
|
+
<FormKit
|
|
33
|
+
type="file"
|
|
34
|
+
:name="name"
|
|
35
|
+
:accept="accept"
|
|
36
|
+
:disabled="disabled || uploading"
|
|
37
|
+
@input="handleFormKitInput"
|
|
38
|
+
/>
|
|
39
|
+
<div
|
|
40
|
+
v-if="isDragging"
|
|
41
|
+
class="absolute inset-0 flex items-center justify-center bg-blue-50/80 rounded-md pointer-events-none"
|
|
42
|
+
>
|
|
43
|
+
<span class="text-sm text-blue-600 font-medium">Drop image here</span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<p v-if="uploading" class="text-sm text-gray-500 mt-1">Uploading...</p>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup>
|
|
51
|
+
import { ref, inject } from 'vue'
|
|
52
|
+
import { Icons } from '@/plugins/fontawesome'
|
|
53
|
+
import { sparkNotificationService } from '@/composables/sparkNotificationService'
|
|
54
|
+
|
|
55
|
+
const props = defineProps({
|
|
56
|
+
modelValue: {
|
|
57
|
+
type: String,
|
|
58
|
+
default: '',
|
|
59
|
+
},
|
|
60
|
+
uploadUrl: {
|
|
61
|
+
type: String,
|
|
62
|
+
required: true,
|
|
63
|
+
},
|
|
64
|
+
fieldName: {
|
|
65
|
+
type: String,
|
|
66
|
+
default: 'file',
|
|
67
|
+
},
|
|
68
|
+
extraFields: {
|
|
69
|
+
type: Object,
|
|
70
|
+
default: () => ({}),
|
|
71
|
+
},
|
|
72
|
+
label: {
|
|
73
|
+
type: String,
|
|
74
|
+
default: '',
|
|
75
|
+
},
|
|
76
|
+
name: {
|
|
77
|
+
type: String,
|
|
78
|
+
default: 'image_file',
|
|
79
|
+
},
|
|
80
|
+
height: {
|
|
81
|
+
type: String,
|
|
82
|
+
default: 'h-16',
|
|
83
|
+
},
|
|
84
|
+
accept: {
|
|
85
|
+
type: String,
|
|
86
|
+
default: 'image/*',
|
|
87
|
+
},
|
|
88
|
+
disabled: {
|
|
89
|
+
type: Boolean,
|
|
90
|
+
default: false,
|
|
91
|
+
},
|
|
92
|
+
responseParser: {
|
|
93
|
+
type: Function,
|
|
94
|
+
default: (response) => response.data.url || response.data,
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const emit = defineEmits(['update:modelValue', 'upload-start', 'upload-success', 'upload-error'])
|
|
99
|
+
|
|
100
|
+
const axios = inject('axios')
|
|
101
|
+
const uploading = ref(false)
|
|
102
|
+
const isDragging = ref(false)
|
|
103
|
+
|
|
104
|
+
function clearImage() {
|
|
105
|
+
emit('update:modelValue', '')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function onDragOver() {
|
|
109
|
+
if (props.disabled || uploading.value) return
|
|
110
|
+
isDragging.value = true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function onDragLeave() {
|
|
114
|
+
isDragging.value = false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function onDrop(event) {
|
|
118
|
+
isDragging.value = false
|
|
119
|
+
if (props.disabled || uploading.value) return
|
|
120
|
+
|
|
121
|
+
const file = event.dataTransfer?.files?.[0]
|
|
122
|
+
if (!file) return
|
|
123
|
+
|
|
124
|
+
// Validate file type against accept prop
|
|
125
|
+
if (!isFileTypeAccepted(file)) {
|
|
126
|
+
sparkNotificationService.show({
|
|
127
|
+
type: 'danger',
|
|
128
|
+
message: 'File type not accepted.',
|
|
129
|
+
})
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
uploadFile(file)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isFileTypeAccepted(file) {
|
|
137
|
+
// Parse accept prop (e.g., "image/*", "image/png,image/jpeg", ".png,.jpg")
|
|
138
|
+
const acceptTypes = props.accept.split(',').map((t) => t.trim())
|
|
139
|
+
|
|
140
|
+
for (const acceptType of acceptTypes) {
|
|
141
|
+
if (acceptType === '*/*') return true
|
|
142
|
+
if (acceptType.endsWith('/*')) {
|
|
143
|
+
// Wildcard like "image/*"
|
|
144
|
+
const category = acceptType.slice(0, -2)
|
|
145
|
+
if (file.type.startsWith(category + '/')) return true
|
|
146
|
+
} else if (acceptType.startsWith('.')) {
|
|
147
|
+
// Extension like ".png"
|
|
148
|
+
if (file.name.toLowerCase().endsWith(acceptType.toLowerCase())) return true
|
|
149
|
+
} else {
|
|
150
|
+
// Exact MIME type like "image/png"
|
|
151
|
+
if (file.type === acceptType) return true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle FormKit file input
|
|
159
|
+
function handleFormKitInput(files) {
|
|
160
|
+
if (!files || files.length === 0) return
|
|
161
|
+
const file = files[0].file
|
|
162
|
+
if (!file) return
|
|
163
|
+
uploadFile(file)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Core upload logic
|
|
167
|
+
async function uploadFile(file) {
|
|
168
|
+
uploading.value = true
|
|
169
|
+
emit('upload-start')
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const formData = new FormData()
|
|
173
|
+
formData.append(props.fieldName, file)
|
|
174
|
+
|
|
175
|
+
// Add any extra fields
|
|
176
|
+
Object.entries(props.extraFields).forEach(([key, value]) => {
|
|
177
|
+
formData.append(key, value)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const response = await axios.post(props.uploadUrl, formData, {
|
|
181
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const url = props.responseParser(response)
|
|
185
|
+
emit('update:modelValue', url)
|
|
186
|
+
emit('upload-success', response)
|
|
187
|
+
|
|
188
|
+
sparkNotificationService.show({
|
|
189
|
+
type: 'success',
|
|
190
|
+
message: 'Image uploaded!',
|
|
191
|
+
})
|
|
192
|
+
} catch (error) {
|
|
193
|
+
emit('upload-error', error)
|
|
194
|
+
sparkNotificationService.show({
|
|
195
|
+
type: 'danger',
|
|
196
|
+
message: error.response?.data?.message || 'Failed to upload image.',
|
|
197
|
+
})
|
|
198
|
+
} finally {
|
|
199
|
+
uploading.value = false
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
</script>
|
package/src/components/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { default as SparkBrandSelector } from './SparkBrandSelector.vue'
|
|
|
6
6
|
export { default as SparkButton } from './SparkButton.vue'
|
|
7
7
|
export { default as SparkButtonGroup } from './SparkButtonGroup.vue'
|
|
8
8
|
export { default as SparkCard } from './SparkCard.vue'
|
|
9
|
+
export { default as SparkImageUpload } from './SparkImageUpload.vue'
|
|
9
10
|
export { default as SparkModalContainer } from './SparkModalContainer.vue'
|
|
10
11
|
export { default as SparkModalDialog } from './SparkModalDialog.vue'
|
|
11
12
|
export { default as SparkOverlay } from './SparkOverlay.vue'
|
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'
|