@jskit-ai/users-web 0.1.81 → 0.1.82
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.descriptor.mjs +51 -9
- package/package.json +21 -10
- package/src/client/bulkActions.js +47 -0
- package/src/client/components/AccountSettingsClientElement.vue +69 -24
- package/src/client/components/CrudAddEditScreen.vue +186 -0
- package/src/client/components/CrudListBulkActionSurface.vue +126 -0
- package/src/client/components/CrudListFilterSurface.vue +377 -0
- package/src/client/components/CrudListScreen.vue +434 -0
- package/src/client/components/CrudViewScreen.vue +186 -0
- package/src/client/components/ProfileClientElement.vue +19 -12
- package/src/client/composables/records/useAddEdit.js +23 -2
- package/src/client/composables/records/useCrudList.js +5 -1
- package/src/client/composables/records/useView.js +1 -0
- package/src/client/composables/runtime/operationUiHelpers.js +7 -3
- package/src/client/composables/runtime/useEndpointResource.js +12 -2
- package/src/client/composables/runtime/useUiFeedback.js +4 -2
- package/src/client/composables/support/resourceLoadStateHelpers.js +33 -1
- package/src/client/composables/useAccountSettingsRuntime.js +10 -1
- package/src/client/composables/useCrudAddEditScreen.js +88 -0
- package/src/client/composables/useCrudListBulkActions.js +147 -0
- package/src/client/composables/useCrudListScreen.js +107 -0
- package/src/client/composables/useCrudViewScreen.js +67 -0
- package/src/client/composables/usePagedCollection.js +6 -1
- package/src/client/filters.js +15 -0
- package/src/client/index.js +5 -0
- package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +34 -8
- package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +34 -8
- package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +34 -8
- package/test/crudListBulkActionSurface.test.js +27 -0
- package/test/crudListFilterSurface.test.js +45 -0
- package/test/crudScreenComponents.test.js +62 -0
- package/test/errorIntentContract.test.js +31 -0
- package/test/exportsContract.test.js +11 -0
- package/test/resourceLoadStateHelpers.test.js +35 -1
- package/test/settingsPlacementContract.test.js +61 -0
- package/test/useCrudListBulkActions.test.js +65 -0
package/package.descriptor.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { HOME_COG_OUTLET } from "./src/shared/toolsOutletContracts.js";
|
|
|
3
3
|
export default Object.freeze({
|
|
4
4
|
packageVersion: 1,
|
|
5
5
|
packageId: "@jskit-ai/users-web",
|
|
6
|
-
version: "0.1.
|
|
6
|
+
version: "0.1.82",
|
|
7
7
|
kind: "runtime",
|
|
8
8
|
description: "Users web module: account/profile UI plus shared users web widgets.",
|
|
9
9
|
dependsOn: [
|
|
@@ -50,6 +50,34 @@ export default Object.freeze({
|
|
|
50
50
|
subpath: "./client/components/AccountSettingsClientElement",
|
|
51
51
|
summary: "Exports the package-owned account settings host that renders placement-backed account sections."
|
|
52
52
|
},
|
|
53
|
+
{
|
|
54
|
+
subpath: "./client/components/CrudAddEditScreen",
|
|
55
|
+
summary: "Exports the package-owned CRUD add/edit screen shell used by generated form pages."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
subpath: "./client/components/CrudListBulkActionSurface",
|
|
59
|
+
summary: "Exports the adaptive CRUD list bulk-action surface for generated list pages."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
subpath: "./client/components/CrudListFilterSurface",
|
|
63
|
+
summary: "Exports the adaptive CRUD list filter surface for generated list pages."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
subpath: "./client/components/CrudListScreen",
|
|
67
|
+
summary: "Exports the package-owned CRUD list screen shell used by generated list pages."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
subpath: "./client/components/CrudViewScreen",
|
|
71
|
+
summary: "Exports the package-owned CRUD view screen shell used by generated detail pages."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
subpath: "./client/bulkActions",
|
|
75
|
+
summary: "Exports client-side CRUD list bulk-action definition helpers."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
subpath: "./client/filters",
|
|
79
|
+
summary: "Exports client-side CRUD list filter definition helpers."
|
|
80
|
+
},
|
|
53
81
|
{
|
|
54
82
|
subpath: "./client/components/ProfileClientElement",
|
|
55
83
|
summary: "Exports profile settings client element scaffold component."
|
|
@@ -78,10 +106,26 @@ export default Object.freeze({
|
|
|
78
106
|
subpath: "./client/composables/useCrudListFilterLookups",
|
|
79
107
|
summary: "Exports lookup-backed CRUD list filter helper for remote autocomplete filters."
|
|
80
108
|
},
|
|
109
|
+
{
|
|
110
|
+
subpath: "./client/composables/useCrudAddEditScreen",
|
|
111
|
+
summary: "Exports the package-owned add/edit screen runtime for generated form pages."
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
subpath: "./client/composables/useCrudListBulkActions",
|
|
115
|
+
summary: "Exports selected-record state and execution runtime for generated CRUD list bulk actions."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
subpath: "./client/composables/useCrudListScreen",
|
|
119
|
+
summary: "Exports the package-owned list screen runtime for generated list pages."
|
|
120
|
+
},
|
|
81
121
|
{
|
|
82
122
|
subpath: "./client/composables/useView",
|
|
83
123
|
summary: "Exports read/view operation composable."
|
|
84
124
|
},
|
|
125
|
+
{
|
|
126
|
+
subpath: "./client/composables/useCrudViewScreen",
|
|
127
|
+
summary: "Exports the package-owned view screen runtime for generated detail pages."
|
|
128
|
+
},
|
|
85
129
|
{
|
|
86
130
|
subpath: "./client/composables/usePagedCollection",
|
|
87
131
|
summary: "Exports paged collection query composable."
|
|
@@ -233,15 +277,13 @@ export default Object.freeze({
|
|
|
233
277
|
mutations: {
|
|
234
278
|
dependencies: {
|
|
235
279
|
runtime: {
|
|
236
|
-
"@tanstack/vue-query": "5.92.12",
|
|
237
280
|
"@mdi/js": "^7.4.47",
|
|
238
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
239
|
-
"@jskit-ai/realtime": "0.1.
|
|
240
|
-
"@jskit-ai/kernel": "0.1.
|
|
241
|
-
"@jskit-ai/shell-web": "0.1.
|
|
242
|
-
"@jskit-ai/uploads-image-web": "0.1.
|
|
243
|
-
"@jskit-ai/users-core": "0.1.
|
|
244
|
-
vuetify: "^4.0.0"
|
|
281
|
+
"@jskit-ai/http-runtime": "0.1.66",
|
|
282
|
+
"@jskit-ai/realtime": "0.1.66",
|
|
283
|
+
"@jskit-ai/kernel": "0.1.67",
|
|
284
|
+
"@jskit-ai/shell-web": "0.1.66",
|
|
285
|
+
"@jskit-ai/uploads-image-web": "0.1.45",
|
|
286
|
+
"@jskit-ai/users-core": "0.1.77"
|
|
245
287
|
},
|
|
246
288
|
dev: {}
|
|
247
289
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/users-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.82",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -8,22 +8,33 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
"./client": "./src/client/index.js",
|
|
10
10
|
"./client/components/AccountSettingsClientElement": "./src/client/components/AccountSettingsClientElement.vue",
|
|
11
|
+
"./client/components/CrudAddEditScreen": "./src/client/components/CrudAddEditScreen.vue",
|
|
12
|
+
"./client/components/CrudListBulkActionSurface": "./src/client/components/CrudListBulkActionSurface.vue",
|
|
13
|
+
"./client/components/CrudListFilterSurface": "./src/client/components/CrudListFilterSurface.vue",
|
|
14
|
+
"./client/components/CrudListScreen": "./src/client/components/CrudListScreen.vue",
|
|
15
|
+
"./client/components/CrudViewScreen": "./src/client/components/CrudViewScreen.vue",
|
|
11
16
|
"./client/account-settings/sections": "./src/client/account-settings/sections.js",
|
|
17
|
+
"./client/bulkActions": "./src/client/bulkActions.js",
|
|
18
|
+
"./client/filters": "./src/client/filters.js",
|
|
12
19
|
"./client/composables/useAddEdit": "./src/client/composables/records/useAddEdit.js",
|
|
13
20
|
"./client/composables/useAccess": "./src/client/composables/useAccess.js",
|
|
14
21
|
"./client/composables/useCommand": "./src/client/composables/useCommand.js",
|
|
15
22
|
"./client/composables/useCrudAddEdit": "./src/client/composables/records/useCrudAddEdit.js",
|
|
23
|
+
"./client/composables/useCrudAddEditScreen": "./src/client/composables/useCrudAddEditScreen.js",
|
|
24
|
+
"./client/composables/useCrudListBulkActions": "./src/client/composables/useCrudListBulkActions.js",
|
|
16
25
|
"./client/composables/crudLookupFieldRuntime": "./src/client/composables/crud/crudLookupFieldRuntime.js",
|
|
17
26
|
"./client/composables/useCrudListFilterLookups": "./src/client/composables/useCrudListFilterLookups.js",
|
|
18
27
|
"./client/composables/useCrudListFilters": "./src/client/composables/useCrudListFilters.js",
|
|
19
28
|
"./client/composables/useEndpointResource": "./src/client/composables/runtime/useEndpointResource.js",
|
|
20
29
|
"./client/composables/useList": "./src/client/composables/records/useList.js",
|
|
21
30
|
"./client/composables/useCrudList": "./src/client/composables/records/useCrudList.js",
|
|
31
|
+
"./client/composables/useCrudListScreen": "./src/client/composables/useCrudListScreen.js",
|
|
22
32
|
"./client/composables/useCrudListParentTitle": "./src/client/composables/useCrudListParentTitle.js",
|
|
23
33
|
"./client/composables/useRealtimeQueryInvalidation": "./src/client/composables/useRealtimeQueryInvalidation.js",
|
|
24
34
|
"./client/composables/useSurfaceRouteContext": "./src/client/composables/useSurfaceRouteContext.js",
|
|
25
35
|
"./client/composables/useView": "./src/client/composables/records/useView.js",
|
|
26
36
|
"./client/composables/useCrudView": "./src/client/composables/records/useCrudView.js",
|
|
37
|
+
"./client/composables/useCrudViewScreen": "./src/client/composables/useCrudViewScreen.js",
|
|
27
38
|
"./client/composables/usePagedCollection": "./src/client/composables/usePagedCollection.js",
|
|
28
39
|
"./client/composables/usePaths": "./src/client/composables/usePaths.js",
|
|
29
40
|
"./client/composables/runtime/useUiFeedback": "./src/client/composables/runtime/useUiFeedback.js",
|
|
@@ -32,18 +43,18 @@
|
|
|
32
43
|
"./client/support/contractGuards": "./src/client/support/contractGuards.js"
|
|
33
44
|
},
|
|
34
45
|
"dependencies": {
|
|
35
|
-
"@tanstack/vue-query": "5.92.12",
|
|
36
46
|
"@mdi/js": "^7.4.47",
|
|
37
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
38
|
-
"@jskit-ai/kernel": "0.1.
|
|
39
|
-
"@jskit-ai/realtime": "0.1.
|
|
40
|
-
"@jskit-ai/shell-web": "0.1.
|
|
41
|
-
"@jskit-ai/uploads-image-web": "0.1.
|
|
42
|
-
"@jskit-ai/users-core": "0.1.
|
|
43
|
-
"vuetify": "^4.0.0"
|
|
47
|
+
"@jskit-ai/http-runtime": "0.1.66",
|
|
48
|
+
"@jskit-ai/kernel": "0.1.67",
|
|
49
|
+
"@jskit-ai/realtime": "0.1.66",
|
|
50
|
+
"@jskit-ai/shell-web": "0.1.66",
|
|
51
|
+
"@jskit-ai/uploads-image-web": "0.1.45",
|
|
52
|
+
"@jskit-ai/users-core": "0.1.77"
|
|
44
53
|
},
|
|
45
54
|
"peerDependencies": {
|
|
55
|
+
"@tanstack/vue-query": "^5.90.5",
|
|
46
56
|
"vue": "^3.5.13",
|
|
47
|
-
"vue-router": "^5.0.4"
|
|
57
|
+
"vue-router": "^5.0.4",
|
|
58
|
+
"vuetify": "^4.0.0"
|
|
48
59
|
}
|
|
49
60
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
function normalizeCrudListBulkAction(rawAction = {}, index = 0) {
|
|
4
|
+
if (!rawAction || typeof rawAction !== "object" || Array.isArray(rawAction)) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const key = normalizeText(rawAction.key || rawAction.id || `action-${index + 1}`);
|
|
9
|
+
const label = normalizeText(rawAction.label || rawAction.title);
|
|
10
|
+
if (!key || !label) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
key,
|
|
16
|
+
label,
|
|
17
|
+
icon: normalizeText(rawAction.icon),
|
|
18
|
+
color: normalizeText(rawAction.color, { fallback: "primary" }),
|
|
19
|
+
variant: normalizeText(rawAction.variant, { fallback: "tonal" }),
|
|
20
|
+
confirmLabel: normalizeText(rawAction.confirmLabel),
|
|
21
|
+
run: typeof rawAction.run === "function" ? rawAction.run : null,
|
|
22
|
+
disabled: rawAction.disabled
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function defineCrudListBulkActions(actions = []) {
|
|
27
|
+
if (!Array.isArray(actions)) {
|
|
28
|
+
throw new TypeError("defineCrudListBulkActions requires an array.");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const normalizedActions = [];
|
|
32
|
+
const seenKeys = new Set();
|
|
33
|
+
|
|
34
|
+
actions.forEach((rawAction, index) => {
|
|
35
|
+
const action = normalizeCrudListBulkAction(rawAction, index);
|
|
36
|
+
if (!action || seenKeys.has(action.key)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
seenKeys.add(action.key);
|
|
41
|
+
normalizedActions.push(action);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return Object.freeze(normalizedActions);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { defineCrudListBulkActions };
|
|
@@ -56,28 +56,41 @@ const activeTab = computed({
|
|
|
56
56
|
|
|
57
57
|
<template>
|
|
58
58
|
<section class="settings-view py-2 py-md-4">
|
|
59
|
-
<v-
|
|
60
|
-
<
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
59
|
+
<v-sheet class="settings-panel" rounded="lg" border>
|
|
60
|
+
<header class="settings-panel__header">
|
|
61
|
+
<div>
|
|
62
|
+
<h1 class="settings-panel__title">Account settings</h1>
|
|
63
|
+
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
64
|
+
Global profile, preferences, notifications, and account controls.
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
<v-btn
|
|
68
|
+
variant="text"
|
|
69
|
+
color="secondary"
|
|
70
|
+
:to="runtime.backNavigationTarget.value.sameOrigin ? runtime.backNavigationTarget.value.href : undefined"
|
|
71
|
+
:href="runtime.backNavigationTarget.value.sameOrigin ? undefined : runtime.backNavigationTarget.value.href"
|
|
72
|
+
>
|
|
73
|
+
Back
|
|
74
|
+
</v-btn>
|
|
75
|
+
</header>
|
|
75
76
|
|
|
76
|
-
<
|
|
77
|
+
<div class="settings-panel__body">
|
|
77
78
|
<template v-if="runtime.loadingSettings.value">
|
|
78
79
|
<v-skeleton-loader type="text@2, list-item-two-line@4" class="mb-4" />
|
|
79
80
|
<v-skeleton-loader type="text@2, paragraph, button" />
|
|
80
81
|
</template>
|
|
82
|
+
<div v-else-if="runtime.settingsLoadError.value" class="settings-panel__state">
|
|
83
|
+
<h2 class="text-h6 mb-2">Unable to load account settings</h2>
|
|
84
|
+
<p class="text-body-2 text-medium-emphasis mb-4">{{ runtime.settingsLoadError.value }}</p>
|
|
85
|
+
<v-btn
|
|
86
|
+
color="primary"
|
|
87
|
+
variant="tonal"
|
|
88
|
+
:loading="runtime.refreshingSettings.value"
|
|
89
|
+
@click="runtime.refreshSettings"
|
|
90
|
+
>
|
|
91
|
+
Retry
|
|
92
|
+
</v-btn>
|
|
93
|
+
</div>
|
|
81
94
|
<template v-else-if="sections.length < 1">
|
|
82
95
|
<p class="text-body-2 text-medium-emphasis mb-0">No account settings sections are registered.</p>
|
|
83
96
|
</template>
|
|
@@ -109,20 +122,42 @@ const activeTab = computed({
|
|
|
109
122
|
</v-col>
|
|
110
123
|
</v-row>
|
|
111
124
|
</template>
|
|
112
|
-
</
|
|
113
|
-
</v-
|
|
125
|
+
</div>
|
|
126
|
+
</v-sheet>
|
|
114
127
|
</section>
|
|
115
128
|
</template>
|
|
116
129
|
|
|
117
130
|
<style scoped>
|
|
118
|
-
.panel
|
|
131
|
+
.settings-panel {
|
|
119
132
|
background-color: rgb(var(--v-theme-surface));
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.settings-panel__header {
|
|
137
|
+
align-items: flex-start;
|
|
138
|
+
display: flex;
|
|
139
|
+
gap: 1rem;
|
|
140
|
+
justify-content: space-between;
|
|
141
|
+
padding: 1rem 1rem 0;
|
|
120
142
|
}
|
|
121
143
|
|
|
122
|
-
.
|
|
123
|
-
font-size:
|
|
124
|
-
font-weight:
|
|
125
|
-
letter-spacing: 0.
|
|
144
|
+
.settings-panel__title {
|
|
145
|
+
font-size: clamp(1.35rem, 2vw, 1.85rem);
|
|
146
|
+
font-weight: 650;
|
|
147
|
+
letter-spacing: -0.02em;
|
|
148
|
+
line-height: 1.15;
|
|
149
|
+
margin: 0 0 0.35rem;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.settings-panel__body {
|
|
153
|
+
padding: 1rem;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.settings-panel__state {
|
|
157
|
+
margin-inline: auto;
|
|
158
|
+
max-width: 30rem;
|
|
159
|
+
padding: 2rem 1rem;
|
|
160
|
+
text-align: center;
|
|
126
161
|
}
|
|
127
162
|
|
|
128
163
|
.settings-section-list {
|
|
@@ -139,4 +174,14 @@ const activeTab = computed({
|
|
|
139
174
|
:deep(.settings-sections-window .v-window-x-reverse-transition-leave-active) {
|
|
140
175
|
transition: none !important;
|
|
141
176
|
}
|
|
177
|
+
|
|
178
|
+
@media (max-width: 960px) {
|
|
179
|
+
.settings-panel__header {
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.settings-panel__header :deep(.v-btn) {
|
|
184
|
+
min-height: 48px;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
142
187
|
</style>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, unref } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
screen: {
|
|
6
|
+
type: Object,
|
|
7
|
+
required: true
|
|
8
|
+
},
|
|
9
|
+
resolveLookupItems: {
|
|
10
|
+
type: Function,
|
|
11
|
+
default: null
|
|
12
|
+
},
|
|
13
|
+
resolveLookupLoading: {
|
|
14
|
+
type: Function,
|
|
15
|
+
default: null
|
|
16
|
+
},
|
|
17
|
+
resolveLookupSearch: {
|
|
18
|
+
type: Function,
|
|
19
|
+
default: null
|
|
20
|
+
},
|
|
21
|
+
setLookupSearch: {
|
|
22
|
+
type: Function,
|
|
23
|
+
default: null
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const formRuntime = computed(() => props.screen?.formRuntime || {});
|
|
28
|
+
const addEdit = computed(() => props.screen?.addEdit || formRuntime.value?.addEdit || {});
|
|
29
|
+
const formState = computed(() => props.screen?.formState || formRuntime.value?.form || {});
|
|
30
|
+
const mode = computed(() => String(unref(props.screen?.mode) || "new").trim() || "new");
|
|
31
|
+
const title = computed(() => String(unref(props.screen?.title) || "").trim());
|
|
32
|
+
const subtitle = computed(() => String(unref(props.screen?.subtitle) || "").trim());
|
|
33
|
+
const saveLabel = computed(() => String(unref(props.screen?.saveLabel) || "Save").trim() || "Save");
|
|
34
|
+
const cancelTo = computed(() => unref(props.screen?.cancelTo) || "");
|
|
35
|
+
|
|
36
|
+
function resolveFieldErrors(fieldKey) {
|
|
37
|
+
if (typeof props.screen?.resolveFieldErrors === "function") {
|
|
38
|
+
return props.screen.resolveFieldErrors(fieldKey);
|
|
39
|
+
}
|
|
40
|
+
if (typeof formRuntime.value?.resolveFieldErrors === "function") {
|
|
41
|
+
return formRuntime.value.resolveFieldErrors(fieldKey);
|
|
42
|
+
}
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveCancelTo(target = cancelTo.value) {
|
|
47
|
+
if (typeof props.screen?.resolveCancelTo === "function") {
|
|
48
|
+
return props.screen.resolveCancelTo(target);
|
|
49
|
+
}
|
|
50
|
+
return target || "";
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<section class="generated-ui-screen generated-ui-screen--operator ui-generator-add-edit-form d-flex flex-column ga-4">
|
|
56
|
+
<header class="ui-generator-add-edit-form__header">
|
|
57
|
+
<div class="ui-generator-add-edit-form__copy">
|
|
58
|
+
<h1 class="ui-generator-add-edit-form__title">{{ title }}</h1>
|
|
59
|
+
<p v-if="subtitle" class="text-body-2 text-medium-emphasis mb-0">{{ subtitle }}</p>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="ui-generator-add-edit-form__actions">
|
|
62
|
+
<v-btn v-if="cancelTo" color="primary" variant="outlined" :to="resolveCancelTo(cancelTo)">Cancel</v-btn>
|
|
63
|
+
<v-btn
|
|
64
|
+
color="primary"
|
|
65
|
+
variant="flat"
|
|
66
|
+
:loading="addEdit.isSaving"
|
|
67
|
+
:disabled="addEdit.isSubmitDisabled"
|
|
68
|
+
@click="addEdit.submit"
|
|
69
|
+
>
|
|
70
|
+
{{ saveLabel }}
|
|
71
|
+
</v-btn>
|
|
72
|
+
</div>
|
|
73
|
+
</header>
|
|
74
|
+
|
|
75
|
+
<v-sheet rounded="lg" border class="ui-generator-add-edit-form__panel">
|
|
76
|
+
<div v-if="addEdit.loadError" class="ui-generator-add-edit-form__state">
|
|
77
|
+
<h2 class="text-h6 mb-2">Unable to load form</h2>
|
|
78
|
+
<p class="text-body-2 text-medium-emphasis mb-4">
|
|
79
|
+
{{ addEdit.loadError }}
|
|
80
|
+
</p>
|
|
81
|
+
<v-btn
|
|
82
|
+
v-if="addEdit.canRetryLoad"
|
|
83
|
+
color="primary"
|
|
84
|
+
variant="tonal"
|
|
85
|
+
:loading="addEdit.isFetching"
|
|
86
|
+
@click="addEdit.refresh"
|
|
87
|
+
>
|
|
88
|
+
Retry
|
|
89
|
+
</v-btn>
|
|
90
|
+
</div>
|
|
91
|
+
<template v-else-if="formRuntime.showFormSkeleton">
|
|
92
|
+
<div class="pa-4">
|
|
93
|
+
<v-skeleton-loader type="heading, text@2, article" />
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
96
|
+
<v-form v-else class="pa-4" @submit.prevent="addEdit.submit" novalidate>
|
|
97
|
+
<v-progress-linear v-if="addEdit.isRefetching" indeterminate class="mb-4" />
|
|
98
|
+
<v-row class="ui-generator-add-edit-form__fields">
|
|
99
|
+
<slot
|
|
100
|
+
name="fields"
|
|
101
|
+
:mode="mode"
|
|
102
|
+
:form-runtime="formRuntime"
|
|
103
|
+
:form-state="formState"
|
|
104
|
+
:add-edit="addEdit"
|
|
105
|
+
:resolve-field-errors="resolveFieldErrors"
|
|
106
|
+
:resolve-lookup-items="resolveLookupItems"
|
|
107
|
+
:resolve-lookup-loading="resolveLookupLoading"
|
|
108
|
+
:resolve-lookup-search="resolveLookupSearch"
|
|
109
|
+
:set-lookup-search="setLookupSearch"
|
|
110
|
+
/>
|
|
111
|
+
</v-row>
|
|
112
|
+
</v-form>
|
|
113
|
+
</v-sheet>
|
|
114
|
+
</section>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<style scoped>
|
|
118
|
+
.generated-ui-screen {
|
|
119
|
+
--generated-ui-screen-title-size: clamp(1.35rem, 2vw, 1.85rem);
|
|
120
|
+
--generated-ui-screen-state-padding: 2.5rem 1.25rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.generated-ui-screen--operator {
|
|
124
|
+
--generated-ui-screen-state-padding: 2rem 1rem;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.ui-generator-add-edit-form__header {
|
|
128
|
+
align-items: flex-start;
|
|
129
|
+
display: flex;
|
|
130
|
+
gap: 1rem;
|
|
131
|
+
justify-content: space-between;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.ui-generator-add-edit-form__copy {
|
|
135
|
+
min-width: 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.ui-generator-add-edit-form__title {
|
|
139
|
+
font-size: var(--generated-ui-screen-title-size);
|
|
140
|
+
font-weight: 650;
|
|
141
|
+
letter-spacing: -0.02em;
|
|
142
|
+
line-height: 1.15;
|
|
143
|
+
margin: 0 0 0.35rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.ui-generator-add-edit-form__actions {
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-wrap: wrap;
|
|
149
|
+
gap: 0.5rem;
|
|
150
|
+
justify-content: flex-end;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.ui-generator-add-edit-form__panel {
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.ui-generator-add-edit-form__state {
|
|
158
|
+
margin-inline: auto;
|
|
159
|
+
max-width: 30rem;
|
|
160
|
+
padding: var(--generated-ui-screen-state-padding);
|
|
161
|
+
text-align: center;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.ui-generator-add-edit-form__fields :deep(.v-col) {
|
|
165
|
+
min-width: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@media (max-width: 960px) {
|
|
169
|
+
.ui-generator-add-edit-form__header {
|
|
170
|
+
flex-direction: column;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.ui-generator-add-edit-form__actions {
|
|
174
|
+
width: 100%;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.ui-generator-add-edit-form__actions :deep(.v-btn) {
|
|
178
|
+
min-height: 48px;
|
|
179
|
+
flex: 1 1 10rem;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.ui-generator-add-edit-form__state :deep(.v-btn) {
|
|
183
|
+
min-height: 48px;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
</style>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { useDisplay } from "vuetify";
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
runtime: {
|
|
7
|
+
type: Object,
|
|
8
|
+
default: null
|
|
9
|
+
},
|
|
10
|
+
title: {
|
|
11
|
+
type: String,
|
|
12
|
+
default: "Selection"
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const display = useDisplay();
|
|
17
|
+
const isCompactLayout = computed(() => {
|
|
18
|
+
const displayName = String(display?.name?.value || "").trim().toLowerCase();
|
|
19
|
+
return displayName === "xs" || displayName === "sm";
|
|
20
|
+
});
|
|
21
|
+
const actions = computed(() => Array.isArray(props.runtime?.actions) ? props.runtime.actions : []);
|
|
22
|
+
const selectedCount = computed(() => Number(props.runtime?.selectedCount?.value || 0));
|
|
23
|
+
const shouldRender = computed(() =>
|
|
24
|
+
Boolean(props.runtime?.hasActions?.value) &&
|
|
25
|
+
Boolean(props.runtime?.hasSelection?.value) &&
|
|
26
|
+
selectedCount.value > 0
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
function clearSelection() {
|
|
30
|
+
props.runtime?.clearSelection?.();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function execute(action = {}) {
|
|
34
|
+
props.runtime?.execute?.(action);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isActionDisabled(action = {}) {
|
|
38
|
+
return Boolean(props.runtime?.isActionDisabled?.(action));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isActionExecuting(action = {}) {
|
|
42
|
+
return Boolean(props.runtime?.isActionExecuting?.(action));
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<section v-if="shouldRender" class="crud-list-bulk-action-surface">
|
|
48
|
+
<div class="crud-list-bulk-action-surface__summary">
|
|
49
|
+
<div class="crud-list-bulk-action-surface__copy">
|
|
50
|
+
<span class="text-overline text-medium-emphasis">{{ title }}</span>
|
|
51
|
+
<strong>{{ selectedCount }} selected</strong>
|
|
52
|
+
</div>
|
|
53
|
+
<v-btn size="small" variant="text" @click="clearSelection">Clear</v-btn>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div v-if="!isCompactLayout" class="crud-list-bulk-action-surface__actions">
|
|
57
|
+
<v-btn
|
|
58
|
+
v-for="action in actions"
|
|
59
|
+
:key="action.key"
|
|
60
|
+
:color="action.color"
|
|
61
|
+
:variant="action.variant"
|
|
62
|
+
:prepend-icon="action.icon || undefined"
|
|
63
|
+
:disabled="isActionDisabled(action)"
|
|
64
|
+
:loading="isActionExecuting(action)"
|
|
65
|
+
@click="execute(action)"
|
|
66
|
+
>
|
|
67
|
+
{{ action.label }}
|
|
68
|
+
</v-btn>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<v-menu v-else location="bottom end">
|
|
72
|
+
<template #activator="{ props: menuProps }">
|
|
73
|
+
<v-btn v-bind="menuProps" color="primary" variant="tonal">Bulk actions</v-btn>
|
|
74
|
+
</template>
|
|
75
|
+
<v-list density="compact" min-width="180">
|
|
76
|
+
<v-list-item
|
|
77
|
+
v-for="action in actions"
|
|
78
|
+
:key="action.key"
|
|
79
|
+
:title="action.label"
|
|
80
|
+
:prepend-icon="action.icon || undefined"
|
|
81
|
+
:disabled="isActionDisabled(action)"
|
|
82
|
+
@click="execute(action)"
|
|
83
|
+
/>
|
|
84
|
+
</v-list>
|
|
85
|
+
</v-menu>
|
|
86
|
+
</section>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<style scoped>
|
|
90
|
+
.crud-list-bulk-action-surface {
|
|
91
|
+
align-items: center;
|
|
92
|
+
background: rgba(var(--v-theme-primary), 0.08);
|
|
93
|
+
border-block: 1px solid rgba(var(--v-theme-primary), 0.18);
|
|
94
|
+
display: flex;
|
|
95
|
+
gap: 0.75rem;
|
|
96
|
+
justify-content: space-between;
|
|
97
|
+
min-width: 0;
|
|
98
|
+
padding: 0.75rem 1rem;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.crud-list-bulk-action-surface__summary,
|
|
102
|
+
.crud-list-bulk-action-surface__actions {
|
|
103
|
+
align-items: center;
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-wrap: wrap;
|
|
106
|
+
gap: 0.5rem;
|
|
107
|
+
min-width: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.crud-list-bulk-action-surface__copy {
|
|
111
|
+
display: grid;
|
|
112
|
+
gap: 0.1rem;
|
|
113
|
+
min-width: 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.crud-list-bulk-action-surface :deep(.v-btn) {
|
|
117
|
+
min-height: 48px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@media (max-width: 640px) {
|
|
121
|
+
.crud-list-bulk-action-surface {
|
|
122
|
+
align-items: stretch;
|
|
123
|
+
flex-direction: column;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
</style>
|