diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index d54c65099..ef43c43a2 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -34,7 +34,6 @@ interface DirectoryTableProps { search: string; focusIndex: number; setFocusIndex: (_: number) => void; - setFileName: (_: string) => void; setSearch: (_: string) => void; setSelectedPath: (_: string) => void; setRefreshVersion: React.Dispatch>; @@ -130,7 +129,6 @@ function DirectoryTable({ search, focusIndex, setFocusIndex, - setFileName, setSearch, setSelectedPath, setRefreshVersion, @@ -301,7 +299,6 @@ function DirectoryTable({ table={table} search={search} focusIndex={focusIndex} - setFileName={setFileName} setFocusIndex={setFocusIndex} setSearch={setSearch} setSelectedPath={setSelectedPath} @@ -314,7 +311,6 @@ function DirectoryTable({ table={table} search={search} focusIndex={focusIndex} - setFileName={setFileName} setFocusIndex={setFocusIndex} setSearch={setSearch} setSelectedPath={setSelectedPath} @@ -332,7 +328,6 @@ interface TableBodyProps { search: string; focusIndex: number; setFocusIndex: (_: number) => void; - setFileName: (_: string) => void; setSearch: (_: string) => void; setSelectedPath: (_: string) => void; setRefreshVersion: React.Dispatch>; @@ -345,7 +340,6 @@ function TableBody({ search, focusIndex, setFocusIndex, - setFileName, setSearch, setSelectedPath, setRefreshVersion, @@ -478,7 +472,7 @@ function TableBody({ key={row.id} onDoubleClick={() => { const newFileName = row.getValue("path") as string; - setFileName(newFileName); + model.goHistory(newFileName); setSearch(""); }} onClick={() => setFocusIndex(idx)} @@ -495,7 +489,7 @@ function TableBody({ ))} ), - [setSearch, setFileName, handleFileContextMenu, setFocusIndex, focusIndex] + [setSearch, handleFileContextMenu, setFocusIndex, focusIndex] ); const handleScrollbarInitialized = (instance) => { @@ -535,7 +529,7 @@ const MemoizedTableBody = React.memo( ) as typeof TableBody; interface DirectoryPreviewProps { - fileNameAtom: jotai.WritableAtom; + fileNameAtom: jotai.Atom; model: PreviewModel; } @@ -544,7 +538,7 @@ function DirectoryPreview({ fileNameAtom, model }: DirectoryPreviewProps) { const [focusIndex, setFocusIndex] = useState(0); const [unfilteredData, setUnfilteredData] = useState([]); const [filteredData, setFilteredData] = useState([]); - const [fileName, setFileName] = jotai.useAtom(fileNameAtom); + const fileName = jotai.useAtomValue(fileNameAtom); const showHiddenFiles = jotai.useAtomValue(model.showHiddenFiles); const [selectedPath, setSelectedPath] = useState(""); const [refreshVersion, setRefreshVersion] = jotai.useAtom(model.refreshVersion); @@ -597,7 +591,7 @@ function DirectoryPreview({ fileNameAtom, model }: DirectoryPreviewProps) { if (filteredData.length == 0) { return; } - setFileName(selectedPath); + model.goHistory(selectedPath); setSearchText(""); return true; } @@ -645,7 +639,6 @@ function DirectoryPreview({ fileNameAtom, model }: DirectoryPreviewProps) { data={filteredData} search={searchText} focusIndex={focusIndex} - setFileName={setFileName} setFocusIndex={setFocusIndex} setSearch={setSearchText} setSelectedPath={setSelectedPath} diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index fda095a72..11a19b87f 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -7,6 +7,8 @@ import { createBlock, globalStore, useBlockAtom } from "@/store/global"; import * as services from "@/store/services"; import * as WOS from "@/store/wos"; import { getWebServerEndpoint } from "@/util/endpoints"; +import * as historyutil from "@/util/historyutil"; +import * as keyutil from "@/util/keyutil"; import * as util from "@/util/util"; import clsx from "clsx"; import * as jotai from "jotai"; @@ -43,7 +45,7 @@ export class PreviewModel implements ViewModel { ceReadOnly: jotai.PrimitiveAtom; isCeView: jotai.PrimitiveAtom; - fileName: jotai.WritableAtom; + fileName: jotai.Atom; connection: jotai.Atom; statFile: jotai.Atom>; fullFile: jotai.Atom>; @@ -80,20 +82,26 @@ export class PreviewModel implements ViewModel { icon: "folder-open", longClick: (e: React.MouseEvent) => { let menuItems: ContextMenuItem[] = []; - menuItems.push({ label: "Go to Home", click: () => globalStore.set(this.fileName, "~") }); + menuItems.push({ + label: "Go to Home", + click: () => this.goHistory("~"), + }); menuItems.push({ label: "Go to Desktop", - click: () => globalStore.set(this.fileName, "~/Desktop"), + click: () => this.goHistory("~/Desktop"), }); menuItems.push({ label: "Go to Downloads", - click: () => globalStore.set(this.fileName, "~/Downloads"), + click: () => this.goHistory("~/Downloads"), }); menuItems.push({ label: "Go to Documents", - click: () => globalStore.set(this.fileName, "~/Documents"), + click: () => this.goHistory("~/Documents"), + }); + menuItems.push({ + label: "Go to Root", + click: () => this.goHistory("/"), }); - menuItems.push({ label: "Go to Root", click: () => globalStore.set(this.fileName, "/") }); ContextMenuModel.showContextMenu(menuItems, e); }, }; @@ -164,7 +172,7 @@ export class PreviewModel implements ViewModel { return { elemtype: "iconbutton", icon: "chevron-left", - click: this.handleBack.bind(this), + click: this.goParentDirectory.bind(this), }; }); this.endIconButtons = jotai.atom((get) => { @@ -188,18 +196,13 @@ export class PreviewModel implements ViewModel { } return null; }); - this.fileName = jotai.atom( - (get) => { - const file = get(this.blockAtom)?.meta?.file; - if (util.isBlank(file)) { - return "~"; - } - return file; - }, - (get, set, update) => { - services.ObjectService.UpdateObjectMeta(`block:${blockId}`, { file: update }); + this.fileName = jotai.atom((get) => { + const file = get(this.blockAtom)?.meta?.file; + if (util.isBlank(file)) { + return "~"; } - ); + return file; + }); this.connection = jotai.atom((get) => { return get(this.blockAtom)?.meta?.connection; }); @@ -232,20 +235,58 @@ export class PreviewModel implements ViewModel { }); this.newFileContent = jotai.atom(null) as jotai.PrimitiveAtom; - this.handleBack = this.handleBack.bind(this); + this.goParentDirectory = this.goParentDirectory.bind(this); } - handleBack() { + goHistory(newPath: string) { + const blockMeta = globalStore.get(this.blockAtom)?.meta; const fileName = globalStore.get(this.fileName); if (fileName == null) { return; } - const splitPath = fileName.split("/"); - console.log("splitPath-1", splitPath); - splitPath.pop(); - console.log("splitPath-2", splitPath); - const newPath = splitPath.join("/"); - globalStore.set(this.fileName, newPath); + const updateMeta = historyutil.goHistory("file", fileName, newPath, blockMeta); + if (updateMeta == null) { + return; + } + const blockOref = WOS.makeORef("block", this.blockId); + services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); + } + + goParentDirectory() { + const blockMeta = globalStore.get(this.blockAtom)?.meta; + const fileName = globalStore.get(this.fileName); + if (fileName == null) { + return; + } + const newPath = historyutil.getParentDirectory(fileName); + const updateMeta = historyutil.goHistory("file", fileName, newPath, blockMeta); + if (updateMeta == null) { + return; + } + const blockOref = WOS.makeORef("block", this.blockId); + services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); + } + + goHistoryBack() { + const blockMeta = globalStore.get(this.blockAtom)?.meta; + const curPath = globalStore.get(this.fileName); + const updateMeta = historyutil.goHistoryBack("file", curPath, blockMeta, true); + if (updateMeta == null) { + return; + } + const blockOref = WOS.makeORef("block", this.blockId); + services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); + } + + goHistoryForward() { + const blockMeta = globalStore.get(this.blockAtom)?.meta; + const curPath = globalStore.get(this.fileName); + const updateMeta = historyutil.goHistoryForward("file", curPath, blockMeta); + if (updateMeta == null) { + return; + } + const blockOref = WOS.makeORef("block", this.blockId); + services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); } toggleCodeEditorReadOnly(readOnly: boolean) { @@ -317,6 +358,23 @@ export class PreviewModel implements ViewModel { } return false; } + + keyDownHandler(e: WaveKeyboardEvent): boolean { + if (keyutil.checkKeyPressed(e, "Cmd:ArrowLeft")) { + this.goHistoryBack(); + return true; + } + if (keyutil.checkKeyPressed(e, "Cmd:ArrowRight")) { + this.goHistoryForward(); + return true; + } + if (keyutil.checkKeyPressed(e, "Cmd:ArrowUp")) { + // handle up directory + this.goParentDirectory(); + return true; + } + return false; + } } function makePreviewModel(blockId: string): PreviewModel { diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 86b69cfa0..d98b045c2 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -208,6 +208,8 @@ declare global { file?: string; url?: string; connection?: string; + history?: string[]; + "history:forward"?: string[]; icon?: string; "icon:color"?: string; frame?: boolean; diff --git a/frontend/util/historyutil.ts b/frontend/util/historyutil.ts new file mode 100644 index 000000000..2fa490023 --- /dev/null +++ b/frontend/util/historyutil.ts @@ -0,0 +1,75 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import * as util from "@/util/util"; + +const MaxHistory = 20; + +// this needs to be fixed for windows +function getParentDirectory(path: string): string { + if (util.isBlank(path) == null) { + // this not great, ideally we'd never be passed a null path + return "/"; + } + if (path == "/") { + return "/"; + } + const splitPath = path.split("/"); + splitPath.pop(); + if (splitPath.length == 1 && splitPath[0] == "") { + return "/"; + } + const newPath = splitPath.join("/"); + return newPath; +} + +function goHistoryBack(curValKey: "url" | "file", curVal: string, meta: MetaType, backToParent: boolean): MetaType { + const rtnMeta: MetaType = {}; + const history = (meta?.history ?? []).slice(); + const historyForward = (meta?.["history:forward"] ?? []).slice(); + if (history == null || history.length == 0) { + if (backToParent) { + const parentDir = getParentDirectory(curVal); + if (parentDir == curVal) { + return null; + } + historyForward.unshift(curVal); + while (historyForward.length > MaxHistory) { + historyForward.pop(); + } + return { [curValKey]: parentDir, "history:forward": historyForward }; + } else { + return null; + } + } + const lastVal = history.pop(); + historyForward.unshift(curVal); + return { [curValKey]: lastVal, history: history, "history:forward": historyForward }; +} + +function goHistoryForward(curValKey: "url" | "file", curVal: string, meta: MetaType): MetaType { + const rtnMeta: MetaType = {}; + let history = (meta?.history ?? []).slice(); + const historyForward = (meta?.["history:forward"] ?? []).slice(); + if (historyForward == null || historyForward.length == 0) { + return null; + } + const lastVal = historyForward.shift(); + history.push(curVal); + if (history.length > MaxHistory) { + history.shift(); + } + return { [curValKey]: lastVal, history: history, "history:forward": historyForward }; +} + +function goHistory(curValKey: "url" | "file", curVal: string, newVal: string, meta: MetaType): MetaType { + const rtnMeta: MetaType = {}; + const history = (meta?.history ?? []).slice(); + history.push(curVal); + if (history.length > MaxHistory) { + history.shift(); + } + return { [curValKey]: newVal, history: history, "history:forward": [] }; +} + +export { getParentDirectory, goHistory, goHistoryBack, goHistoryForward }; diff --git a/pkg/wstore/wstore_meta.go b/pkg/wstore/wstore_meta.go index a4cf0a1c8..50418b4ff 100644 --- a/pkg/wstore/wstore_meta.go +++ b/pkg/wstore/wstore_meta.go @@ -27,6 +27,7 @@ const ( MetaKey_File = "file" MetaKey_Url = "url" MetaKey_Connection = "connection" + MetaKey_History = "history" // stores an array of history items specific to the block MetaKey_Icon = "icon" MetaKey_IconColor = "icon:color" @@ -60,12 +61,14 @@ const ( // for typescript typing type MetaTSType struct { // shared - View string `json:"view,omitempty"` - Controller string `json:"controller,omitempty"` - Title string `json:"title,omitempty"` - File string `json:"file,omitempty"` - Url string `json:"url,omitempty"` - Connection string `json:"connection,omitempty"` + View string `json:"view,omitempty"` + Controller string `json:"controller,omitempty"` + Title string `json:"title,omitempty"` + File string `json:"file,omitempty"` + Url string `json:"url,omitempty"` + Connection string `json:"connection,omitempty"` + History []string `json:"history,omitempty"` + HistoryForward []string `json:"history:forward,omitempty"` Icon string `json:"icon,omitempty"` IconColor string `json:"icon:color,omitempty"`