@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.
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/http-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"./client/support/contractGuards": "./src/client/support/contractGuards.js"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
48
|
-
"@jskit-ai/kernel": "0.1.
|
|
49
|
-
"@jskit-ai/realtime": "0.1.
|
|
50
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
51
|
-
"@jskit-ai/shell-web": "0.1.
|
|
47
|
+
"@jskit-ai/http-runtime": "0.1.158",
|
|
48
|
+
"@jskit-ai/kernel": "0.1.160",
|
|
49
|
+
"@jskit-ai/realtime": "0.1.157",
|
|
50
|
+
"@jskit-ai/resource-crud-core": "0.1.102",
|
|
51
|
+
"@jskit-ai/shell-web": "0.1.164",
|
|
52
52
|
"@mdi/js": "^7.4.47"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
@@ -64,11 +64,7 @@
|
|
|
64
64
|
"provides": [
|
|
65
65
|
"runtime.http-web"
|
|
66
66
|
],
|
|
67
|
-
"requires": [
|
|
68
|
-
"runtime.http-client",
|
|
69
|
-
"runtime.web-error",
|
|
70
|
-
"runtime.web-placement"
|
|
71
|
-
]
|
|
67
|
+
"requires": []
|
|
72
68
|
},
|
|
73
69
|
"runtime": {
|
|
74
70
|
"server": {
|
|
@@ -111,40 +107,18 @@
|
|
|
111
107
|
},
|
|
112
108
|
{
|
|
113
109
|
"subpath": "./client/components/CrudListScreen",
|
|
114
|
-
"summary": "Exports the shared
|
|
110
|
+
"summary": "Exports the shared CRUD list screen."
|
|
115
111
|
},
|
|
116
112
|
{
|
|
117
113
|
"subpath": "./client/components/CrudViewScreen",
|
|
118
|
-
"summary": "Exports the shared
|
|
114
|
+
"summary": "Exports the shared CRUD view screen."
|
|
119
115
|
},
|
|
120
116
|
{
|
|
121
117
|
"subpath": "./client/components/CrudAddEditScreen",
|
|
122
|
-
"summary": "Exports the shared
|
|
118
|
+
"summary": "Exports the shared CRUD add/edit screen."
|
|
123
119
|
}
|
|
124
|
-
]
|
|
125
|
-
"containerTokens": {
|
|
126
|
-
"server": [],
|
|
127
|
-
"client": []
|
|
128
|
-
}
|
|
120
|
+
]
|
|
129
121
|
}
|
|
130
|
-
},
|
|
131
|
-
"mutations": {
|
|
132
|
-
"dependencies": {
|
|
133
|
-
"runtime": {
|
|
134
|
-
"@jskit-ai/http-runtime": "0.1.156",
|
|
135
|
-
"@jskit-ai/kernel": "0.1.158",
|
|
136
|
-
"@jskit-ai/realtime": "0.1.155",
|
|
137
|
-
"@jskit-ai/resource-crud-core": "0.1.100",
|
|
138
|
-
"@jskit-ai/shell-web": "0.1.162",
|
|
139
|
-
"@mdi/js": "^7.4.47"
|
|
140
|
-
},
|
|
141
|
-
"dev": {}
|
|
142
|
-
},
|
|
143
|
-
"packageJson": {
|
|
144
|
-
"scripts": {}
|
|
145
|
-
},
|
|
146
|
-
"procfile": {},
|
|
147
|
-
"files": []
|
|
148
122
|
}
|
|
149
123
|
}
|
|
150
124
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: crud/crud-screen-set
|
|
3
|
+
title: CRUD list, view, create, and edit screens
|
|
4
|
+
summary: Build a routed CRUD user interface as thin application pages over JSKIT's shared screen components and composables.
|
|
5
|
+
keywords: actions, add, crud, delete, edit, filters, list, material, routes, view, vue
|
|
6
|
+
requires: @jskit-ai/http-web, @jskit-ai/resource-crud-core
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# CRUD list, view, create, and edit screens
|
|
10
|
+
|
|
11
|
+
## Use when
|
|
12
|
+
|
|
13
|
+
Use this pattern when a shared CRUD resource should have normal list, detail,
|
|
14
|
+
create, edit, and delete routes. The shared JSKIT screens own responsive chrome,
|
|
15
|
+
loading skeletons, load-error retry states, mutation feedback, route-aware data
|
|
16
|
+
operations, and common extensions. Application pages supply product copy,
|
|
17
|
+
fields, routes, and optional actions.
|
|
18
|
+
|
|
19
|
+
## Do not use when
|
|
20
|
+
|
|
21
|
+
Do not use this pattern for a one-off command, an embedded read-only value, or a
|
|
22
|
+
domain whose operations are not CRUD. Do not replace the shared screens simply
|
|
23
|
+
to add filters, row actions, bulk actions, permission gating, includes, or
|
|
24
|
+
supporting detail content; those are public extension seams.
|
|
25
|
+
|
|
26
|
+
## Product decisions
|
|
27
|
+
|
|
28
|
+
Decide which operations exist, the route hierarchy, record title, visible
|
|
29
|
+
fields, create/edit participation, destructive confirmation language, access
|
|
30
|
+
surface, filters, and domain actions. The resource contract and product intent
|
|
31
|
+
are authoritative. Do not derive these decisions from a live table or a field
|
|
32
|
+
questionnaire.
|
|
33
|
+
|
|
34
|
+
## Invariants
|
|
35
|
+
|
|
36
|
+
- Route pages stay thin and use `useCrudListScreen()`, `useCrudViewScreen()`,
|
|
37
|
+
or `useCrudAddEditScreen()`.
|
|
38
|
+
- Transport and validators come from the shared resource contract.
|
|
39
|
+
- Initial visible loading uses structure-matching skeletons; pending buttons use
|
|
40
|
+
stable disabled labels, never spinners.
|
|
41
|
+
- Resource-load errors stay inside the screen with retry. Command success and
|
|
42
|
+
failure use JSKIT's shared toast/snackbar feedback path.
|
|
43
|
+
- URLs are route templates or route objects resolved through the screen runtime.
|
|
44
|
+
- Product field components are ordinary application source.
|
|
45
|
+
- Query keys and placement sources describe the resource, not an authoring tool.
|
|
46
|
+
- No generated-file markers, overwrite contracts, receipts, or provenance remain.
|
|
47
|
+
|
|
48
|
+
## Framework APIs
|
|
49
|
+
|
|
50
|
+
Use `CrudListScreen`, `CrudViewScreen`, `CrudAddEditScreen`, and
|
|
51
|
+
`CrudDeleteAction` with their matching composables from `@jskit-ai/http-web`.
|
|
52
|
+
Use `defineCrudListFilters()`, `defineCrudListRowActions()`, and
|
|
53
|
+
`defineCrudListBulkActions()` for optional list extensions. The resource comes
|
|
54
|
+
from `defineCrudResource()` in `@jskit-ai/resource-crud-core`.
|
|
55
|
+
|
|
56
|
+
## Example files
|
|
57
|
+
|
|
58
|
+
`example/books/` contains concrete list, view, new, and edit pages for the
|
|
59
|
+
owner-scoped `bookResource` pattern. `BookFormFields.vue` and `formFields.js`
|
|
60
|
+
show the small product-owned form seam. `listExtensions.js` makes optional
|
|
61
|
+
filters and actions explicit without adding empty screen machinery elsewhere.
|
|
62
|
+
|
|
63
|
+
## Variation points
|
|
64
|
+
|
|
65
|
+
Change routes, labels, visible fields, field components, filters, query params,
|
|
66
|
+
realtime events, delete availability, and action definitions. Use the screen
|
|
67
|
+
slots for domain-specific content. Use a lower-level request composable only
|
|
68
|
+
when the wire contract genuinely is not CRUD.
|
|
69
|
+
|
|
70
|
+
## Verification
|
|
71
|
+
|
|
72
|
+
- Mount each route with a representative resource response.
|
|
73
|
+
- Verify skeleton, empty, success, local load-error, and retry states.
|
|
74
|
+
- Verify create, edit, validation failure, delete confirmation, and navigation.
|
|
75
|
+
- Verify filters and actions when declared.
|
|
76
|
+
- Exercise compact, medium, and expanded widths with Playwright.
|
|
77
|
+
- Confirm keyboard access, 48 CSS-pixel compact targets, no horizontal overflow,
|
|
78
|
+
and toast-based mutation feedback.
|
|
79
|
+
|
|
80
|
+
## Avoid
|
|
81
|
+
|
|
82
|
+
- route-local `fetch()` or duplicated request serializers
|
|
83
|
+
- copying shared screen chrome into every route
|
|
84
|
+
- spinner or circular-progress loading states
|
|
85
|
+
- transient in-page command-error banners that push the layout
|
|
86
|
+
- generator-branded CSS classes, cache keys, or placement ids
|
|
87
|
+
- comments or metadata reserving fields for a future authoring command
|
|
88
|
+
- a second source of truth for resource field validation
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useRoute } from "vue-router";
|
|
4
|
+
import CrudAddEditScreen from "@jskit-ai/http-web/client/components/CrudAddEditScreen";
|
|
5
|
+
import { useCrudAddEditScreen } from "@jskit-ai/http-web/client/composables/useCrudAddEditScreen";
|
|
6
|
+
import { bookResource } from "@local/books/shared";
|
|
7
|
+
import BookFormFields from "./BookFormFields.vue";
|
|
8
|
+
import { bookFormFields } from "./formFields.js";
|
|
9
|
+
|
|
10
|
+
const route = useRoute();
|
|
11
|
+
const routeRecordId = computed(() => String(route.params.bookId || "").trim());
|
|
12
|
+
|
|
13
|
+
const screen = useCrudAddEditScreen({
|
|
14
|
+
mode: "edit",
|
|
15
|
+
title: "Edit book",
|
|
16
|
+
subtitle: "Update this catalogue entry.",
|
|
17
|
+
saveLabel: "Save changes",
|
|
18
|
+
cancelTo: "/books/:bookId",
|
|
19
|
+
preserveCancelQuery: true,
|
|
20
|
+
resource: bookResource,
|
|
21
|
+
operationName: "patch",
|
|
22
|
+
formFields: bookFormFields,
|
|
23
|
+
addEditOptions: {
|
|
24
|
+
apiUrlTemplate: "/books/:bookId",
|
|
25
|
+
placementSource: "crud.books.edit",
|
|
26
|
+
writeMethod: "PATCH",
|
|
27
|
+
requestRecoveryLabel: "Book",
|
|
28
|
+
fallbackLoadError: "Unable to load this book.",
|
|
29
|
+
fallbackSaveError: "Unable to save this book.",
|
|
30
|
+
recordIdParam: "bookId",
|
|
31
|
+
routeRecordId,
|
|
32
|
+
viewUrlTemplate: "/books/:bookId",
|
|
33
|
+
listUrlTemplate: "/books"
|
|
34
|
+
},
|
|
35
|
+
saveSuccess: {
|
|
36
|
+
invalidateQueryKey: ["crud", "books"],
|
|
37
|
+
listUrlTemplate: "/books"
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<CrudAddEditScreen :screen="screen">
|
|
44
|
+
<template #fields="fieldRuntime">
|
|
45
|
+
<BookFormFields
|
|
46
|
+
v-model:title="fieldRuntime.formState.title"
|
|
47
|
+
v-model:author="fieldRuntime.formState.author"
|
|
48
|
+
v-model:notes="fieldRuntime.formState.notes"
|
|
49
|
+
:readonly="fieldRuntime.addEdit.isFieldLocked"
|
|
50
|
+
:resolve-field-errors="fieldRuntime.resolveFieldErrors"
|
|
51
|
+
/>
|
|
52
|
+
</template>
|
|
53
|
+
</CrudAddEditScreen>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const title = defineModel("title", { type: String, default: "" });
|
|
3
|
+
const author = defineModel("author", { type: String, default: "" });
|
|
4
|
+
const notes = defineModel("notes", { type: String, default: "" });
|
|
5
|
+
|
|
6
|
+
defineProps({
|
|
7
|
+
readonly: {
|
|
8
|
+
type: Boolean,
|
|
9
|
+
default: false
|
|
10
|
+
},
|
|
11
|
+
resolveFieldErrors: {
|
|
12
|
+
type: Function,
|
|
13
|
+
required: true
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<v-col cols="12" md="6">
|
|
20
|
+
<v-text-field
|
|
21
|
+
v-model="title"
|
|
22
|
+
label="Title"
|
|
23
|
+
variant="outlined"
|
|
24
|
+
density="comfortable"
|
|
25
|
+
maxlength="255"
|
|
26
|
+
:readonly="readonly"
|
|
27
|
+
:error-messages="resolveFieldErrors('title')"
|
|
28
|
+
/>
|
|
29
|
+
</v-col>
|
|
30
|
+
<v-col cols="12" md="6">
|
|
31
|
+
<v-text-field
|
|
32
|
+
v-model="author"
|
|
33
|
+
label="Author"
|
|
34
|
+
variant="outlined"
|
|
35
|
+
density="comfortable"
|
|
36
|
+
maxlength="255"
|
|
37
|
+
:readonly="readonly"
|
|
38
|
+
:error-messages="resolveFieldErrors('author')"
|
|
39
|
+
/>
|
|
40
|
+
</v-col>
|
|
41
|
+
<v-col cols="12">
|
|
42
|
+
<v-textarea
|
|
43
|
+
v-model="notes"
|
|
44
|
+
label="Notes"
|
|
45
|
+
variant="outlined"
|
|
46
|
+
density="comfortable"
|
|
47
|
+
auto-grow
|
|
48
|
+
:readonly="readonly"
|
|
49
|
+
:error-messages="resolveFieldErrors('notes')"
|
|
50
|
+
/>
|
|
51
|
+
</v-col>
|
|
52
|
+
</template>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import CrudListScreen from "@jskit-ai/http-web/client/components/CrudListScreen";
|
|
3
|
+
import { useCrudListScreen } from "@jskit-ai/http-web/client/composables/useCrudListScreen";
|
|
4
|
+
import { bookResource } from "@local/books/shared";
|
|
5
|
+
import { listBulkActions, listFilters, listRowActions } from "./listExtensions.js";
|
|
6
|
+
|
|
7
|
+
const screen = useCrudListScreen({
|
|
8
|
+
resource: bookResource,
|
|
9
|
+
resourceNamespace: "books",
|
|
10
|
+
apiSuffix: "/books",
|
|
11
|
+
recordIdParam: "bookId",
|
|
12
|
+
recordIdSelector: (book = {}) => book.id,
|
|
13
|
+
titleFallbackFieldKey: "title",
|
|
14
|
+
viewUrlTemplate: "/books/:bookId",
|
|
15
|
+
editUrlTemplate: "/books/:bookId/edit",
|
|
16
|
+
newUrlTemplate: "/books/new",
|
|
17
|
+
listFilters,
|
|
18
|
+
listBulkActions,
|
|
19
|
+
listRowActions,
|
|
20
|
+
requestRecoveryLabel: "Books",
|
|
21
|
+
fallbackLoadError: "Unable to load books."
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<CrudListScreen
|
|
27
|
+
:screen="screen"
|
|
28
|
+
title-label="Books"
|
|
29
|
+
heading-title="My books"
|
|
30
|
+
subtitle="Search and maintain your catalogue."
|
|
31
|
+
create-label="Add book"
|
|
32
|
+
empty-title="No books yet"
|
|
33
|
+
empty-body="Add your first book to begin the catalogue."
|
|
34
|
+
>
|
|
35
|
+
<template #card-fields="{ record, formatListCardValue }">
|
|
36
|
+
<dl class="d-grid ga-2 mb-0">
|
|
37
|
+
<div>
|
|
38
|
+
<dt class="text-caption text-medium-emphasis">Author</dt>
|
|
39
|
+
<dd class="text-body-2 mb-0">{{ formatListCardValue(record.author) }}</dd>
|
|
40
|
+
</div>
|
|
41
|
+
</dl>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<template #table-header>
|
|
45
|
+
<th>Title</th>
|
|
46
|
+
<th>Author</th>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<template #table-row="{ record }">
|
|
50
|
+
<td>{{ record.title }}</td>
|
|
51
|
+
<td>{{ record.author }}</td>
|
|
52
|
+
</template>
|
|
53
|
+
</CrudListScreen>
|
|
54
|
+
</template>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import CrudAddEditScreen from "@jskit-ai/http-web/client/components/CrudAddEditScreen";
|
|
3
|
+
import { useCrudAddEditScreen } from "@jskit-ai/http-web/client/composables/useCrudAddEditScreen";
|
|
4
|
+
import { bookResource } from "@local/books/shared";
|
|
5
|
+
import BookFormFields from "./BookFormFields.vue";
|
|
6
|
+
import { bookFormFields } from "./formFields.js";
|
|
7
|
+
|
|
8
|
+
const screen = useCrudAddEditScreen({
|
|
9
|
+
mode: "new",
|
|
10
|
+
title: "Add book",
|
|
11
|
+
subtitle: "Add a book to your catalogue.",
|
|
12
|
+
saveLabel: "Save book",
|
|
13
|
+
cancelTo: "/books",
|
|
14
|
+
resource: bookResource,
|
|
15
|
+
operationName: "create",
|
|
16
|
+
formFields: bookFormFields,
|
|
17
|
+
addEditOptions: {
|
|
18
|
+
apiSuffix: "/books",
|
|
19
|
+
placementSource: "crud.books.new",
|
|
20
|
+
readEnabled: false,
|
|
21
|
+
writeMethod: "POST",
|
|
22
|
+
recordIdParam: "bookId",
|
|
23
|
+
viewUrlTemplate: "/books/:bookId",
|
|
24
|
+
listUrlTemplate: "/books"
|
|
25
|
+
},
|
|
26
|
+
saveSuccess: {
|
|
27
|
+
invalidateQueryKey: ["crud", "books"],
|
|
28
|
+
listUrlTemplate: "/books"
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<CrudAddEditScreen :screen="screen">
|
|
35
|
+
<template #fields="fieldRuntime">
|
|
36
|
+
<BookFormFields
|
|
37
|
+
v-model:title="fieldRuntime.formState.title"
|
|
38
|
+
v-model:author="fieldRuntime.formState.author"
|
|
39
|
+
v-model:notes="fieldRuntime.formState.notes"
|
|
40
|
+
:readonly="fieldRuntime.addEdit.isFieldLocked"
|
|
41
|
+
:resolve-field-errors="fieldRuntime.resolveFieldErrors"
|
|
42
|
+
/>
|
|
43
|
+
</template>
|
|
44
|
+
</CrudAddEditScreen>
|
|
45
|
+
</template>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import CrudDeleteAction from "@jskit-ai/http-web/client/components/CrudDeleteAction";
|
|
3
|
+
import CrudViewScreen from "@jskit-ai/http-web/client/components/CrudViewScreen";
|
|
4
|
+
import { useCrudDeleteAction } from "@jskit-ai/http-web/client/composables/useCrudDeleteAction";
|
|
5
|
+
import { useCrudViewScreen } from "@jskit-ai/http-web/client/composables/useCrudViewScreen";
|
|
6
|
+
import { bookResource } from "@local/books/shared";
|
|
7
|
+
|
|
8
|
+
const screen = useCrudViewScreen({
|
|
9
|
+
resource: bookResource,
|
|
10
|
+
resourceNamespace: "books",
|
|
11
|
+
apiUrlTemplate: "/books/:bookId",
|
|
12
|
+
recordIdParam: "bookId",
|
|
13
|
+
titleFallbackFieldKey: "title",
|
|
14
|
+
listUrlTemplate: "/books",
|
|
15
|
+
editUrlTemplate: "/books/:bookId/edit",
|
|
16
|
+
requestRecoveryLabel: "Book",
|
|
17
|
+
fallbackLoadError: "Unable to load this book."
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const deleteAction = useCrudDeleteAction({
|
|
21
|
+
screen,
|
|
22
|
+
resource: bookResource,
|
|
23
|
+
resourceNamespace: "books",
|
|
24
|
+
apiUrlTemplate: "/books/:bookId"
|
|
25
|
+
});
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<CrudViewScreen
|
|
30
|
+
:screen="screen"
|
|
31
|
+
resource-singular-title="Book"
|
|
32
|
+
resource-plural-title="Books"
|
|
33
|
+
description="Review this catalogue entry."
|
|
34
|
+
>
|
|
35
|
+
<template #actions>
|
|
36
|
+
<CrudDeleteAction :action="deleteAction" resource-singular-title="Book" />
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<template #fields="{ view }">
|
|
40
|
+
<v-col cols="12" md="6">
|
|
41
|
+
<div class="text-caption text-medium-emphasis">Author</div>
|
|
42
|
+
<div class="text-body-1">{{ view.record?.author }}</div>
|
|
43
|
+
</v-col>
|
|
44
|
+
<v-col cols="12">
|
|
45
|
+
<div class="text-caption text-medium-emphasis">Notes</div>
|
|
46
|
+
<div class="text-body-1 text-pre-wrap">{{ view.record?.notes || "No notes" }}</div>
|
|
47
|
+
</v-col>
|
|
48
|
+
</template>
|
|
49
|
+
</CrudViewScreen>
|
|
50
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const bookFormFields = Object.freeze([
|
|
2
|
+
Object.freeze({
|
|
3
|
+
key: "title",
|
|
4
|
+
label: "Title",
|
|
5
|
+
type: "string",
|
|
6
|
+
nullable: false,
|
|
7
|
+
inputType: "text",
|
|
8
|
+
component: "text",
|
|
9
|
+
maxLength: 255
|
|
10
|
+
}),
|
|
11
|
+
Object.freeze({
|
|
12
|
+
key: "author",
|
|
13
|
+
label: "Author",
|
|
14
|
+
type: "string",
|
|
15
|
+
nullable: false,
|
|
16
|
+
inputType: "text",
|
|
17
|
+
component: "text",
|
|
18
|
+
maxLength: 255
|
|
19
|
+
}),
|
|
20
|
+
Object.freeze({
|
|
21
|
+
key: "notes",
|
|
22
|
+
label: "Notes",
|
|
23
|
+
type: "string",
|
|
24
|
+
nullable: true,
|
|
25
|
+
inputType: "textarea",
|
|
26
|
+
component: "textarea",
|
|
27
|
+
maxLength: 65535
|
|
28
|
+
})
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export { bookFormFields };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineCrudListBulkActions } from "@jskit-ai/http-web/client/bulkActions";
|
|
2
|
+
import { defineCrudListFilters } from "@jskit-ai/http-web/client/filters";
|
|
3
|
+
import { defineCrudListRowActions } from "@jskit-ai/http-web/client/rowActions";
|
|
4
|
+
|
|
5
|
+
const listFilters = defineCrudListFilters({});
|
|
6
|
+
const listBulkActions = defineCrudListBulkActions([]);
|
|
7
|
+
const listRowActions = defineCrudListRowActions([]);
|
|
8
|
+
|
|
9
|
+
export { listBulkActions, listFilters, listRowActions };
|
|
@@ -52,28 +52,27 @@ function resolveCancelTo(target = cancelTo.value) {
|
|
|
52
52
|
</script>
|
|
53
53
|
|
|
54
54
|
<template>
|
|
55
|
-
<section class="
|
|
56
|
-
<header class="
|
|
57
|
-
<div class="
|
|
58
|
-
<h1 class="
|
|
55
|
+
<section class="crud-screen crud-screen--operator crud-add-edit-form d-flex flex-column ga-4">
|
|
56
|
+
<header class="crud-add-edit-form__header">
|
|
57
|
+
<div class="crud-add-edit-form__copy">
|
|
58
|
+
<h1 class="crud-add-edit-form__title">{{ title }}</h1>
|
|
59
59
|
<p v-if="subtitle" class="text-body-2 text-medium-emphasis mb-0">{{ subtitle }}</p>
|
|
60
60
|
</div>
|
|
61
|
-
<div class="
|
|
61
|
+
<div class="crud-add-edit-form__actions">
|
|
62
62
|
<v-btn v-if="cancelTo" color="primary" variant="outlined" :to="resolveCancelTo(cancelTo)">Cancel</v-btn>
|
|
63
63
|
<v-btn
|
|
64
64
|
color="primary"
|
|
65
65
|
variant="flat"
|
|
66
|
-
:loading="addEdit.isSaving"
|
|
67
66
|
:disabled="addEdit.isSubmitDisabled"
|
|
68
67
|
@click="addEdit.submit"
|
|
69
68
|
>
|
|
70
|
-
{{ saveLabel }}
|
|
69
|
+
{{ addEdit.isSaving ? "Saving…" : saveLabel }}
|
|
71
70
|
</v-btn>
|
|
72
71
|
</div>
|
|
73
72
|
</header>
|
|
74
73
|
|
|
75
|
-
<v-sheet rounded="lg" border class="
|
|
76
|
-
<div v-if="addEdit.loadError" class="
|
|
74
|
+
<v-sheet rounded="lg" border class="crud-add-edit-form__panel">
|
|
75
|
+
<div v-if="addEdit.loadError" class="crud-add-edit-form__state">
|
|
77
76
|
<h2 class="text-h6 mb-2">Unable to load form</h2>
|
|
78
77
|
<p class="text-body-2 text-medium-emphasis mb-4">
|
|
79
78
|
{{ addEdit.loadError }}
|
|
@@ -82,10 +81,10 @@ function resolveCancelTo(target = cancelTo.value) {
|
|
|
82
81
|
v-if="addEdit.canRetryLoad"
|
|
83
82
|
color="primary"
|
|
84
83
|
variant="tonal"
|
|
85
|
-
:
|
|
84
|
+
:disabled="addEdit.isFetching"
|
|
86
85
|
@click="addEdit.refresh"
|
|
87
86
|
>
|
|
88
|
-
Retry
|
|
87
|
+
{{ addEdit.isFetching ? "Retrying…" : "Retry" }}
|
|
89
88
|
</v-btn>
|
|
90
89
|
</div>
|
|
91
90
|
<template v-else-if="formRuntime.showFormSkeleton">
|
|
@@ -94,8 +93,7 @@ function resolveCancelTo(target = cancelTo.value) {
|
|
|
94
93
|
</div>
|
|
95
94
|
</template>
|
|
96
95
|
<v-form v-else class="pa-4" @submit.prevent="addEdit.submit" novalidate>
|
|
97
|
-
<v-
|
|
98
|
-
<v-row class="ui-generator-add-edit-form__fields">
|
|
96
|
+
<v-row class="crud-add-edit-form__fields">
|
|
99
97
|
<slot
|
|
100
98
|
name="fields"
|
|
101
99
|
:mode="mode"
|
|
@@ -115,71 +113,71 @@ function resolveCancelTo(target = cancelTo.value) {
|
|
|
115
113
|
</template>
|
|
116
114
|
|
|
117
115
|
<style scoped>
|
|
118
|
-
.
|
|
119
|
-
--
|
|
120
|
-
--
|
|
116
|
+
.crud-screen {
|
|
117
|
+
--crud-screen-title-size: clamp(1.35rem, 2vw, 1.85rem);
|
|
118
|
+
--crud-screen-state-padding: 2.5rem 1.25rem;
|
|
121
119
|
}
|
|
122
120
|
|
|
123
|
-
.
|
|
124
|
-
--
|
|
121
|
+
.crud-screen--operator {
|
|
122
|
+
--crud-screen-state-padding: 2rem 1rem;
|
|
125
123
|
}
|
|
126
124
|
|
|
127
|
-
.
|
|
125
|
+
.crud-add-edit-form__header {
|
|
128
126
|
align-items: flex-start;
|
|
129
127
|
display: flex;
|
|
130
128
|
gap: 1rem;
|
|
131
129
|
justify-content: space-between;
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
.
|
|
132
|
+
.crud-add-edit-form__copy {
|
|
135
133
|
min-width: 0;
|
|
136
134
|
}
|
|
137
135
|
|
|
138
|
-
.
|
|
139
|
-
font-size: var(--
|
|
136
|
+
.crud-add-edit-form__title {
|
|
137
|
+
font-size: var(--crud-screen-title-size);
|
|
140
138
|
font-weight: 650;
|
|
141
139
|
letter-spacing: -0.02em;
|
|
142
140
|
line-height: 1.15;
|
|
143
141
|
margin: 0 0 0.35rem;
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
.
|
|
144
|
+
.crud-add-edit-form__actions {
|
|
147
145
|
display: flex;
|
|
148
146
|
flex-wrap: wrap;
|
|
149
147
|
gap: 0.5rem;
|
|
150
148
|
justify-content: flex-end;
|
|
151
149
|
}
|
|
152
150
|
|
|
153
|
-
.
|
|
151
|
+
.crud-add-edit-form__panel {
|
|
154
152
|
overflow: hidden;
|
|
155
153
|
}
|
|
156
154
|
|
|
157
|
-
.
|
|
155
|
+
.crud-add-edit-form__state {
|
|
158
156
|
margin-inline: auto;
|
|
159
157
|
max-width: 30rem;
|
|
160
|
-
padding: var(--
|
|
158
|
+
padding: var(--crud-screen-state-padding);
|
|
161
159
|
text-align: center;
|
|
162
160
|
}
|
|
163
161
|
|
|
164
|
-
.
|
|
162
|
+
.crud-add-edit-form__fields :deep(.v-col) {
|
|
165
163
|
min-width: 0;
|
|
166
164
|
}
|
|
167
165
|
|
|
168
166
|
@media (max-width: 960px) {
|
|
169
|
-
.
|
|
167
|
+
.crud-add-edit-form__header {
|
|
170
168
|
flex-direction: column;
|
|
171
169
|
}
|
|
172
170
|
|
|
173
|
-
.
|
|
171
|
+
.crud-add-edit-form__actions {
|
|
174
172
|
width: 100%;
|
|
175
173
|
}
|
|
176
174
|
|
|
177
|
-
.
|
|
175
|
+
.crud-add-edit-form__actions :deep(.v-btn) {
|
|
178
176
|
min-height: 48px;
|
|
179
177
|
flex: 1 1 10rem;
|
|
180
178
|
}
|
|
181
179
|
|
|
182
|
-
.
|
|
180
|
+
.crud-add-edit-form__state :deep(.v-btn) {
|
|
183
181
|
min-height: 48px;
|
|
184
182
|
}
|
|
185
183
|
}
|
|
@@ -81,11 +81,10 @@ function handleDialogModelUpdate(isOpen) {
|
|
|
81
81
|
<v-btn
|
|
82
82
|
color="error"
|
|
83
83
|
variant="flat"
|
|
84
|
-
:loading="action.isDeleting"
|
|
85
84
|
:disabled="!action.canDelete"
|
|
86
85
|
@click="action.confirm"
|
|
87
86
|
>
|
|
88
|
-
Delete
|
|
87
|
+
{{ action.isDeleting ? "Deleting…" : "Delete" }}
|
|
89
88
|
</v-btn>
|
|
90
89
|
</v-card-actions>
|
|
91
90
|
</v-card>
|
|
@@ -61,10 +61,9 @@ function isActionExecuting(action = {}) {
|
|
|
61
61
|
:variant="action.variant"
|
|
62
62
|
:prepend-icon="action.icon || undefined"
|
|
63
63
|
:disabled="isActionDisabled(action)"
|
|
64
|
-
:loading="isActionExecuting(action)"
|
|
65
64
|
@click="execute(action)"
|
|
66
65
|
>
|
|
67
|
-
{{ action.label }}
|
|
66
|
+
{{ isActionExecuting(action) ? `${action.label}…` : action.label }}
|
|
68
67
|
</v-btn>
|
|
69
68
|
</div>
|
|
70
69
|
|
|
@@ -76,7 +75,7 @@ function isActionExecuting(action = {}) {
|
|
|
76
75
|
<v-list-item
|
|
77
76
|
v-for="action in actions"
|
|
78
77
|
:key="action.key"
|
|
79
|
-
:title="action.label"
|
|
78
|
+
:title="isActionExecuting(action) ? `${action.label}…` : action.label"
|
|
80
79
|
:prepend-icon="action.icon || undefined"
|
|
81
80
|
:disabled="isActionDisabled(action)"
|
|
82
81
|
@click="execute(action)"
|