File Explorer Page
The File Explorer page is a server-side filesystem browser covering three named roots:
- AdminDirectory — server-wide admin area.
- ProjectDirectory — the currently-loaded project directory (omitted from the root list when no project is loaded).
- ResourceDir — the shared resource tree under
AdminDirectory + "Resource/".
wwwroot-src/src/components/FileExplorer.vue holds the whole browser — toolbar, tree, editor panel
and preview. wwwroot-src/src/pages/FileExplorerPage.vue is a thin route wrapper around it, and
wwwroot-src/src/components/widgets/FileExplorerDialog.vue mounts the same component in a modal as
the app's file picker (see Picker Mode).
Note
Absolute filesystem paths never leave the server through the File Explorer's own endpoints. The /roots endpoint returns only { name, displayName }, and Common/FileExplorerController.cs routes every exception message through a scrub that replaces root prefixes with the tokens <Admin> / <Project> / <Resource> before returning it. The scrub lives on Common/NamedRootResolver.cs, and the STL preview and CSV strip endpoints the editor column also calls apply the same one — see STL Preview Pane.
Layout
- File Explorer Page
- Toolbar
- Root Selector Dropdown — display-name only, sourced from
GET /roots; a caller that locks the root gets the name as plain text instead. - Up Button
- Path Input — editable;
Enternavigates. - Refresh / New Folder / New File / Upload Buttons.
- Sort Dropdown — sort key (Name / Size / Modified / Type), Ascending / Descending, and a “Folders first” checkbox.
- Editor Panel Toggle (
edit_notepencil) — shows or hides the editor slave panel.
- Root Selector Dropdown — display-name only, sourced from
- Breadcrumb Row — single-click navigation up the tree, plus a file-type filter select whenever the host supplies filters. The page supplies none, so the select belongs to picker mode.
- Explorer | Editor Splitter — the divider collapses to zero width while the editor panel is hidden.
- Listing Tree (
<q-tree>) — lazy: a folder lists its children the first time it is expanded. Each row is icon + name + a fixed-width size / modified tail, with the hover action strip laid over that tail so nothing reflows when the pointer enters a row. Ordering follows the toolbar's sort control.- Row link (the page only, not the pickers) — the icon, name and tail are an
<a>to the URL that selecting the row writes, so the browser's own context menu offers Open link in new tab / new window and Copy link address, and a Ctrl, Shift or middle click opens the row there while this tab stays as it is. A plain click stays in the page and selects as below. The action strip sits outside the link, and the link is not draggable: dropped on the open editor it would type its address into the file. - Row select:
- Directory → expand it and make it the current directory (the toolbar ops and the URL follow).
- Text file → load it into the editor panel, or into the paged viewer when
read-textanswerstooLarge;.stl→ open the STL preview in that same slot;.csv→ open the CSV strip viewer there. All only while the panel is shown.
- Row double-click on a file opens it in the editor panel — or the preview, for an
.stl, or the strip viewer, for a.csv— even when the panel is hidden. This is the gesture that opens a file for editing; the action strip deliberately carries no Edit button, since a second pencil beside Rename'sdrive_file_rename_outlinewould be two near-identical icons meaning different things. The empty editor's hint names the same gesture: "Double-click a file to load it here." - Actions: Download (files), Download ZIP (folders), Extract ZIP (
.ziponly), Rename, Duplicate, Delete (double-click to confirm). - Pick Column (picker mode only) — a radio or a checkbox on every pickable row, rendered as an empty cell elsewhere so unpickable rows keep the same indent.
- Row link (the page only, not the pickers) — the icon, name and tail are an
- Editor Slave Panel (right)
- Bar, left to right —
Root / relative/pathlabel with a red*while the buffer is dirty, the Edit | Split | View toggle (only while the language ismarkdown),Auto Savecheckbox,Savebutton (disabled while Auto Save is on), mode select, close icon. The mode select lists the six editor languages and, while the buffer holds a.csv, Chart viewer last, which settles the buffer (flushes Auto Save or confirms a discard) and returns to the strip viewer of the same file. - Body —
wwwroot-src/src/components/widgets/TextEditorPane.vue, filling the panel: one<TextEditor>(CodeMirror 6) for every language — a language switch keeps the same editor — and, formarkdown,wwwroot-src/src/components/markdown/MarkdownViewer.vuebeside it (Split: two columns behind a draggable divider, scrolled together by source line) or instead of it (View). Each side is created the first time its mode needs it and only hidden afterwards, so undo history, caret and scroll survive a mode switch; the viewer is a lazily loaded chunk, so a session that stays in Edit never downloads the renderer. - STL Preview — an
.stlrow hands this slot to<StlPreviewPane>, a server-rendered 3D view that replaces the editor's bar and body and stays up until it is closed; see STL Preview Pane. The text buffer and any unsaved edits survive underneath and return when the preview closes. - CSV Strip Viewer — a
.csvrow hands this slot toCsvStripPane.vue, overapi/csv-strip. Its bar carries the path label and*, a parse-status chip, the Series, X, Y and Lines dropdowns, a reset-zoom button, thenSave, the mode select (parked on Chart viewer; a language opens the file as text in the editor, the pane staying mounted but hidden) and close, laid out like the editor bar's right end. Its body is a horizontal splitter: a min/max band chart of the picked columns over the file's own text, an embeddedPagedTextViewer.vuewhose cells are editable in place, with rows inserted or deleted from a right-click menu.Savewrites the pending line edits throughwrite-linesfirst, then the settings, as<name>.csv.strip.jsonbeside the CSV. Unsaved cell edits are settled before anything unmounts, hides or re-targets the pane. - Paged Viewer — a text file over the
read-textcap opens read-only inPagedTextViewer.vue, which fetches lines throughread-linesas they scroll into view; the line count grows while the server indexes the file or the file is still being written.
- Bar, left to right —
- Listing Tree (
- Toolbar
Behavior
- Path-traversal defence. Every request is resolved via
Path.GetFullPathand validated withHi.Common.PathUtils.PathUtil.IsDescendant(root, absolute)before any IO. Attempts likerelativePath=../outsideare rejected with HTTP 400. - Root names are exact, and one can be absent.
AdminDirectory,ProjectDirectoryandResourceDirare matched as written.ProjectDirectoryresolves only while a project is open; before that it is refused with HTTP 400, exactly like an unknown name, so a caller staging files for a project it has yet to create writes them underAdminDirectory. - No cross-root copy.
/copyduplicates beside the source and/renamerefuses to move between roots, so a file travels from one root to another only by download and upload — or by a resource loader, which writes the project's own copy itself. - Duplicate.
POST /copytries{name}-Copy-00through{name}-Copy-19and returns the first free slot; 400 if all 20 are taken. - UTF-8 without BOM.
WriteTextuses a cachednew UTF8Encoding(encoderShouldEmitUTF8Identifier: false)so round-tripped files do not accumulate a 3-byte BOM on every save. - Binary gating.
/read-textreports a binary file rather than refusing it: it answerscontent=null,isBinary=true, and the panel toasts. The client decides before the round-trip — an extension set (.stl,.zip,.dll,.exe, images,.pdf, Office documents,.sqlite/.db, …) keeps a click on a binary row from opening the editor at all, and.stlis routed to the STL preview instead. - Editor cap.
read-texthands the editor at most 128 MiB — the whole text travels as one JSON string, and the cap keeps it under the serializer's own limit. Above it the answer iscontent=null,tooLarge=true,length(the file's size in bytes) andmaxLength(the cap), and the panel opens the paged viewer instead; a CSV switched to text from the strip viewer is refused with a toast naming both sizes, and the strip viewer stays up, its text half being where such a file is edited. - Line endings. The buffer is LF-normalized on load, because CodeMirror 6 joins lines with LF and would otherwise echo a normalize-only change that falsely dirties a freshly opened CRLF file. Save converts back to the style the file was read with.
- Auto Save. Checked, the panel writes 800 ms after typing stops and the
Savebutton disables. Switching files, closing the panel and leaving the page each flush a pending write first; with Auto Save off, dirty edits raise a discard confirm instead, andsettleEditorBeforeClose()keeps the user on the page when that confirm is cancelled. - Markdown.
wwwroot-src/src/components/markdown/markdownRender.tsrenders with markdown-it (GFM tables, strikethrough, autolinks with a scheme), footnotes, and rules of its own for task lists, the five GitHub alerts, front matter, heading anchors and math; the result is sanitized by a DOMPurify instance of its own that keeps only the HTML a document needs — no scripts, forms, styles, or class names other than the renderer's, since the page shares the application's document. KaTeX draws the formulas and mermaid the diagrams afterwards, mermaid at itsstrictsecurity level, with every key that reaches a diagram's stylesheet (themeCSS, the theme variables, fonts) locked against the file's own settings, each drawing in a shadow root, throughwwwroot-src/src/utils/mermaidRuntime.ts, the queue that also serves the Equipment topology graph atloose. Relative images load through/download, an.svgas adata:URL; relative links — a diagram's included — open in the panel, resolved against the open file under that file's root, and no link navigates the application's own tab. - A document the host writes replaces the editor state. Opening another file into the editor builds a fresh CodeMirror state, with its own undo history: Ctrl+Z after opening a file never brings back the previous file's text.
- Per-device view prefs. Editor shown, splitter position, Auto Save, the sort spec, the Markdown mode (
markdownView, View by default) and the Markdown split (markdownSplit) persist inlocalStorageunderhinc.fileExplorer.viewPrefs.v1. Nothing is written until one of them is changed, and the page's own default is editor shown — so a first visit, a private window or a cleared browser all open with the editor panel up and single-click file opening already live. Dialog instances seed the splitter, Auto Save and the sort from that blob and write those three back, but they neither read nor write the panel-visibility part: a picker always opens with the editor panel hidden, and a one-off edit inside one does not rearrange the page.
Picker Mode
The same component runs inside wwwroot-src/src/components/widgets/FileExplorerDialog.vue, which
adds an apply bar and emits each pick as "{rootName}:{relativePath}". Props steer it:
pickable (file / folder / any / none) turns the pick column on, multi swaps radios for
checkboxes, filters supplies the file-type dropdown (“All Files” is appended when the caller's
list omits it), lockRoot and allowedRoots narrow the root switcher, and mode: 'save' turns the
path input into a save target with a forcedExtension appended on confirm.
That dialog is how the app opens and saves files: the menu bar's File ▾ entries, the
Object Management Menu Button, FilePathInput, and the
Mission command panels.
Syntax Highlighting
wwwroot-src/src/components/widgets/TextEditor.vue wraps CodeMirror 6 so the editor gets real syntax highlighting for the common project-file formats listed below.
Stack:
codemirror+@codemirror/state+@codemirror/view(core).@codemirror/lang-xml+@codemirror/lang-json+@codemirror/lang-markdown+@replit/codemirror-lang-csharp+ the in-repo mission-script mode fromwwwroot-src/src/components/widgets/missionScriptLanguage.ts(5 language modes).@codemirror/autocomplete, wired only when a completion source is supplied.
Props: modelValue (two-way), language: 'xml' | 'json' | 'markdown' | 'csharp' | 'mission-script' | 'text' (default text), readonly, and completionSource — an optional CodeMirror completion source that overrides the language-provided completions, read once at mount. Language and readonly are swapped via Compartment.reconfigure, so the view never has to tear down on mode change.
Extension → language mapping lives in wwwroot-src/src/components/widgets/editorLanguage.ts (single source of truth imported by both TextEditor.vue and FileExplorer.vue):
| Language | Extensions |
|---|---|
xml |
xml, hincproj, CoatingMaterial, CutterMaterial, Holder, WorkpieceMaterial, mp, MillingPara, SpindleCapability, StickMachiningTool, GeneralMechanism, general-mech, MachineTool, mt, Controller, SoftNcRunner, Cutter, CoolantHeatCondition, MachiningToolHouse, Fixture |
markdown |
md, markdown |
json |
json |
csharp |
cs, csx |
mission-script |
none — the mode is picked from the editor's language select, and wwwroot-src/src/components/mission/ScriptCommandPanel.vue sets it directly on its own editor |
text (fallback) |
everything else, including .nc / .ptp / .mpf / .h |
| — (chart viewer) | .csv — a click opens the CSV strip viewer, not the editor; the file reaches the editor only when a language is picked in the viewer's mode select, and opens in that language |
The mode select in the editor bar offers all six languages, and Chart viewer while the file is a .csv, so an auto-detected mode can be overridden per file.
Source Code Path
See HiNC App Anatomy for git repository links.
HiNC-2025-webservice (Quasar CLI SPA):
wwwroot-src/src/components/FileExplorer.vue— the browser itself: toolbar, sort control, breadcrumb + filter, the lazy tree with its hover actions, the editor slave panel with Auto Save, the STL preview slot, and the view prefs.wwwroot-src/src/pages/FileExplorerPage.vue— routed page at/util/file-explorer; mirrors the selection and the panel's file in the URL and reads them back, gives the tree's rows their link addresses, and guards the route leave.wwwroot-src/src/components/widgets/FileExplorerDialog.vue— modal wrapper: apply bar, save mode,"{rootName}:{relativePath}"picks.wwwroot-src/src/components/explorerSort.ts— sort keys, comparators,DEFAULT_SORTandcoerceSortSpecbehind the Sort dropdown.wwwroot-src/src/components/widgets/fileFilter.ts— theFileFiltershape driving the file-type select.wwwroot-src/src/utils/collapsibleSplit.ts— collapses the editor divider while the panel is hidden.wwwroot-src/src/components/StlPreviewPane.vue— the 3D preview that takes over the panel for.stlfiles, overDisp/StlPreviewController.cs.wwwroot-src/src/components/CsvStripPane.vue— the CSV strip viewer that takes over the panel for.csvfiles: the bar with its Series / X / Y / Lines dropdowns, Save and mode select, the band chart over the embedded text, and the sidecar config.wwwroot-src/src/components/PagedTextViewer.vue— the read-only paged viewer for files over the editor cap, and the strip viewer's editable text half (cells, row insert / delete, the pending-edit overlay).wwwroot-src/src/utils/lineOverlay.ts— the replaced / deleted / inserted-line overlay the text half keeps until Save.wwwroot-src/src/utils/csvLine.ts— splits a CSV line into cells for the text half.wwwroot-src/src/api/csvStrip.ts— typed wrapper over/api/csv-strip/*.wwwroot-src/src/components/widgets/TextEditor.vue— CodeMirror 6 wrapper; line wrapping for Markdown, and the scroll API Split mode follows.wwwroot-src/src/components/widgets/TextEditorPane.vue— the editor panel's body: the editor for every language, the Markdown viewer beside or instead of it, Split's scroll sync and divider.wwwroot-src/src/components/markdown/markdownRender.ts,MarkdownViewer.vue,markdown.scss,markdownView.ts— the Markdown renderer and sanitizer, the drawn page, its styles, and the mode types the page and the explorer share.wwwroot-src/src/utils/mermaidRuntime.ts— the one mermaid render queue; every caller's configuration is re-applied before its render.wwwroot-src/src/components/widgets/editorLanguage.ts— extension →EditorLanguagemapping.wwwroot-src/src/components/widgets/missionScriptLanguage.ts— the in-repo mission-script mode.wwwroot-src/src/api/fileExplorer.ts— typed wrapper over/api/file-explorer/*.wwwroot-src/src/router/routes.ts—/util/file-explorerentry (route nameutil-file-explorer).wwwroot-src/src/components/AppMenuBar.vue— theFile Explorerpage switch, the fourth, after the three workflow pages; lit on the bare route and on every deep path.Common/NamedRootResolver.cs— resolvesAdminDirectory/ProjectDirectory/ResourceDir; shared with every other controller that reads or writes under a named root.Common/TextLineIndexService.cs— the background line index behindread-lines: one checkpoint per 64 KiB, extended as the file grows and rebuilt under a newgenerationwhen it is rewritten.Common/TextLinePatch.cs— the line patch behindwrite-lines: theoldText/ length checks, then the write — in place when it only replaces lines and each keeps its byte count, otherwise through a temp file and a replace.Common/CsvStripController.cs— REST endpoints under/api/csv-strip, overCommon/CsvStripService.cs(the background parse cache and the bucketing) andCommon/CsvStrip.cs(the CSV reader and schema sampling):
| Method | Path | Notes |
|---|---|---|
| GET | /schema?rootName=&relativePath=&headerLine=&skipLines= |
Delimiter, the columns with a kind sampled from the first 64 data rows, the suggested x column, and the file's first lines for the header / skip pickers. |
| GET | /strip?…&xColumn=&xFormat=&columns=&begin=&end=&buckets= |
Min/max buckets of up to 32 columns over the data-row window [begin, end) (end=-1 for every row parsed so far), about 800 buckets by default. The first call starts a background parse; the answer covers the rows parsed so far, and complete says when to stop polling. |
| GET | /locate?…&line= or &row= |
Maps a physical line to its data row, or a row to its line — the chart ↔ text link. 404 when the file has no data row. |
| GET | /config?rootName=&relativePath= |
Reads the sidecar <name>.csv.strip.json, kept by the server as opaque JSON. |
| PUT | /config |
Writes the sidecar as indented JSON beside the CSV, which must exist. |
| DELETE | /config?… |
Removes the sidecar; succeeds when there is none. |
Common/FileExplorerController.cs— REST endpoints under/api/file-explorer:
| Method | Path | Notes |
|---|---|---|
| GET | /roots |
List available roots; Project is omitted when no project is loaded. Returns display names only. |
| GET | /list?rootName=&relativePath= |
Directories-first, name-ascending listing; the client re-sorts it per the toolbar's sort control. |
| GET | /read-text?… |
UTF-8 text; when the file is detected as binary returns content=null, isBinary=true. A text file over 128 MiB returns content=null, tooLarge=true, length (its size in bytes) and maxLength (the cap). |
| GET | /read-lines?rootName=&relativePath=&offset=&count= |
A page of lines from a text file of any size: count lines from the 0-based offset, default 256 and at most 1000 (0 only reports the index state). A line over 16 KiB is served cut to that length and listed in clipped with its omittedBytes. lineCount covers only the bytes indexed so far and grows on later calls; a greater generation means the index was rebuilt and earlier pages are stale. |
| POST | /write-text |
Body {rootName, relativePath, content}; creates parents as needed. |
| POST | /write-lines |
Body {rootName, relativePath, expectedLength, edits, deletes, inserts} — lines replaced (line, text, oldText), removed (line, oldText) and inserted after a line (after, texts, oldText; -1 for the start of the file), every number naming the file as it is on disk before the write. 409 when the file's length is no longer expectedLength, or a line no longer reads as its oldText; untouched bytes are copied as they are. Answers length, edited, deleted, inserted and inPlace. |
| POST | /create-file |
Fails if target exists. |
| POST | /create-directory |
Recursive create; no-op if already a directory. |
| POST | /rename |
Body {rootName, relativePath, newRelativePath}; refuses cross-root moves. |
| POST | /copy |
{name}-Copy-00..-Copy-19; uses Hi.Common.FileLines.FileUtil.CopyDirectory for directories. |
| DELETE | /delete?… |
Files → delete, directories → recursive; refuses to delete the root itself. |
| GET | /download?… |
Streams a file as application/octet-stream. |
| GET | /download-zip?… |
Zips a directory in-memory and streams application/zip. |
| POST | /upload?rootName=&relativePath= |
Multipart single-file upload in the form field file; creates missing parent folders and overwrites the target. |
| POST | /extract-zip |
Body {rootName, relativePath}; extracts next to the archive into <name>/. |
Addressing
The selection is mirrored into the URL by an optional catch-all: /util/file-explorer/{RootTitle}/{relative/path}, where the path is what the path box shows — the selected file, or the browsed folder while no file is selected. The query adds the file the editor panel shows when it is not the selection — ?file={RootTitle}/{relative/path}, in the text editor, the STL preview, the CSV strip viewer or the paged viewer — and, where the panel's file offers a choice, &mode=: edit, split or view for Markdown (it also shows a .txt as Markdown), text for a CSV taken out of the chart viewer. A URL whose path names a file selects it and opens it as a click on its row would — in the panel while the panel is shown, and whenever a mode comes with it; a ?file= opens that file in the panel, the editor's usual settle rules applying in both cases. Each tree row links to the URL with that row as the path. The bare /util/file-explorer still resolves through the named route the menu bar's File Explorer switch links to, so both a deep link and a switch click from another page land correctly. The switch is lit on every one of these URLs, and a click on the lit switch does nothing, so it leaves a deep path where it is rather than returning to the root. Browsing and opening files rewrite the URL with router.replace — never while another page is shown or the page's route-leave guard runs, since a replace there would cancel the leave — and an external URL change — a paste, a bookmark, browser back — drives the explorer the other way. Coming back through the bare /util/file-explorer, the page writes the URL of what it still shows.
The named roots (Admin / Project / Resource) are the only addressing the client sees, and the absolute path is formed and kept on the server; an error message that could carry it is scrubbed before it leaves (the note at the head of this page).
See Also
File Explorer (manual) — the end-user task: the roots, and renaming a file to move it
Object Management Menu Button — shares the XML editor dialog pattern and targets the same project / resource folders through a different endpoint family.
STL Preview Pane — the 3D preview this page's editor column hands over to for an
.stlrow.Driving the Web Service over HTTP — the same routes from a script's side: which root to stage inputs in, and what each loader does with a file