Skip to content

Commit 22464cc

Browse files
committed
Merge PR_26179_ALFA_013 Objects Asset Links
2 parents 76ef2bc + 8a75ce7 commit 22464cc

15 files changed

Lines changed: 1040 additions & 931 deletions

assets/toolbox/objects/js/index.js

Lines changed: 218 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
validateObjectDefinition,
1212
} from "../../../../src/engine/object-model/index.js";
1313
import { createAssetToolApiRepository } from "../../../js/shared/assets-api-client.js";
14+
import { listMessages } from "../../../../toolbox/messages/messages-api-client.js";
1415

1516
const constants = readServerToolConstants("objects");
1617

@@ -53,10 +54,13 @@ let editingRow = null;
5354
let storageIssue = null;
5455
let selectedObjectKey = "";
5556
let detailDraftBaseline = null;
57+
let messageRecords = null;
58+
let messageLoadIssue = null;
5659

5760
const elements = {
5861
addRow: document.querySelector("[data-objects-add-row]"),
5962
assetStatus: document.querySelector("[data-objects-asset-status]"),
63+
assetLinks: document.querySelector("[data-objects-asset-links]"),
6064
count: document.querySelector("[data-objects-count]"),
6165
detailActive: document.querySelector("[data-objects-detail-active]"),
6266
detailAudio: document.querySelector("[data-objects-detail-audio]"),
@@ -65,6 +69,7 @@ const elements = {
6569
detailDescription: document.querySelector("[data-objects-detail-description]"),
6670
detailEmpty: document.querySelector("[data-objects-details-empty]"),
6771
detailForm: document.querySelector("[data-objects-details-form]"),
72+
detailMessage: document.querySelector("[data-objects-detail-message]"),
6873
detailName: document.querySelector("[data-objects-detail-name]"),
6974
detailSave: document.querySelector("[data-objects-detail-save]"),
7075
detailSelected: document.querySelector("[data-objects-detail-selected]"),
@@ -153,6 +158,7 @@ function normalizeObjectDetails(source = {}) {
153158
audioReference: normalizeText(details.audioReference ?? source.audioReference),
154159
defaultValues: normalizeText(details.defaultValues ?? source.defaultValues),
155160
description: normalizeText(details.description ?? source.description),
161+
messageReference: normalizeText(details.messageReference ?? source.messageReference),
156162
spriteReference: normalizeText(details.spriteReference ?? source.spriteReference) || normalizeText(source.render?.assetKey),
157163
tags: Object.freeze(parseTags(details.tags ?? source.tags)),
158164
visible: normalizeBoolean(details.visible ?? source.visible, true),
@@ -481,6 +487,7 @@ function detailFieldElements() {
481487
elements.detailAudio,
482488
elements.detailDefaults,
483489
elements.detailDescription,
490+
elements.detailMessage,
484491
elements.detailName,
485492
elements.detailSprite,
486493
elements.detailTags,
@@ -503,6 +510,7 @@ function detailSnapshotForObject(object = {}) {
503510
audioReference: details.audioReference,
504511
defaultValues: details.defaultValues,
505512
description: details.description,
513+
messageReference: details.messageReference,
506514
name: normalizeText(object.name),
507515
spriteReference: details.spriteReference,
508516
tags: [...details.tags],
@@ -517,6 +525,7 @@ function detailSnapshotFromForm() {
517525
audioReference: normalizeText(elements.detailAudio?.value),
518526
defaultValues: normalizeText(elements.detailDefaults?.value),
519527
description: normalizeText(elements.detailDescription?.value),
528+
messageReference: normalizeText(elements.detailMessage?.value),
520529
name: normalizeText(elements.detailName?.value),
521530
spriteReference: normalizeText(elements.detailSprite?.value),
522531
tags: parseTags(elements.detailTags?.value),
@@ -606,6 +615,9 @@ function populateDetailForm(object) {
606615
if (elements.detailAudio) {
607616
elements.detailAudio.value = snapshot.audioReference;
608617
}
618+
if (elements.detailMessage) {
619+
elements.detailMessage.value = snapshot.messageReference;
620+
}
609621
if (elements.detailDefaults) {
610622
elements.detailDefaults.value = snapshot.defaultValues;
611623
}
@@ -661,6 +673,20 @@ function objectDetailFindings(snapshot, originalId = "") {
661673
label: "Sprite reference",
662674
});
663675
}
676+
if (snapshot.audioReference && !linkedAudioAsset(snapshot.audioReference)) {
677+
findings.push({
678+
action: "Use an existing audio asset reference, or clear the Audio reference before saving Object Details.",
679+
label: "Audio reference",
680+
});
681+
}
682+
if (snapshot.messageReference && !linkedMessageRecord(snapshot.messageReference)) {
683+
findings.push({
684+
action: messageLoadIssue
685+
? "Messages could not be checked. Reload Objects after the API is available, or clear the Message reference before saving Object Details."
686+
: "Use an existing message reference, or clear the Message reference before saving Object Details.",
687+
label: "Message reference",
688+
});
689+
}
664690
return findings;
665691
}
666692

@@ -680,6 +706,7 @@ function objectFromDetailSnapshot(object, snapshot) {
680706
audioReference: snapshot.audioReference,
681707
defaultValues: snapshot.defaultValues,
682708
description: snapshot.description,
709+
messageReference: snapshot.messageReference,
683710
spriteReference: snapshot.spriteReference,
684711
tags: snapshot.tags,
685712
visible: snapshot.visible,
@@ -857,12 +884,97 @@ function listAssetRecords() {
857884
return Array.isArray(tables?.asset_library_items) ? tables.asset_library_items : [];
858885
}
859886

887+
function assetReferenceValues(asset = {}) {
888+
return [
889+
asset.id,
890+
asset.key,
891+
asset.name,
892+
asset.fileName,
893+
asset.originalName,
894+
asset.reference,
895+
asset.path,
896+
asset.storedPath,
897+
asset.storageObjectKey,
898+
asset.targetFilePath,
899+
].map(normalizeText).filter(Boolean);
900+
}
901+
902+
function assetHasReference(asset, reference) {
903+
const key = normalizeText(reference);
904+
return Boolean(key) && assetReferenceValues(asset).some((value) => value === key);
905+
}
906+
907+
function assetRoleText(asset = {}) {
908+
return [
909+
asset.assetRole,
910+
asset.assetRoleLabel,
911+
asset.assetType,
912+
asset.mimeType,
913+
asset.role,
914+
asset.type,
915+
asset.usage,
916+
].map(normalizeText).join(" ").toLowerCase();
917+
}
918+
919+
function isSpriteAsset(asset = {}) {
920+
const text = assetRoleText(asset);
921+
return text.includes("sprite") || text.includes("image") || text.includes("png") || text.includes("jpeg") || text.includes("webp");
922+
}
923+
924+
function isAudioAsset(asset = {}) {
925+
const text = assetRoleText(asset);
926+
return text.includes("audio") || text.includes("sound") || text.includes("music") || text.includes("voice") || text.includes("wav") || text.includes("mpeg") || text.includes("ogg");
927+
}
928+
929+
function linkedAssetByReference(reference, predicate) {
930+
const key = normalizeText(reference);
931+
if (!key) {
932+
return null;
933+
}
934+
return listAssetRecords().find((asset) => assetHasReference(asset, key) && predicate(asset)) || null;
935+
}
936+
860937
function linkedSpriteAsset(assetKey) {
861-
const key = normalizeText(assetKey);
938+
return linkedAssetByReference(assetKey, isSpriteAsset);
939+
}
940+
941+
function linkedAudioAsset(reference) {
942+
return linkedAssetByReference(reference, isAudioAsset);
943+
}
944+
945+
function listMessageRecords() {
946+
if (messageRecords !== null) {
947+
return messageRecords;
948+
}
949+
try {
950+
const result = listMessages();
951+
messageRecords = Array.isArray(result?.messages) ? result.messages : [];
952+
messageLoadIssue = null;
953+
} catch {
954+
messageRecords = [];
955+
messageLoadIssue = {
956+
action: "Messages could not be checked. Reload Objects after the API is available.",
957+
label: "Message references",
958+
};
959+
}
960+
return messageRecords;
961+
}
962+
963+
function messageReferenceValues(message = {}) {
964+
return [
965+
message.key,
966+
message.id,
967+
message.name,
968+
message.slug,
969+
].map(normalizeText).filter(Boolean);
970+
}
971+
972+
function linkedMessageRecord(reference) {
973+
const key = normalizeText(reference);
862974
if (!key) {
863975
return null;
864976
}
865-
return listAssetRecords().find((asset) => asset.id === key) || null;
977+
return listMessageRecords().find((message) => messageReferenceValues(message).some((value) => value === key)) || null;
866978
}
867979

868980
function assetDisplayText(asset, fallbackKey = "") {
@@ -1177,6 +1289,107 @@ function renderOutput(objects, findings) {
11771289
}
11781290
}
11791291

1292+
function messageDisplayText(message, fallbackKey = "") {
1293+
const key = normalizeText(message?.key || message?.id || fallbackKey);
1294+
const name = normalizeText(message?.name);
1295+
if (key && name && name !== key) {
1296+
return `${name} (${key})`;
1297+
}
1298+
return name || key || "Message missing";
1299+
}
1300+
1301+
function referenceStatusItem({
1302+
action = null,
1303+
label,
1304+
linkDataName = "",
1305+
linkText = "",
1306+
linkValue = "",
1307+
linkedText = "",
1308+
missingText = "",
1309+
reference,
1310+
warningName = "",
1311+
}) {
1312+
const item = document.createElement("li");
1313+
const heading = document.createElement("strong");
1314+
heading.textContent = `${label}:`;
1315+
item.append(heading, " ");
1316+
if (!reference) {
1317+
item.append("No reference set.");
1318+
return item;
1319+
}
1320+
if (!linkedText) {
1321+
item.dataset.objectsReferenceWarning = warningName || label;
1322+
item.append(missingText);
1323+
if (action) {
1324+
item.append(" ", actionLink(linkText, action, linkDataName, linkValue));
1325+
}
1326+
return item;
1327+
}
1328+
item.append(linkedText);
1329+
if (action) {
1330+
item.append(" ", actionLink(linkText, action, linkDataName, linkValue));
1331+
}
1332+
return item;
1333+
}
1334+
1335+
function renderAssetLinks() {
1336+
if (!elements.assetLinks) {
1337+
return;
1338+
}
1339+
elements.assetLinks.replaceChildren();
1340+
const object = selectedObject();
1341+
if (!object) {
1342+
elements.assetLinks.append(listItem("Select an object to review sprite, audio, and message links."));
1343+
return;
1344+
}
1345+
1346+
const details = detailsForObject(object);
1347+
const spriteAsset = details.spriteReference ? linkedSpriteAsset(details.spriteReference) : null;
1348+
const audioAsset = details.audioReference ? linkedAudioAsset(details.audioReference) : null;
1349+
const messageRecord = details.messageReference ? linkedMessageRecord(details.messageReference) : null;
1350+
1351+
elements.assetLinks.append(
1352+
referenceStatusItem({
1353+
action: spriteAsset ? spriteEditorHref({
1354+
...object,
1355+
render: { assetKey: details.spriteReference, type: "Sprite" },
1356+
}) : null,
1357+
label: "Sprite",
1358+
linkDataName: "objectsAssetLinkSprite",
1359+
linkText: "Edit Sprite",
1360+
linkValue: objectId(object),
1361+
linkedText: spriteAsset ? assetDisplayText(spriteAsset, details.spriteReference) : "",
1362+
missingText: `Sprite reference "${details.spriteReference}" is not linked to an existing sprite asset.`,
1363+
reference: details.spriteReference,
1364+
warningName: "sprite",
1365+
}),
1366+
referenceStatusItem({
1367+
action: details.audioReference ? "/toolbox/assets/index.html" : null,
1368+
label: "Audio",
1369+
linkDataName: "objectsAssetLinkAudio",
1370+
linkText: "Open Assets",
1371+
linkValue: normalizeText(audioAsset?.id || details.audioReference),
1372+
linkedText: audioAsset ? assetDisplayText(audioAsset, details.audioReference) : "",
1373+
missingText: `Audio reference "${details.audioReference}" is not linked to an existing audio asset.`,
1374+
reference: details.audioReference,
1375+
warningName: "audio",
1376+
}),
1377+
referenceStatusItem({
1378+
action: details.messageReference ? "/toolbox/messages/index.html" : null,
1379+
label: "Message",
1380+
linkDataName: "objectsMessageLink",
1381+
linkText: "Open Messages",
1382+
linkValue: normalizeText(messageRecord?.key || details.messageReference),
1383+
linkedText: messageRecord ? messageDisplayText(messageRecord, details.messageReference) : "",
1384+
missingText: messageLoadIssue
1385+
? "Message references could not be checked. Reload Objects after the API is available."
1386+
: `Message reference "${details.messageReference}" is not linked to an existing Message.`,
1387+
reference: details.messageReference,
1388+
warningName: "message",
1389+
}),
1390+
);
1391+
}
1392+
11801393
function renderTemplateCatalog() {
11811394
if (elements.templateCatalog) {
11821395
elements.templateCatalog.replaceChildren();
@@ -1229,6 +1442,7 @@ function render() {
12291442
renderOutput(draftedObjects, findings);
12301443
renderValidation(findings);
12311444
renderDetailsPanel({ preserveDirty: true });
1445+
renderAssetLinks();
12321446
if (elements.addRow) {
12331447
elements.addRow.disabled = Boolean(editingRow);
12341448
}
@@ -1428,6 +1642,8 @@ function resetTable() {
14281642
}
14291643

14301644
function refreshLinkedRenderAssetDisplay() {
1645+
messageRecords = null;
1646+
messageLoadIssue = null;
14311647
if (!editingRow) {
14321648
render();
14331649
return;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# PR_26179_ALFA_013 Branch Validation
2+
3+
Branch: PR_26179_ALFA_013-objects-asset-links
4+
Base stack: PR_26179_ALFA_012-objects-properties-mvp
5+
6+
## Gates
7+
- Current branch is PR_26179_ALFA_013-objects-asset-links: PASS
8+
- Project Instructions loaded from `dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md`: PASS
9+
- Batch governance addendum loaded from `dev/build/ProjectInstructions/addendums/batch_governance_mode.md`: PASS
10+
- Canonical report path `dev/reports/`: PASS
11+
- Canonical ZIP path `dev/workspace/zips/`: PASS
12+
- No `docs_build/` report output created: PASS
13+
- No `tmp/` ZIP output created: PASS
14+
- One PR purpose only: PASS
15+
- No API architecture change: PASS
16+
17+
## Validation Commands
18+
- `node --check assets/toolbox/objects/js/index.js`: PASS
19+
- `node --check src/dev-runtime/toolbox-api/alfa-tool-services.mjs`: PASS
20+
- `node --check dev/tests/playwright/tools/ObjectsTool.spec.mjs`: PASS
21+
- `node --test dev/tests/dev-runtime/ObjectsApiService.test.mjs`: PASS
22+
- `npx playwright test dev/tests/playwright/tools/ObjectsTool.spec.mjs --grep "Object Details panel saves reviewable properties through shared DB"`: PASS
23+
- `npx playwright test dev/tests/playwright/tools/ObjectsTool.spec.mjs`: PASS
24+
- `git diff --check`: PASS
25+
- `npm run validate:canonical-structure`: PASS
26+
27+
Branch validation: PASS
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# PR_26179_ALFA_013 Manual Validation Notes
2+
3+
## Manual Review Path
4+
1. Open `toolbox/objects/index.html` with the API server running.
5+
2. Add an object with Sprite render enabled.
6+
3. Select `Details` on the object row.
7+
4. Confirm Object Details includes Sprite reference, Audio reference, and Message reference.
8+
5. Confirm the Asset Links panel shows the selected object's sprite, audio, and message statuses.
9+
6. Enter a missing audio or message reference and press Save Details.
10+
7. Confirm friendly validation appears without exposing API/database internals.
11+
8. Enter an existing message reference and save.
12+
9. Refresh the page; confirm the message reference reloads and the Asset Links panel resolves it.
13+
14+
## Expected Owner Review Result
15+
The Objects inspector is reviewable as a product surface for asset and message references. It keeps object details, reference links, and missing-reference guidance in one place without exposing a behavior editor, Rules integration, Worlds integration, JSON editor, or engine internals.
16+
17+
## Known Out-of-Scope Items
18+
- Creating new audio assets from Objects.
19+
- Creating new messages from Objects.
20+
- Behavior editor.
21+
- Rules integration.
22+
- Worlds integration.
23+
- Database schema expansion.
24+
- New API routes.

0 commit comments

Comments
 (0)