@jskit-ai/http-web 0.1.3 → 0.1.5

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.
Files changed (25) hide show
  1. package/package.json +11 -37
  2. package/patterns/crud-screen-set/PATTERN.md +88 -0
  3. package/patterns/crud-screen-set/example/books/BookEditPage.vue +54 -0
  4. package/patterns/crud-screen-set/example/books/BookFormFields.vue +52 -0
  5. package/patterns/crud-screen-set/example/books/BookListPage.vue +54 -0
  6. package/patterns/crud-screen-set/example/books/BookNewPage.vue +45 -0
  7. package/patterns/crud-screen-set/example/books/BookViewPage.vue +50 -0
  8. package/patterns/crud-screen-set/example/books/formFields.js +31 -0
  9. package/patterns/crud-screen-set/example/books/listExtensions.js +9 -0
  10. package/src/client/components/CrudAddEditScreen.vue +29 -31
  11. package/src/client/components/CrudDeleteAction.vue +1 -2
  12. package/src/client/components/CrudListBulkActionSurface.vue +2 -3
  13. package/src/client/components/CrudListRecordActionMenu.vue +3 -9
  14. package/src/client/components/CrudListScreen.vue +80 -63
  15. package/src/client/components/CrudViewScreen.vue +32 -33
  16. package/src/client/composables/crud/crudSchemaFormHelpers.js +1 -1
  17. package/src/client/composables/support/errorMessageHelpers.js +4 -2
  18. package/src/client/composables/useCrudDeleteAction.js +2 -2
  19. package/src/client/composables/useCrudListScreen.js +2 -2
  20. package/src/client/composables/useCrudViewScreen.js +2 -2
  21. package/test/crudScreenComponents.test.js +31 -14
  22. package/test/crudScreenPattern.test.js +60 -0
  23. package/test/errorMessageHelpers.test.js +13 -2
  24. package/test/useCrudAddEdit.test.js +20 -0
  25. package/test/useCrudDeleteAction.test.js +1 -1
@@ -100,19 +100,13 @@ function runRowAction(action = {}) {
100
100
  <v-list-item
101
101
  v-for="action in visibleRowActions"
102
102
  :key="action.key"
103
- :title="action.label"
103
+ :title="isRowActionLoading(action) ? `${action.label}…` : action.label"
104
104
  :base-color="action.color"
105
105
  :disabled="isRowActionDisabled(action)"
106
106
  @click="runRowAction(action)"
107
107
  >
108
- <template v-if="isRowActionLoading(action) || action.icon" #prepend>
109
- <v-progress-circular
110
- v-if="isRowActionLoading(action)"
111
- indeterminate
112
- size="18"
113
- width="2"
114
- />
115
- <v-icon v-else :icon="action.icon" />
108
+ <template v-if="action.icon" #prepend>
109
+ <v-icon :icon="action.icon" />
116
110
  </template>
117
111
  </v-list-item>
118
112
  </v-list>
@@ -126,18 +126,25 @@ function setSelectableRowsSelected(selected = true) {
126
126
  </script>
127
127
 
128
128
  <template>
129
- <section class="generated-ui-screen generated-ui-screen--operator ui-generator-list-element d-flex flex-column ga-4">
130
- <header class="ui-generator-list-header">
131
- <div class="ui-generator-list-header__copy">
129
+ <section class="crud-screen crud-screen--operator crud-list-element d-flex flex-column ga-4">
130
+ <header class="crud-list-header">
131
+ <div class="crud-list-header__copy">
132
132
  <p class="text-overline text-medium-emphasis mb-1">{{ titleLabel }}</p>
133
- <h1 class="ui-generator-list-header__title">{{ resolvedHeadingTitle }}</h1>
133
+ <h1 class="crud-list-header__title">{{ resolvedHeadingTitle }}</h1>
134
134
  <p class="text-body-2 text-medium-emphasis mb-0">{{ resolvedSubtitle }}</p>
135
135
  </div>
136
- <div class="ui-generator-list-header__actions">
137
- <v-btn color="primary" variant="tonal" :loading="records.isFetching" @click="records.reload">Refresh</v-btn>
136
+ <div class="crud-list-header__actions">
137
+ <v-btn
138
+ color="primary"
139
+ variant="tonal"
140
+ :disabled="records.isFetching"
141
+ @click="records.reload"
142
+ >
143
+ {{ records.isFetching ? "Refreshing…" : "Refresh" }}
144
+ </v-btn>
138
145
  <v-btn
139
146
  v-if="listPrimaryAction"
140
- class="ui-generator-list-header__primary-action"
147
+ class="crud-list-header__primary-action"
141
148
  color="primary"
142
149
  variant="flat"
143
150
  :to="listPrimaryAction"
@@ -147,8 +154,8 @@ function setSelectableRowsSelected(selected = true) {
147
154
  </div>
148
155
  </header>
149
156
 
150
- <v-sheet rounded="lg" border class="ui-generator-list-panel">
151
- <div class="ui-generator-list-toolbar">
157
+ <v-sheet rounded="lg" border class="crud-list-panel">
158
+ <div class="crud-list-toolbar">
152
159
  <v-text-field
153
160
  v-if="records.searchEnabled"
154
161
  v-model="records.searchQuery"
@@ -158,8 +165,8 @@ function setSelectableRowsSelected(selected = true) {
158
165
  density="comfortable"
159
166
  hide-details="auto"
160
167
  clearable
161
- class="ui-generator-list-search"
162
- :loading="records.isSearchDebouncing"
168
+ class="crud-list-search"
169
+ :aria-busy="records.isSearchDebouncing ? 'true' : undefined"
163
170
  />
164
171
  <CrudListFilterSurface
165
172
  :filters="listFilters"
@@ -174,15 +181,20 @@ function setSelectableRowsSelected(selected = true) {
174
181
  </div>
175
182
  </template>
176
183
  <template v-else>
177
- <v-progress-linear v-if="records.isRefetching" indeterminate />
178
-
179
- <div v-if="records.loadError" class="ui-generator-list-state">
184
+ <div v-if="records.loadError" class="crud-list-state">
180
185
  <h2 class="text-h6 mb-2">{{ loadErrorTitle }}</h2>
181
186
  <p class="text-body-2 text-medium-emphasis mb-4">{{ loadErrorBody }}</p>
182
- <v-btn color="primary" variant="tonal" :loading="records.isFetching" @click="records.reload">Retry</v-btn>
187
+ <v-btn
188
+ color="primary"
189
+ variant="tonal"
190
+ :disabled="records.isFetching"
191
+ @click="records.reload"
192
+ >
193
+ {{ records.isFetching ? "Retrying…" : "Retry" }}
194
+ </v-btn>
183
195
  </div>
184
196
 
185
- <div v-else-if="displayRows.length < 1" class="ui-generator-list-state">
197
+ <div v-else-if="displayRows.length < 1" class="crud-list-state">
186
198
  <h2 class="text-h6 mb-2">{{ emptyTitle }}</h2>
187
199
  <p class="text-body-2 text-medium-emphasis mb-4">{{ emptyBody }}</p>
188
200
  <v-btn v-if="listPrimaryAction" color="primary" variant="flat" :to="listPrimaryAction">
@@ -191,24 +203,24 @@ function setSelectableRowsSelected(selected = true) {
191
203
  </div>
192
204
 
193
205
  <template v-else>
194
- <div class="ui-generator-list-cards">
206
+ <div class="crud-list-cards">
195
207
  <v-sheet
196
208
  v-for="row in displayRows"
197
209
  :key="row.key"
198
210
  rounded="lg"
199
211
  border
200
- class="ui-generator-list-card"
212
+ class="crud-list-card"
201
213
  >
202
- <div class="ui-generator-list-card__header">
214
+ <div class="crud-list-card__header">
203
215
  <v-checkbox-btn
204
216
  v-if="hasBulkActions && row.selectable !== false"
205
217
  :model-value="isBulkRowSelected(row)"
206
218
  :aria-label="`Select ${resolveListRecordTitle(row.record)}`"
207
- class="ui-generator-list-card__select"
219
+ class="crud-list-card__select"
208
220
  @update:model-value="setBulkRowSelected(row, $event)"
209
221
  />
210
222
  <div class="min-w-0">
211
- <div class="ui-generator-list-card__title">{{ resolveListRecordTitle(row.record) }}</div>
223
+ <div class="crud-list-card__title">{{ resolveListRecordTitle(row.record) }}</div>
212
224
  <div class="text-caption text-medium-emphasis">
213
225
  {{ row.recordKey }}
214
226
  </div>
@@ -224,7 +236,7 @@ function setSelectableRowsSelected(selected = true) {
224
236
  :edit-location="resolveEditLocation(row.record)"
225
237
  />
226
238
  </div>
227
- <div class="ui-generator-list-card__fields">
239
+ <div class="crud-list-card__fields">
228
240
  <slot
229
241
  name="card-fields"
230
242
  :record="row.record"
@@ -237,11 +249,11 @@ function setSelectableRowsSelected(selected = true) {
237
249
  </v-sheet>
238
250
  </div>
239
251
 
240
- <div class="ui-generator-list-table">
252
+ <div class="crud-list-table">
241
253
  <v-table density="comfortable">
242
254
  <thead>
243
255
  <tr>
244
- <th v-if="hasBulkActions" class="ui-generator-list-table__select">
256
+ <th v-if="hasBulkActions" class="crud-list-table__select">
245
257
  <v-checkbox-btn
246
258
  :model-value="allSelectableRowsSelected()"
247
259
  :indeterminate="
@@ -260,7 +272,7 @@ function setSelectableRowsSelected(selected = true) {
260
272
  </thead>
261
273
  <tbody>
262
274
  <tr v-for="row in displayRows" :key="row.key">
263
- <td v-if="hasBulkActions" class="ui-generator-list-table__select">
275
+ <td v-if="hasBulkActions" class="crud-list-table__select">
264
276
  <v-checkbox-btn
265
277
  v-if="row.selectable !== false"
266
278
  :model-value="isBulkRowSelected(row)"
@@ -316,8 +328,13 @@ function setSelectableRowsSelected(selected = true) {
316
328
  </template>
317
329
 
318
330
  <div v-if="records.hasMore" class="d-flex justify-center pa-4">
319
- <v-btn color="primary" variant="outlined" :loading="records.isLoadingMore" @click="records.loadMore">
320
- Load more
331
+ <v-btn
332
+ color="primary"
333
+ variant="outlined"
334
+ :disabled="records.isLoadingMore"
335
+ @click="records.loadMore"
336
+ >
337
+ {{ records.isLoadingMore ? "Loading more…" : "Load more" }}
321
338
  </v-btn>
322
339
  </div>
323
340
  </template>
@@ -325,7 +342,7 @@ function setSelectableRowsSelected(selected = true) {
325
342
 
326
343
  <v-btn
327
344
  v-if="listPrimaryAction"
328
- class="ui-generator-list-fab"
345
+ class="crud-list-fab"
329
346
  color="primary"
330
347
  variant="flat"
331
348
  :to="listPrimaryAction"
@@ -336,103 +353,103 @@ function setSelectableRowsSelected(selected = true) {
336
353
  </template>
337
354
 
338
355
  <style scoped>
339
- .generated-ui-screen {
340
- --generated-ui-screen-title-size: clamp(1.35rem, 2vw, 1.85rem);
341
- --generated-ui-screen-state-padding: 2.5rem 1.25rem;
356
+ .crud-screen {
357
+ --crud-screen-title-size: clamp(1.35rem, 2vw, 1.85rem);
358
+ --crud-screen-state-padding: 2.5rem 1.25rem;
342
359
  }
343
360
 
344
- .generated-ui-screen--operator {
345
- --generated-ui-screen-state-padding: 2rem 1rem;
361
+ .crud-screen--operator {
362
+ --crud-screen-state-padding: 2rem 1rem;
346
363
  }
347
364
 
348
- .ui-generator-list-header {
365
+ .crud-list-header {
349
366
  align-items: flex-start;
350
367
  display: flex;
351
368
  gap: 1rem;
352
369
  justify-content: space-between;
353
370
  }
354
371
 
355
- .ui-generator-list-header__copy {
372
+ .crud-list-header__copy {
356
373
  min-width: 0;
357
374
  }
358
375
 
359
- .ui-generator-list-header__title {
360
- font-size: var(--generated-ui-screen-title-size);
376
+ .crud-list-header__title {
377
+ font-size: var(--crud-screen-title-size);
361
378
  font-weight: 650;
362
379
  letter-spacing: -0.02em;
363
380
  line-height: 1.15;
364
381
  margin: 0 0 0.35rem;
365
382
  }
366
383
 
367
- .ui-generator-list-header__actions {
384
+ .crud-list-header__actions {
368
385
  display: flex;
369
386
  flex-wrap: wrap;
370
387
  gap: 0.5rem;
371
388
  justify-content: flex-end;
372
389
  }
373
390
 
374
- .ui-generator-list-panel {
391
+ .crud-list-panel {
375
392
  overflow: hidden;
376
393
  }
377
394
 
378
- .ui-generator-list-toolbar {
395
+ .crud-list-toolbar {
379
396
  padding: 1rem;
380
397
  }
381
398
 
382
- .ui-generator-list-search {
399
+ .crud-list-search {
383
400
  max-width: 26rem;
384
401
  }
385
402
 
386
- .ui-generator-list-state {
403
+ .crud-list-state {
387
404
  margin-inline: auto;
388
405
  max-width: 30rem;
389
- padding: var(--generated-ui-screen-state-padding);
406
+ padding: var(--crud-screen-state-padding);
390
407
  text-align: center;
391
408
  }
392
409
 
393
- .ui-generator-list-cards {
410
+ .crud-list-cards {
394
411
  display: flex;
395
412
  flex-direction: column;
396
413
  gap: 0.75rem;
397
414
  padding: 0 1rem 1rem;
398
415
  }
399
416
 
400
- .ui-generator-list-card {
417
+ .crud-list-card {
401
418
  padding: 0.875rem;
402
419
  }
403
420
 
404
- .ui-generator-list-card__header {
421
+ .crud-list-card__header {
405
422
  align-items: flex-start;
406
423
  display: flex;
407
424
  gap: 0.75rem;
408
425
  justify-content: space-between;
409
426
  }
410
427
 
411
- .ui-generator-list-card__select {
428
+ .crud-list-card__select {
412
429
  flex: 0 0 auto;
413
430
  margin-inline-start: -0.35rem;
414
431
  margin-top: -0.35rem;
415
432
  }
416
433
 
417
- .ui-generator-list-card__title {
434
+ .crud-list-card__title {
418
435
  font-size: 1rem;
419
436
  font-weight: 650;
420
437
  line-height: 1.25;
421
438
  overflow-wrap: anywhere;
422
439
  }
423
440
 
424
- .ui-generator-list-card__fields {
441
+ .crud-list-card__fields {
425
442
  display: grid;
426
443
  gap: 0.65rem;
427
444
  margin-top: 0.85rem;
428
445
  }
429
446
 
430
- .ui-generator-list-card__field {
447
+ .crud-list-card__field {
431
448
  display: grid;
432
449
  gap: 0.15rem;
433
450
  }
434
451
 
435
- .ui-generator-list-card__field-label {
452
+ .crud-list-card__field-label {
436
453
  color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
437
454
  font-size: 0.72rem;
438
455
  letter-spacing: 0.04em;
@@ -440,22 +457,22 @@ function setSelectableRowsSelected(selected = true) {
440
457
  text-transform: uppercase;
441
458
  }
442
459
 
443
- .ui-generator-list-card__field-value {
460
+ .crud-list-card__field-value {
444
461
  font-size: 0.95rem;
445
462
  line-height: 1.35;
446
463
  overflow-wrap: anywhere;
447
464
  }
448
465
 
449
- .ui-generator-list-table {
466
+ .crud-list-table {
450
467
  display: none;
451
468
  overflow-x: auto;
452
469
  }
453
470
 
454
- .ui-generator-list-table__select {
471
+ .crud-list-table__select {
455
472
  width: 3rem;
456
473
  }
457
474
 
458
- .ui-generator-list-fab {
475
+ .crud-list-fab {
459
476
  bottom: calc(5rem + env(safe-area-inset-bottom, 0px));
460
477
  display: flex;
461
478
  position: fixed;
@@ -464,37 +481,37 @@ function setSelectableRowsSelected(selected = true) {
464
481
  }
465
482
 
466
483
  @media (min-width: 960px) {
467
- .ui-generator-list-cards {
484
+ .crud-list-cards {
468
485
  display: none;
469
486
  }
470
487
 
471
- .ui-generator-list-table {
488
+ .crud-list-table {
472
489
  display: block;
473
490
  }
474
491
 
475
- .ui-generator-list-fab {
492
+ .crud-list-fab {
476
493
  display: none;
477
494
  }
478
495
  }
479
496
 
480
497
  @media (max-width: 960px) {
481
- .ui-generator-list-header {
498
+ .crud-list-header {
482
499
  flex-direction: column;
483
500
  }
484
501
 
485
- .ui-generator-list-header__actions {
502
+ .crud-list-header__actions {
486
503
  width: 100%;
487
504
  }
488
505
 
489
- .ui-generator-list-element :deep(.v-btn) {
506
+ .crud-list-element :deep(.v-btn) {
490
507
  min-height: 48px;
491
508
  }
492
509
 
493
- .ui-generator-list-header__primary-action {
510
+ .crud-list-header__primary-action {
494
511
  display: none;
495
512
  }
496
513
 
497
- .ui-generator-list-search {
514
+ .crud-list-search {
498
515
  max-width: none;
499
516
  }
500
517
  }
@@ -34,14 +34,14 @@ const resolvedDescription = computed(() =>
34
34
  </script>
35
35
 
36
36
  <template>
37
- <section class="generated-ui-screen generated-ui-screen--operator ui-generator-view-element d-flex flex-column ga-4">
38
- <header class="ui-generator-view-header">
39
- <div class="ui-generator-view-header__copy">
37
+ <section class="crud-screen crud-screen--operator crud-view-element d-flex flex-column ga-4">
38
+ <header class="crud-view-header">
39
+ <div class="crud-view-header__copy">
40
40
  <p class="text-overline text-medium-emphasis mb-1">{{ resourceSingularTitle }}</p>
41
- <h1 class="ui-generator-view-header__title">{{ recordTitle }}</h1>
41
+ <h1 class="crud-view-header__title">{{ recordTitle }}</h1>
42
42
  <p class="text-body-2 text-medium-emphasis mb-0">{{ resolvedDescription }}</p>
43
43
  </div>
44
- <div class="ui-generator-view-header__actions">
44
+ <div class="crud-view-header__actions">
45
45
  <v-btn
46
46
  v-if="listLocation"
47
47
  color="primary"
@@ -62,21 +62,21 @@ const resolvedDescription = computed(() =>
62
62
  </div>
63
63
  </header>
64
64
 
65
- <v-sheet rounded="lg" border class="ui-generator-view-panel">
66
- <div v-if="view.loadError || view.isNotFound" class="ui-generator-view-state">
65
+ <v-sheet rounded="lg" border class="crud-view-panel">
66
+ <div v-if="view.loadError || view.isNotFound" class="crud-view-state">
67
67
  <h2 class="text-h6 mb-2">{{ unavailableTitle }}</h2>
68
68
  <p class="text-body-2 text-medium-emphasis mb-4">
69
69
  {{ view.loadError || `This ${resourceSingularTitle} could not be found.` }}
70
70
  </p>
71
- <div class="ui-generator-view-state__actions">
71
+ <div class="crud-view-state__actions">
72
72
  <v-btn
73
73
  v-if="view.loadError"
74
74
  color="primary"
75
75
  variant="tonal"
76
- :loading="view.isFetching"
76
+ :disabled="view.isFetching"
77
77
  @click="view.refresh"
78
78
  >
79
- Retry
79
+ {{ view.isFetching ? "Retrying…" : "Retry" }}
80
80
  </v-btn>
81
81
  <v-btn
82
82
  v-else-if="listLocation"
@@ -96,10 +96,9 @@ const resolvedDescription = computed(() =>
96
96
  </template>
97
97
 
98
98
  <template v-else>
99
- <v-progress-linear v-if="view.isRefetching" indeterminate />
100
99
  <div class="pa-4">
101
100
  <slot name="before-fields" :view="view" />
102
- <v-row class="ui-generator-view-fields">
101
+ <v-row class="crud-view-fields">
103
102
  <slot name="fields" :view="view" />
104
103
  </v-row>
105
104
  <slot name="after-fields" :view="view" />
@@ -107,35 +106,35 @@ const resolvedDescription = computed(() =>
107
106
  </template>
108
107
  </v-sheet>
109
108
 
110
- <div v-if="$slots['supporting-content']" class="ui-generator-view-supporting-content">
109
+ <div v-if="$slots['supporting-content']" class="crud-view-supporting-content">
111
110
  <slot name="supporting-content" :view="view" />
112
111
  </div>
113
112
  </section>
114
113
  </template>
115
114
 
116
115
  <style scoped>
117
- .generated-ui-screen {
118
- --generated-ui-screen-title-size: clamp(1.35rem, 2vw, 1.85rem);
119
- --generated-ui-screen-state-padding: 2.5rem 1.25rem;
116
+ .crud-screen {
117
+ --crud-screen-title-size: clamp(1.35rem, 2vw, 1.85rem);
118
+ --crud-screen-state-padding: 2.5rem 1.25rem;
120
119
  }
121
120
 
122
- .generated-ui-screen--operator {
123
- --generated-ui-screen-state-padding: 2rem 1rem;
121
+ .crud-screen--operator {
122
+ --crud-screen-state-padding: 2rem 1rem;
124
123
  }
125
124
 
126
- .ui-generator-view-header {
125
+ .crud-view-header {
127
126
  align-items: flex-start;
128
127
  display: flex;
129
128
  gap: 1rem;
130
129
  justify-content: space-between;
131
130
  }
132
131
 
133
- .ui-generator-view-header__copy {
132
+ .crud-view-header__copy {
134
133
  min-width: 0;
135
134
  }
136
135
 
137
- .ui-generator-view-header__title {
138
- font-size: var(--generated-ui-screen-title-size);
136
+ .crud-view-header__title {
137
+ font-size: var(--crud-screen-title-size);
139
138
  font-weight: 650;
140
139
  letter-spacing: -0.02em;
141
140
  line-height: 1.15;
@@ -143,54 +142,54 @@ const resolvedDescription = computed(() =>
143
142
  overflow-wrap: anywhere;
144
143
  }
145
144
 
146
- .ui-generator-view-header__actions {
145
+ .crud-view-header__actions {
147
146
  display: flex;
148
147
  flex-wrap: wrap;
149
148
  gap: 0.5rem;
150
149
  justify-content: flex-end;
151
150
  }
152
151
 
153
- .ui-generator-view-panel {
152
+ .crud-view-panel {
154
153
  overflow: hidden;
155
154
  }
156
155
 
157
- .ui-generator-view-state {
156
+ .crud-view-state {
158
157
  margin-inline: auto;
159
158
  max-width: 30rem;
160
- padding: var(--generated-ui-screen-state-padding);
159
+ padding: var(--crud-screen-state-padding);
161
160
  text-align: center;
162
161
  }
163
162
 
164
- .ui-generator-view-state__actions {
163
+ .crud-view-state__actions {
165
164
  display: flex;
166
165
  flex-wrap: wrap;
167
166
  gap: 0.5rem;
168
167
  justify-content: center;
169
168
  }
170
169
 
171
- .ui-generator-view-fields :deep(.v-col) {
170
+ .crud-view-fields :deep(.v-col) {
172
171
  min-width: 0;
173
172
  }
174
173
 
175
- .ui-generator-view-supporting-content {
174
+ .crud-view-supporting-content {
176
175
  min-width: 0;
177
176
  }
178
177
 
179
178
  @media (max-width: 960px) {
180
- .ui-generator-view-header {
179
+ .crud-view-header {
181
180
  flex-direction: column;
182
181
  }
183
182
 
184
- .ui-generator-view-header__actions {
183
+ .crud-view-header__actions {
185
184
  width: 100%;
186
185
  }
187
186
 
188
- .ui-generator-view-header__actions :deep(.v-btn) {
187
+ .crud-view-header__actions :deep(.v-btn) {
189
188
  min-height: 48px;
190
189
  flex: 1 1 10rem;
191
190
  }
192
191
 
193
- .ui-generator-view-state__actions :deep(.v-btn) {
192
+ .crud-view-state__actions :deep(.v-btn) {
194
193
  min-height: 48px;
195
194
  }
196
195
  }
@@ -296,7 +296,7 @@ function buildCrudFormPayload(fields = [], model = {}) {
296
296
  }
297
297
 
298
298
  if (rawValue == null) {
299
- if (clearAsNull) {
299
+ if (isNullableFormField(field)) {
300
300
  payload[fieldKey] = null;
301
301
  }
302
302
  continue;
@@ -48,11 +48,13 @@ function toQueryErrorMessage(error, fallbackMessage = "", defaultMessage = "Requ
48
48
 
49
49
  function toUiErrorMessage(error, fallbackMessage = "", defaultMessage = "Request failed.") {
50
50
  const normalizedFallback = String(fallbackMessage || "").trim();
51
+ const normalizedMessage = String(error?.message || "").trim();
52
+ if (normalizedMessage && !isGenericTransportMessage(error)) {
53
+ return normalizedMessage;
54
+ }
51
55
  if (normalizedFallback) {
52
56
  return normalizedFallback;
53
57
  }
54
-
55
- const normalizedMessage = String(error?.message || "").trim();
56
58
  if (normalizedMessage) {
57
59
  return normalizedMessage;
58
60
  }
@@ -62,7 +62,7 @@ function useCrudDeleteAction({
62
62
 
63
63
  async function handleDeleteSuccess(_response, context = {}) {
64
64
  await context?.queryClient?.invalidateQueries?.({
65
- queryKey: ["ui-generator", namespace]
65
+ queryKey: ["crud", namespace]
66
66
  });
67
67
 
68
68
  const listLocation = resolveListLocation(resolvedScreen);
@@ -90,7 +90,7 @@ function useCrudDeleteAction({
90
90
  transport: resolveCrudJsonApiTransport(undefined, resource, {
91
91
  mode: "delete"
92
92
  }),
93
- placementSource: `ui-generator.${namespace}.view.delete`,
93
+ placementSource: `crud.${namespace}.view.delete`,
94
94
  fallbackRunError: fallbackDeleteError,
95
95
  onRunSuccess: handleDeleteSuccess,
96
96
  onRunError: handleDeleteError,
@@ -145,7 +145,7 @@ function useCrudListScreen({
145
145
  resource,
146
146
  apiSuffix,
147
147
  queryKeyFactory: (surfaceId = "", workspaceSlug = "") => [
148
- "ui-generator",
148
+ "crud",
149
149
  normalizedResourceNamespace,
150
150
  "list",
151
151
  String(surfaceId || ""),
@@ -164,7 +164,7 @@ function useCrudListScreen({
164
164
  queryParams: true,
165
165
  queryParamBlacklist: routeQueryBlacklist
166
166
  },
167
- placementSource: `ui-generator.${normalizedResourceNamespace}.list`,
167
+ placementSource: `crud.${normalizedResourceNamespace}.list`,
168
168
  requestQueryParams,
169
169
  requestFieldsets,
170
170
  readEnabled,
@@ -23,7 +23,7 @@ function useCrudViewScreen({
23
23
  const route = useRoute();
24
24
  const normalizedResourceNamespace = String(resourceNamespace || "resource").trim() || "resource";
25
25
  const defaultQueryKeyFactory = (surfaceId = "", workspaceSlug = "") => [
26
- "ui-generator",
26
+ "crud",
27
27
  normalizedResourceNamespace,
28
28
  "view",
29
29
  String(surfaceId || ""),
@@ -39,7 +39,7 @@ function useCrudViewScreen({
39
39
  requestQueryParams,
40
40
  requestFieldsets,
41
41
  readEnabled,
42
- placementSource: `ui-generator.${normalizedResourceNamespace}.view`,
42
+ placementSource: `crud.${normalizedResourceNamespace}.view`,
43
43
  requestRecoveryLabel,
44
44
  fallbackLoadError,
45
45
  notFoundMessage,