@jskit-ai/http-web 0.1.1

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 (110) hide show
  1. package/fixtures/crud-list-date-filters/App.vue +107 -0
  2. package/fixtures/crud-list-date-filters/index.html +12 -0
  3. package/fixtures/crud-list-date-filters/main.js +24 -0
  4. package/fixtures/crud-list-date-filters/vite.config.mjs +18 -0
  5. package/package.json +150 -0
  6. package/src/client/bulkActions.js +47 -0
  7. package/src/client/components/CrudAddEditScreen.vue +186 -0
  8. package/src/client/components/CrudDeleteAction.vue +93 -0
  9. package/src/client/components/CrudListBulkActionSurface.vue +126 -0
  10. package/src/client/components/CrudListDateFilterControl.vue +188 -0
  11. package/src/client/components/CrudListFilterSurface.vue +388 -0
  12. package/src/client/components/CrudListRecordActionMenu.vue +120 -0
  13. package/src/client/components/CrudListScreen.vue +501 -0
  14. package/src/client/components/CrudViewScreen.vue +197 -0
  15. package/src/client/composables/crud/crudBindingSupport.js +75 -0
  16. package/src/client/composables/crud/crudHttpClientSupport.js +57 -0
  17. package/src/client/composables/crud/crudJsonApiTransportSupport.js +145 -0
  18. package/src/client/composables/crud/crudLookupFieldLabelSupport.js +151 -0
  19. package/src/client/composables/crud/crudLookupFieldRuntime.js +251 -0
  20. package/src/client/composables/crud/crudSchemaFormHelpers.js +464 -0
  21. package/src/client/composables/internal/crudListFilterLookupSupport.js +161 -0
  22. package/src/client/composables/internal/crudListParentTitleSupport.js +171 -0
  23. package/src/client/composables/internal/useOperationScope.js +144 -0
  24. package/src/client/composables/records/useAddEdit.js +224 -0
  25. package/src/client/composables/records/useCrudAddEdit.js +227 -0
  26. package/src/client/composables/records/useCrudList.js +101 -0
  27. package/src/client/composables/records/useCrudView.js +47 -0
  28. package/src/client/composables/records/useList.js +500 -0
  29. package/src/client/composables/records/useView.js +164 -0
  30. package/src/client/composables/runtime/addEditUiRuntime.js +130 -0
  31. package/src/client/composables/runtime/listUiRuntime.js +123 -0
  32. package/src/client/composables/runtime/modelStateHelpers.js +90 -0
  33. package/src/client/composables/runtime/operationAdapters.js +34 -0
  34. package/src/client/composables/runtime/operationUiHelpers.js +125 -0
  35. package/src/client/composables/runtime/operationValidationHelpers.js +51 -0
  36. package/src/client/composables/runtime/useAddEditCore.js +112 -0
  37. package/src/client/composables/runtime/useCommandCore.js +123 -0
  38. package/src/client/composables/runtime/useEndpointResource.js +199 -0
  39. package/src/client/composables/runtime/useFieldErrorBag.js +61 -0
  40. package/src/client/composables/runtime/useListCore.js +112 -0
  41. package/src/client/composables/runtime/useUiFeedback.js +98 -0
  42. package/src/client/composables/runtime/useViewCore.js +93 -0
  43. package/src/client/composables/runtime/viewUiRuntime.js +96 -0
  44. package/src/client/composables/support/errorMessageHelpers.js +66 -0
  45. package/src/client/composables/support/listQueryParamSupport.js +473 -0
  46. package/src/client/composables/support/listSearchSupport.js +70 -0
  47. package/src/client/composables/support/refValueHelpers.js +19 -0
  48. package/src/client/composables/support/requestQueryRuntimeSupport.js +141 -0
  49. package/src/client/composables/support/resourceLoadStateHelpers.js +208 -0
  50. package/src/client/composables/support/routeTemplateHelpers.js +285 -0
  51. package/src/client/composables/support/scopeHelpers.js +145 -0
  52. package/src/client/composables/useAccess.js +109 -0
  53. package/src/client/composables/useCommand.js +118 -0
  54. package/src/client/composables/useCrudAddEditScreen.js +88 -0
  55. package/src/client/composables/useCrudDeleteAction.js +160 -0
  56. package/src/client/composables/useCrudListBulkActions.js +147 -0
  57. package/src/client/composables/useCrudListFilterLookups.js +146 -0
  58. package/src/client/composables/useCrudListFilters.js +339 -0
  59. package/src/client/composables/useCrudListParentTitle.js +151 -0
  60. package/src/client/composables/useCrudListRowActions.js +144 -0
  61. package/src/client/composables/useCrudListScreen.js +237 -0
  62. package/src/client/composables/useCrudViewScreen.js +77 -0
  63. package/src/client/composables/usePagedCollection.js +181 -0
  64. package/src/client/composables/useRealtimeQueryInvalidation.js +156 -0
  65. package/src/client/composables/useScopeRuntime.js +135 -0
  66. package/src/client/filters.js +15 -0
  67. package/src/client/index.js +24 -0
  68. package/src/client/lib/httpClient.js +47 -0
  69. package/src/client/lib/permissions.js +34 -0
  70. package/src/client/rowActions.js +47 -0
  71. package/src/client/support/contractGuards.js +34 -0
  72. package/src/client/support/crudListDateFilterSupport.js +91 -0
  73. package/test/addEditUiRuntime.test.js +110 -0
  74. package/test/crudBindingSupport.test.js +110 -0
  75. package/test/crudHttpClientSupport.test.js +77 -0
  76. package/test/crudJsonApiTransportSupport.test.js +179 -0
  77. package/test/crudListBulkActionSurface.test.js +27 -0
  78. package/test/crudListDateFilterSupport.test.js +92 -0
  79. package/test/crudListDateFilterSurface.browser.test.js +182 -0
  80. package/test/crudListFilterSurface.test.js +90 -0
  81. package/test/crudLookupFieldRuntime.test.js +264 -0
  82. package/test/crudScreenComponents.test.js +110 -0
  83. package/test/errorIntentContract.test.js +31 -0
  84. package/test/errorMessageHelpers.test.js +28 -0
  85. package/test/exportsContract.test.js +68 -0
  86. package/test/listQueryParamSupport.test.js +223 -0
  87. package/test/listUiRuntime.test.js +102 -0
  88. package/test/operationValidationHelpers.test.js +64 -0
  89. package/test/packageBoundary.test.js +31 -0
  90. package/test/permissions.test.js +35 -0
  91. package/test/refValueHelpers.test.js +14 -0
  92. package/test/requestTransportOptions.test.js +457 -0
  93. package/test/resourceLoadStateHelpers.test.js +189 -0
  94. package/test/routeTemplateHelpers.test.js +85 -0
  95. package/test/scopeHelpers.test.js +57 -0
  96. package/test/useAddEditCore.test.js +124 -0
  97. package/test/useAddEditRequestQueryParams.test.js +190 -0
  98. package/test/useCrudAddEdit.test.js +415 -0
  99. package/test/useCrudDeleteAction.test.js +139 -0
  100. package/test/useCrudListBulkActions.test.js +65 -0
  101. package/test/useCrudListFilterLookups.test.js +114 -0
  102. package/test/useCrudListFilters.test.js +293 -0
  103. package/test/useCrudListParentTitle.test.js +262 -0
  104. package/test/useCrudListRowActions.test.js +77 -0
  105. package/test/useListRouteInitialization.test.js +337 -0
  106. package/test/useListSearchSupport.test.js +61 -0
  107. package/test/usePagedCollection.test.js +53 -0
  108. package/test/useViewRequestQueryParams.test.js +20 -0
  109. package/test/viewCoreLoading.test.js +44 -0
  110. package/test/viewUiRuntime.test.js +95 -0
@@ -0,0 +1,107 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import CrudListFilterSurface from "../../src/client/components/CrudListFilterSurface.vue";
4
+ import { useCrudListFilters } from "../../src/client/composables/useCrudListFilters.js";
5
+
6
+ const filters = {
7
+ status: {
8
+ key: "status",
9
+ queryKey: "status",
10
+ type: "enum",
11
+ label: "Status",
12
+ options: [
13
+ { value: "active", label: "Active" },
14
+ { value: "archived", label: "Archived" }
15
+ ]
16
+ },
17
+ submittedOn: {
18
+ key: "submittedOn",
19
+ queryKey: "submittedOn",
20
+ type: "date",
21
+ label: "Submitted date"
22
+ },
23
+ arrivalDate: {
24
+ key: "arrivalDate",
25
+ queryKey: "arrivalDate",
26
+ type: "dateRange",
27
+ label: "Arrival"
28
+ },
29
+ assignment: {
30
+ key: "assignment",
31
+ queryKey: "assignment",
32
+ type: "presence",
33
+ label: "Assignment",
34
+ options: [
35
+ { value: "present", label: "Assigned" },
36
+ { value: "missing", label: "Unassigned" }
37
+ ]
38
+ }
39
+ };
40
+ const runtime = useCrudListFilters(filters);
41
+ const query = new URLSearchParams(window.location.search);
42
+
43
+ for (const [key, param] of Object.entries(runtime.queryParams)) {
44
+ if (query.has(key)) {
45
+ param.value = query.get(key);
46
+ }
47
+ }
48
+
49
+ const contractSnapshot = computed(() => JSON.stringify({
50
+ values: {
51
+ submittedOn: runtime.values.submittedOn,
52
+ arrivalDate: {
53
+ from: runtime.values.arrivalDate.from,
54
+ to: runtime.values.arrivalDate.to
55
+ }
56
+ },
57
+ query: {
58
+ submittedOn: runtime.queryParams.submittedOn.value,
59
+ arrivalDate: runtime.queryParams.arrivalDate.value
60
+ }
61
+ }));
62
+ </script>
63
+
64
+ <template>
65
+ <v-app>
66
+ <v-main>
67
+ <v-container class="date-filter-fixture py-8">
68
+ <div class="mb-6">
69
+ <div class="text-overline text-primary">JSKIT http-web</div>
70
+ <h1 class="text-h4 mb-2">Date filter controls</h1>
71
+ <p class="text-body-1 text-medium-emphasis mb-0">
72
+ Date and date-range filters beside ordinary Vuetify selects.
73
+ </p>
74
+ </div>
75
+
76
+ <CrudListFilterSurface :filters="filters" :runtime="runtime" />
77
+
78
+ <output data-testid="date-filter-contract" class="date-filter-fixture__contract">
79
+ {{ contractSnapshot }}
80
+ </output>
81
+ </v-container>
82
+ </v-main>
83
+ </v-app>
84
+ </template>
85
+
86
+ <style>
87
+ html {
88
+ overflow-x: hidden;
89
+ }
90
+
91
+ body {
92
+ background: rgb(var(--v-theme-background));
93
+ }
94
+
95
+ .date-filter-fixture {
96
+ max-width: 90rem;
97
+ }
98
+
99
+ .date-filter-fixture__contract {
100
+ display: block;
101
+ font-family: monospace;
102
+ height: 1px;
103
+ overflow: hidden;
104
+ position: absolute;
105
+ width: 1px;
106
+ }
107
+ </style>
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CRUD date filter visual regression</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/main.js"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,24 @@
1
+ import { createApp } from "vue";
2
+ import "vuetify/styles";
3
+ import { createVuetify } from "vuetify";
4
+ import * as components from "vuetify/components";
5
+ import * as directives from "vuetify/directives";
6
+ import { aliases as mdiAliases, mdi } from "vuetify/iconsets/mdi-svg";
7
+ import App from "./App.vue";
8
+
9
+ const vuetify = createVuetify({
10
+ components,
11
+ directives,
12
+ theme: {
13
+ defaultTheme: "light"
14
+ },
15
+ icons: {
16
+ defaultSet: "mdi",
17
+ aliases: mdiAliases,
18
+ sets: { mdi }
19
+ }
20
+ });
21
+
22
+ createApp(App)
23
+ .use(vuetify)
24
+ .mount("#app");
@@ -0,0 +1,18 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { defineConfig } from "vite";
4
+ import vue from "@vitejs/plugin-vue";
5
+
6
+ const fixtureRoot = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ export default defineConfig({
9
+ root: fixtureRoot,
10
+ plugins: [vue()],
11
+ resolve: {
12
+ preserveSymlinks: true
13
+ },
14
+ server: {
15
+ host: "127.0.0.1",
16
+ strictPort: true
17
+ }
18
+ });
package/package.json ADDED
@@ -0,0 +1,150 @@
1
+ {
2
+ "name": "@jskit-ai/http-web",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "scripts": {
6
+ "test": "node --test"
7
+ },
8
+ "exports": {
9
+ "./client": "./src/client/index.js",
10
+ "./client/components/CrudAddEditScreen": "./src/client/components/CrudAddEditScreen.vue",
11
+ "./client/components/CrudDeleteAction": "./src/client/components/CrudDeleteAction.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",
16
+ "./client/bulkActions": "./src/client/bulkActions.js",
17
+ "./client/filters": "./src/client/filters.js",
18
+ "./client/rowActions": "./src/client/rowActions.js",
19
+ "./client/composables/useAddEdit": "./src/client/composables/records/useAddEdit.js",
20
+ "./client/composables/useAccess": "./src/client/composables/useAccess.js",
21
+ "./client/composables/useCommand": "./src/client/composables/useCommand.js",
22
+ "./client/composables/useCrudAddEdit": "./src/client/composables/records/useCrudAddEdit.js",
23
+ "./client/composables/useCrudAddEditScreen": "./src/client/composables/useCrudAddEditScreen.js",
24
+ "./client/composables/useCrudDeleteAction": "./src/client/composables/useCrudDeleteAction.js",
25
+ "./client/composables/useCrudList": "./src/client/composables/records/useCrudList.js",
26
+ "./client/composables/useCrudListBulkActions": "./src/client/composables/useCrudListBulkActions.js",
27
+ "./client/composables/useCrudListFilterLookups": "./src/client/composables/useCrudListFilterLookups.js",
28
+ "./client/composables/useCrudListFilters": "./src/client/composables/useCrudListFilters.js",
29
+ "./client/composables/useCrudListParentTitle": "./src/client/composables/useCrudListParentTitle.js",
30
+ "./client/composables/useCrudListRowActions": "./src/client/composables/useCrudListRowActions.js",
31
+ "./client/composables/useCrudListScreen": "./src/client/composables/useCrudListScreen.js",
32
+ "./client/composables/useCrudView": "./src/client/composables/records/useCrudView.js",
33
+ "./client/composables/useCrudViewScreen": "./src/client/composables/useCrudViewScreen.js",
34
+ "./client/composables/crudLookupFieldRuntime": "./src/client/composables/crud/crudLookupFieldRuntime.js",
35
+ "./client/composables/useEndpointResource": "./src/client/composables/runtime/useEndpointResource.js",
36
+ "./client/composables/useList": "./src/client/composables/records/useList.js",
37
+ "./client/composables/usePagedCollection": "./src/client/composables/usePagedCollection.js",
38
+ "./client/composables/useRealtimeQueryInvalidation": "./src/client/composables/useRealtimeQueryInvalidation.js",
39
+ "./client/composables/useUiFeedback": "./src/client/composables/runtime/useUiFeedback.js",
40
+ "./client/composables/useView": "./src/client/composables/records/useView.js",
41
+ "./client/crudHttpClient": "./src/client/composables/crud/crudHttpClientSupport.js",
42
+ "./client/lib/httpClient": "./src/client/lib/httpClient.js",
43
+ "./client/lib/permissions": "./src/client/lib/permissions.js",
44
+ "./client/support/contractGuards": "./src/client/support/contractGuards.js"
45
+ },
46
+ "dependencies": {
47
+ "@jskit-ai/http-runtime": "0.1.154",
48
+ "@jskit-ai/kernel": "0.1.156",
49
+ "@jskit-ai/realtime": "0.1.153",
50
+ "@jskit-ai/resource-crud-core": "0.1.98",
51
+ "@jskit-ai/shell-web": "0.1.160",
52
+ "@mdi/js": "^7.4.47"
53
+ },
54
+ "peerDependencies": {
55
+ "@tanstack/vue-query": "^5.90.5",
56
+ "vue": "^3.5.13",
57
+ "vue-router": "^5.0.4",
58
+ "vuetify": "^4.0.0"
59
+ },
60
+ "description": "Neutral Vue request, command, resource, and CRUD UI runtime for JSKIT web applications.",
61
+ "jskit": {
62
+ "kind": "runtime",
63
+ "capabilities": {
64
+ "provides": [
65
+ "runtime.http-web"
66
+ ],
67
+ "requires": [
68
+ "runtime.http-client",
69
+ "runtime.web-error",
70
+ "runtime.web-placement"
71
+ ]
72
+ },
73
+ "runtime": {
74
+ "server": {
75
+ "providers": []
76
+ },
77
+ "client": {
78
+ "providers": []
79
+ }
80
+ },
81
+ "metadata": {
82
+ "apiSummary": {
83
+ "surfaces": [
84
+ {
85
+ "subpath": "./client",
86
+ "summary": "Exports neutral web request, operation, permission, and CRUD UI APIs."
87
+ },
88
+ {
89
+ "subpath": "./client/lib/httpClient",
90
+ "summary": "Exports the configurable web HTTP client with credentials, CSRF handling, and safe-read retries."
91
+ },
92
+ {
93
+ "subpath": "./client/composables/useCommand",
94
+ "summary": "Exports the route-aware mutation runtime with validation, field errors, feedback, and duplicate-submit protection."
95
+ },
96
+ {
97
+ "subpath": "./client/composables/useEndpointResource",
98
+ "summary": "Exports the TanStack Query-backed custom endpoint primitive."
99
+ },
100
+ {
101
+ "subpath": "./client/composables/useList",
102
+ "summary": "Exports the route-aware list operation runtime."
103
+ },
104
+ {
105
+ "subpath": "./client/composables/useView",
106
+ "summary": "Exports the route-aware record-view operation runtime."
107
+ },
108
+ {
109
+ "subpath": "./client/composables/useAddEdit",
110
+ "summary": "Exports the route-aware add/edit operation runtime."
111
+ },
112
+ {
113
+ "subpath": "./client/components/CrudListScreen",
114
+ "summary": "Exports the shared generated CRUD list screen."
115
+ },
116
+ {
117
+ "subpath": "./client/components/CrudViewScreen",
118
+ "summary": "Exports the shared generated CRUD view screen."
119
+ },
120
+ {
121
+ "subpath": "./client/components/CrudAddEditScreen",
122
+ "summary": "Exports the shared generated CRUD add/edit screen."
123
+ }
124
+ ],
125
+ "containerTokens": {
126
+ "server": [],
127
+ "client": []
128
+ }
129
+ }
130
+ },
131
+ "mutations": {
132
+ "dependencies": {
133
+ "runtime": {
134
+ "@jskit-ai/http-runtime": "0.1.154",
135
+ "@jskit-ai/kernel": "0.1.156",
136
+ "@jskit-ai/realtime": "0.1.153",
137
+ "@jskit-ai/resource-crud-core": "0.1.98",
138
+ "@jskit-ai/shell-web": "0.1.160",
139
+ "@mdi/js": "^7.4.47"
140
+ },
141
+ "dev": {}
142
+ },
143
+ "packageJson": {
144
+ "scripts": {}
145
+ },
146
+ "procfile": {},
147
+ "files": []
148
+ }
149
+ }
150
+ }
@@ -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 };
@@ -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,93 @@
1
+ <script setup>
2
+ import { computed, useId } from "vue";
3
+ import { mdiDeleteOutline } from "@mdi/js";
4
+
5
+ const props = defineProps({
6
+ action: {
7
+ type: Object,
8
+ required: true
9
+ },
10
+ resourceSingularTitle: {
11
+ type: String,
12
+ default: "Record"
13
+ }
14
+ });
15
+
16
+ const resourceLabel = computed(() =>
17
+ String(props.resourceSingularTitle || "Record").trim() || "Record"
18
+ );
19
+ const componentId = useId();
20
+ const titleId = `${componentId}-delete-title`;
21
+ const descriptionId = `${componentId}-delete-description`;
22
+
23
+ function closeDialog() {
24
+ props.action.cancel();
25
+ }
26
+
27
+ function handleDialogModelUpdate(isOpen) {
28
+ if (!isOpen) {
29
+ closeDialog();
30
+ }
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <v-btn
36
+ color="error"
37
+ variant="tonal"
38
+ :prepend-icon="mdiDeleteOutline"
39
+ min-height="48"
40
+ :disabled="!action.canDelete"
41
+ @click="action.request"
42
+ >
43
+ Delete {{ resourceLabel }}
44
+ </v-btn>
45
+
46
+ <v-dialog
47
+ :model-value="action.isOpen"
48
+ max-width="32rem"
49
+ role="alertdialog"
50
+ aria-modal="true"
51
+ :aria-labelledby="titleId"
52
+ :aria-describedby="descriptionId"
53
+ :persistent="action.isDeleting"
54
+ @update:model-value="handleDialogModelUpdate"
55
+ >
56
+ <v-card>
57
+ <v-card-title :id="titleId">Delete {{ resourceLabel }}?</v-card-title>
58
+ <v-card-text>
59
+ <p :id="descriptionId" class="mb-0">
60
+ This permanently deletes this {{ resourceLabel.toLowerCase() }}. This action cannot be undone.
61
+ </p>
62
+ <v-alert
63
+ v-if="action.error"
64
+ class="mt-4"
65
+ type="error"
66
+ variant="tonal"
67
+ >
68
+ {{ action.error }}
69
+ </v-alert>
70
+ </v-card-text>
71
+ <v-card-actions>
72
+ <v-spacer />
73
+ <v-btn
74
+ autofocus
75
+ variant="text"
76
+ :disabled="action.isDeleting"
77
+ @click="closeDialog"
78
+ >
79
+ Cancel
80
+ </v-btn>
81
+ <v-btn
82
+ color="error"
83
+ variant="flat"
84
+ :loading="action.isDeleting"
85
+ :disabled="!action.canDelete"
86
+ @click="action.confirm"
87
+ >
88
+ Delete
89
+ </v-btn>
90
+ </v-card-actions>
91
+ </v-card>
92
+ </v-dialog>
93
+ </template>