@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.
@@ -77,12 +77,8 @@ function normalizeRequestRecoveryMethod(value = "") {
77
77
  return normalizeText(value).toUpperCase();
78
78
  }
79
79
 
80
- function isRequestRecoveryMetaDisabled(sourceJskitMeta = {}, sourceMeta = {}) {
81
- return Boolean(
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, sourceMeta)) {
165
+ if (isRequestRecoveryMetaDisabled(sourceJskitMeta)) {
187
166
  return {};
188
167
  }
189
168
 
190
- const hasExplicitLabel = hasRequestRecoveryMetaValue(sourceJskitMeta, sourceMeta, "Label");
191
- const hasExplicitSource = hasRequestRecoveryMetaValue(sourceJskitMeta, sourceMeta, "Source");
192
- const hasExplicitDedupeKey = hasRequestRecoveryMetaValue(sourceJskitMeta, sourceMeta, "DedupeKey");
193
- const hasExplicitDedupeWindowMs = hasRequestRecoveryMetaValue(sourceJskitMeta, sourceMeta, "DedupeWindowMs");
194
- const hasExplicitMethod = hasRequestRecoveryMetaValue(sourceJskitMeta, sourceMeta, "Method");
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 { useSurfaceRouteContext } from "./useSurfaceRouteContext.js";
8
- import { usePaths } from "./usePaths.js";
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,
@@ -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";
@@ -174,6 +174,6 @@ test("resolveCrudJsonApiTransport rejects explicit CRUD transport overrides", ()
174
174
  mode: "view"
175
175
  }
176
176
  ),
177
- /no longer accept explicit transport/
177
+ /do not accept explicit transport/
178
178
  );
179
179
  });
@@ -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(`http://127.0.0.1:${page.__jskitPort}/${QUERY}`, {
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 port = await reservePort();
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 viteRuntime.waitFor(new RegExp(`http://127\\.0\\.0\\.1:${port}/`, "u"));
175
- const chromiumExecutablePath = String(
176
- process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || ""
177
- ).trim();
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(`http://127.0.0.1:${port}/${QUERY}`, { waitUntil: "networkidle" });
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(listSource, /min-height:\s*48px/);
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 descriptor from "../package.descriptor.mjs";
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 = descriptor.mutations.files.find((entry) => entry.id === id);
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 descriptor from "../package.descriptor.mjs";
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 = descriptor?.metadata?.ui?.placements?.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 = descriptor?.metadata?.ui?.placements?.topology?.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 = descriptor?.metadata?.ui?.placements?.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 = descriptor?.mutations?.text;
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 = descriptor?.mutations?.source;
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 = descriptor?.mutations?.files;
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 descriptor metadata advertises home cog outlet and standard home settings placements", () => {
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
  [