@veristone/nuxt-v-app 0.2.1 → 0.2.3

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.
@@ -89,6 +89,7 @@ const handleDelete = async () => {
89
89
  <template>
90
90
  <UModal v-model:open="isOpen">
91
91
  <UButton
92
+ v-if="open === undefined"
92
93
  :icon="triggerIcon"
93
94
  :color="triggerColor as any"
94
95
  :variant="triggerVariant as any"
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * useVCrud - Veristone CRUD Composable
3
3
  * Optimized for V-App data flows.
4
- *
4
+ *
5
5
  * Uses $fetch for mutations (POST, PUT, PATCH, DELETE) to avoid Nuxt's useFetch caching issues.
6
6
  * Uses useVFetch only for initial data loading (SSR compatible).
7
7
  */
@@ -156,8 +156,7 @@ export const useVCrud = <T = any>(endpoint: string, options: VCrudOptions<T> = {
156
156
  onUpdated,
157
157
  onDeleted,
158
158
  } = options
159
-
160
-
159
+
161
160
  // Renamed internal state for clarity
162
161
  const isBusy = useState<boolean>(`v-crud-busy-${endpoint}`, () => false)
163
162
  const lastError = useState<string | null>(`v-crud-error-${endpoint}`, () => null)
@@ -222,12 +221,12 @@ export const useVCrud = <T = any>(endpoint: string, options: VCrudOptions<T> = {
222
221
  'Content-Type': 'application/json',
223
222
  'X-Client-Version': 'v-app-1.0'
224
223
  }
225
-
224
+
226
225
  const token = getAuthToken()
227
226
  if (token) {
228
227
  headers['Authorization'] = `Bearer ${token}`
229
228
  }
230
-
229
+
231
230
  return headers
232
231
  }
233
232
 
@@ -344,7 +343,7 @@ export const useVCrud = <T = any>(endpoint: string, options: VCrudOptions<T> = {
344
343
  isBusy.value = true
345
344
  lastError.value = null
346
345
  isCreating.value = true
347
-
346
+
348
347
  try {
349
348
  const hasFile = payload instanceof FormData || Object.values(payload || {}).some((v: any) => v instanceof File || v instanceof FileList)
350
349
  let body: any = payload
@@ -599,6 +598,44 @@ export const useVCrud = <T = any>(endpoint: string, options: VCrudOptions<T> = {
599
598
  isBusy.value = false
600
599
  }
601
600
  }
601
+ // CUSTOM - For nested resource operations like /tapes/:id/loans
602
+ const custom = async <T = any>(
603
+ subPath: string,
604
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET',
605
+ payload?: any,
606
+ id?: string | number
607
+ ) => {
608
+ isBusy.value = true
609
+ lastError.value = null
610
+ try {
611
+ const url = id !== undefined
612
+ ? `${constructUrl(endpoint, id)}/${subPath}`
613
+ : `${constructUrl(endpoint)}/${subPath}`
614
+
615
+ const hasFile = payload instanceof FormData || Object.values(payload || {}).some((v: any) => v instanceof File || v instanceof FileList)
616
+ let body: any = payload
617
+ if (hasFile && !(payload instanceof FormData)) {
618
+ body = new FormData()
619
+ Object.entries(payload || {}).forEach(([k, v]: any) => {
620
+ if (v instanceof FileList) Array.from(v).forEach(f => body.append(k, f))
621
+ else if (v instanceof File) body.append(k, v)
622
+ else if (v !== undefined && v !== null) body.append(k, String(v))
623
+ })
624
+ }
625
+ const result = await $fetch<T>(url, {
626
+ method,
627
+ baseURL: getBaseUrl(),
628
+ headers: getMutationHeaders(body),
629
+ body: method !== 'GET' ? body : undefined,
630
+ params: method === 'GET' && payload ? payload : undefined
631
+ })
632
+ return result
633
+ } catch (err) {
634
+ handleError(`Custom ${method}`, err)
635
+ } finally {
636
+ isBusy.value = false
637
+ }
638
+ }
602
639
 
603
640
  // Auto-refresh list view when list inputs change.
604
641
  watch([filterValues, searchTerm, sortConfig], async () => {
@@ -632,6 +669,7 @@ export const useVCrud = <T = any>(endpoint: string, options: VCrudOptions<T> = {
632
669
  patch,
633
670
  remove,
634
671
  bulkRemove,
672
+ custom,
635
673
  loading: isBusy, // Alias internal 'isBusy' to public 'loading'
636
674
  errorState: lastError, // Alias internal 'lastError' to public 'errorState'
637
675
 
@@ -1,7 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  const endpoint = "http://localhost:3002/admin/portfolio/loans";
3
+ const tapesEndpoint = "http://localhost:3002/admin/portfolio/tapes";
3
4
 
4
5
  const crud = useVCrud(endpoint, { immediate: false });
6
+ const tapesCrud = useVCrud(tapesEndpoint, { immediate: false });
5
7
 
6
8
  // Store results for each method
7
9
  const results = ref<Record<string, any>>({});
@@ -100,6 +102,22 @@ const testDelete = async () => {
100
102
  }
101
103
  };
102
104
 
105
+ // Test CUSTOM - nested resource (tapes/:id/loans)
106
+ const testCustom = async () => {
107
+ const tapeId = "5e8523a8-43bf-44ac-a682-7b48e1c823e8";
108
+ loading.value.custom = true;
109
+ errors.value.custom = "";
110
+ try {
111
+ // This will call: POST /admin/portfolio/tapes/5e8523a8-43bf-44ac-a682-7b48e1c823e8/loans
112
+ const data = await tapesCrud.custom("loans", "POST", {}, tapeId);
113
+ results.value.custom = data;
114
+ } catch (err: any) {
115
+ errors.value.custom = err.message || "Failed";
116
+ } finally {
117
+ loading.value.custom = false;
118
+ }
119
+ };
120
+
103
121
  // Run all tests
104
122
  const testAll = async () => {
105
123
  await testGet();
@@ -266,5 +284,33 @@ const testAll = async () => {
266
284
  >{{ JSON.stringify(results.delete, null, 2) }}</pre
267
285
  >
268
286
  </div>
287
+
288
+ <!-- CUSTOM (nested resource) -->
289
+ <div class="border-2 border-indigo-500 p-4 rounded">
290
+ <div class="flex items-center justify-between mb-4">
291
+ <h2 class="text-xl font-semibold">CUSTOM (nested resource)</h2>
292
+ <button
293
+ @click="testCustom"
294
+ :disabled="loading.custom"
295
+ class="px-3 py-1 bg-indigo-500 text-white rounded hover:bg-indigo-600 disabled:opacity-50"
296
+ >
297
+ {{ loading.custom ? "Loading..." : "Test CUSTOM" }}
298
+ </button>
299
+ </div>
300
+ <div class="text-sm text-gray-500 mb-2">
301
+ POST /admin/portfolio/tapes/5e8523a8-43bf-44ac-a682-7b48e1c823e8/loans
302
+ </div>
303
+ <div class="text-xs text-gray-400 mb-2">
304
+ Using: tapesCrud.custom("loans", "POST", {}, tapeId)
305
+ </div>
306
+ <div v-if="errors.custom" class="text-red-500 mb-2">
307
+ Error: {{ errors.custom }}
308
+ </div>
309
+ <pre
310
+ v-if="results.custom"
311
+ class="text-sm bg-gray-100 p-4 rounded overflow-auto max-h-48"
312
+ >{{ JSON.stringify(results.custom, null, 2) }}</pre
313
+ >
314
+ </div>
269
315
  </div>
270
316
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veristone/nuxt-v-app",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "description": "Veristone Nuxt App Layer - Shared components, composables, and layouts",
6
6
  "keywords": [