@jskit-ai/users-web 0.1.166 → 0.1.170
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +489 -10
- package/src/client/components/CrudDeleteAction.vue +93 -0
- package/src/client/components/CrudListScreen.vue +1 -1
- package/src/client/composables/crud/crudJsonApiTransportSupport.js +1 -1
- package/src/client/composables/support/resourceLoadStateHelpers.js +8 -29
- package/src/client/composables/useScopeRuntime.js +2 -2
- package/src/client/index.js +1 -0
- package/test/crudJsonApiTransportSupport.test.js +1 -1
- package/test/crudListDateFilterSurface.browser.test.js +12 -103
- package/test/crudScreenComponents.test.js +10 -1
- package/test/exportsContract.test.js +1 -1
- package/test/{packageDescriptorOwnership.test.js → packageMetadataOwnership.test.js} +4 -2
- package/test/settingsPlacementContract.test.js +10 -8
- package/package.descriptor.mjs +0 -468
- package/src/client/composables/usePaths.js +0 -120
- package/src/client/composables/useSurfaceRouteContext.js +0 -25
|
@@ -77,12 +77,8 @@ function normalizeRequestRecoveryMethod(value = "") {
|
|
|
77
77
|
return normalizeText(value).toUpperCase();
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function isRequestRecoveryMetaDisabled(sourceJskitMeta = {}
|
|
81
|
-
return
|
|
82
|
-
sourceJskitMeta.requestRecovery === false ||
|
|
83
|
-
sourceMeta.jskitRequestRecovery === false ||
|
|
84
|
-
sourceMeta.requestRecovery === false
|
|
85
|
-
);
|
|
80
|
+
function isRequestRecoveryMetaDisabled(sourceJskitMeta = {}) {
|
|
81
|
+
return sourceJskitMeta.requestRecovery === false;
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
function hasUsableMetaValue(source = {}, key = "") {
|
|
@@ -161,37 +157,20 @@ function buildRequestRecoveryMeta(requestRecovery = null, defaults = {}) {
|
|
|
161
157
|
return Object.keys(jskit).length > 0 ? { jskit } : {};
|
|
162
158
|
}
|
|
163
159
|
|
|
164
|
-
function hasRequestRecoveryMetaValue(sourceJskitMeta = {}, sourceMeta = {}, key = "") {
|
|
165
|
-
const suffix = String(key || "").trim();
|
|
166
|
-
if (!suffix) {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const jskitKey = `requestRecovery${suffix}`;
|
|
171
|
-
const legacyJskitKey = `jskitRequestRecovery${suffix}`;
|
|
172
|
-
const legacyKey = `requestRecovery${suffix}`;
|
|
173
|
-
|
|
174
|
-
return Boolean(
|
|
175
|
-
hasUsableMetaValue(sourceJskitMeta, jskitKey) ||
|
|
176
|
-
hasUsableMetaValue(sourceMeta, legacyJskitKey) ||
|
|
177
|
-
hasUsableMetaValue(sourceMeta, legacyKey)
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
160
|
function resolveRequestRecoveryDefaults(queryOptions = null, defaults = {}) {
|
|
182
161
|
const sourceOptions = normalizePlainObject(queryOptions);
|
|
183
162
|
const sourceMeta = normalizePlainObject(sourceOptions.meta);
|
|
184
163
|
const sourceJskitMeta = normalizePlainObject(sourceMeta.jskit);
|
|
185
164
|
const fallback = normalizePlainObject(defaults);
|
|
186
|
-
if (isRequestRecoveryMetaDisabled(sourceJskitMeta
|
|
165
|
+
if (isRequestRecoveryMetaDisabled(sourceJskitMeta)) {
|
|
187
166
|
return {};
|
|
188
167
|
}
|
|
189
168
|
|
|
190
|
-
const hasExplicitLabel =
|
|
191
|
-
const hasExplicitSource =
|
|
192
|
-
const hasExplicitDedupeKey =
|
|
193
|
-
const hasExplicitDedupeWindowMs =
|
|
194
|
-
const hasExplicitMethod =
|
|
169
|
+
const hasExplicitLabel = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryLabel");
|
|
170
|
+
const hasExplicitSource = hasUsableMetaValue(sourceJskitMeta, "requestRecoverySource");
|
|
171
|
+
const hasExplicitDedupeKey = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryDedupeKey");
|
|
172
|
+
const hasExplicitDedupeWindowMs = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryDedupeWindowMs");
|
|
173
|
+
const hasExplicitMethod = hasUsableMetaValue(sourceJskitMeta, "requestRecoveryMethod");
|
|
195
174
|
|
|
196
175
|
return {
|
|
197
176
|
...fallback,
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
ROUTE_VISIBILITY_WORKSPACE
|
|
5
5
|
} from "@jskit-ai/kernel/shared/support/visibility";
|
|
6
6
|
import { useAccess } from "./useAccess.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { usePaths } from "@jskit-ai/shell-web/client/navigation/usePaths";
|
|
8
|
+
import { useSurfaceRouteContext } from "@jskit-ai/shell-web/client/navigation/useSurfaceRouteContext";
|
|
9
9
|
import { resolveSurfaceDefinitionFromPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
10
10
|
import {
|
|
11
11
|
asPlainObject,
|
package/src/client/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { UsersWebClientProvider } from "./providers/UsersWebClientProvider.js";
|
|
|
3
3
|
export { UsersWebClientProvider } from "./providers/UsersWebClientProvider.js";
|
|
4
4
|
export { default as AccountSettingsClientElement } from "./components/AccountSettingsClientElement.vue";
|
|
5
5
|
export { default as CrudAddEditScreen } from "./components/CrudAddEditScreen.vue";
|
|
6
|
+
export { default as CrudDeleteAction } from "./components/CrudDeleteAction.vue";
|
|
6
7
|
export { default as CrudListBulkActionSurface } from "./components/CrudListBulkActionSurface.vue";
|
|
7
8
|
export { default as CrudListFilterSurface } from "./components/CrudListFilterSurface.vue";
|
|
8
9
|
export { default as CrudListScreen } from "./components/CrudListScreen.vue";
|
|
@@ -1,99 +1,22 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import net from "node:net";
|
|
4
2
|
import path from "node:path";
|
|
5
3
|
import process from "node:process";
|
|
6
4
|
import test from "node:test";
|
|
7
5
|
import { fileURLToPath } from "node:url";
|
|
8
6
|
import { chromium } from "@playwright/test";
|
|
7
|
+
import {
|
|
8
|
+
createChromiumLaunchOptions,
|
|
9
|
+
startViteFixture,
|
|
10
|
+
stopProcess
|
|
11
|
+
} from "../../../tooling/testUtils/browserFixture.mjs";
|
|
9
12
|
|
|
10
13
|
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const REPOSITORY_ROOT = path.resolve(TEST_DIRECTORY, "../../..");
|
|
12
14
|
const PACKAGE_ROOT = path.resolve(TEST_DIRECTORY, "..");
|
|
13
15
|
const FIXTURE_ROOT = path.join(PACKAGE_ROOT, "fixtures", "crud-list-date-filters");
|
|
14
|
-
const VITE_CLI = path.join(REPOSITORY_ROOT, "node_modules", "vite", "bin", "vite.js");
|
|
15
16
|
const RUN_BROWSER_TEST = process.env.JSKIT_USERS_WEB_DATE_FILTER_VISUAL_INTEGRATION === "1";
|
|
16
17
|
const VIEWPORT_WIDTHS = Object.freeze([375, 768, 1280, 1440]);
|
|
17
18
|
const QUERY = "?status=active&submittedOn=2026-04-18&arrivalDate=2026-05-04..2026-05-10";
|
|
18
19
|
|
|
19
|
-
function startCapturedProcess(command, args, { cwd } = {}) {
|
|
20
|
-
const child = spawn(command, args, {
|
|
21
|
-
cwd,
|
|
22
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
23
|
-
env: {
|
|
24
|
-
...process.env,
|
|
25
|
-
NO_COLOR: "1",
|
|
26
|
-
FORCE_COLOR: "0"
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
let output = "";
|
|
30
|
-
|
|
31
|
-
child.stdout.on("data", (chunk) => {
|
|
32
|
-
output += chunk.toString("utf8");
|
|
33
|
-
});
|
|
34
|
-
child.stderr.on("data", (chunk) => {
|
|
35
|
-
output += chunk.toString("utf8");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
function waitFor(pattern, timeout = 30_000) {
|
|
39
|
-
return new Promise((resolve, reject) => {
|
|
40
|
-
const startedAt = Date.now();
|
|
41
|
-
const timer = setInterval(() => {
|
|
42
|
-
if (pattern.test(output)) {
|
|
43
|
-
clearInterval(timer);
|
|
44
|
-
resolve();
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
if (child.exitCode !== null) {
|
|
48
|
-
clearInterval(timer);
|
|
49
|
-
reject(new Error(`Vite exited before ${pattern}.\n${output}`));
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (Date.now() - startedAt >= timeout) {
|
|
53
|
-
clearInterval(timer);
|
|
54
|
-
reject(new Error(`Timed out waiting for ${pattern}.\n${output}`));
|
|
55
|
-
}
|
|
56
|
-
}, 50);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
child,
|
|
62
|
-
readOutput: () => output,
|
|
63
|
-
waitFor
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function stopProcess(runtime) {
|
|
68
|
-
const child = runtime?.child;
|
|
69
|
-
if (!child || child.exitCode !== null || child.signalCode !== null) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
await new Promise((resolve) => {
|
|
74
|
-
const forceTimer = setTimeout(() => child.kill("SIGKILL"), 5_000);
|
|
75
|
-
child.once("exit", () => {
|
|
76
|
-
clearTimeout(forceTimer);
|
|
77
|
-
resolve();
|
|
78
|
-
});
|
|
79
|
-
child.kill("SIGTERM");
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function reservePort() {
|
|
84
|
-
const server = net.createServer();
|
|
85
|
-
await new Promise((resolve, reject) => {
|
|
86
|
-
server.once("error", reject);
|
|
87
|
-
server.listen(0, "127.0.0.1", resolve);
|
|
88
|
-
});
|
|
89
|
-
const address = server.address();
|
|
90
|
-
assert.ok(address && typeof address === "object");
|
|
91
|
-
await new Promise((resolve, reject) => {
|
|
92
|
-
server.close((error) => error ? reject(error) : resolve());
|
|
93
|
-
});
|
|
94
|
-
return address.port;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
20
|
async function readContract(page) {
|
|
98
21
|
return JSON.parse(await page.getByTestId("date-filter-contract").textContent());
|
|
99
22
|
}
|
|
@@ -108,7 +31,7 @@ async function openCompactFilters(page) {
|
|
|
108
31
|
|
|
109
32
|
async function assertResponsiveDateControls(page, width) {
|
|
110
33
|
await page.setViewportSize({ width, height: 900 });
|
|
111
|
-
await page.goto(
|
|
34
|
+
await page.goto(`/${QUERY}`, {
|
|
112
35
|
waitUntil: "networkidle"
|
|
113
36
|
});
|
|
114
37
|
await openCompactFilters(page);
|
|
@@ -158,37 +81,23 @@ test("CRUD date filters render and operate without clipping across responsive la
|
|
|
158
81
|
: "set JSKIT_USERS_WEB_DATE_FILTER_VISUAL_INTEGRATION=1 to run the browser visual regression",
|
|
159
82
|
timeout: 180_000
|
|
160
83
|
}, async () => {
|
|
161
|
-
const
|
|
162
|
-
const viteRuntime = startCapturedProcess(process.execPath, [
|
|
163
|
-
VITE_CLI,
|
|
164
|
-
"--config",
|
|
165
|
-
path.join(FIXTURE_ROOT, "vite.config.mjs"),
|
|
166
|
-
"--port",
|
|
167
|
-
String(port),
|
|
168
|
-
"--clearScreen",
|
|
169
|
-
"false"
|
|
170
|
-
], { cwd: FIXTURE_ROOT });
|
|
84
|
+
const viteRuntime = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
171
85
|
let browser = null;
|
|
172
86
|
|
|
173
87
|
try {
|
|
174
|
-
await
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
browser = await chromium.launch({
|
|
179
|
-
headless: true,
|
|
180
|
-
...(chromiumExecutablePath ? { executablePath: chromiumExecutablePath } : {})
|
|
88
|
+
browser = await chromium.launch(createChromiumLaunchOptions());
|
|
89
|
+
const context = await browser.newContext({
|
|
90
|
+
baseURL: viteRuntime.baseURL,
|
|
91
|
+
locale: "en-US"
|
|
181
92
|
});
|
|
182
|
-
const context = await browser.newContext({ locale: "en-US" });
|
|
183
93
|
const page = await context.newPage();
|
|
184
|
-
page.__jskitPort = port;
|
|
185
94
|
|
|
186
95
|
for (const width of VIEWPORT_WIDTHS) {
|
|
187
96
|
await assertResponsiveDateControls(page, width);
|
|
188
97
|
}
|
|
189
98
|
|
|
190
99
|
await page.setViewportSize({ width: 1280, height: 900 });
|
|
191
|
-
await page.goto(
|
|
100
|
+
await page.goto(`/${QUERY}`, { waitUntil: "networkidle" });
|
|
192
101
|
assert.deepEqual(await readContract(page), {
|
|
193
102
|
values: {
|
|
194
103
|
submittedOn: "2026-04-18",
|
|
@@ -19,6 +19,7 @@ test("CRUD screen components own generated list/view/form chrome centrally", asy
|
|
|
19
19
|
const listSource = await readComponent("CrudListScreen.vue");
|
|
20
20
|
const viewSource = await readComponent("CrudViewScreen.vue");
|
|
21
21
|
const addEditSource = await readComponent("CrudAddEditScreen.vue");
|
|
22
|
+
const deleteActionSource = await readComponent("CrudDeleteAction.vue");
|
|
22
23
|
|
|
23
24
|
assert.match(listSource, /CrudListBulkActionSurface/);
|
|
24
25
|
assert.match(listSource, /CrudListFilterSurface/);
|
|
@@ -36,7 +37,10 @@ test("CRUD screen components own generated list/view/form chrome centrally", asy
|
|
|
36
37
|
);
|
|
37
38
|
assert.match(listSource, /button-label="More"/);
|
|
38
39
|
assert.match(listSource, /selectableRows/);
|
|
39
|
-
assert.match(
|
|
40
|
+
assert.match(
|
|
41
|
+
listSource,
|
|
42
|
+
/\.ui-generator-list-element :deep\(\.v-btn\)\s*\{\s*min-height:\s*48px;/
|
|
43
|
+
);
|
|
40
44
|
assert.match(listSource, /<slot[\s\S]*name="card-fields"/);
|
|
41
45
|
assert.match(listSource, /<slot name="table-header"/);
|
|
42
46
|
assert.match(listSource, /<slot[\s\S]*name="table-row"/);
|
|
@@ -55,6 +59,11 @@ test("CRUD screen components own generated list/view/form chrome centrally", asy
|
|
|
55
59
|
assert.match(addEditSource, /addEdit\.canRetryLoad/);
|
|
56
60
|
assert.match(addEditSource, /@click="addEdit\.refresh"/);
|
|
57
61
|
assert.match(addEditSource, /<slot[\s\S]*name="fields"/);
|
|
62
|
+
|
|
63
|
+
assert.match(deleteActionSource, /@click="action\.request"/);
|
|
64
|
+
assert.match(deleteActionSource, /@click="action\.confirm"/);
|
|
65
|
+
assert.match(deleteActionSource, /@click="closeDialog"/);
|
|
66
|
+
assert.doesNotMatch(deleteActionSource, /\bactivator=/);
|
|
58
67
|
});
|
|
59
68
|
|
|
60
69
|
test("CRUD screen composables expose generated page extension inputs", async () => {
|
|
@@ -17,6 +17,7 @@ test("users-web exports are explicit and aligned with production/template usage"
|
|
|
17
17
|
"./client",
|
|
18
18
|
"./client/components/AccountSettingsClientElement",
|
|
19
19
|
"./client/components/CrudAddEditScreen",
|
|
20
|
+
"./client/components/CrudDeleteAction",
|
|
20
21
|
"./client/components/CrudListBulkActionSurface",
|
|
21
22
|
"./client/components/CrudListFilterSurface",
|
|
22
23
|
"./client/components/CrudListScreen",
|
|
@@ -29,7 +30,6 @@ test("users-web exports are explicit and aligned with production/template usage"
|
|
|
29
30
|
"./client/composables/useCommand",
|
|
30
31
|
"./client/composables/useEndpointResource",
|
|
31
32
|
"./client/composables/useList",
|
|
32
|
-
"./client/composables/usePaths",
|
|
33
33
|
"./client/composables/useView",
|
|
34
34
|
"./client/composables/useCrudAddEdit",
|
|
35
35
|
"./client/composables/useCrudAddEditScreen",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import
|
|
3
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
4
|
+
|
|
5
|
+
const packageMetadata = packageJson.jskit;
|
|
4
6
|
|
|
5
7
|
test("users-web leaves account surface scaffolds app-owned", () => {
|
|
6
8
|
const expectedIds = [
|
|
@@ -11,7 +13,7 @@ test("users-web leaves account surface scaffolds app-owned", () => {
|
|
|
11
13
|
];
|
|
12
14
|
|
|
13
15
|
for (const id of expectedIds) {
|
|
14
|
-
const mutation =
|
|
16
|
+
const mutation = packageMetadata.mutations.files.find((entry) => entry.id === id);
|
|
15
17
|
assert.ok(mutation, `Missing users-web scaffold mutation ${id}.`);
|
|
16
18
|
assert.equal(mutation.ownership, "app", `${id} must remain app-owned across package updates.`);
|
|
17
19
|
}
|
|
@@ -4,13 +4,15 @@ import test from "node:test";
|
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { assertGeneratedUiSourceContract } from "@jskit-ai/kernel/shared/support/generatedUiContract";
|
|
7
|
-
import
|
|
7
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
8
|
+
|
|
9
|
+
const packageMetadata = packageJson.jskit;
|
|
8
10
|
|
|
9
11
|
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
10
12
|
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
11
13
|
|
|
12
14
|
function readOutlets(host = "") {
|
|
13
|
-
const outlets =
|
|
15
|
+
const outlets = packageMetadata?.metadata?.ui?.placements?.outlets;
|
|
14
16
|
const normalizedTarget = String(host || "").trim();
|
|
15
17
|
return Array.isArray(outlets)
|
|
16
18
|
? outlets.filter((entry) => String(entry?.target || "").trim() === normalizedTarget)
|
|
@@ -18,7 +20,7 @@ function readOutlets(host = "") {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
function findTopology(id, owner = "") {
|
|
21
|
-
const placements =
|
|
23
|
+
const placements = packageMetadata?.metadata?.ui?.placements?.topology?.placements;
|
|
22
24
|
const normalizedId = String(id || "").trim();
|
|
23
25
|
const normalizedOwner = String(owner || "").trim();
|
|
24
26
|
return Array.isArray(placements)
|
|
@@ -31,28 +33,28 @@ function findTopology(id, owner = "") {
|
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
function findContribution(id) {
|
|
34
|
-
const contributions =
|
|
36
|
+
const contributions = packageMetadata?.metadata?.ui?.placements?.contributions;
|
|
35
37
|
return Array.isArray(contributions)
|
|
36
38
|
? contributions.find((entry) => String(entry?.id || "").trim() === id) || null
|
|
37
39
|
: null;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
function findTextMutation(id) {
|
|
41
|
-
const textMutations =
|
|
43
|
+
const textMutations = packageMetadata?.mutations?.text;
|
|
42
44
|
return Array.isArray(textMutations)
|
|
43
45
|
? textMutations.find((entry) => String(entry?.id || "").trim() === id) || null
|
|
44
46
|
: null;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
function findSourceMutation(id) {
|
|
48
|
-
const sourceMutations =
|
|
50
|
+
const sourceMutations = packageMetadata?.mutations?.source;
|
|
49
51
|
return Array.isArray(sourceMutations)
|
|
50
52
|
? sourceMutations.find((entry) => String(entry?.id || "").trim() === id) || null
|
|
51
53
|
: null;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function findFileMutation(id) {
|
|
55
|
-
const fileMutations =
|
|
57
|
+
const fileMutations = packageMetadata?.mutations?.files;
|
|
56
58
|
return Array.isArray(fileMutations)
|
|
57
59
|
? fileMutations.find((entry) => String(entry?.id || "").trim() === id) || null
|
|
58
60
|
: null;
|
|
@@ -197,7 +199,7 @@ test("users-web account settings section templates use direct settings panels",
|
|
|
197
199
|
}
|
|
198
200
|
});
|
|
199
201
|
|
|
200
|
-
test("users-web
|
|
202
|
+
test("users-web packageMetadata metadata advertises home cog outlet and standard home settings placements", () => {
|
|
201
203
|
assert.deepEqual(
|
|
202
204
|
readOutlets("home-cog:primary-menu"),
|
|
203
205
|
[
|