@jskit-ai/users-web 0.1.80 → 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 +137 -21
- 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/components/UsersHomeToolsWidget.vue +0 -1
- 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/src/shared/toolsOutletContracts.js +0 -4
- 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 +146 -14
- package/test/useCrudListBulkActions.test.js +65 -0
|
@@ -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));
|
|
@@ -16,6 +17,19 @@ function readOutlets(host = "") {
|
|
|
16
17
|
: [];
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
function findTopology(id, owner = "") {
|
|
21
|
+
const placements = descriptor?.metadata?.ui?.placements?.topology?.placements;
|
|
22
|
+
const normalizedId = String(id || "").trim();
|
|
23
|
+
const normalizedOwner = String(owner || "").trim();
|
|
24
|
+
return Array.isArray(placements)
|
|
25
|
+
? placements.find((entry) => {
|
|
26
|
+
const entryId = String(entry?.id || "").trim();
|
|
27
|
+
const entryOwner = String(entry?.owner || "").trim();
|
|
28
|
+
return entryId === normalizedId && entryOwner === normalizedOwner;
|
|
29
|
+
}) || null
|
|
30
|
+
: null;
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
function findContribution(id) {
|
|
20
34
|
const contributions = descriptor?.metadata?.ui?.placements?.contributions;
|
|
21
35
|
return Array.isArray(contributions)
|
|
@@ -77,7 +91,8 @@ test("users-web home tools widget exposes home-cog outlet", async () => {
|
|
|
77
91
|
assert.match(source, /import \{ HOME_COG_OUTLET \} from "\.\.\/\.\.\/shared\/toolsOutletContracts\.js";/);
|
|
78
92
|
assert.match(source, /<ShellOutletMenuWidget/);
|
|
79
93
|
assert.match(source, /:target="HOME_COG_OUTLET\.target"/);
|
|
80
|
-
assert.match(source, /:
|
|
94
|
+
assert.match(source, /:aria-label="HOME_COG_OUTLET\.ariaLabel"/);
|
|
95
|
+
assert.doesNotMatch(source, /default-link-component-token/);
|
|
81
96
|
});
|
|
82
97
|
|
|
83
98
|
test("users-web account page template uses the package-owned account settings host", async () => {
|
|
@@ -96,19 +111,78 @@ test("users-web package-owned account settings host is fully placement-backed",
|
|
|
96
111
|
"utf8"
|
|
97
112
|
);
|
|
98
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
|
+
});
|
|
99
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/);
|
|
100
133
|
assert.doesNotMatch(source, /AccountSettingsProfileSection/);
|
|
101
134
|
assert.doesNotMatch(source, /AccountSettingsPreferencesSection/);
|
|
102
135
|
assert.doesNotMatch(source, /AccountSettingsNotificationsSection/);
|
|
103
136
|
});
|
|
104
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
|
+
|
|
105
180
|
test("users-web descriptor metadata advertises home cog outlet and standard home settings placements", () => {
|
|
106
181
|
assert.deepEqual(
|
|
107
182
|
readOutlets("home-cog:primary-menu"),
|
|
108
183
|
[
|
|
109
184
|
{
|
|
110
185
|
target: "home-cog:primary-menu",
|
|
111
|
-
defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
112
186
|
surfaces: ["home"],
|
|
113
187
|
source: "src/client/components/UsersHomeToolsWidget.vue"
|
|
114
188
|
}
|
|
@@ -124,18 +198,61 @@ test("users-web descriptor metadata advertises home cog outlet and standard home
|
|
|
124
198
|
}
|
|
125
199
|
]
|
|
126
200
|
);
|
|
201
|
+
assert.deepEqual(findTopology("home.tools-menu"), {
|
|
202
|
+
id: "home.tools-menu",
|
|
203
|
+
description: "Home surface tools menu actions.",
|
|
204
|
+
surfaces: ["home"],
|
|
205
|
+
variants: {
|
|
206
|
+
compact: {
|
|
207
|
+
outlet: "home-cog:primary-menu",
|
|
208
|
+
renderers: {
|
|
209
|
+
link: "local.main.ui.surface-aware-menu-link-item"
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
medium: {
|
|
213
|
+
outlet: "home-cog:primary-menu",
|
|
214
|
+
renderers: {
|
|
215
|
+
link: "local.main.ui.surface-aware-menu-link-item"
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
expanded: {
|
|
219
|
+
outlet: "home-cog:primary-menu",
|
|
220
|
+
renderers: {
|
|
221
|
+
link: "local.main.ui.surface-aware-menu-link-item"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
assert.deepEqual(findTopology("settings.sections", "account-settings"), {
|
|
227
|
+
id: "settings.sections",
|
|
228
|
+
owner: "account-settings",
|
|
229
|
+
description: "Account settings content sections.",
|
|
230
|
+
surfaces: ["account"],
|
|
231
|
+
variants: {
|
|
232
|
+
compact: {
|
|
233
|
+
outlet: "account-settings:sections"
|
|
234
|
+
},
|
|
235
|
+
medium: {
|
|
236
|
+
outlet: "account-settings:sections"
|
|
237
|
+
},
|
|
238
|
+
expanded: {
|
|
239
|
+
outlet: "account-settings:sections"
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
127
243
|
|
|
128
244
|
expectContribution("users.profile.menu.settings", {
|
|
129
|
-
target: "auth
|
|
245
|
+
target: "auth.profile-menu",
|
|
246
|
+
kind: "link",
|
|
130
247
|
surfaces: ["*"],
|
|
131
248
|
order: 500,
|
|
132
|
-
componentToken: "auth.web.profile.menu.link-item",
|
|
133
249
|
when: "auth.authenticated === true",
|
|
134
250
|
source: "mutations.text#users-web-profile-settings-placement"
|
|
135
251
|
});
|
|
136
252
|
|
|
137
253
|
expectContribution("users.home.tools.widget", {
|
|
138
|
-
target: "shell
|
|
254
|
+
target: "shell.status",
|
|
255
|
+
kind: "component",
|
|
139
256
|
surfaces: ["home"],
|
|
140
257
|
order: 900,
|
|
141
258
|
componentToken: "users.web.home.tools.widget",
|
|
@@ -144,30 +261,36 @@ test("users-web descriptor metadata advertises home cog outlet and standard home
|
|
|
144
261
|
});
|
|
145
262
|
|
|
146
263
|
expectContribution("users.home.menu.settings", {
|
|
147
|
-
target: "home-
|
|
264
|
+
target: "home.tools-menu",
|
|
265
|
+
kind: "link",
|
|
148
266
|
surfaces: ["home"],
|
|
149
267
|
order: 100,
|
|
150
|
-
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
151
268
|
when: "auth.authenticated === true",
|
|
152
269
|
source: "mutations.text#users-web-home-tools-placement"
|
|
153
270
|
});
|
|
154
271
|
assert.equal(findContribution("users.home.settings.general"), null);
|
|
155
272
|
expectContribution("users.account.settings.profile", {
|
|
156
|
-
target: "
|
|
273
|
+
target: "settings.sections",
|
|
274
|
+
owner: "account-settings",
|
|
275
|
+
kind: "component",
|
|
157
276
|
surfaces: ["account"],
|
|
158
277
|
order: 100,
|
|
159
278
|
componentToken: "local.main.account-settings.section.profile",
|
|
160
279
|
source: "mutations.text#users-web-account-settings-sections-placement"
|
|
161
280
|
});
|
|
162
281
|
expectContribution("users.account.settings.preferences", {
|
|
163
|
-
target: "
|
|
282
|
+
target: "settings.sections",
|
|
283
|
+
owner: "account-settings",
|
|
284
|
+
kind: "component",
|
|
164
285
|
surfaces: ["account"],
|
|
165
286
|
order: 200,
|
|
166
287
|
componentToken: "local.main.account-settings.section.preferences",
|
|
167
288
|
source: "mutations.text#users-web-account-settings-sections-placement"
|
|
168
289
|
});
|
|
169
290
|
expectContribution("users.account.settings.notifications", {
|
|
170
|
-
target: "
|
|
291
|
+
target: "settings.sections",
|
|
292
|
+
owner: "account-settings",
|
|
293
|
+
kind: "component",
|
|
171
294
|
surfaces: ["account"],
|
|
172
295
|
order: 300,
|
|
173
296
|
componentToken: "local.main.account-settings.section.notifications",
|
|
@@ -182,18 +305,23 @@ test("users-web descriptor metadata advertises home cog outlet and standard home
|
|
|
182
305
|
'id: "users.home.tools.widget"',
|
|
183
306
|
'componentToken: "users.web.home.tools.widget"',
|
|
184
307
|
'id: "users.home.menu.settings"',
|
|
185
|
-
'target: "home-
|
|
186
|
-
'
|
|
308
|
+
'target: "home.tools-menu"',
|
|
309
|
+
'kind: "link"',
|
|
187
310
|
'scopedSuffix: "/settings"',
|
|
188
311
|
'unscopedSuffix: "/settings"'
|
|
189
312
|
]
|
|
190
313
|
});
|
|
314
|
+
assert.equal(findTextMutation("users-web-home-tools-topology")?.file, "src/placementTopology.js");
|
|
315
|
+
assert.match(findTextMutation("users-web-home-tools-topology")?.value || "", /id: "home\.tools-menu"/);
|
|
316
|
+
assert.match(findTextMutation("users-web-home-tools-topology")?.value || "", /outlet: "home-cog:primary-menu"/);
|
|
191
317
|
expectTextMutation("users-web-account-settings-sections-placement", {
|
|
192
318
|
reason: "Append users-web account settings section placements into the app-owned placement registry.",
|
|
193
319
|
category: "users-web",
|
|
194
320
|
skipIfContains: 'id: "users.account.settings.profile"',
|
|
195
321
|
snippets: [
|
|
196
322
|
'id: "users.account.settings.profile"',
|
|
323
|
+
'target: "settings.sections"',
|
|
324
|
+
'owner: "account-settings"',
|
|
197
325
|
'componentToken: "local.main.account-settings.section.profile"',
|
|
198
326
|
'value: "profile"',
|
|
199
327
|
'id: "users.account.settings.preferences"',
|
|
@@ -204,6 +332,10 @@ test("users-web descriptor metadata advertises home cog outlet and standard home
|
|
|
204
332
|
'value: "notifications"'
|
|
205
333
|
]
|
|
206
334
|
});
|
|
335
|
+
assert.equal(findTextMutation("users-web-account-settings-topology")?.file, "src/placementTopology.js");
|
|
336
|
+
assert.match(findTextMutation("users-web-account-settings-topology")?.value || "", /id: "settings\.sections"/);
|
|
337
|
+
assert.match(findTextMutation("users-web-account-settings-topology")?.value || "", /owner: "account-settings"/);
|
|
338
|
+
assert.match(findTextMutation("users-web-account-settings-topology")?.value || "", /outlet: "account-settings:sections"/);
|
|
207
339
|
|
|
208
340
|
expectTextMutation("users-web-profile-settings-placement", {
|
|
209
341
|
reason: "Append users-web profile settings menu placement into app-owned placement registry.",
|
|
@@ -211,8 +343,8 @@ test("users-web descriptor metadata advertises home cog outlet and standard home
|
|
|
211
343
|
skipIfContains: 'id: "users.profile.menu.settings"',
|
|
212
344
|
snippets: [
|
|
213
345
|
'id: "users.profile.menu.settings"',
|
|
214
|
-
'target: "auth
|
|
215
|
-
'
|
|
346
|
+
'target: "auth.profile-menu"',
|
|
347
|
+
'kind: "link"',
|
|
216
348
|
'label: "Settings"',
|
|
217
349
|
'to: "/account"'
|
|
218
350
|
]
|
|
@@ -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
|
+
});
|