@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
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
CRUD_LIST_FILTER_TYPE_FLAG,
|
|
3
|
+
CRUD_LIST_FILTER_TYPE_ENUM,
|
|
4
|
+
CRUD_LIST_FILTER_TYPE_ENUM_MANY,
|
|
5
|
+
CRUD_LIST_FILTER_TYPE_RECORD_ID,
|
|
6
|
+
CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY,
|
|
7
|
+
CRUD_LIST_FILTER_TYPE_DATE,
|
|
8
|
+
CRUD_LIST_FILTER_TYPE_DATE_RANGE,
|
|
9
|
+
CRUD_LIST_FILTER_TYPE_NUMBER_RANGE,
|
|
10
|
+
CRUD_LIST_FILTER_TYPE_PRESENCE,
|
|
11
|
+
CRUD_LIST_FILTER_PRESENCE_PRESENT,
|
|
12
|
+
CRUD_LIST_FILTER_PRESENCE_MISSING,
|
|
13
|
+
CRUD_LIST_FILTER_PRESENCE_OPTIONS,
|
|
14
|
+
defineCrudListFilters
|
|
15
|
+
} from "@jskit-ai/kernel/shared/support/crudListFilters";
|
package/src/client/index.js
CHANGED
|
@@ -2,6 +2,11 @@ import { UsersWebClientProvider } from "./providers/UsersWebClientProvider.js";
|
|
|
2
2
|
|
|
3
3
|
export { UsersWebClientProvider } from "./providers/UsersWebClientProvider.js";
|
|
4
4
|
export { default as AccountSettingsClientElement } from "./components/AccountSettingsClientElement.vue";
|
|
5
|
+
export { default as CrudAddEditScreen } from "./components/CrudAddEditScreen.vue";
|
|
6
|
+
export { default as CrudListBulkActionSurface } from "./components/CrudListBulkActionSurface.vue";
|
|
7
|
+
export { default as CrudListFilterSurface } from "./components/CrudListFilterSurface.vue";
|
|
8
|
+
export { default as CrudListScreen } from "./components/CrudListScreen.vue";
|
|
9
|
+
export { default as CrudViewScreen } from "./components/CrudViewScreen.vue";
|
|
5
10
|
|
|
6
11
|
const clientProviders = Object.freeze([UsersWebClientProvider]);
|
|
7
12
|
|
|
@@ -10,12 +10,11 @@ const notifications = props.runtime.notifications;
|
|
|
10
10
|
</script>
|
|
11
11
|
|
|
12
12
|
<template>
|
|
13
|
-
<v-
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
</
|
|
17
|
-
<
|
|
18
|
-
<v-card-text>
|
|
13
|
+
<v-sheet rounded="lg" border class="account-settings-section">
|
|
14
|
+
<header class="account-settings-section__header">
|
|
15
|
+
<h2 class="account-settings-section__title">Notifications</h2>
|
|
16
|
+
</header>
|
|
17
|
+
<div class="account-settings-section__body">
|
|
19
18
|
<v-form @submit.prevent="notifications.submit" novalidate>
|
|
20
19
|
<v-switch
|
|
21
20
|
v-model="notifications.form.productUpdates"
|
|
@@ -50,6 +49,33 @@ const notifications = props.runtime.notifications;
|
|
|
50
49
|
Save notification settings
|
|
51
50
|
</v-btn>
|
|
52
51
|
</v-form>
|
|
53
|
-
</
|
|
54
|
-
</v-
|
|
52
|
+
</div>
|
|
53
|
+
</v-sheet>
|
|
55
54
|
</template>
|
|
55
|
+
|
|
56
|
+
<style scoped>
|
|
57
|
+
.account-settings-section {
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.account-settings-section__header {
|
|
62
|
+
padding: 1rem 1rem 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.account-settings-section__title {
|
|
66
|
+
font-size: 1rem;
|
|
67
|
+
font-weight: 650;
|
|
68
|
+
line-height: 1.2;
|
|
69
|
+
margin: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.account-settings-section__body {
|
|
73
|
+
padding: 1rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@media (max-width: 640px) {
|
|
77
|
+
.account-settings-section__body :deep(.v-btn) {
|
|
78
|
+
min-height: 48px;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -10,12 +10,11 @@ const preferences = props.runtime.preferences;
|
|
|
10
10
|
</script>
|
|
11
11
|
|
|
12
12
|
<template>
|
|
13
|
-
<v-
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
</
|
|
17
|
-
<
|
|
18
|
-
<v-card-text>
|
|
13
|
+
<v-sheet rounded="lg" border class="account-settings-section">
|
|
14
|
+
<header class="account-settings-section__header">
|
|
15
|
+
<h2 class="account-settings-section__title">Preferences</h2>
|
|
16
|
+
</header>
|
|
17
|
+
<div class="account-settings-section__body">
|
|
19
18
|
<v-form @submit.prevent="preferences.submit" novalidate>
|
|
20
19
|
<v-row>
|
|
21
20
|
<v-col cols="12" md="4">
|
|
@@ -120,6 +119,33 @@ const preferences = props.runtime.preferences;
|
|
|
120
119
|
Save preferences
|
|
121
120
|
</v-btn>
|
|
122
121
|
</v-form>
|
|
123
|
-
</
|
|
124
|
-
</v-
|
|
122
|
+
</div>
|
|
123
|
+
</v-sheet>
|
|
125
124
|
</template>
|
|
125
|
+
|
|
126
|
+
<style scoped>
|
|
127
|
+
.account-settings-section {
|
|
128
|
+
overflow: hidden;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.account-settings-section__header {
|
|
132
|
+
padding: 1rem 1rem 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.account-settings-section__title {
|
|
136
|
+
font-size: 1rem;
|
|
137
|
+
font-weight: 650;
|
|
138
|
+
line-height: 1.2;
|
|
139
|
+
margin: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.account-settings-section__body {
|
|
143
|
+
padding: 1rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@media (max-width: 640px) {
|
|
147
|
+
.account-settings-section__body :deep(.v-btn) {
|
|
148
|
+
min-height: 48px;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
</style>
|
|
@@ -10,12 +10,11 @@ const profile = props.runtime.profile;
|
|
|
10
10
|
</script>
|
|
11
11
|
|
|
12
12
|
<template>
|
|
13
|
-
<v-
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
</
|
|
17
|
-
<
|
|
18
|
-
<v-card-text>
|
|
13
|
+
<v-sheet rounded="lg" border class="account-settings-section">
|
|
14
|
+
<header class="account-settings-section__header">
|
|
15
|
+
<h2 class="account-settings-section__title">Profile</h2>
|
|
16
|
+
</header>
|
|
17
|
+
<div class="account-settings-section__body">
|
|
19
18
|
<v-form @submit.prevent="profile.submit" novalidate>
|
|
20
19
|
<v-row class="mb-2">
|
|
21
20
|
<v-col cols="12" md="4" class="d-flex flex-column align-center justify-center">
|
|
@@ -89,6 +88,33 @@ const profile = props.runtime.profile;
|
|
|
89
88
|
Save profile
|
|
90
89
|
</v-btn>
|
|
91
90
|
</v-form>
|
|
92
|
-
</
|
|
93
|
-
</v-
|
|
91
|
+
</div>
|
|
92
|
+
</v-sheet>
|
|
94
93
|
</template>
|
|
94
|
+
|
|
95
|
+
<style scoped>
|
|
96
|
+
.account-settings-section {
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.account-settings-section__header {
|
|
101
|
+
padding: 1rem 1rem 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.account-settings-section__title {
|
|
105
|
+
font-size: 1rem;
|
|
106
|
+
font-weight: 650;
|
|
107
|
+
line-height: 1.2;
|
|
108
|
+
margin: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.account-settings-section__body {
|
|
112
|
+
padding: 1rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@media (max-width: 640px) {
|
|
116
|
+
.account-settings-section__body :deep(.v-btn) {
|
|
117
|
+
min-height: 48px;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
9
|
+
|
|
10
|
+
test("CrudListBulkActionSurface stays adaptive and runtime-owned", async () => {
|
|
11
|
+
const source = await readFile(
|
|
12
|
+
path.join(PACKAGE_DIR, "src", "client", "components", "CrudListBulkActionSurface.vue"),
|
|
13
|
+
"utf8"
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
assert.match(source, /defineProps/);
|
|
17
|
+
assert.match(source, /useDisplay/);
|
|
18
|
+
assert.match(source, /v-if="shouldRender"/);
|
|
19
|
+
assert.match(source, /selectedCount/);
|
|
20
|
+
assert.match(source, /hasActions/);
|
|
21
|
+
assert.match(source, /hasSelection/);
|
|
22
|
+
assert.match(source, /v-menu/);
|
|
23
|
+
assert.match(source, /Bulk actions/);
|
|
24
|
+
assert.match(source, /min-height:\s*48px/);
|
|
25
|
+
assert.doesNotMatch(source, /useCrudList\(/);
|
|
26
|
+
assert.doesNotMatch(source, /apiSuffix|repository|server/);
|
|
27
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
9
|
+
|
|
10
|
+
test("users-web exposes client-side CRUD list filter definition helpers", async () => {
|
|
11
|
+
const { defineCrudListFilters } = await import("@jskit-ai/users-web/client/filters");
|
|
12
|
+
|
|
13
|
+
const filters = defineCrudListFilters({
|
|
14
|
+
status: {
|
|
15
|
+
type: "enum",
|
|
16
|
+
label: "Status",
|
|
17
|
+
options: [
|
|
18
|
+
{ value: "active", label: "Active" }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
assert.equal(filters.status.queryKey, "status");
|
|
24
|
+
assert.equal(filters.status.options[0].label, "Active");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("CrudListFilterSurface provides adaptive controls without owning server semantics", async () => {
|
|
28
|
+
const source = await readFile(
|
|
29
|
+
path.join(PACKAGE_DIR, "src", "client", "components", "CrudListFilterSurface.vue"),
|
|
30
|
+
"utf8"
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
assert.match(source, /defineProps/);
|
|
34
|
+
assert.match(source, /useDisplay/);
|
|
35
|
+
assert.match(source, /v-if="shouldRender"/);
|
|
36
|
+
assert.match(source, /filterEntries/);
|
|
37
|
+
assert.match(source, /runtimeValues\[filter\.key\]/);
|
|
38
|
+
assert.match(source, /activeChips/);
|
|
39
|
+
assert.match(source, /clearChip/);
|
|
40
|
+
assert.match(source, /clearFilters/);
|
|
41
|
+
assert.match(source, /v-dialog/);
|
|
42
|
+
assert.match(source, /min-height:\s*48px/);
|
|
43
|
+
assert.doesNotMatch(source, /useCrudList\(/);
|
|
44
|
+
assert.doesNotMatch(source, /apiSuffix|server|repository/);
|
|
45
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
9
|
+
|
|
10
|
+
async function readComponent(name) {
|
|
11
|
+
return readFile(path.join(PACKAGE_DIR, "src", "client", "components", name), "utf8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test("CRUD screen components own generated list/view/form chrome centrally", async () => {
|
|
15
|
+
const listSource = await readComponent("CrudListScreen.vue");
|
|
16
|
+
const viewSource = await readComponent("CrudViewScreen.vue");
|
|
17
|
+
const addEditSource = await readComponent("CrudAddEditScreen.vue");
|
|
18
|
+
|
|
19
|
+
assert.match(listSource, /CrudListBulkActionSurface/);
|
|
20
|
+
assert.match(listSource, /CrudListFilterSurface/);
|
|
21
|
+
assert.match(listSource, /ui-generator-list-cards d-md-none/);
|
|
22
|
+
assert.match(listSource, /ui-generator-list-table d-none d-md-block/);
|
|
23
|
+
assert.match(listSource, /class="ui-generator-list-fab d-md-none"/);
|
|
24
|
+
assert.match(listSource, /#activator[\s\S]*Actions/);
|
|
25
|
+
assert.match(listSource, /min-height:\s*48px/);
|
|
26
|
+
assert.match(listSource, /<slot[\s\S]*name="card-fields"/);
|
|
27
|
+
assert.match(listSource, /<slot name="table-header"/);
|
|
28
|
+
assert.match(listSource, /<slot name="table-row"/);
|
|
29
|
+
|
|
30
|
+
assert.match(viewSource, /generated-ui-screen generated-ui-screen--operator ui-generator-view-element/);
|
|
31
|
+
assert.match(viewSource, /ui-generator-view-panel/);
|
|
32
|
+
assert.match(viewSource, /@click="view\.refresh"/);
|
|
33
|
+
assert.match(viewSource, /<slot name="fields"/);
|
|
34
|
+
|
|
35
|
+
assert.match(addEditSource, /generated-ui-screen generated-ui-screen--operator ui-generator-add-edit-form/);
|
|
36
|
+
assert.match(addEditSource, /addEdit\.canRetryLoad/);
|
|
37
|
+
assert.match(addEditSource, /@click="addEdit\.refresh"/);
|
|
38
|
+
assert.match(addEditSource, /<slot[\s\S]*name="fields"/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("account settings load state exposes a local retry action", async () => {
|
|
42
|
+
const source = await readComponent("AccountSettingsClientElement.vue");
|
|
43
|
+
|
|
44
|
+
assert.match(source, /settingsLoadError/);
|
|
45
|
+
assert.match(source, /@click="runtime\.refreshSettings"/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("CRUD screen composables are importable package APIs", async () => {
|
|
49
|
+
const [
|
|
50
|
+
listModule,
|
|
51
|
+
viewModule,
|
|
52
|
+
addEditModule
|
|
53
|
+
] = await Promise.all([
|
|
54
|
+
import("@jskit-ai/users-web/client/composables/useCrudListScreen"),
|
|
55
|
+
import("@jskit-ai/users-web/client/composables/useCrudViewScreen"),
|
|
56
|
+
import("@jskit-ai/users-web/client/composables/useCrudAddEditScreen")
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
assert.equal(typeof listModule.useCrudListScreen, "function");
|
|
60
|
+
assert.equal(typeof viewModule.useCrudViewScreen, "function");
|
|
61
|
+
assert.equal(typeof addEditModule.useCrudAddEditScreen, "function");
|
|
62
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
9
|
+
|
|
10
|
+
async function readClientSource(relativePath) {
|
|
11
|
+
return readFile(path.join(PACKAGE_DIR, "src", "client", relativePath), "utf8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test("users-web reports load errors as local resource-load intent by default", async () => {
|
|
15
|
+
const source = await readClientSource("composables/runtime/operationUiHelpers.js");
|
|
16
|
+
|
|
17
|
+
assert.match(source, /loadChannel = ""/);
|
|
18
|
+
assert.match(source, /notFoundChannel = ""/);
|
|
19
|
+
assert.match(source, /intent = "resource-load"/);
|
|
20
|
+
assert.match(source, /intent: "resource-load"/);
|
|
21
|
+
assert.doesNotMatch(source, /loadChannel = "banner"/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("users-web action feedback lets shell policy choose snackbar presentation", async () => {
|
|
25
|
+
const source = await readClientSource("composables/runtime/useUiFeedback.js");
|
|
26
|
+
|
|
27
|
+
assert.match(source, /successChannel = ""/);
|
|
28
|
+
assert.match(source, /errorChannel = ""/);
|
|
29
|
+
assert.match(source, /intent: "action-feedback"/);
|
|
30
|
+
assert.doesNotMatch(source, /errorChannel = "banner"/);
|
|
31
|
+
});
|
|
@@ -16,7 +16,14 @@ test("users-web exports are explicit and aligned with production/template usage"
|
|
|
16
16
|
requiredExports: [
|
|
17
17
|
"./client",
|
|
18
18
|
"./client/components/AccountSettingsClientElement",
|
|
19
|
+
"./client/components/CrudAddEditScreen",
|
|
20
|
+
"./client/components/CrudListBulkActionSurface",
|
|
21
|
+
"./client/components/CrudListFilterSurface",
|
|
22
|
+
"./client/components/CrudListScreen",
|
|
23
|
+
"./client/components/CrudViewScreen",
|
|
19
24
|
"./client/account-settings/sections",
|
|
25
|
+
"./client/bulkActions",
|
|
26
|
+
"./client/filters",
|
|
20
27
|
"./client/composables/useAddEdit",
|
|
21
28
|
"./client/composables/useCommand",
|
|
22
29
|
"./client/composables/useEndpointResource",
|
|
@@ -24,10 +31,14 @@ test("users-web exports are explicit and aligned with production/template usage"
|
|
|
24
31
|
"./client/composables/usePaths",
|
|
25
32
|
"./client/composables/useView",
|
|
26
33
|
"./client/composables/useCrudAddEdit",
|
|
34
|
+
"./client/composables/useCrudAddEditScreen",
|
|
35
|
+
"./client/composables/useCrudListBulkActions",
|
|
27
36
|
"./client/composables/useCrudListFilterLookups",
|
|
28
37
|
"./client/composables/useCrudListFilters",
|
|
29
38
|
"./client/composables/useCrudList",
|
|
39
|
+
"./client/composables/useCrudListScreen",
|
|
30
40
|
"./client/composables/useCrudView",
|
|
41
|
+
"./client/composables/useCrudViewScreen",
|
|
31
42
|
"./client/lib/httpClient"
|
|
32
43
|
]
|
|
33
44
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { ref } from "vue";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
hasResolvedQueryData,
|
|
6
|
+
mergeQueryMeta
|
|
7
|
+
} from "../src/client/composables/support/resourceLoadStateHelpers.js";
|
|
5
8
|
|
|
6
9
|
test("hasResolvedQueryData returns true when the query succeeded", () => {
|
|
7
10
|
const query = {
|
|
@@ -37,3 +40,34 @@ test("hasResolvedQueryData returns false when query and payload are unresolved",
|
|
|
37
40
|
data: ref(null)
|
|
38
41
|
}), false);
|
|
39
42
|
});
|
|
43
|
+
|
|
44
|
+
test("mergeQueryMeta preserves caller metadata while adding JSKIT refresh policy", () => {
|
|
45
|
+
assert.deepEqual(
|
|
46
|
+
mergeQueryMeta(
|
|
47
|
+
{
|
|
48
|
+
staleTime: 1000,
|
|
49
|
+
meta: {
|
|
50
|
+
owner: "contacts",
|
|
51
|
+
jskit: {
|
|
52
|
+
feature: "list"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
jskit: {
|
|
58
|
+
refreshOnPull: true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
),
|
|
62
|
+
{
|
|
63
|
+
staleTime: 1000,
|
|
64
|
+
meta: {
|
|
65
|
+
owner: "contacts",
|
|
66
|
+
jskit: {
|
|
67
|
+
feature: "list",
|
|
68
|
+
refreshOnPull: true
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
});
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { assertGeneratedUiSourceContract } from "@jskit-ai/kernel/shared/support/generatedUiContract";
|
|
6
7
|
import descriptor from "../package.descriptor.mjs";
|
|
7
8
|
|
|
8
9
|
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -110,12 +111,72 @@ test("users-web package-owned account settings host is fully placement-backed",
|
|
|
110
111
|
"utf8"
|
|
111
112
|
);
|
|
112
113
|
|
|
114
|
+
assertGeneratedUiSourceContract(source, {
|
|
115
|
+
forbidCardShell: true,
|
|
116
|
+
sourceName: "AccountSettingsClientElement.vue",
|
|
117
|
+
requiredPatterns: [
|
|
118
|
+
{
|
|
119
|
+
id: "account-settings-header",
|
|
120
|
+
pattern: /settings-panel__header/,
|
|
121
|
+
message: "Account settings host needs a direct settings panel header."
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "account-settings-sections",
|
|
125
|
+
pattern: /useAccountSettingsSections/,
|
|
126
|
+
message: "Account settings host must remain placement-backed."
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
});
|
|
113
130
|
assert.match(source, /useAccountSettingsSections/);
|
|
131
|
+
assert.match(source, /settings-panel__header/);
|
|
132
|
+
assert.doesNotMatch(source, /<v-card\b|v-card-title|v-card-subtitle/);
|
|
114
133
|
assert.doesNotMatch(source, /AccountSettingsProfileSection/);
|
|
115
134
|
assert.doesNotMatch(source, /AccountSettingsPreferencesSection/);
|
|
116
135
|
assert.doesNotMatch(source, /AccountSettingsNotificationsSection/);
|
|
117
136
|
});
|
|
118
137
|
|
|
138
|
+
test("users-web profile form element uses a direct panel instead of card scaffolding", async () => {
|
|
139
|
+
const source = await readFile(path.join(PACKAGE_DIR, "src", "client", "components", "ProfileClientElement.vue"), "utf8");
|
|
140
|
+
|
|
141
|
+
assertGeneratedUiSourceContract(source, {
|
|
142
|
+
forbidCardShell: true,
|
|
143
|
+
sourceName: "ProfileClientElement.vue",
|
|
144
|
+
requiredPatterns: [
|
|
145
|
+
{
|
|
146
|
+
id: "profile-panel-body",
|
|
147
|
+
pattern: /profile-client-panel__body/,
|
|
148
|
+
message: "Profile editor needs a direct panel body."
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
});
|
|
152
|
+
assert.match(source, /profile-client-panel__body/);
|
|
153
|
+
assert.doesNotMatch(source, /<v-card\b|v-card-title|v-card-subtitle|v-card-text|v-card-item/);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("users-web account settings section templates use direct settings panels", async () => {
|
|
157
|
+
for (const relativePath of [
|
|
158
|
+
path.join("templates", "src", "components", "account", "settings", "AccountSettingsProfileSection.vue"),
|
|
159
|
+
path.join("templates", "src", "components", "account", "settings", "AccountSettingsPreferencesSection.vue"),
|
|
160
|
+
path.join("templates", "src", "components", "account", "settings", "AccountSettingsNotificationsSection.vue")
|
|
161
|
+
]) {
|
|
162
|
+
const source = await readFile(path.join(PACKAGE_DIR, relativePath), "utf8");
|
|
163
|
+
|
|
164
|
+
assertGeneratedUiSourceContract(source, {
|
|
165
|
+
forbidCardShell: true,
|
|
166
|
+
sourceName: relativePath,
|
|
167
|
+
requiredPatterns: [
|
|
168
|
+
{
|
|
169
|
+
id: "account-settings-section",
|
|
170
|
+
pattern: /account-settings-section/,
|
|
171
|
+
message: "Account settings sections need the direct section panel primitive."
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
});
|
|
175
|
+
assert.match(source, /account-settings-section/);
|
|
176
|
+
assert.doesNotMatch(source, /<v-card\b|v-card-title|v-card-subtitle/);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
119
180
|
test("users-web descriptor metadata advertises home cog outlet and standard home settings placements", () => {
|
|
120
181
|
assert.deepEqual(
|
|
121
182
|
readOutlets("home-cog:primary-menu"),
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { nextTick } from "vue";
|
|
4
|
+
|
|
5
|
+
test("useCrudListBulkActions manages selection and executes action context", async () => {
|
|
6
|
+
const { defineCrudListBulkActions } = await import("@jskit-ai/users-web/client/bulkActions");
|
|
7
|
+
const { useCrudListBulkActions } = await import("@jskit-ai/users-web/client/composables/useCrudListBulkActions");
|
|
8
|
+
const calls = [];
|
|
9
|
+
const actions = defineCrudListBulkActions([
|
|
10
|
+
{
|
|
11
|
+
key: "archive",
|
|
12
|
+
label: "Archive",
|
|
13
|
+
async run(context) {
|
|
14
|
+
calls.push(context);
|
|
15
|
+
context.clearSelection();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
]);
|
|
19
|
+
const runtime = useCrudListBulkActions(actions, {
|
|
20
|
+
resolveRecordId: (record) => record.id,
|
|
21
|
+
resolveContext: () => ({
|
|
22
|
+
reload: "reload-token"
|
|
23
|
+
})
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
assert.equal(runtime.hasActions.value, true);
|
|
27
|
+
assert.equal(runtime.hasSelection.value, false);
|
|
28
|
+
|
|
29
|
+
runtime.setRecordSelected({ id: "10", label: "A" }, 0, true);
|
|
30
|
+
runtime.setRecordSelected({ id: "11", label: "B" }, 1, true);
|
|
31
|
+
await nextTick();
|
|
32
|
+
|
|
33
|
+
assert.deepEqual(runtime.selectedIds.value, ["10", "11"]);
|
|
34
|
+
assert.equal(runtime.selectedCount.value, 2);
|
|
35
|
+
assert.equal(runtime.allVisibleSelected([{ id: "10" }, { id: "11" }]), true);
|
|
36
|
+
assert.equal(runtime.someVisibleSelected([{ id: "10" }, { id: "12" }]), true);
|
|
37
|
+
|
|
38
|
+
await runtime.execute("archive");
|
|
39
|
+
|
|
40
|
+
assert.equal(calls.length, 1);
|
|
41
|
+
assert.deepEqual(calls[0].selectedIds, ["10", "11"]);
|
|
42
|
+
assert.deepEqual(calls[0].ids, ["10", "11"]);
|
|
43
|
+
assert.equal(calls[0].reload, "reload-token");
|
|
44
|
+
assert.equal(runtime.selectedCount.value, 0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("defineCrudListBulkActions skips malformed and duplicate actions", async () => {
|
|
48
|
+
const { defineCrudListBulkActions } = await import("@jskit-ai/users-web/client/bulkActions");
|
|
49
|
+
|
|
50
|
+
const actions = defineCrudListBulkActions([
|
|
51
|
+
null,
|
|
52
|
+
{ key: "archive", label: "Archive" },
|
|
53
|
+
{ key: "archive", label: "Archive again" },
|
|
54
|
+
{ key: "missing-label" },
|
|
55
|
+
{ label: "Generated key" }
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
actions.map((action) => [action.key, action.label, action.color, action.variant]),
|
|
60
|
+
[
|
|
61
|
+
["archive", "Archive", "primary", "tonal"],
|
|
62
|
+
["action-5", "Generated key", "primary", "tonal"]
|
|
63
|
+
]
|
|
64
|
+
);
|
|
65
|
+
});
|