fix(spa): extract 8hex UUID suffix from module DB id for @{uuid} refs
Bug A (`@{name}` → `@{uuid}`) shipped with a follow-on issue: I made
`wcSuggestions` return `m.id` (the full DB id like `wc_test2_ea67b173`),
but the syntax spec locks the wildcard ref form to `@{8hex}` only and
the tokenizer regex (`@\{[0-9a-f]{8}\}`) rejects anything else. So the
textarea ended up with raw `@{wc_test2_ea67b173}` text, no chip.
`ModuleRepository._gen_id` on the Python side builds ids as
`<prefix>_<slug>_<token_hex(4)>` — the trailing 8 hex chars are the
canonical UUID. This commit:
- Adds `extractModuleUuid(id)` helper in src/manager/utils/slug.ts
that returns the suffix or null for non-conforming ids
- Switches `WildcardEditor.wcSuggestions` and `nameByUuid` to key
by the 8hex UUID, not the full DB id
- Pins behaviour with 3 new tests on the helper
Runtime catalog population (`__wp_catalog__` is currently `{}` in
`wp_api/test_runner.py`) is a separate issue tracked under the upcoming
ComfyUI-bridge brainstorm; this commit only fixes the SPA-side syntax
correctness.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { toIdentifier, VALID_IDENTIFIER_RE } from "../utils/slug";
|
||||
import { extractModuleUuid, toIdentifier, VALID_IDENTIFIER_RE } from "../utils/slug";
|
||||
|
||||
describe("toIdentifier", () => {
|
||||
it("lowercases and slug-joins runs of non-identifier chars", () => {
|
||||
@@ -34,3 +34,25 @@ describe("toIdentifier", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractModuleUuid", () => {
|
||||
it("returns the trailing 8-hex suffix from a normal DB id", () => {
|
||||
expect(extractModuleUuid("wc_test2_ea67b173")).toBe("ea67b173");
|
||||
expect(extractModuleUuid("co_subject_aabbccdd")).toBe("aabbccdd");
|
||||
expect(extractModuleUuid("fv_my_consts_00112233")).toBe("00112233");
|
||||
});
|
||||
|
||||
it("accepts ids whose slug section contains digits and underscores", () => {
|
||||
expect(extractModuleUuid("wc_test_2_ea67b173")).toBe("ea67b173");
|
||||
expect(extractModuleUuid("wc_a_b_c_d_e_deadbeef")).toBe("deadbeef");
|
||||
});
|
||||
|
||||
it("returns null when the id does not end in `_<8 lowercase hex>`", () => {
|
||||
expect(extractModuleUuid("wc_test2")).toBeNull();
|
||||
expect(extractModuleUuid("wc_test2_EA67B173")).toBeNull(); // uppercase rejected
|
||||
expect(extractModuleUuid("wc_test2_eaa67b173")).toBeNull(); // 9 chars
|
||||
expect(extractModuleUuid("wc_test2_ea67b17")).toBeNull(); // 7 chars
|
||||
expect(extractModuleUuid("ea67b173")).toBeNull(); // missing underscore prefix
|
||||
expect(extractModuleUuid("")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,3 +19,23 @@ export function toIdentifier(input: string): string {
|
||||
}
|
||||
|
||||
export const VALID_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||
|
||||
/**
|
||||
* Extract the canonical 8-hex wildcard UUID from a full module DB id.
|
||||
*
|
||||
* `ModuleRepository._gen_id` builds ids as `<prefix>_<slug>_<token_hex(4)>`,
|
||||
* e.g. `wc_test2_ea67b173`. The syntax spec (§2.4 in 2026-04-28 design doc)
|
||||
* locks the wildcard ref form to `@{8hex}` — the trailing 8 hex chars are
|
||||
* the UUID. SPA editors that surface refs in autocomplete must extract this
|
||||
* suffix instead of inserting the whole DB id verbatim, otherwise the
|
||||
* tokenizer regex (matching `@\{[0-9a-f]{8}\}`) rejects the token and the
|
||||
* user sees raw `@{wc_test2_ea67b173}` text in the textarea with no chip.
|
||||
*
|
||||
* Returns null when the id does not end in `_<8hex>` (legacy/fixture data,
|
||||
* imported bundles with non-standard ids, etc.) so callers can decide
|
||||
* whether to skip or fall back.
|
||||
*/
|
||||
export function extractModuleUuid(id: string): string | null {
|
||||
const m = id.match(/_([0-9a-f]{8})$/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import RichTextInput from "../components/RichTextInput.vue";
|
||||
import { useToast } from "../composables/useToast";
|
||||
import { useModuleStore } from "../stores/moduleStore";
|
||||
import { useCategoryStore } from "../stores/categoryStore";
|
||||
import { toIdentifier, VALID_IDENTIFIER_RE } from "../utils/slug";
|
||||
import { extractModuleUuid, toIdentifier, VALID_IDENTIFIER_RE } from "../utils/slug";
|
||||
import { appendSnapshot, readHistory } from "../utils/history";
|
||||
import type {
|
||||
CombinePayload,
|
||||
@@ -54,18 +54,27 @@ const saving = ref(false);
|
||||
const isEdit = computed(() => !!props.id);
|
||||
const historyEntries = ref<ModuleHistoryEntry[]>([]);
|
||||
|
||||
// Suggestions: every other wildcard's UUID (excluding self) for `@`-trigger
|
||||
// nested-reference autocomplete. The canonical stored form is `@{8hex}` per
|
||||
// the syntax spec — the popover surfaces the human display name (via
|
||||
// `uuidToName` below) but the inserted token is the UUID. Returning names
|
||||
// here was the bug that caused `@{module_name}` to appear in the textarea
|
||||
// instead of `@{a1b2c3d4}`.
|
||||
// Suggestions: every other wildcard's 8-hex UUID (excluding self) for the
|
||||
// `@`-trigger nested-reference autocomplete. The canonical stored form is
|
||||
// `@{8hex}` per the syntax spec — the popover surfaces the human display
|
||||
// name (via `nameByUuid` below) but the inserted token is the bare UUID.
|
||||
//
|
||||
// Module DB ids are `<prefix>_<slug>_<8hex>` (see `ModuleRepository._gen_id`).
|
||||
// We strip everything except the trailing 8 hex chars: that is what the
|
||||
// tokenizer regex `@\{[0-9a-f]{8}\}` accepts, and it matches the runtime
|
||||
// catalog key shape consumed by `engine.modules.get_module(uuid)`.
|
||||
//
|
||||
// Two earlier shapes were wrong:
|
||||
// 1. var_binding/name → produced `@{test2}` (caught by users)
|
||||
// 2. full DB id (e.g. `wc_test2_ea67b173`) → tokenizer rejected it and the
|
||||
// textarea showed raw `@{wc_test2_ea67b173}` text with no ref chip
|
||||
const wcSuggestions = computed<string[]>(() => {
|
||||
const out: string[] = [];
|
||||
for (const m of moduleStore.items) {
|
||||
if (m.type !== "wildcard") continue;
|
||||
if (props.id && m.id === props.id) continue;
|
||||
out.push(m.id);
|
||||
const uuid = extractModuleUuid(m.id);
|
||||
if (uuid) out.push(uuid);
|
||||
}
|
||||
// Sort by display name so the popover orders alphabetically by what the
|
||||
// user actually sees, not by UUID hex.
|
||||
@@ -77,14 +86,18 @@ const wcSuggestions = computed<string[]>(() => {
|
||||
});
|
||||
|
||||
// UUID → display-name map used by RichTextInput to render `@{uuid}` chips
|
||||
// and the `@`-trigger autocomplete popover with human labels.
|
||||
// and the `@`-trigger autocomplete popover with human labels. Keyed by the
|
||||
// same 8-hex UUID surfaced through `wcSuggestions` so the popover, the
|
||||
// inline chip, and the resolver all agree on identity.
|
||||
const nameByUuid = computed<Map<string, string>>(() => {
|
||||
const m = new Map<string, string>();
|
||||
for (const mod of moduleStore.items) {
|
||||
if (mod.type !== "wildcard") continue;
|
||||
const uuid = extractModuleUuid(mod.id);
|
||||
if (!uuid) continue;
|
||||
const p = (mod.payload ?? {}) as { var_binding?: string };
|
||||
const display = (p.var_binding && p.var_binding.trim()) || toIdentifier(mod.name);
|
||||
if (display) m.set(mod.id, display);
|
||||
if (display) m.set(uuid, display);
|
||||
}
|
||||
return m;
|
||||
});
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/wp/images/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/wp/images/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/wp/images/site.webmanifest" />
|
||||
<script type="module" crossorigin src="/wp/assets/manager-DiDV9tbM.js"></script>
|
||||
<script type="module" crossorigin src="/wp/assets/manager-yLWQMHbh.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/wp/assets/manager-Dgq4_qmM.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user