Skip to content

Commit 45eb35a

Browse files
committed
Make Admin Notes current folder listing live and add open folder link - PR_26156_191-admin-notes-live-folder-listing
1 parent 5237675 commit 45eb35a

30 files changed

Lines changed: 339 additions & 309 deletions

admin/notes.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ <h2 id="admin-notes-title" data-admin-notes-title>index.txt</h2>
5454
<nav class="callout" aria-labelledby="admin-notes-directory-title" data-admin-notes-directory>
5555
<div class="content-cluster">
5656
<h3 id="admin-notes-directory-title">Current Folder</h3>
57+
<a class="btn btn--compact primary" href="#" target="_blank" rel="noreferrer" data-admin-notes-open-folder>Open folder</a>
5758
<a class="btn btn--compact primary" href="admin/notes.html" data-admin-note-link="index" data-admin-notes-root-link>Return to root index</a>
5859
</div>
60+
<div class="status" role="status" data-admin-notes-folder-diagnostic></div>
5961
<div class="action-group action-group--tight" data-admin-notes-directory-links></div>
6062
</nav>
6163
<section class="callout action-group action-group--tight" aria-label="Status Legend" data-admin-notes-legend>

admin/notes.js

Lines changed: 93 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
const NOTES_DIRECTORY = "docs_build/dev/admin-notes";
2-
const DIRECTORY_INDEX_PATH = `${NOTES_DIRECTORY}/directory-index.json`;
32
const DEFAULT_NOTE = "index";
43
const LINK_CLASS = "btn btn--compact primary";
54
const NOTE_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
65
const STATUS_ICON_PATTERN = /^\[([ xX.!?])\]\s*(.*)$/;
76
const NOTE_INDEX_FILE = "index.txt";
7+
const DIRECTORY_LIST_QUERY = "adminNotesDirectory";
88
const STATUS_MARKERS = {
99
" ": {
1010
marker: "[ ]",
@@ -53,8 +53,9 @@ class AdminNotesViewer {
5353
this.content = documentRef.querySelector("[data-admin-notes-content]");
5454
this.directory = documentRef.querySelector("[data-admin-notes-directory]");
5555
this.directoryLinks = documentRef.querySelector("[data-admin-notes-directory-links]");
56+
this.openFolderLink = documentRef.querySelector("[data-admin-notes-open-folder]");
57+
this.folderDiagnostic = documentRef.querySelector("[data-admin-notes-folder-diagnostic]");
5658
this.legendList = documentRef.querySelector("[data-admin-notes-legend-list]");
57-
this.directoryIndex = null;
5859
this.bindEvents();
5960
}
6061

@@ -71,6 +72,16 @@ class AdminNotesViewer {
7172
}
7273
this.openNote(link.dataset.adminNoteLink || DEFAULT_NOTE, true);
7374
});
75+
this.openFolderLink?.addEventListener("click", (event) => {
76+
if (!this.openFolderLink.href || this.openFolderLink.getAttribute("aria-disabled") === "true") {
77+
event.preventDefault();
78+
this.setFolderDiagnostic("Open folder is available only for safe Admin Notes folders in local/dev mode.");
79+
return;
80+
}
81+
event.preventDefault();
82+
this.documentRef.defaultView?.open(this.openFolderLink.href, "_blank", "noopener,noreferrer");
83+
this.setFolderDiagnostic("If the folder did not open, your browser may require local file or dev-server mode for folder links.");
84+
});
7485
}
7586

7687
start() {
@@ -213,6 +224,17 @@ class AdminNotesViewer {
213224
return normalizedPath;
214225
}
215226

227+
adminNotesFolderPathFromValue(value) {
228+
const folderPath = this.rootRelativeFolderPathFromValue(value);
229+
if (!folderPath) {
230+
return null;
231+
}
232+
if (folderPath !== NOTES_DIRECTORY && !folderPath.startsWith(`${NOTES_DIRECTORY}/`)) {
233+
return null;
234+
}
235+
return folderPath;
236+
}
237+
216238
fileNameForPath(filePath) {
217239
return String(filePath || "").split("/").pop() || NOTE_INDEX_FILE;
218240
}
@@ -250,7 +272,10 @@ class AdminNotesViewer {
250272
}
251273

252274
async renderDirectoryLinks(folderPath, currentFilePath) {
253-
const entries = await this.directoryEntriesForFolder(folderPath);
275+
const listing = await this.directoryListingForFolder(folderPath);
276+
this.updateOpenFolderLink(listing);
277+
this.setFolderDiagnostic(listing.error || "");
278+
const entries = listing.entries;
254279
const safeEntries = entries
255280
.map((entry) => this.safeDirectoryEntry(entry))
256281
.filter(Boolean)
@@ -263,27 +288,68 @@ class AdminNotesViewer {
263288
});
264289
}
265290

266-
async directoryEntriesForFolder(folderPath) {
267-
const safeFolderPath = this.rootRelativeFolderPathFromValue(folderPath);
291+
async directoryListingForFolder(folderPath) {
292+
const safeFolderPath = this.adminNotesFolderPathFromValue(folderPath);
268293
if (!safeFolderPath) {
269-
return [];
294+
return {
295+
entries: [],
296+
error: "Current Folder listing is restricted to docs_build/dev/admin-notes/."
297+
};
298+
}
299+
try {
300+
const response = await fetch(this.directoryListingUrl(safeFolderPath), { cache: "no-store" });
301+
if (!response.ok) {
302+
return {
303+
entries: [],
304+
error: "Current Folder listing is unavailable. Start the local/dev server to read Admin Notes folders from the filesystem."
305+
};
306+
}
307+
const listing = await response.json();
308+
return {
309+
entries: Array.isArray(listing.entries) ? listing.entries : [],
310+
folderPath: this.adminNotesFolderPathFromValue(listing.folderPath),
311+
folderFileUrl: this.safeFolderFileUrl(listing.folderFileUrl),
312+
error: ""
313+
};
314+
} catch {
315+
return {
316+
entries: [],
317+
error: "Current Folder listing is unavailable. Start the local/dev server to read Admin Notes folders from the filesystem."
318+
};
270319
}
271-
const directoryIndex = await this.loadDirectoryIndex();
272-
const entries = directoryIndex.folders?.[safeFolderPath];
273-
return Array.isArray(entries) ? entries : [];
274320
}
275321

276-
async loadDirectoryIndex() {
277-
if (this.directoryIndex) {
278-
return this.directoryIndex;
322+
directoryListingUrl(folderPath) {
323+
const folderUrl = folderPath.endsWith("/") ? folderPath : `${folderPath}/`;
324+
return `${folderUrl}?${DIRECTORY_LIST_QUERY}=1`;
325+
}
326+
327+
safeFolderFileUrl(value) {
328+
const rawValue = String(value || "").trim();
329+
if (!rawValue || !rawValue.startsWith("file://")) {
330+
return "";
279331
}
280-
try {
281-
const response = await fetch(DIRECTORY_INDEX_PATH, { cache: "no-store" });
282-
this.directoryIndex = response.ok ? await response.json() : { folders: {} };
283-
} catch {
284-
this.directoryIndex = { folders: {} };
332+
return rawValue;
333+
}
334+
335+
setFolderDiagnostic(message) {
336+
if (this.folderDiagnostic) {
337+
this.folderDiagnostic.textContent = message;
338+
}
339+
}
340+
341+
updateOpenFolderLink(listing) {
342+
if (!this.openFolderLink) {
343+
return;
344+
}
345+
const folderUrl = listing.folderFileUrl || "";
346+
if (!folderUrl) {
347+
this.openFolderLink.href = "#";
348+
this.openFolderLink.setAttribute("aria-disabled", "true");
349+
return;
285350
}
286-
return this.directoryIndex;
351+
this.openFolderLink.href = folderUrl;
352+
this.openFolderLink.setAttribute("aria-disabled", "false");
287353
}
288354

289355
safeDirectoryEntry(entry) {
@@ -296,36 +362,26 @@ class AdminNotesViewer {
296362
if (!path.endsWith(`/${NOTE_INDEX_FILE}`)) {
297363
return null;
298364
}
299-
const folderPath = this.folderPathForFile(path);
300-
const noteName = this.noteNameForDirectoryPath(folderPath);
301-
return { label, noteName, path, type: "folder" };
365+
if (!this.adminNotesFolderPathFromValue(this.folderPathForFile(path))) {
366+
return null;
367+
}
368+
return { label, path, type: "folder" };
302369
}
303370
if (entry.type === "file") {
371+
if (!this.adminNotesFolderPathFromValue(this.folderPathForFile(path))) {
372+
return null;
373+
}
304374
return { label, path, type: "file" };
305375
}
306376
return null;
307377
}
308378

309-
noteNameForDirectoryPath(folderPath) {
310-
const prefix = `${NOTES_DIRECTORY}/`;
311-
if (!folderPath.startsWith(prefix)) {
312-
return null;
313-
}
314-
const noteName = folderPath.slice(prefix.length);
315-
return this.validNoteName(noteName) ? noteName : null;
316-
}
317-
318379
directoryLink(entry) {
319380
const link = this.documentRef.createElement("a");
320381
link.className = LINK_CLASS;
321382
link.textContent = entry.label;
322-
if (entry.type === "folder" && entry.noteName) {
323-
link.href = this.hrefForNote(entry.noteName);
324-
link.dataset.adminNoteLink = entry.noteName;
325-
} else {
326-
link.href = this.hrefForFile(entry.path);
327-
link.dataset.adminNoteFile = entry.path;
328-
}
383+
link.href = this.hrefForFile(entry.path);
384+
link.dataset.adminNoteFile = entry.path;
329385
link.dataset.adminNotesDirectoryLink = entry.type;
330386
return link;
331387
}

0 commit comments

Comments
 (0)