@jskit-ai/http-web 0.1.4 → 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.
- package/package.json +11 -37
- package/patterns/crud-screen-set/PATTERN.md +88 -0
- package/patterns/crud-screen-set/example/books/BookEditPage.vue +54 -0
- package/patterns/crud-screen-set/example/books/BookFormFields.vue +52 -0
- package/patterns/crud-screen-set/example/books/BookListPage.vue +54 -0
- package/patterns/crud-screen-set/example/books/BookNewPage.vue +45 -0
- package/patterns/crud-screen-set/example/books/BookViewPage.vue +50 -0
- package/patterns/crud-screen-set/example/books/formFields.js +31 -0
- package/patterns/crud-screen-set/example/books/listExtensions.js +9 -0
- package/src/client/components/CrudAddEditScreen.vue +29 -31
- package/src/client/components/CrudDeleteAction.vue +1 -2
- package/src/client/components/CrudListBulkActionSurface.vue +2 -3
- package/src/client/components/CrudListRecordActionMenu.vue +3 -9
- package/src/client/components/CrudListScreen.vue +80 -63
- package/src/client/components/CrudViewScreen.vue +32 -33
- package/src/client/composables/crud/crudSchemaFormHelpers.js +1 -1
- package/src/client/composables/support/errorMessageHelpers.js +4 -2
- package/src/client/composables/useCrudDeleteAction.js +2 -2
- package/src/client/composables/useCrudListScreen.js +2 -2
- package/src/client/composables/useCrudViewScreen.js +2 -2
- package/test/crudScreenComponents.test.js +31 -14
- package/test/crudScreenPattern.test.js +60 -0
- package/test/errorMessageHelpers.test.js +13 -2
- package/test/useCrudAddEdit.test.js +20 -0
- 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="
|
|
109
|
-
<v-
|
|
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="
|
|
130
|
-
<header class="
|
|
131
|
-
<div class="
|
|
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="
|
|
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="
|
|
137
|
-
<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="
|
|
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="
|
|
151
|
-
<div class="
|
|
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="
|
|
162
|
-
:
|
|
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
|
-
<
|
|
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
|
|
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="
|
|
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="
|
|
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="
|
|
212
|
+
class="crud-list-card"
|
|
201
213
|
>
|
|
202
|
-
<div class="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
252
|
+
<div class="crud-list-table">
|
|
241
253
|
<v-table density="comfortable">
|
|
242
254
|
<thead>
|
|
243
255
|
<tr>
|
|
244
|
-
<th v-if="hasBulkActions" class="
|
|
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="
|
|
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
|
|
320
|
-
|
|
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="
|
|
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
|
-
.
|
|
340
|
-
--
|
|
341
|
-
--
|
|
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
|
-
.
|
|
345
|
-
--
|
|
361
|
+
.crud-screen--operator {
|
|
362
|
+
--crud-screen-state-padding: 2rem 1rem;
|
|
346
363
|
}
|
|
347
364
|
|
|
348
|
-
.
|
|
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
|
-
.
|
|
372
|
+
.crud-list-header__copy {
|
|
356
373
|
min-width: 0;
|
|
357
374
|
}
|
|
358
375
|
|
|
359
|
-
.
|
|
360
|
-
font-size: var(--
|
|
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
|
-
.
|
|
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
|
-
.
|
|
391
|
+
.crud-list-panel {
|
|
375
392
|
overflow: hidden;
|
|
376
393
|
}
|
|
377
394
|
|
|
378
|
-
.
|
|
395
|
+
.crud-list-toolbar {
|
|
379
396
|
padding: 1rem;
|
|
380
397
|
}
|
|
381
398
|
|
|
382
|
-
.
|
|
399
|
+
.crud-list-search {
|
|
383
400
|
max-width: 26rem;
|
|
384
401
|
}
|
|
385
402
|
|
|
386
|
-
.
|
|
403
|
+
.crud-list-state {
|
|
387
404
|
margin-inline: auto;
|
|
388
405
|
max-width: 30rem;
|
|
389
|
-
padding: var(--
|
|
406
|
+
padding: var(--crud-screen-state-padding);
|
|
390
407
|
text-align: center;
|
|
391
408
|
}
|
|
392
409
|
|
|
393
|
-
.
|
|
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
|
-
.
|
|
417
|
+
.crud-list-card {
|
|
401
418
|
padding: 0.875rem;
|
|
402
419
|
}
|
|
403
420
|
|
|
404
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
447
|
+
.crud-list-card__field {
|
|
431
448
|
display: grid;
|
|
432
449
|
gap: 0.15rem;
|
|
433
450
|
}
|
|
434
451
|
|
|
435
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
466
|
+
.crud-list-table {
|
|
450
467
|
display: none;
|
|
451
468
|
overflow-x: auto;
|
|
452
469
|
}
|
|
453
470
|
|
|
454
|
-
.
|
|
471
|
+
.crud-list-table__select {
|
|
455
472
|
width: 3rem;
|
|
456
473
|
}
|
|
457
474
|
|
|
458
|
-
.
|
|
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
|
-
.
|
|
484
|
+
.crud-list-cards {
|
|
468
485
|
display: none;
|
|
469
486
|
}
|
|
470
487
|
|
|
471
|
-
.
|
|
488
|
+
.crud-list-table {
|
|
472
489
|
display: block;
|
|
473
490
|
}
|
|
474
491
|
|
|
475
|
-
.
|
|
492
|
+
.crud-list-fab {
|
|
476
493
|
display: none;
|
|
477
494
|
}
|
|
478
495
|
}
|
|
479
496
|
|
|
480
497
|
@media (max-width: 960px) {
|
|
481
|
-
.
|
|
498
|
+
.crud-list-header {
|
|
482
499
|
flex-direction: column;
|
|
483
500
|
}
|
|
484
501
|
|
|
485
|
-
.
|
|
502
|
+
.crud-list-header__actions {
|
|
486
503
|
width: 100%;
|
|
487
504
|
}
|
|
488
505
|
|
|
489
|
-
.
|
|
506
|
+
.crud-list-element :deep(.v-btn) {
|
|
490
507
|
min-height: 48px;
|
|
491
508
|
}
|
|
492
509
|
|
|
493
|
-
.
|
|
510
|
+
.crud-list-header__primary-action {
|
|
494
511
|
display: none;
|
|
495
512
|
}
|
|
496
513
|
|
|
497
|
-
.
|
|
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="
|
|
38
|
-
<header class="
|
|
39
|
-
<div class="
|
|
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="
|
|
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="
|
|
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="
|
|
66
|
-
<div v-if="view.loadError || view.isNotFound" class="
|
|
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="
|
|
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
|
-
:
|
|
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="
|
|
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="
|
|
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
|
-
.
|
|
118
|
-
--
|
|
119
|
-
--
|
|
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
|
-
.
|
|
123
|
-
--
|
|
121
|
+
.crud-screen--operator {
|
|
122
|
+
--crud-screen-state-padding: 2rem 1rem;
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
.
|
|
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
|
-
.
|
|
132
|
+
.crud-view-header__copy {
|
|
134
133
|
min-width: 0;
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
.
|
|
138
|
-
font-size: var(--
|
|
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
|
-
.
|
|
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
|
-
.
|
|
152
|
+
.crud-view-panel {
|
|
154
153
|
overflow: hidden;
|
|
155
154
|
}
|
|
156
155
|
|
|
157
|
-
.
|
|
156
|
+
.crud-view-state {
|
|
158
157
|
margin-inline: auto;
|
|
159
158
|
max-width: 30rem;
|
|
160
|
-
padding: var(--
|
|
159
|
+
padding: var(--crud-screen-state-padding);
|
|
161
160
|
text-align: center;
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
.
|
|
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
|
-
.
|
|
170
|
+
.crud-view-fields :deep(.v-col) {
|
|
172
171
|
min-width: 0;
|
|
173
172
|
}
|
|
174
173
|
|
|
175
|
-
.
|
|
174
|
+
.crud-view-supporting-content {
|
|
176
175
|
min-width: 0;
|
|
177
176
|
}
|
|
178
177
|
|
|
179
178
|
@media (max-width: 960px) {
|
|
180
|
-
.
|
|
179
|
+
.crud-view-header {
|
|
181
180
|
flex-direction: column;
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
.
|
|
183
|
+
.crud-view-header__actions {
|
|
185
184
|
width: 100%;
|
|
186
185
|
}
|
|
187
186
|
|
|
188
|
-
.
|
|
187
|
+
.crud-view-header__actions :deep(.v-btn) {
|
|
189
188
|
min-height: 48px;
|
|
190
189
|
flex: 1 1 10rem;
|
|
191
190
|
}
|
|
192
191
|
|
|
193
|
-
.
|
|
192
|
+
.crud-view-state__actions :deep(.v-btn) {
|
|
194
193
|
min-height: 48px;
|
|
195
194
|
}
|
|
196
195
|
}
|
|
@@ -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: ["
|
|
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: `
|
|
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
|
-
"
|
|
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: `
|
|
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
|
-
"
|
|
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: `
|
|
42
|
+
placementSource: `crud.${normalizedResourceNamespace}.view`,
|
|
43
43
|
requestRecoveryLabel,
|
|
44
44
|
fallbackLoadError,
|
|
45
45
|
notFoundMessage,
|