@jskit-ai/users-web 0.1.165 → 0.1.168

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.
@@ -69,12 +69,20 @@ function toTimeInputValue(value) {
69
69
  return "";
70
70
  }
71
71
 
72
- const twentyFourHourMatch = normalized.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/u);
72
+ const twentyFourHourMatch = normalized.match(/^(\d{1,2}):(\d{2})(?::(\d{2})(\.\d+)?)?$/u);
73
73
  if (twentyFourHourMatch) {
74
74
  const hours = Number(twentyFourHourMatch[1]);
75
75
  const minutes = Number(twentyFourHourMatch[2]);
76
- if (hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) {
77
- return `${padDateTimePart(hours)}:${padDateTimePart(minutes)}`;
76
+ const seconds = Number(twentyFourHourMatch[3] || 0);
77
+ if (
78
+ hours >= 0 && hours <= 23 &&
79
+ minutes >= 0 && minutes <= 59 &&
80
+ seconds >= 0 && seconds <= 59
81
+ ) {
82
+ const secondsValue = twentyFourHourMatch[3]
83
+ ? `:${padDateTimePart(seconds)}${twentyFourHourMatch[4] || ""}`
84
+ : "";
85
+ return `${padDateTimePart(hours)}:${padDateTimePart(minutes)}${secondsValue}`;
78
86
  }
79
87
  return normalized;
80
88
  }
@@ -103,16 +111,30 @@ function toDateTimeLocalInputValue(value) {
103
111
  return "";
104
112
  }
105
113
 
114
+ const sourceText = typeof value === "string" ? value.trim() : "";
115
+ const sourceFraction = sourceText.match(/T\d{2}:\d{2}:\d{2}\.(\d+)/u)?.[1] || "";
106
116
  const date = value instanceof Date ? value : new Date(value);
107
117
  if (Number.isNaN(date.getTime())) {
108
118
  return String(value);
109
119
  }
110
120
 
111
- return [
121
+ const minuteValue = [
112
122
  date.getFullYear(),
113
123
  padDateTimePart(date.getMonth() + 1),
114
124
  padDateTimePart(date.getDate())
115
125
  ].join("-") + `T${padDateTimePart(date.getHours())}:${padDateTimePart(date.getMinutes())}`;
126
+ if (sourceFraction && !/^0+$/u.test(sourceFraction)) {
127
+ return `${minuteValue}:${padDateTimePart(date.getSeconds())}.${sourceFraction}`;
128
+ }
129
+
130
+ const milliseconds = date.getMilliseconds();
131
+ if (milliseconds > 0) {
132
+ return `${minuteValue}:${padDateTimePart(date.getSeconds())}.${String(milliseconds).padStart(3, "0")}`;
133
+ }
134
+ if (date.getSeconds() > 0) {
135
+ return `${minuteValue}:${padDateTimePart(date.getSeconds())}`;
136
+ }
137
+ return minuteValue;
116
138
  }
117
139
 
118
140
  function toDateInputValue(value) {
@@ -146,7 +168,31 @@ function toDateInputValue(value) {
146
168
  return normalized;
147
169
  }
148
170
 
149
- function toIsoUtcDateTimeValue(value) {
171
+ function applyDateTimePrecision(isoValue, temporalPrecision) {
172
+ if (!Number.isInteger(temporalPrecision) || temporalPrecision < 0) {
173
+ return isoValue;
174
+ }
175
+
176
+ const match = String(isoValue || "").match(/^(.*?)(?:\.(\d+))?Z$/u);
177
+ if (!match?.[2]) {
178
+ return isoValue;
179
+ }
180
+
181
+ const fraction = match[2];
182
+ if (fraction.length <= temporalPrecision) {
183
+ return isoValue;
184
+ }
185
+
186
+ const excess = fraction.slice(temporalPrecision);
187
+ if (!/^0+$/u.test(excess)) {
188
+ return isoValue;
189
+ }
190
+
191
+ const retained = fraction.slice(0, temporalPrecision);
192
+ return `${match[1]}${retained ? `.${retained}` : ""}Z`;
193
+ }
194
+
195
+ function toIsoUtcDateTimeValue(value, temporalPrecision) {
150
196
  const normalized = String(value ?? "").trim();
151
197
  if (!normalized) {
152
198
  return "";
@@ -157,7 +203,12 @@ function toIsoUtcDateTimeValue(value) {
157
203
  return normalized;
158
204
  }
159
205
 
160
- return date.toISOString();
206
+ const sourceFraction = normalized.match(/T\d{2}:\d{2}:\d{2}\.(\d+)/u)?.[1] || "";
207
+ const dateIsoValue = date.toISOString();
208
+ const preciseIsoValue = sourceFraction
209
+ ? dateIsoValue.replace(/\.\d{3}Z$/u, `.${sourceFraction}Z`)
210
+ : dateIsoValue;
211
+ return applyDateTimePrecision(preciseIsoValue, temporalPrecision);
161
212
  }
162
213
 
163
214
  function resolveFormFieldInitialValue(field = {}) {
@@ -278,7 +329,7 @@ function buildCrudFormPayload(fields = [], model = {}) {
278
329
  }
279
330
 
280
331
  if (fieldFormat === "date-time") {
281
- const normalizedValue = toIsoUtcDateTimeValue(rawValue);
332
+ const normalizedValue = toIsoUtcDateTimeValue(rawValue, field.temporalPrecision);
282
333
  if (!normalizedValue) {
283
334
  if (clearAsNull) {
284
335
  payload[fieldKey] = null;
@@ -0,0 +1,160 @@
1
+ import { computed, proxyRefs, ref, unref } from "vue";
2
+ import { useRouter } from "vue-router";
3
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import { resolveCrudJsonApiTransport } from "./crud/crudJsonApiTransportSupport.js";
5
+ import { toQueryErrorMessage } from "./support/errorMessageHelpers.js";
6
+ import { useCommand } from "./useCommand.js";
7
+
8
+ function requireCrudDeleteOperation(resource = null) {
9
+ const operation = resource?.operations?.delete;
10
+ if (!operation || typeof operation !== "object" || Array.isArray(operation)) {
11
+ throw new TypeError("useCrudDeleteAction requires resource.operations.delete.");
12
+ }
13
+ if (normalizeText(operation.method).toUpperCase() !== "DELETE") {
14
+ throw new TypeError("useCrudDeleteAction requires resource.operations.delete.method to be DELETE.");
15
+ }
16
+ return operation;
17
+ }
18
+
19
+ function requireCrudViewScreen(screen = null) {
20
+ if (!screen || typeof screen !== "object" || typeof screen?.view?.resolveParams !== "function") {
21
+ throw new TypeError("useCrudDeleteAction requires a useCrudViewScreen() result.");
22
+ }
23
+ return screen;
24
+ }
25
+
26
+ function resolveDeleteApiSuffix(screen, apiUrlTemplate = "") {
27
+ const template = normalizeText(unref(apiUrlTemplate));
28
+ return template ? normalizeText(screen.view.resolveParams(template)) : "";
29
+ }
30
+
31
+ function resolveListLocation(screen) {
32
+ return unref(screen?.listLocation) || null;
33
+ }
34
+
35
+ function useCrudDeleteAction({
36
+ screen = null,
37
+ resource = null,
38
+ resourceNamespace = "",
39
+ apiUrlTemplate = "",
40
+ access = "auto",
41
+ client = null,
42
+ router: routerOverride = null,
43
+ fallbackDeleteError = "Unable to delete record."
44
+ } = {}) {
45
+ const resolvedScreen = requireCrudViewScreen(screen);
46
+ const deleteOperation = requireCrudDeleteOperation(resource);
47
+ const namespace = normalizeText(resourceNamespace, {
48
+ fallback: resource?.namespace
49
+ });
50
+ if (!namespace) {
51
+ throw new TypeError("useCrudDeleteAction requires resourceNamespace or resource.namespace.");
52
+ }
53
+
54
+ const activeRouter = routerOverride || useRouter();
55
+ if (!activeRouter || typeof activeRouter.push !== "function") {
56
+ throw new TypeError("useCrudDeleteAction requires an installed Vue Router.");
57
+ }
58
+
59
+ const isOpen = ref(false);
60
+ const error = ref("");
61
+ const deleteApiSuffix = computed(() => resolveDeleteApiSuffix(resolvedScreen, apiUrlTemplate));
62
+
63
+ async function handleDeleteSuccess(_response, context = {}) {
64
+ await context?.queryClient?.invalidateQueries?.({
65
+ queryKey: ["ui-generator", namespace]
66
+ });
67
+
68
+ const listLocation = resolveListLocation(resolvedScreen);
69
+ if (!listLocation) {
70
+ throw new Error("Deleted the record, but the generated list route is unavailable.");
71
+ }
72
+
73
+ isOpen.value = false;
74
+ await activeRouter.push(listLocation);
75
+ }
76
+
77
+ function handleDeleteError(cause) {
78
+ error.value = toQueryErrorMessage(
79
+ cause,
80
+ fallbackDeleteError,
81
+ "Unable to delete record."
82
+ );
83
+ }
84
+
85
+ const command = useCommand({
86
+ access,
87
+ apiSuffix: deleteApiSuffix,
88
+ writeMethod: deleteOperation.method,
89
+ client,
90
+ transport: resolveCrudJsonApiTransport(undefined, resource, {
91
+ mode: "delete"
92
+ }),
93
+ placementSource: `ui-generator.${namespace}.view.delete`,
94
+ fallbackRunError: fallbackDeleteError,
95
+ onRunSuccess: handleDeleteSuccess,
96
+ onRunError: handleDeleteError,
97
+ suppressSuccessMessage: true
98
+ });
99
+
100
+ const isDeleting = computed(() => Boolean(command.isRunning));
101
+ const canDelete = computed(() => {
102
+ const view = resolvedScreen.view;
103
+ return Boolean(
104
+ command.canRun &&
105
+ deleteApiSuffix.value &&
106
+ resolveListLocation(resolvedScreen) &&
107
+ unref(view.recordId) &&
108
+ unref(view.record) &&
109
+ !unref(view.isLoading) &&
110
+ !unref(view.isNotFound)
111
+ );
112
+ });
113
+
114
+ function request() {
115
+ if (!canDelete.value) {
116
+ return false;
117
+ }
118
+ error.value = "";
119
+ isOpen.value = true;
120
+ return true;
121
+ }
122
+
123
+ function cancel() {
124
+ if (isDeleting.value) {
125
+ return false;
126
+ }
127
+ error.value = "";
128
+ isOpen.value = false;
129
+ return true;
130
+ }
131
+
132
+ async function confirm() {
133
+ if (!isOpen.value || !canDelete.value || isDeleting.value) {
134
+ return null;
135
+ }
136
+
137
+ error.value = "";
138
+ try {
139
+ return await command.run();
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ return proxyRefs({
146
+ isOpen,
147
+ isDeleting,
148
+ canDelete,
149
+ error,
150
+ request,
151
+ cancel,
152
+ confirm
153
+ });
154
+ }
155
+
156
+ export {
157
+ requireCrudDeleteOperation,
158
+ resolveDeleteApiSuffix,
159
+ useCrudDeleteAction
160
+ };
@@ -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";
@@ -11,6 +12,7 @@ export {
11
12
  normalizeCrudApiAccess,
12
13
  resolveCrudHttpClient
13
14
  } from "./composables/crud/crudHttpClientSupport.js";
15
+ export { useCrudDeleteAction } from "./composables/useCrudDeleteAction.js";
14
16
 
15
17
  const clientProviders = Object.freeze([UsersWebClientProvider]);
16
18
 
@@ -69,6 +69,19 @@ test("inferCrudJsonApiTransport infers record request/response transport for CRU
69
69
  );
70
70
  });
71
71
 
72
+ test("inferCrudJsonApiTransport infers record response transport for CRUD delete", () => {
73
+ assert.deepEqual(
74
+ inferCrudJsonApiTransport(resource, {
75
+ mode: "delete"
76
+ }),
77
+ {
78
+ kind: "jsonapi-resource",
79
+ responseType: "pets",
80
+ responseKind: "record"
81
+ }
82
+ );
83
+ });
84
+
72
85
  test("inferCrudLookupJsonApiTransport infers collection transport from lookup namespace", () => {
73
86
  assert.deepEqual(
74
87
  inferCrudLookupJsonApiTransport({
@@ -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"/);
@@ -45,6 +49,7 @@ test("CRUD screen components own generated list/view/form chrome centrally", asy
45
49
  assert.match(viewSource, /generated-ui-screen generated-ui-screen--operator ui-generator-view-element/);
46
50
  assert.match(viewSource, /ui-generator-view-panel/);
47
51
  assert.match(viewSource, /@click="view\.refresh"/);
52
+ assert.match(viewSource, /<slot name="actions" :screen="screen" :view="view" \/>/);
48
53
  assert.match(viewSource, /<slot name="before-fields"/);
49
54
  assert.match(viewSource, /<slot name="fields"/);
50
55
  assert.match(viewSource, /<slot name="after-fields"/);
@@ -54,6 +59,11 @@ test("CRUD screen components own generated list/view/form chrome centrally", asy
54
59
  assert.match(addEditSource, /addEdit\.canRetryLoad/);
55
60
  assert.match(addEditSource, /@click="addEdit\.refresh"/);
56
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=/);
57
67
  });
58
68
 
59
69
  test("CRUD screen composables expose generated page extension inputs", async () => {
@@ -87,13 +97,15 @@ test("CRUD screen composables are importable package APIs", async () => {
87
97
  viewModule,
88
98
  addEditModule,
89
99
  rowActionsModule,
90
- rowActionsRuntimeModule
100
+ rowActionsRuntimeModule,
101
+ deleteActionModule
91
102
  ] = await Promise.all([
92
103
  import("@jskit-ai/users-web/client/composables/useCrudListScreen"),
93
104
  import("@jskit-ai/users-web/client/composables/useCrudViewScreen"),
94
105
  import("@jskit-ai/users-web/client/composables/useCrudAddEditScreen"),
95
106
  import("@jskit-ai/users-web/client/rowActions"),
96
- import("@jskit-ai/users-web/client/composables/useCrudListRowActions")
107
+ import("@jskit-ai/users-web/client/composables/useCrudListRowActions"),
108
+ import("@jskit-ai/users-web/client/composables/useCrudDeleteAction")
97
109
  ]);
98
110
 
99
111
  assert.equal(typeof listModule.useCrudListScreen, "function");
@@ -101,4 +113,5 @@ test("CRUD screen composables are importable package APIs", async () => {
101
113
  assert.equal(typeof addEditModule.useCrudAddEditScreen, "function");
102
114
  assert.equal(typeof rowActionsModule.defineCrudListRowActions, "function");
103
115
  assert.equal(typeof rowActionsRuntimeModule.useCrudListRowActions, "function");
116
+ assert.equal(typeof deleteActionModule.useCrudDeleteAction, "function");
104
117
  });
@@ -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",
@@ -33,6 +34,7 @@ test("users-web exports are explicit and aligned with production/template usage"
33
34
  "./client/composables/useView",
34
35
  "./client/composables/useCrudAddEdit",
35
36
  "./client/composables/useCrudAddEditScreen",
37
+ "./client/composables/useCrudDeleteAction",
36
38
  "./client/composables/useCrudListBulkActions",
37
39
  "./client/composables/useCrudListRowActions",
38
40
  "./client/composables/useCrudListFilterLookups",
@@ -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
  [