Skip to content

Commit 5ec070a

Browse files
committed
Sync minimap generic metadata handling
1 parent 858bda6 commit 5ec070a

4 files changed

Lines changed: 173 additions & 30 deletions

File tree

tools/minimap/src/roadmap.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ function makeBoardItemSummary(itemSummary) {
343343
commitment: itemSummary.commitment,
344344
milestone: itemSummary.milestone,
345345
kind: itemSummary.kind,
346+
metadata: { ...itemSummary.metadata },
346347
overviewHeading: itemSummary.overviewHeading,
347348
overviewExcerpt: itemSummary.overviewExcerpt,
348349
};
@@ -1100,8 +1101,8 @@ export async function readItemById(repoRoot, id) {
11001101
kind: item.kind,
11011102
filePath: path.relative(repoRoot, item.filePath),
11021103
metadata: {
1103-
...item.parsed.frontmatter,
1104-
milestone: item.parsed.frontmatter.milestone ?? "",
1104+
...item.parsed.metadataValues,
1105+
milestone: item.parsed.metadataValues.milestone ?? "",
11051106
},
11061107
sections: Object.fromEntries(KNOWN_SECTIONS.map((heading) => [heading, item.parsed.sections[heading] ?? ""])),
11071108
sectionOrder: item.parsed.segments.map((segment) => segment.heading),
@@ -1251,3 +1252,5 @@ export async function saveBoardByGroups(repoRoot, groupsPayload) {
12511252

12521253

12531254

1255+
1256+

tools/minimap/ui/app.js

Lines changed: 161 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ const fields = {
100100
commitment: document.querySelector("#field-commitment"),
101101
boardGroup: document.querySelector("#field-board-group"),
102102
milestone: document.querySelector("#field-milestone"),
103+
extraMetadataContainer: document.querySelector("#extra-metadata-fields"),
103104
};
104105

105106
function loadStoredScopePreference() {
@@ -891,28 +892,79 @@ function getBadgeTone(field, value) {
891892
return "neutral";
892893
}
893894

894-
function renderBadge(value, field = "") {
895+
const CORE_METADATA_FIELDS = ["status", "priority", "commitment", "milestone"];
896+
const RESERVED_METADATA_KEYS = new Set(["id", "title", "kind", "labels", ...CORE_METADATA_FIELDS]);
897+
898+
function normalizeMetadataValue(value) {
899+
if (value === null || value === undefined || Array.isArray(value) || typeof value === "object") {
900+
return "";
901+
}
902+
903+
return String(value).trim();
904+
}
905+
906+
function getCustomMetadataEntries(metadata, options = {}) {
907+
const excludeKey = normalizeBadgeToken(options.excludeKey || "");
908+
const maxCount = Number.isFinite(options.maxCount) ? options.maxCount : Number.POSITIVE_INFINITY;
909+
const entries = Object.entries(metadata || {})
910+
.map(([key, value]) => ({ key, value: normalizeMetadataValue(value) }))
911+
.filter((entry) => entry.value && !RESERVED_METADATA_KEYS.has(entry.key) && normalizeBadgeToken(entry.key) !== excludeKey)
912+
.sort((left, right) => humanizeFilterKey(left.key).localeCompare(humanizeFilterKey(right.key)));
913+
914+
return entries.slice(0, maxCount);
915+
}
916+
917+
function buildMetadataBadgeEntries(metadata, options = {}) {
918+
const excludeKey = normalizeBadgeToken(options.excludeKey || "");
919+
const cardMode = options.cardMode === true;
920+
const entries = CORE_METADATA_FIELDS
921+
.filter((field) => normalizeBadgeToken(field) !== excludeKey)
922+
.map((field) => ({ field, value: normalizeMetadataValue(metadata?.[field]), showFieldLabel: false }))
923+
.filter((entry) => entry.value);
924+
925+
const customEntries = getCustomMetadataEntries(metadata, { excludeKey, maxCount: cardMode ? 2 : Number.POSITIVE_INFINITY })
926+
.map((entry) => ({ field: entry.key, value: entry.value, showFieldLabel: true }));
927+
928+
return [...entries, ...customEntries];
929+
}
930+
931+
function renderBadge(value, field = "", options = {}) {
895932
const normalizedField = normalizeBadgeToken(field);
896933
const tone = getBadgeTone(normalizedField, value);
897934
const classes = ["badge", `badge-tone-${tone}`];
898935
if (normalizedField) {
899936
classes.push(`badge-field-${normalizedField}`);
900937
}
901-
return `<span class="${classes.join(" ")}">${escapeHtml(value)}</span>`;
938+
const label = options.showFieldLabel && normalizedField
939+
? `${humanizeFilterKey(normalizedField)}: ${value}`
940+
: value;
941+
return `<span class="${classes.join(" ")}">${escapeHtml(label)}</span>`;
942+
}
943+
944+
function getBadgeMetadata(item) {
945+
if (!item || typeof item !== "object") {
946+
return {};
947+
}
948+
949+
if (item.metadata && typeof item.metadata === "object") {
950+
return item.metadata;
951+
}
952+
953+
return Object.fromEntries(CORE_METADATA_FIELDS
954+
.map((field) => [field, normalizeMetadataValue(item[field])])
955+
.filter(([, value]) => value));
902956
}
903957

904-
function renderBadges(item, excludeKey = "") {
905-
return [
906-
excludeKey === "status" ? null : { field: "status", value: item.status },
907-
excludeKey === "priority" ? null : { field: "priority", value: item.priority },
908-
excludeKey === "commitment" ? null : { field: "commitment", value: item.commitment },
909-
excludeKey === "milestone" ? null : { field: "milestone", value: item.milestone },
910-
]
911-
.filter((entry) => entry?.value)
912-
.map((entry) => renderBadge(entry.value, entry.field))
958+
function renderMetadataBadges(metadata, excludeKey = "", options = {}) {
959+
return buildMetadataBadgeEntries(metadata || {}, { ...options, excludeKey })
960+
.map((entry) => renderBadge(entry.value, entry.field, { showFieldLabel: entry.showFieldLabel }))
913961
.join("");
914962
}
915963

964+
function renderBadges(item, excludeKey = "", options = {}) {
965+
return renderMetadataBadges(getBadgeMetadata(item), excludeKey, options);
966+
}
967+
916968
function updateDocumentTitle() {
917969
const repoName = state.workspace?.repoName || repoNameElement.textContent || "Roadmap";
918970
repoNameElement.textContent = repoName;
@@ -1626,6 +1678,66 @@ function renderBoardGroupField(itemId = state.selectedItemId) {
16261678
fields.boardGroup.value = selectedIndex >= 0 ? String(selectedIndex) : "";
16271679
}
16281680

1681+
function getEditableMetadataOptions(key, currentValue = "") {
1682+
const values = [];
1683+
const lens = state.workspace?.availableLenses?.find((entry) => entry.key === key);
1684+
const facet = state.workspace?.availableFilters?.find((entry) => entry.key === key);
1685+
1686+
if (Array.isArray(lens?.values)) {
1687+
values.push(...lens.values);
1688+
}
1689+
if (Array.isArray(facet?.values)) {
1690+
values.push(...facet.values);
1691+
}
1692+
if (currentValue) {
1693+
values.push(currentValue);
1694+
}
1695+
1696+
return Array.from(new Set(values.map((value) => String(value).trim()).filter(Boolean)));
1697+
}
1698+
1699+
function renderExtraMetadataFields(item = state.currentItem) {
1700+
if (!fields.extraMetadataContainer) {
1701+
return;
1702+
}
1703+
1704+
const entries = getCustomMetadataEntries(item?.metadata, { maxCount: Number.POSITIVE_INFINITY });
1705+
if (entries.length === 0) {
1706+
fields.extraMetadataContainer.hidden = true;
1707+
fields.extraMetadataContainer.innerHTML = "";
1708+
return;
1709+
}
1710+
1711+
fields.extraMetadataContainer.hidden = false;
1712+
fields.extraMetadataContainer.innerHTML = entries.map((entry) => {
1713+
const key = escapeHtml(entry.key);
1714+
const label = escapeHtml(humanizeFilterKey(entry.key));
1715+
const value = escapeHtml(entry.value);
1716+
const options = getEditableMetadataOptions(entry.key, entry.value);
1717+
1718+
if (options.length >= 2) {
1719+
const optionMarkup = ['<option value=""></option>', ...options.map((option) => {
1720+
const selected = option === entry.value ? " selected" : "";
1721+
return `<option value="${escapeHtml(option)}"${selected}>${escapeHtml(option)}</option>`;
1722+
})].join("");
1723+
return `<label><span>${label}</span><select data-extra-metadata-key="${key}">${optionMarkup}</select></label>`;
1724+
}
1725+
1726+
return `<label><span>${label}</span><input data-extra-metadata-key="${key}" type="text" value="${value}" /></label>`;
1727+
}).join("");
1728+
1729+
for (const input of fields.extraMetadataContainer.querySelectorAll("[data-extra-metadata-key]")) {
1730+
input.addEventListener("input", () => {
1731+
setDirtyState("structured", true);
1732+
renderPreview();
1733+
});
1734+
input.addEventListener("change", () => {
1735+
setDirtyState("structured", true);
1736+
renderPreview();
1737+
});
1738+
}
1739+
}
1740+
16291741
function buildBoardGroupsWithMovedItem(itemId, targetGroupIndex, boardGroups = buildBoardGroupsPayload()) {
16301742
if (!itemId || !Number.isInteger(targetGroupIndex) || targetGroupIndex < 0 || targetGroupIndex >= boardGroups.length) {
16311743
return null;
@@ -1725,7 +1837,7 @@ function buildBoardCardBodyMarkup(item, activeLensKey, extraMetaHtml = "") {
17251837
</span>
17261838
<span class="board-item-id">${escapeHtml(item.id)}</span>
17271839
${overview}
1728-
<span class="badge-row">${renderBadges(item, activeLensKey)}</span>
1840+
<span class="badge-row">${renderBadges(item, activeLensKey, { cardMode: true })}</span>
17291841
`;
17301842
}
17311843

@@ -2468,6 +2580,10 @@ function resetEditor() {
24682580
fields.boardGroup.innerHTML = "";
24692581
fields.boardGroup.disabled = true;
24702582
}
2583+
if (fields.extraMetadataContainer) {
2584+
fields.extraMetadataContainer.hidden = true;
2585+
fields.extraMetadataContainer.innerHTML = "";
2586+
}
24712587
sectionsContainer.innerHTML = "";
24722588
rawTextElement.value = "";
24732589
previewElement.className = "preview-surface preview-empty";
@@ -2548,14 +2664,27 @@ function getStructuredSections() {
25482664
}
25492665

25502666
function getStructuredMetadata() {
2551-
return {
2667+
const metadata = {
2668+
...(state.currentItem?.metadata || {}),
25522669
id: fields.id.value,
25532670
title: fields.title.value,
25542671
status: fields.status.value,
25552672
priority: fields.priority.value,
25562673
commitment: fields.commitment.value,
25572674
milestone: fields.milestone.value.trim(),
25582675
};
2676+
2677+
if (fields.extraMetadataContainer) {
2678+
for (const input of fields.extraMetadataContainer.querySelectorAll("[data-extra-metadata-key]")) {
2679+
const key = input.dataset.extraMetadataKey;
2680+
if (!key) {
2681+
continue;
2682+
}
2683+
metadata[key] = input.value.trim();
2684+
}
2685+
}
2686+
2687+
return metadata;
25592688
}
25602689

25612690
function renderPreview() {
@@ -2565,19 +2694,15 @@ function renderPreview() {
25652694
return;
25662695
}
25672696

2568-
const metadata = getStructuredMetadata();
2569-
const sections = getStructuredSections();
2570-
const orderedSections = getStructuredSectionHeadings().filter((heading) => Object.hasOwn(sections, heading));
2571-
const previewBadges = [
2572-
{ field: "status", value: metadata.status },
2573-
{ field: "priority", value: metadata.priority },
2574-
{ field: "commitment", value: metadata.commitment },
2575-
{ field: "milestone", value: metadata.milestone },
2576-
]
2577-
.filter((entry) => entry.value)
2578-
.map((entry) => renderBadge(entry.value, entry.field))
2579-
.join("");
2580-
const sectionHtml = orderedSections.map((heading) => `
2697+
const useDraftState = state.dirtyStructured;
2698+
const metadata = useDraftState ? getStructuredMetadata() : state.currentItem.metadata;
2699+
const orderedSections = getStructuredSectionHeadings(state.currentItem);
2700+
const sections = useDraftState
2701+
? getStructuredSections()
2702+
: Object.fromEntries(orderedSections.map((heading) => [heading, getSectionValueFromItem(state.currentItem, heading)]));
2703+
const visibleSections = orderedSections.filter((heading) => Object.hasOwn(sections, heading));
2704+
const previewBadges = renderMetadataBadges(metadata);
2705+
const sectionHtml = visibleSections.map((heading) => `
25812706
<section class="preview-section">
25822707
<div class="preview-section-header">
25832708
<h3>${escapeHtml(heading)}</h3>
@@ -2606,6 +2731,7 @@ function renderItem(item) {
26062731
ensureSelectValue(fields.commitment, item.metadata.commitment || "uncommitted");
26072732
renderBoardGroupField(item.metadata.id || item.id);
26082733
fields.milestone.value = item.metadata.milestone || "";
2734+
renderExtraMetadataFields(item);
26092735
renderStructuredSections(item);
26102736
rawTextElement.value = item.rawText || "";
26112737
autosizeStructuredTextareas();
@@ -3047,7 +3173,10 @@ saveButton.addEventListener("click", () => {
30473173
});
30483174

30493175
refreshButton.addEventListener("click", () => {
3050-
void loadWorkspace();
3176+
void loadWorkspace(state.selectedItemId, {
3177+
forceReloadItem: Boolean(state.selectedItemId),
3178+
replaceRoute: true,
3179+
});
30513180
});
30523181

30533182
setupViewElement.addEventListener("click", (event) => {
@@ -3267,3 +3396,8 @@ void loadWorkspace(initialRoute.itemId || state.selectedItemId, {
32673396
}
32683397
});
32693398

3399+
3400+
3401+
3402+
3403+

tools/minimap/ui/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ <h2 id="editor-title">Item</h2>
9191
<details class="metadata-details">
9292
<summary id="metadata-toggle" class="metadata-summary">
9393
<span class="metadata-summary-title">Item details</span>
94-
<span class="metadata-summary-copy">Title, board group, status, priority, commitment, milestone</span>
94+
<span class="metadata-summary-copy">Title, board group, status, priority, commitment, milestone, and repo metadata</span>
9595
</summary>
9696

9797
<div class="metadata-grid">
@@ -141,6 +141,8 @@ <h2 id="editor-title">Item</h2>
141141
<span>Milestone</span>
142142
<input id="field-milestone" name="milestone" type="text" placeholder="Optional" />
143143
</label>
144+
145+
<div id="extra-metadata-fields" class="metadata-extra-fields" hidden></div>
144146
</div>
145147

146148
<p class="muted editor-note">Markdown is allowed inside every section. Minimap keeps unknown frontmatter and extra sections intact.</p>

tools/minimap/ui/styles.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,10 @@ h3 {
10811081
gap: 12px;
10821082
}
10831083

1084+
.metadata-extra-fields {
1085+
display: contents;
1086+
}
1087+
10841088
.metadata-grid label,
10851089
.section-stack label {
10861090
display: grid;

0 commit comments

Comments
 (0)