11const NOTES_DIRECTORY = "docs_build/dev/admin-notes" ;
2- const DIRECTORY_INDEX_PATH = `${ NOTES_DIRECTORY } /directory-index.json` ;
32const DEFAULT_NOTE = "index" ;
43const LINK_CLASS = "btn btn--compact primary" ;
54const NOTE_NAME_PATTERN = / ^ [ A - Z a - z 0 - 9 _ - ] + $ / ;
65const STATUS_ICON_PATTERN = / ^ \[ ( [ x X . ! ? ] ) \] \s * ( .* ) $ / ;
76const NOTE_INDEX_FILE = "index.txt" ;
7+ const DIRECTORY_LIST_QUERY = "adminNotesDirectory" ;
88const 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