From 63cfe1d279f5a5ce19bda027c922535242cf4b6e Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Sun, 1 Sep 2024 02:57:45 +0800 Subject: [PATCH] preview edit (#302) --- frontend/app/block/block.less | 17 +- frontend/app/block/blockframe.tsx | 6 +- frontend/app/element/button.less | 8 + frontend/app/view/codeeditor/codeeditor.tsx | 3 - frontend/app/view/preview/preview.tsx | 217 +++++++++++--------- frontend/types/custom.d.ts | 2 + 6 files changed, 146 insertions(+), 107 deletions(-) diff --git a/frontend/app/block/block.less b/frontend/app/block/block.less index 96cfaca4..185e8c3d 100644 --- a/frontend/app/block/block.less +++ b/frontend/app/block/block.less @@ -1,6 +1,8 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +@import "../mixins.less"; + .block { display: flex; flex-direction: column; @@ -111,12 +113,23 @@ } .block-frame-text { + .ellipsis(); font: var(--fixed-font); font-size: 11px; opacity: 0.7; overflow-x: hidden; - text-wrap: nowrap; - text-overflow: ellipsis; + flex-grow: 1; + + &.preview-filename { + span { + height: 100%; + cursor: pointer; + + &:hover { + background: var(--highlight-bg-color); + } + } + } } .iconbutton { diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 99facd9c..22a8fc2e 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -217,8 +217,10 @@ const HeaderTextElem = React.memo(({ elem, preview }: { elem: HeaderElem; previe return ; } else if (elem.elemtype == "text") { return ( -
- {elem.text} +
+ elem?.onClick()}> + {elem.text} +
); } else if (elem.elemtype == "textbutton") { diff --git a/frontend/app/element/button.less b/frontend/app/element/button.less index 03f49e8c..dffa5a89 100644 --- a/frontend/app/element/button.less +++ b/frontend/app/element/button.less @@ -100,4 +100,12 @@ padding-left: 10px; padding-right: 10px; } + + .font-size-11 { + font-size: 11px; + } + + .font-weight-500 { + font-weight: 500; + } } diff --git a/frontend/app/view/codeeditor/codeeditor.tsx b/frontend/app/view/codeeditor/codeeditor.tsx index 60c49a69..ba4bd32f 100644 --- a/frontend/app/view/codeeditor/codeeditor.tsx +++ b/frontend/app/view/codeeditor/codeeditor.tsx @@ -64,7 +64,6 @@ interface CodeEditorProps { parentRef: React.MutableRefObject; text: string; filename: string; - readonly: boolean; language?: string; onChange?: (text: string) => void; onSave?: () => void; @@ -73,7 +72,6 @@ interface CodeEditorProps { } export function CodeEditor({ - readonly = false, parentRef, text, language, @@ -150,7 +148,6 @@ export function CodeEditor({ } const editorOpts = defaultEditorOptions(); - editorOpts.readOnly = readonly; return (
diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index c95b8c44..f1594933 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -4,7 +4,7 @@ import { TypeAheadModal } from "@/app/modals/typeaheadmodal"; import { ContextMenuModel } from "@/app/store/contextmenu"; import { Markdown } from "@/element/markdown"; -import { atoms, createBlock, globalStore, useBlockAtom } from "@/store/global"; +import { createBlock, globalStore, useBlockAtom } from "@/store/global"; import * as services from "@/store/services"; import * as WOS from "@/store/wos"; import { getWebServerEndpoint } from "@/util/endpoints"; @@ -35,6 +35,10 @@ function isTextFile(mimeType: string): boolean { ); } +function canPreview(mimeType: string): boolean { + return mimeType.startsWith("text/markdown") || mimeType.startsWith("text/csv"); +} + export class PreviewModel implements ViewModel { viewType: string; blockId: string; @@ -45,9 +49,9 @@ export class PreviewModel implements ViewModel { preIconButton: jotai.Atom; endIconButtons: jotai.Atom; ceReadOnly: jotai.PrimitiveAtom; - isCeView: jotai.PrimitiveAtom; previewTextRef: React.RefObject; editMode: jotai.Atom; + canPreview: jotai.PrimitiveAtom; fileName: jotai.Atom; connection: jotai.Atom; @@ -57,6 +61,7 @@ export class PreviewModel implements ViewModel { fileMimeTypeLoadable: jotai.Atom>; fileContent: jotai.Atom>; newFileContent: jotai.PrimitiveAtom; + openFileModal: jotai.PrimitiveAtom; showHiddenFiles: jotai.PrimitiveAtom; refreshVersion: jotai.PrimitiveAtom; @@ -74,7 +79,8 @@ export class PreviewModel implements ViewModel { this.refreshVersion = jotai.atom(0); this.previewTextRef = createRef(); this.ceReadOnly = jotai.atom(true); - this.isCeView = jotai.atom(false); + this.canPreview = jotai.atom(false); + this.openFileModal = jotai.atom(false); this.blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); this.viewIcon = jotai.atom((get) => { let blockData = get(this.blockAtom); @@ -121,60 +127,55 @@ export class PreviewModel implements ViewModel { }); this.viewName = jotai.atom("Preview"); this.viewText = jotai.atom((get) => { - if (get(this.isCeView)) { - const viewTextChildren: HeaderElem[] = [ - { - elemtype: "input", - value: get(this.fileName), - isDisabled: true, - }, - ]; - if (get(this.ceReadOnly) == false) { - let saveClassName = "secondary"; - if (get(this.newFileContent) !== null) { - saveClassName = "primary"; - } - viewTextChildren.push( - { - elemtype: "textbutton", - text: "Save", - className: clsx( - `${saveClassName} warning border-radius-4 vertical-padding-2 horizontal-padding-10` - ), - onClick: this.handleFileSave.bind(this), - }, - { - elemtype: "textbutton", - text: "Cancel", - className: "secondary border-radius-4 vertical-padding-2 horizontal-padding-10", - onClick: () => this.toggleCodeEditorReadOnly(true), - } - ); - } else { + const blockData = get(this.blockAtom); + const editMode = blockData?.meta?.edit ?? false; + const viewTextChildren: HeaderElem[] = [ + { + elemtype: "text", + text: get(this.fileName), + ref: this.previewTextRef, + className: "preview-filename", + onClick: () => globalStore.set(this.openFileModal, true), + }, + ]; + let saveClassName = "secondary"; + if (get(this.newFileContent) !== null) { + saveClassName = "primary"; + } + if (editMode) { + viewTextChildren.push({ + elemtype: "textbutton", + text: "Save", + className: clsx( + `${saveClassName} warning border-radius-4 vertical-padding-2 horizontal-padding-10 font-size-11 font-weight-500` + ), + onClick: this.handleFileSave.bind(this), + }); + if (get(this.canPreview)) { viewTextChildren.push({ elemtype: "textbutton", - text: "Edit", - className: "secondary border-radius-4 vertical-padding-2 horizontal-padding-10", - onClick: () => this.toggleCodeEditorReadOnly(false), + text: "Preview", + className: + "secondary border-radius-4 vertical-padding-2 horizontal-padding-10 font-size-11 font-weight-500", + onClick: () => this.toggleEditMode(false), }); } - return [ - { - elemtype: "div", - children: viewTextChildren, - }, - ] as HeaderElem[]; - } else { - return [ - { - elemtype: "text", - text: get(this.fileName), - ref: this.previewTextRef, - }, - ]; + } else if (get(this.canPreview)) { + viewTextChildren.push({ + elemtype: "textbutton", + text: "Edit", + className: + "secondary border-radius-4 vertical-padding-2 horizontal-padding-10 font-size-11 font-weight-500", + onClick: () => this.toggleEditMode(true), + }); } + return [ + { + elemtype: "div", + children: viewTextChildren, + }, + ] as HeaderElem[]; }); - this.preIconButton = jotai.atom((get) => { const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), ""); if (mimeType == "directory") { @@ -226,27 +227,15 @@ export class PreviewModel implements ViewModel { const statFile = await services.FileService.StatFile(conn, fileName); return statFile; }); - this.fullFile = jotai.atom>(async (get) => { - const fileName = get(this.fileName); - if (fileName == null) { - return null; - } - const conn = get(this.connection) ?? ""; - const file = await services.FileService.ReadFile(conn, fileName); - return file; - }); this.fileMimeType = jotai.atom>(async (get) => { const fileInfo = await get(this.statFile); return fileInfo?.mimetype; }); this.fileMimeTypeLoadable = loadable(this.fileMimeType); - this.fileContent = jotai.atom>(async (get) => { - const fullFile = await get(this.fullFile); - return util.base64ToString(fullFile?.data64); - }); this.newFileContent = jotai.atom(null) as jotai.PrimitiveAtom; - this.goParentDirectory = this.goParentDirectory.bind(this); + this.toggleEditMode(false); + this.setFileContent(); } async resolvePath(filePath, basePath) { @@ -285,6 +274,7 @@ export class PreviewModel implements ViewModel { stack.push(part); } }); + console.log("===============================", stack.join("/")); return stack.join("/"); } @@ -332,6 +322,7 @@ export class PreviewModel implements ViewModel { if (updateMeta == null) { return; } + updateMeta.edit = false; const blockOref = WOS.makeORef("block", this.blockId); services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); } @@ -343,6 +334,7 @@ export class PreviewModel implements ViewModel { if (updateMeta == null) { return; } + updateMeta.edit = false; const blockOref = WOS.makeORef("block", this.blockId); services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); } @@ -354,12 +346,39 @@ export class PreviewModel implements ViewModel { if (updateMeta == null) { return; } + updateMeta.edit = false; const blockOref = WOS.makeORef("block", this.blockId); services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); } - toggleCodeEditorReadOnly(readOnly: boolean) { - globalStore.set(this.ceReadOnly, readOnly); + setFileContent() { + const fullFileAtom = jotai.atom>(async (get) => { + const fileName = get(this.fileName); + if (fileName == null) { + return null; + } + const conn = get(this.connection) ?? ""; + const file = await services.FileService.ReadFile(conn, fileName); + return file; + }); + + const fileContentAtom = jotai.atom>(async (get) => { + const fullFile = await get(fullFileAtom); + return util.base64ToString(fullFile?.data64); + }); + + this.fullFile = fullFileAtom; + this.fileContent = fileContentAtom; + } + + toggleEditMode(edit: boolean) { + if (!edit) { + this.setFileContent(); + } + + const blockMeta = globalStore.get(this.blockAtom)?.meta; + const blockOref = WOS.makeORef("block", this.blockId); + services.ObjectService.UpdateObjectMeta(blockOref, { ...blockMeta, edit }); } async handleFileSave() { @@ -367,8 +386,10 @@ export class PreviewModel implements ViewModel { const newFileContent = globalStore.get(this.newFileContent); const conn = globalStore.get(this.connection) ?? ""; try { - services.FileService.SaveFile(conn, fileName, util.stringToBase64(newFileContent)); - globalStore.set(this.newFileContent, null); + if (newFileContent != null) { + services.FileService.SaveFile(conn, fileName, util.stringToBase64(newFileContent)); + globalStore.set(this.newFileContent, null); + } } catch (error) { console.error("Error saving file:", error); } @@ -554,40 +575,27 @@ function CodeEditPreview({ parentRef, contentAtom, filename, - readonly, - isCeViewAtom, newFileContentAtom, model, }: { parentRef: React.MutableRefObject; contentAtom: jotai.Atom>; filename: string; - readonly: boolean; - isCeViewAtom: jotai.PrimitiveAtom; newFileContentAtom: jotai.PrimitiveAtom; model: PreviewModel; }) { const fileContent = jotai.useAtomValue(contentAtom); - const setIsCeView = jotai.useSetAtom(isCeViewAtom); const setNewFileContent = jotai.useSetAtom(newFileContentAtom); - useEffect(() => { - setIsCeView(true); - return () => { - setIsCeView(false); - }; - }, [setIsCeView]); - return ( setNewFileContent(text)} onSave={() => model.handleFileSave()} - onCancel={() => model.toggleCodeEditorReadOnly(true)} - onEdit={() => model.toggleCodeEditorReadOnly(false)} + onCancel={() => model.toggleEditMode(true)} + onEdit={() => model.toggleEditMode(false)} /> ); } @@ -656,20 +664,20 @@ function PreviewView({ const fileMimeTypeAtom = model.fileMimeType; const fileContentAtom = model.fileContent; const newFileContentAtom = model.newFileContent; - const ceReadOnlyAtom = model.ceReadOnly; - const isCeViewAtom = model.isCeView; + const editModeAtom = model.editMode; + const openFileModalAtom = model.openFileModal; + const canPreviewAtom = model.canPreview; const mimeType = jotai.useAtomValue(fileMimeTypeAtom) || ""; const fileName = jotai.useAtomValue(fileNameAtom); const fileInfo = jotai.useAtomValue(statFileAtom); - const ceReadOnly = jotai.useAtomValue(ceReadOnlyAtom); const conn = jotai.useAtomValue(model.connection); - const typeAhead = jotai.useAtomValue(atoms.typeAheadModalAtom); + const editMode = jotai.useAtomValue(editModeAtom); + const openFileModal = jotai.useAtomValue(openFileModalAtom); let blockIcon = iconForFile(mimeType, fileName); const [filePath, setFilePath] = useState(""); const [openFileError, setOpenFileError] = useState(""); - const [openFileModal, setOpenFileModal] = useState(false); // ensure consistent hook calls const specializedView = (() => { @@ -686,9 +694,11 @@ function PreviewView({ view = File Not Found{util.isBlank(fileName) ? null : JSON.stringify(fileName)}; } else if (fileInfo.size > MaxFileSize) { view = File Too Large to Preview; - } else if (mimeType === "text/markdown") { + } else if (mimeType === "text/markdown" && !editMode) { + globalStore.set(canPreviewAtom, true); view = ; - } else if (mimeType === "text/csv") { + } else if (mimeType === "text/csv" && !editMode) { + globalStore.set(canPreviewAtom, true); if (fileInfo.size > MaxCSVSize) { view = CSV File Too Large to Preview (1MB Max); } else { @@ -702,20 +712,26 @@ function PreviewView({ ); } } else if (isTextFile(mimeType)) { + model.toggleEditMode(true); view = ( ); } else if (mimeType === "directory") { view = ; + if (editMode) { + globalStore.set(openFileModalAtom, true); + } else { + globalStore.set(canPreviewAtom, false); + } } else { + globalStore.set(canPreviewAtom, false); + model.toggleEditMode(false); view = (
Preview ({mimeType})
@@ -728,7 +744,7 @@ function PreviewView({ const handleKeyDown = useCallback( (waveEvent: WaveKeyboardEvent): boolean => { const updateModalAndError = (isOpen, errorMsg = "") => { - setOpenFileModal(isOpen); + globalStore.set(openFileModalAtom, isOpen); setOpenFileError(errorMsg); }; @@ -767,20 +783,21 @@ function PreviewView({ }); return false; }, - [typeAhead, model, blockId, filePath, fileName] + [model, blockId, filePath, fileName] ); const handleFileSuggestionSelect = (value) => { - globalStore.set(atoms.typeAheadModalAtom, { - ...(typeAhead as TypeAheadModalType), - [blockId]: false, - }); + globalStore.set(openFileModalAtom, false); }; const handleFileSuggestionChange = (value) => { setFilePath(value); }; + const handleBackDropClick = () => { + globalStore.set(openFileModalAtom, false); + }; + useEffect(() => { const blockIconOverrideAtom = useBlockAtom(blockId, "blockicon:override", () => { return jotai.atom(null); @@ -799,7 +816,7 @@ function PreviewView({ onKeyDown={(e) => keyutil.keydownWrapper(handleKeyDown)(e)} onSelect={handleFileSuggestionSelect} onChange={handleFileSuggestionChange} - onClickBackdrop={() => setOpenFileModal(false)} + onClickBackdrop={handleBackDropClick} /> )}
; + className?: string; + onClick?: () => void; }; type HeaderInput = {