waveterm/frontend/app/view/preview/preview.tsx

811 lines
29 KiB
TypeScript
Raw Normal View History

// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
2024-08-23 09:18:49 +02:00
import { TypeAheadModal } from "@/app/modals/typeaheadmodal";
import { ContextMenuModel } from "@/app/store/contextmenu";
2024-05-14 18:37:41 +02:00
import { Markdown } from "@/element/markdown";
2024-08-23 09:18:49 +02:00
import { atoms, createBlock, globalStore, useBlockAtom } from "@/store/global";
2024-06-12 02:42:10 +02:00
import * as services from "@/store/services";
2024-05-28 21:12:28 +02:00
import * as WOS from "@/store/wos";
2024-07-18 03:49:27 +02:00
import { getWebServerEndpoint } from "@/util/endpoints";
import * as historyutil from "@/util/historyutil";
import * as keyutil from "@/util/keyutil";
2024-05-14 21:29:41 +02:00
import * as util from "@/util/util";
2024-06-04 03:22:26 +02:00
import clsx from "clsx";
2024-05-28 21:12:28 +02:00
import * as jotai from "jotai";
2024-07-09 00:04:48 +02:00
import { loadable } from "jotai/utils";
2024-08-29 08:47:45 +02:00
import { createRef, useCallback, useEffect, useState } from "react";
2024-08-01 21:53:49 +02:00
import { CenteredDiv } from "../../element/quickelems";
import { CodeEditor } from "../codeeditor/codeeditor";
2024-08-01 22:06:18 +02:00
import { CSVView } from "./csvview";
import { DirectoryPreview } from "./directorypreview";
2024-07-18 08:41:33 +02:00
import "./preview.less";
2024-05-17 07:48:23 +02:00
const MaxFileSize = 1024 * 1024 * 10; // 10MB
2024-07-09 00:04:48 +02:00
const MaxCSVSize = 1024 * 1024 * 1; // 1MB
function isTextFile(mimeType: string): boolean {
return (
mimeType.startsWith("text/") ||
mimeType == "application/sql" ||
(mimeType.startsWith("application/") &&
(mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml"))) ||
mimeType == "application/pem-certificate-chain"
);
}
2024-07-09 00:04:48 +02:00
export class PreviewModel implements ViewModel {
2024-08-23 01:25:53 +02:00
viewType: string;
2024-07-09 00:04:48 +02:00
blockId: string;
blockAtom: jotai.Atom<Block>;
viewIcon: jotai.Atom<string | HeaderIconButton>;
2024-07-09 00:04:48 +02:00
viewName: jotai.Atom<string>;
2024-07-18 08:41:33 +02:00
viewText: jotai.Atom<HeaderElem[]>;
preIconButton: jotai.Atom<HeaderIconButton>;
endIconButtons: jotai.Atom<HeaderIconButton[]>;
2024-07-18 08:41:33 +02:00
ceReadOnly: jotai.PrimitiveAtom<boolean>;
isCeView: jotai.PrimitiveAtom<boolean>;
2024-08-29 08:47:45 +02:00
previewTextRef: React.RefObject<HTMLDivElement>;
2024-07-09 00:04:48 +02:00
fileName: jotai.Atom<string>;
connection: jotai.Atom<string>;
2024-07-09 00:04:48 +02:00
statFile: jotai.Atom<Promise<FileInfo>>;
fullFile: jotai.Atom<Promise<FullFile>>;
fileMimeType: jotai.Atom<Promise<string>>;
fileMimeTypeLoadable: jotai.Atom<Loadable<string>>;
fileContent: jotai.Atom<Promise<string>>;
newFileContent: jotai.PrimitiveAtom<string | null>;
2024-07-09 00:04:48 +02:00
showHiddenFiles: jotai.PrimitiveAtom<boolean>;
refreshVersion: jotai.PrimitiveAtom<number>;
refreshCallback: () => void;
directoryInputElem: HTMLInputElement;
2024-07-09 00:04:48 +02:00
setPreviewFileName(fileName: string) {
services.ObjectService.UpdateObjectMeta(`block:${this.blockId}`, { file: fileName });
}
constructor(blockId: string) {
2024-08-23 01:25:53 +02:00
this.viewType = "preview";
2024-07-09 00:04:48 +02:00
this.blockId = blockId;
this.showHiddenFiles = jotai.atom(true);
this.refreshVersion = jotai.atom(0);
2024-08-29 08:47:45 +02:00
this.previewTextRef = createRef();
2024-07-18 08:41:33 +02:00
this.ceReadOnly = jotai.atom(true);
this.isCeView = jotai.atom(false);
2024-07-09 00:04:48 +02:00
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
this.viewIcon = jotai.atom((get) => {
let blockData = get(this.blockAtom);
if (blockData?.meta?.icon) {
return blockData.meta.icon;
}
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
if (mimeType == "directory") {
return {
elemtype: "iconbutton",
icon: "folder-open",
longClick: (e: React.MouseEvent<any>) => {
let menuItems: ContextMenuItem[] = [];
menuItems.push({
label: "Go to Home",
click: () => this.goHistory("~"),
});
menuItems.push({
label: "Go to Desktop",
click: () => this.goHistory("~/Desktop"),
});
menuItems.push({
label: "Go to Downloads",
click: () => this.goHistory("~/Downloads"),
});
menuItems.push({
label: "Go to Documents",
click: () => this.goHistory("~/Documents"),
});
menuItems.push({
label: "Go to Root",
click: () => this.goHistory("/"),
});
ContextMenuModel.showContextMenu(menuItems, e);
},
};
}
2024-07-09 00:04:48 +02:00
const fileName = get(this.fileName);
return iconForFile(mimeType, fileName);
});
this.viewName = jotai.atom("Preview");
this.viewText = jotai.atom((get) => {
2024-07-18 08:41:33 +02:00
if (get(this.isCeView)) {
const viewTextChildren: HeaderElem[] = [
{
elemtype: "input",
value: get(this.fileName),
isDisabled: true,
},
];
2024-07-18 08:41:33 +02:00
if (get(this.ceReadOnly) == false) {
let saveClassName = "secondary";
if (get(this.newFileContent) !== null) {
saveClassName = "primary";
}
2024-07-18 08:41:33 +02:00
viewTextChildren.push(
{
elemtype: "textbutton",
text: "Save",
className: clsx(
`${saveClassName} warning border-radius-4 vertical-padding-2 horizontal-padding-10`
),
2024-07-18 08:41:33 +02:00
onClick: this.handleFileSave.bind(this),
},
{
elemtype: "textbutton",
text: "Cancel",
className: "secondary border-radius-4 vertical-padding-2 horizontal-padding-10",
2024-07-18 08:41:33 +02:00
onClick: () => this.toggleCodeEditorReadOnly(true),
}
);
} else {
viewTextChildren.push({
elemtype: "textbutton",
text: "Edit",
className: "secondary border-radius-4 vertical-padding-2 horizontal-padding-10",
2024-07-18 08:41:33 +02:00
onClick: () => this.toggleCodeEditorReadOnly(false),
});
}
return [
{
elemtype: "div",
children: viewTextChildren,
},
] as HeaderElem[];
} else {
return [
{
elemtype: "text",
text: get(this.fileName),
2024-08-29 08:47:45 +02:00
ref: this.previewTextRef,
},
];
2024-07-18 08:41:33 +02:00
}
2024-07-09 00:04:48 +02:00
});
2024-07-18 08:41:33 +02:00
this.preIconButton = jotai.atom((get) => {
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
if (mimeType == "directory") {
return null;
}
return {
elemtype: "iconbutton",
icon: "chevron-left",
click: this.goParentDirectory.bind(this),
};
});
this.endIconButtons = jotai.atom((get) => {
2024-07-09 00:04:48 +02:00
const mimeType = util.jotaiLoadableValue(get(this.fileMimeTypeLoadable), "");
if (mimeType == "directory") {
let showHiddenFiles = get(this.showHiddenFiles);
return [
{
elemtype: "iconbutton",
icon: showHiddenFiles ? "eye" : "eye-slash",
click: () => {
globalStore.set(this.showHiddenFiles, (prev) => !prev);
},
},
{
elemtype: "iconbutton",
icon: "arrows-rotate",
click: () => this.refreshCallback?.(),
},
];
2024-07-09 00:04:48 +02:00
}
return null;
2024-07-09 00:04:48 +02:00
});
this.fileName = jotai.atom<string>((get) => {
const file = get(this.blockAtom)?.meta?.file;
if (util.isBlank(file)) {
return "~";
2024-07-09 00:04:48 +02:00
}
return file;
});
this.connection = jotai.atom<string>((get) => {
return get(this.blockAtom)?.meta?.connection;
});
2024-07-09 00:04:48 +02:00
this.statFile = jotai.atom<Promise<FileInfo>>(async (get) => {
const fileName = get(this.fileName);
if (fileName == null) {
return null;
}
const conn = get(this.connection) ?? "";
const statFile = await services.FileService.StatFile(conn, fileName);
2024-07-09 00:04:48 +02:00
return statFile;
});
this.fullFile = jotai.atom<Promise<FullFile>>(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);
2024-07-09 00:04:48 +02:00
return file;
});
this.fileMimeType = jotai.atom<Promise<string>>(async (get) => {
const fileInfo = await get(this.statFile);
return fileInfo?.mimetype;
});
this.fileMimeTypeLoadable = loadable(this.fileMimeType);
this.fileContent = jotai.atom<Promise<string>>(async (get) => {
const fullFile = await get(this.fullFile);
return util.base64ToString(fullFile?.data64);
});
this.newFileContent = jotai.atom(null) as jotai.PrimitiveAtom<string | null>;
2024-07-09 00:04:48 +02:00
this.goParentDirectory = this.goParentDirectory.bind(this);
}
2024-08-29 08:47:45 +02:00
async resolvePath(filePath, basePath) {
// Handle paths starting with "~" to refer to the home directory
if (filePath.startsWith("~")) {
try {
const conn = globalStore.get(this.connection);
const sf = await services.FileService.StatFile(conn, "~");
basePath = sf.path; // Update basePath to the fetched home directory path
filePath = basePath + filePath.slice(1); // Replace "~" with the fetched home directory path
} catch (error) {
console.error("Error fetching home directory:", error);
return basePath;
}
}
// If filePath is an absolute path, return it directly
if (filePath.startsWith("/")) {
return filePath;
}
const stack = basePath.split("/");
// Ensure no empty segments from trailing slashes
if (stack[stack.length - 1] === "") {
stack.pop();
}
// Process the filePath parts
filePath.split("/").forEach((part) => {
if (part === "..") {
// Go up one level, avoid going above root level
if (stack.length > 1) {
stack.pop();
}
} else if (part === "." || part === "") {
// Ignore current directory marker and empty parts
} else {
// Normal path part, add to the stack
stack.push(part);
}
});
return stack.join("/");
}
async isValidPath(path) {
try {
const conn = globalStore.get(this.connection);
const sf = await services.FileService.StatFile(conn, path);
const isValid = !sf.notfound;
return isValid;
} catch (error) {
console.error("Error checking path validity:", error);
return false;
}
}
async goHistory(newPath, isValidated = false) {
const fileName = globalStore.get(this.fileName);
if (fileName == null) {
return;
}
2024-08-29 08:47:45 +02:00
if (!isValidated) {
newPath = await this.resolvePath(newPath, fileName);
const isValid = await this.isValidPath(newPath);
if (!isValid) {
return;
}
}
const blockMeta = globalStore.get(this.blockAtom)?.meta;
const updateMeta = historyutil.goHistory("file", fileName, newPath, blockMeta);
if (updateMeta == null) {
return;
}
const blockOref = WOS.makeORef("block", this.blockId);
services.ObjectService.UpdateObjectMeta(blockOref, updateMeta);
2024-07-09 00:04:48 +02:00
}
goParentDirectory() {
const blockMeta = globalStore.get(this.blockAtom)?.meta;
2024-07-09 00:04:48 +02:00
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);
2024-07-09 00:04:48 +02:00
}
2024-07-18 08:41:33 +02:00
toggleCodeEditorReadOnly(readOnly: boolean) {
globalStore.set(this.ceReadOnly, readOnly);
}
async handleFileSave() {
const fileName = globalStore.get(this.fileName);
const newFileContent = globalStore.get(this.newFileContent);
const conn = globalStore.get(this.connection) ?? "";
2024-07-18 08:41:33 +02:00
try {
services.FileService.SaveFile(conn, fileName, util.stringToBase64(newFileContent));
globalStore.set(this.newFileContent, null);
2024-07-18 08:41:33 +02:00
} catch (error) {
console.error("Error saving file:", error);
}
}
getSettingsMenuItems(): ContextMenuItem[] {
const menuItems: ContextMenuItem[] = [];
menuItems.push({
label: "Copy Full Path",
click: () => {
const fileName = globalStore.get(this.fileName);
if (fileName == null) {
return;
}
navigator.clipboard.writeText(fileName);
},
});
menuItems.push({
label: "Copy File Name",
click: () => {
let fileName = globalStore.get(this.fileName);
if (fileName == null) {
return;
}
if (fileName.endsWith("/")) {
fileName = fileName.substring(0, fileName.length - 1);
}
const splitPath = fileName.split("/");
const baseName = splitPath[splitPath.length - 1];
navigator.clipboard.writeText(baseName);
},
});
const mimeType = util.jotaiLoadableValue(globalStore.get(this.fileMimeTypeLoadable), "");
if (mimeType == "directory") {
menuItems.push({
label: "Open Terminal in New Block",
click: async () => {
const termBlockDef: BlockDef = {
meta: {
view: "term",
controller: "shell",
"cmd:cwd": globalStore.get(this.fileName),
},
};
await createBlock(termBlockDef);
},
});
}
return menuItems;
}
giveFocus(): boolean {
if (this.directoryInputElem) {
this.directoryInputElem.focus({ preventScroll: true });
return true;
}
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;
}
2024-07-09 00:04:48 +02:00
}
function makePreviewModel(blockId: string): PreviewModel {
const previewModel = new PreviewModel(blockId);
return previewModel;
}
2024-05-17 07:48:23 +02:00
function DirNav({ cwdAtom }: { cwdAtom: jotai.WritableAtom<string, [string], void> }) {
const [cwd, setCwd] = jotai.useAtom(cwdAtom);
if (cwd == null || cwd == "") {
return null;
}
let splitNav = [cwd];
let remaining = cwd;
let idx = remaining.lastIndexOf("/");
while (idx !== -1) {
remaining = remaining.substring(0, idx);
splitNav.unshift(remaining);
idx = remaining.lastIndexOf("/");
}
if (splitNav.length === 0) {
splitNav = [cwd];
}
return (
<div className="view-nav">
2024-06-04 03:22:26 +02:00
{splitNav.map((item, idx) => {
let splitPath = item.split("/");
if (splitPath.length === 0) {
splitPath = [item];
}
2024-06-04 03:22:26 +02:00
const isLast = idx == splitNav.length - 1;
let baseName = splitPath[splitPath.length - 1];
if (!isLast) {
baseName += "/";
}
return (
2024-06-04 03:22:26 +02:00
<div
className={clsx("view-nav-item", isLast ? "current-file" : "clickable")}
key={`nav-item-${item}`}
onClick={isLast ? null : () => setCwd(item)}
>
{baseName}
2024-06-04 03:22:26 +02:00
</div>
);
})}
2024-06-04 03:22:26 +02:00
<div className="flex-spacer"></div>
</div>
);
}
2024-05-17 07:48:23 +02:00
function MarkdownPreview({ contentAtom }: { contentAtom: jotai.Atom<Promise<string>> }) {
const readmeText = jotai.useAtomValue(contentAtom);
2024-05-14 18:37:41 +02:00
return (
<div className="view-preview view-preview-markdown">
2024-05-14 21:29:41 +02:00
<Markdown text={readmeText} />
2024-05-14 18:37:41 +02:00
</div>
);
2024-05-17 07:48:23 +02:00
}
function StreamingPreview({ connection, fileInfo }: { connection?: string; fileInfo: FileInfo }) {
2024-05-17 07:48:23 +02:00
const filePath = fileInfo.path;
const usp = new URLSearchParams();
usp.set("path", filePath);
if (connection != null) {
usp.set("connection", connection);
}
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?" + usp.toString();
2024-06-03 22:13:41 +02:00
if (fileInfo.mimetype == "application/pdf") {
return (
<div className="view-preview view-preview-pdf">
2024-06-14 20:10:54 +02:00
<iframe src={streamingUrl} width="95%" height="95%" name="pdfview" />
2024-06-03 22:13:41 +02:00
</div>
);
}
2024-05-17 07:48:23 +02:00
if (fileInfo.mimetype.startsWith("video/")) {
return (
<div className="view-preview view-preview-video">
<video controls>
<source src={streamingUrl} />
</video>
</div>
);
}
if (fileInfo.mimetype.startsWith("audio/")) {
return (
<div className="view-preview view-preview-audio">
<audio controls>
<source src={streamingUrl} />
</audio>
</div>
);
}
if (fileInfo.mimetype.startsWith("image/")) {
return (
<div className="view-preview view-preview-image">
<img src={streamingUrl} />
</div>
);
}
return <CenteredDiv>Preview Not Supported</CenteredDiv>;
}
function CodeEditPreview({
parentRef,
contentAtom,
filename,
readonly,
2024-07-18 08:41:33 +02:00
isCeViewAtom,
newFileContentAtom,
model,
}: {
parentRef: React.MutableRefObject<HTMLDivElement>;
contentAtom: jotai.Atom<Promise<string>>;
filename: string;
readonly: boolean;
2024-07-18 08:41:33 +02:00
isCeViewAtom: jotai.PrimitiveAtom<boolean>;
newFileContentAtom: jotai.PrimitiveAtom<string>;
model: PreviewModel;
}) {
const fileContent = jotai.useAtomValue(contentAtom);
2024-07-18 08:41:33 +02:00
const setIsCeView = jotai.useSetAtom(isCeViewAtom);
const setNewFileContent = jotai.useSetAtom(newFileContentAtom);
useEffect(() => {
setIsCeView(true);
return () => {
setIsCeView(false);
};
}, [setIsCeView]);
return (
<CodeEditor
parentRef={parentRef}
2024-07-18 08:41:33 +02:00
readonly={readonly}
text={fileContent}
filename={filename}
onChange={(text) => setNewFileContent(text)}
onSave={() => model.handleFileSave()}
onCancel={() => model.toggleCodeEditorReadOnly(true)}
onEdit={() => model.toggleCodeEditorReadOnly(false)}
2024-07-18 08:41:33 +02:00
/>
);
}
2024-06-24 19:17:35 +02:00
function CSVViewPreview({
parentRef,
contentAtom,
filename,
readonly,
}: {
parentRef: React.MutableRefObject<HTMLDivElement>;
contentAtom: jotai.Atom<Promise<string>>;
filename: string;
readonly: boolean;
}) {
const fileContent = jotai.useAtomValue(contentAtom);
return <CSVView parentRef={parentRef} readonly={true} content={fileContent} filename={filename} />;
}
2024-06-22 01:40:24 +02:00
function iconForFile(mimeType: string, fileName: string): string {
2024-07-09 00:04:48 +02:00
if (mimeType == null) {
mimeType = "unknown";
}
2024-06-22 01:40:24 +02:00
if (mimeType == "application/pdf") {
return "file-pdf";
} else if (mimeType.startsWith("image/")) {
return "image";
} else if (mimeType.startsWith("video/")) {
return "film";
} else if (mimeType.startsWith("audio/")) {
return "headphones";
} else if (mimeType.startsWith("text/markdown")) {
return "file-lines";
2024-06-26 18:39:41 +02:00
} else if (mimeType == "text/csv") {
return "file-csv";
2024-06-22 01:40:24 +02:00
} else if (
mimeType.startsWith("text/") ||
2024-07-09 00:04:48 +02:00
mimeType == "application/sql" ||
2024-06-22 01:40:24 +02:00
(mimeType.startsWith("application/") &&
(mimeType.includes("json") || mimeType.includes("yaml") || mimeType.includes("toml")))
) {
return "file-code";
} else if (mimeType === "directory") {
if (fileName == "~" || fileName == "~/") {
return "home";
}
2024-07-09 00:04:48 +02:00
return "folder-open";
2024-06-22 01:40:24 +02:00
} else {
return "file";
}
}
2024-08-29 08:47:45 +02:00
function PreviewView({
blockId,
blockRef,
contentRef,
model,
}: {
blockId: string;
blockRef: React.RefObject<HTMLDivElement>;
contentRef: React.RefObject<HTMLDivElement>;
model: PreviewModel;
}) {
2024-07-09 00:04:48 +02:00
const fileNameAtom = model.fileName;
const statFileAtom = model.statFile;
const fileMimeTypeAtom = model.fileMimeType;
const fileContentAtom = model.fileContent;
2024-07-18 08:41:33 +02:00
const newFileContentAtom = model.newFileContent;
const ceReadOnlyAtom = model.ceReadOnly;
const isCeViewAtom = model.isCeView;
2024-05-17 07:48:23 +02:00
2024-07-18 08:41:33 +02:00
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);
2024-08-23 09:18:49 +02:00
const typeAhead = jotai.useAtomValue(atoms.typeAheadModalAtom);
2024-06-22 01:40:24 +02:00
let blockIcon = iconForFile(mimeType, fileName);
2024-07-18 08:41:33 +02:00
2024-08-29 08:47:45 +02:00
const [filePath, setFilePath] = useState("");
const [openFileError, setOpenFileError] = useState("");
const [openFileModal, setOpenFileModal] = useState(false);
2024-07-18 08:41:33 +02:00
// ensure consistent hook calls
const specializedView = (() => {
let view: React.ReactNode = null;
blockIcon = iconForFile(mimeType, fileName);
if (
mimeType === "application/pdf" ||
mimeType.startsWith("video/") ||
mimeType.startsWith("audio/") ||
mimeType.startsWith("image/")
) {
view = <StreamingPreview connection={conn} fileInfo={fileInfo} />;
2024-07-18 08:41:33 +02:00
} else if (!fileInfo) {
view = <CenteredDiv>File Not Found{util.isBlank(fileName) ? null : JSON.stringify(fileName)}</CenteredDiv>;
} else if (fileInfo.size > MaxFileSize) {
view = <CenteredDiv>File Too Large to Preview</CenteredDiv>;
} else if (mimeType === "text/markdown") {
view = <MarkdownPreview contentAtom={fileContentAtom} />;
} else if (mimeType === "text/csv") {
if (fileInfo.size > MaxCSVSize) {
view = <CenteredDiv>CSV File Too Large to Preview (1MB Max)</CenteredDiv>;
} else {
view = (
<CSVViewPreview
parentRef={contentRef}
contentAtom={fileContentAtom}
filename={fileName}
readonly={true}
/>
);
}
} else if (isTextFile(mimeType)) {
view = (
<CodeEditPreview
readonly={ceReadOnly}
parentRef={contentRef}
2024-07-11 00:06:19 +02:00
contentAtom={fileContentAtom}
filename={fileName}
2024-07-18 08:41:33 +02:00
isCeViewAtom={isCeViewAtom}
newFileContentAtom={newFileContentAtom}
model={model}
2024-07-11 00:06:19 +02:00
/>
2024-07-09 00:04:48 +02:00
);
2024-07-18 08:41:33 +02:00
} else if (mimeType === "directory") {
view = <DirectoryPreview fileNameAtom={fileNameAtom} model={model} />;
} else {
view = (
<div className="view-preview">
<div>Preview ({mimeType})</div>
</div>
);
2024-07-09 00:04:48 +02:00
}
2024-07-18 08:41:33 +02:00
return view;
})();
2024-08-23 09:18:49 +02:00
const handleKeyDown = useCallback(
(waveEvent: WaveKeyboardEvent): boolean => {
2024-08-29 08:47:45 +02:00
const updateModalAndError = (isOpen, errorMsg = "") => {
setOpenFileModal(isOpen);
setOpenFileError(errorMsg);
};
const handleEnterPress = async () => {
const newPath = await model.resolvePath(filePath, fileName);
const isValidPath = await model.isValidPath(newPath);
if (isValidPath) {
updateModalAndError(false);
await model.goHistory(newPath, true);
} else {
updateModalAndError(true, "The path you entered does not exist.");
}
2024-08-23 09:18:49 +02:00
model.giveFocus();
2024-08-29 08:47:45 +02:00
return isValidPath;
};
const handleCommandOperations = async () => {
if (keyutil.checkKeyPressed(waveEvent, "Cmd:o")) {
updateModalAndError(true);
return true;
}
if (keyutil.checkKeyPressed(waveEvent, "Cmd:d")) {
updateModalAndError(false);
return false;
}
if (keyutil.checkKeyPressed(waveEvent, "Enter")) {
return handleEnterPress();
}
return false;
};
handleCommandOperations().catch((error) => {
console.error("Error handling key down:", error);
updateModalAndError(true, "An error occurred during operation.");
return false;
});
return false;
2024-08-23 09:18:49 +02:00
},
2024-08-29 08:47:45 +02:00
[typeAhead, model, blockId, filePath, fileName]
2024-08-23 09:18:49 +02:00
);
const handleFileSuggestionSelect = (value) => {
globalStore.set(atoms.typeAheadModalAtom, {
...(typeAhead as TypeAheadModalType),
[blockId]: false,
});
};
2024-08-29 08:47:45 +02:00
const handleFileSuggestionChange = (value) => {
setFilePath(value);
};
2024-07-18 08:41:33 +02:00
useEffect(() => {
const blockIconOverrideAtom = useBlockAtom<string>(blockId, "blockicon:override", () => {
return jotai.atom<string>(null);
2024-06-22 01:11:34 +02:00
}) as jotai.PrimitiveAtom<string>;
globalStore.set(blockIconOverrideAtom, blockIcon);
2024-07-18 08:41:33 +02:00
}, [blockId, blockIcon]);
return (
2024-08-23 09:18:49 +02:00
<>
2024-08-29 08:47:45 +02:00
{openFileModal && (
2024-08-23 09:18:49 +02:00
<TypeAheadModal
2024-08-29 08:47:45 +02:00
label="Open file"
suggestions={[]}
blockRef={blockRef}
anchorRef={model.previewTextRef}
2024-08-23 09:18:49 +02:00
onKeyDown={(e) => keyutil.keydownWrapper(handleKeyDown)(e)}
onSelect={handleFileSuggestionSelect}
2024-08-29 08:47:45 +02:00
onChange={handleFileSuggestionChange}
onClickBackdrop={() => setOpenFileModal(false)}
2024-08-23 09:18:49 +02:00
/>
)}
<div
className="full-preview scrollbar-hide-until-hover"
onKeyDown={(e) => keyutil.keydownWrapper(handleKeyDown)(e)}
>
<div ref={contentRef} className="full-preview-content">
{specializedView}
</div>
2024-07-11 00:06:19 +02:00
</div>
2024-08-23 09:18:49 +02:00
</>
);
2024-05-17 07:48:23 +02:00
}
export { makePreviewModel, PreviewView };