bobbai00 commented on code in PR #3654:
URL: https://github.com/apache/texera/pull/3654#discussion_r2330841625


##########
core/gui/package.json:
##########
@@ -46,16 +46,21 @@
     "@types/lodash-es": "4.17.4",
     "@types/plotly.js-basic-dist-min": "2.12.4",
     "ajv": "8.10.0",
+    "angular-markdown-editor": "^3.1.1",
     "backbone": "1.4.1",
+    "bootstrap": "^5.3.7",
+    "bootstrap-markdown": "^2.10.0",
     "content-disposition": "0.5.4",
     "dagre": "0.8.5",
     "deep-map": "2.0.0",
     "edit-distance": "1.0.4",
     "es6-weak-map": "2.0.3",
     "file-saver": "2.0.5",
+    "font-awesome": "^4.7.0",
     "fuse.js": "6.5.3",

Review Comment:
   Ditto



##########
core/gui/package.json:
##########
@@ -46,16 +46,21 @@
     "@types/lodash-es": "4.17.4",
     "@types/plotly.js-basic-dist-min": "2.12.4",
     "ajv": "8.10.0",
+    "angular-markdown-editor": "^3.1.1",
     "backbone": "1.4.1",
+    "bootstrap": "^5.3.7",
+    "bootstrap-markdown": "^2.10.0",
     "content-disposition": "0.5.4",

Review Comment:
   Please fix the versions of the libraries you introduced by removing `^`



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts:
##########
@@ -205,6 +209,149 @@ export class DatasetDetailComponent implements OnInit {
     }
   }
 
+  public onFileChanged(): void {
+    this.userMakeChanges.emit();
+
+    // Get the current filename to re-select after refresh
+    const currentFileName = this.currentDisplayedFileName;
+
+    this.retrieveDatasetVersionList();
+
+    // Wait a bit for the version list to update, then refresh the current 
version
+    setTimeout(() => {
+      if (this.versions.length > 0) {
+        // Select the latest version (newly created)
+        this.selectedVersion = this.versions[0];
+
+        // Refresh the file tree for the new version
+        if (this.did && this.selectedVersion.dvid) {
+          this.datasetService
+            .retrieveDatasetVersionFileTree(this.did, 
this.selectedVersion.dvid, this.isLogin)
+            .pipe(untilDestroyed(this))
+            .subscribe(data => {
+              this.fileTreeNodeList = data.fileNodes;
+              this.currentDatasetVersionSize = data.size;
+
+              // Try to find and re-select the same file we were editing
+              const fileNode = this.findFileInTree(currentFileName);
+              if (fileNode) {
+                this.loadFileContent(fileNode);
+              } else {
+                // Fallback to first file if our file isn't found
+                let currentNode = this.fileTreeNodeList[0];
+                while (currentNode && currentNode.type === "directory" && 
currentNode.children) {
+                  currentNode = currentNode.children[0];
+                }
+                if (currentNode) {
+                  this.loadFileContent(currentNode);
+                }
+              }
+            });
+        }
+      }
+    }, 500); // Small delay to ensure backend has processed the new version
+
+    this.exitEditMode();
+  }
+
+  private findFileInTree(fileName: string, nodes: DatasetFileNode[] = 
this.fileTreeNodeList): DatasetFileNode | null {

Review Comment:
   Remove this, by default, show the README.md(if any) of a dataset version 
when users open a dataset, otherwise, show the first file in the file tree.



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts:
##########
@@ -205,6 +209,149 @@ export class DatasetDetailComponent implements OnInit {
     }
   }
 
+  public onFileChanged(): void {
+    this.userMakeChanges.emit();
+
+    // Get the current filename to re-select after refresh
+    const currentFileName = this.currentDisplayedFileName;
+
+    this.retrieveDatasetVersionList();
+
+    // Wait a bit for the version list to update, then refresh the current 
version
+    setTimeout(() => {
+      if (this.versions.length > 0) {
+        // Select the latest version (newly created)
+        this.selectedVersion = this.versions[0];
+
+        // Refresh the file tree for the new version
+        if (this.did && this.selectedVersion.dvid) {
+          this.datasetService
+            .retrieveDatasetVersionFileTree(this.did, 
this.selectedVersion.dvid, this.isLogin)
+            .pipe(untilDestroyed(this))
+            .subscribe(data => {
+              this.fileTreeNodeList = data.fileNodes;
+              this.currentDatasetVersionSize = data.size;
+
+              // Try to find and re-select the same file we were editing
+              const fileNode = this.findFileInTree(currentFileName);
+              if (fileNode) {
+                this.loadFileContent(fileNode);
+              } else {
+                // Fallback to first file if our file isn't found
+                let currentNode = this.fileTreeNodeList[0];
+                while (currentNode && currentNode.type === "directory" && 
currentNode.children) {
+                  currentNode = currentNode.children[0];
+                }
+                if (currentNode) {
+                  this.loadFileContent(currentNode);
+                }
+              }
+            });
+        }
+      }
+    }, 500); // Small delay to ensure backend has processed the new version
+
+    this.exitEditMode();
+  }
+
+  private findFileInTree(fileName: string, nodes: DatasetFileNode[] = 
this.fileTreeNodeList): DatasetFileNode | null {
+    for (const node of nodes) {
+      if (node.name === fileName && node.type === "file") {
+        return node;
+      }
+      if (node.children) {
+        const found = this.findFileInTree(fileName, node.children);
+        if (found) {
+          return found;
+        }
+      }
+    }
+    return null;
+  }
+
+  public onClickCreateReadme(): void {
+    this.modalService.confirm({
+      nzTitle: "Create README.md",
+      nzContent: "Are you sure you want to create a README.md file for this 
dataset?",
+      nzOkText: "Yes, Create",
+      nzCancelText: "Cancel",
+      nzOnOk: () => {
+        this.createReadmeFile();
+      },
+    });
+  }
+
+  private createReadmeFile(): void {
+    if (!this.did) return;
+
+    this.isCreatingReadme = true;
+    const defaultReadmeContent = "# Dataset README\n\nDescribe your dataset 
here...";
+
+    this.datasetService
+      .getDataset(this.did, this.isLogin)
+      .pipe(
+        switchMap(dashboardDataset => {
+          const datasetName = dashboardDataset.dataset.name;
+          const readmeBlob = new Blob([defaultReadmeContent], { type: 
"text/markdown" });
+          const readmeFile = new File([readmeBlob], "README.md", { type: 
"text/markdown" });
+          return this.datasetService.multipartUpload(
+            datasetName,
+            "README.md",
+            readmeFile,
+            this.chunkSizeMB * 1024 * 1024,
+            this.maxConcurrentChunks
+          );
+        }),
+        switchMap(progress => {
+          if (progress.status === "finished") {
+            return this.datasetService.createDatasetVersion(this.did!, 
"Created README.md");
+          }
+          return of(progress);
+        }),
+        untilDestroyed(this)
+      )
+      .subscribe({
+        next: result => {
+          if (result && typeof result === "object" && "dvid" in result) {

Review Comment:
   remove this `if`



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.ts:
##########
@@ -0,0 +1,306 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  Component,
+  ElementRef,
+  EventEmitter,
+  Input,
+  OnChanges,
+  OnInit,
+  Output,
+  SimpleChanges,
+  ViewChild,
+  ViewEncapsulation,
+} from "@angular/core";
+import { DatasetService } from 
"../../../../../service/user/dataset/dataset.service";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { NotificationService } from 
"../../../../../../common/service/notification/notification.service";
+import { switchMap } from "rxjs/operators";
+import { of } from "rxjs";
+import { EditorInstance, EditorOption } from "angular-markdown-editor";
+import { MarkdownService } from "ngx-markdown";
+
+@UntilDestroy()
+@Component({
+  selector: "texera-user-dataset-file-editor",
+  templateUrl: "./user-dataset-file-editor.component.html",
+  styleUrls: ["./user-dataset-file-editor.component.scss"],
+  encapsulation: ViewEncapsulation.None,
+})
+export class UserDatasetFileEditorComponent implements OnInit, OnChanges {
+  @Input() did: number | undefined;
+  @Input() dvid: number | undefined;
+  @Input() selectedVersion: any | undefined;
+  @Input() datasetName: string = "";
+  @Input() filePath: string = "";
+  @Input() isMaximized: boolean = false;
+  @Input() userHasWriteAccess: boolean = false;
+  @Input() isLogin: boolean = true;
+  @Input() chunkSizeMB!: number;
+  @Input() maxConcurrentChunks!: number;
+  @Input() isEditMode: boolean = true;
+  @Output() userMakeChanges = new EventEmitter<void>();
+  @Output() editCanceled = new EventEmitter<void>();
+
+  @ViewChild("fileTextarea") fileTextarea!: ElementRef<HTMLTextAreaElement>;
+
+  public fileContent: string = "";
+  public fileExists: boolean = false;
+  public isLoading: boolean = false;
+  public editingContent: string = "";
+  public fileType: "markdown" | "text" | "unsupported" = "unsupported";
+  public showFileContent: boolean = false;
+
+  // Angular Markdown Editor properties
+  public bsEditorInstance!: EditorInstance;
+  public editorOptions!: EditorOption;
+
+  constructor(
+    private datasetService: DatasetService,
+    private notificationService: NotificationService,
+    private markdownService: MarkdownService
+  ) {}
+
+  ngOnInit(): void {
+    this.initializeEditorOptions();
+
+    if (this.dvid && this.datasetName && this.selectedVersion && 
this.filePath) {
+      this.determineFileType();
+      this.loadFile();
+      this.isEditMode = true;
+    }
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (
+      (changes["dvid"] || changes["datasetName"] || changes["selectedVersion"] 
|| changes["filePath"]) &&
+      this.dvid &&
+      this.datasetName &&
+      this.selectedVersion &&
+      this.filePath
+    ) {
+      this.isEditMode = false;
+      this.showFileContent = false;
+      this.isLoading = false;
+      this.fileExists = false;
+      this.fileContent = "";
+      this.editingContent = "";
+
+      this.determineFileType();
+      this.loadFile();
+    }
+  }
+
+  private initializeEditorOptions(): void {
+    this.editorOptions = {
+      autofocus: false,
+      iconlibrary: "fa",
+      savable: false,
+      onShow: (e: EditorInstance) => {
+        this.bsEditorInstance = e;
+        console.log("Markdown editor initialized");
+      },
+      onChange: (e: EditorInstance) => {
+        this.editingContent = e.getContent();
+      },
+      parser: (val: string) => this.parseMarkdown(val),
+    };
+  }
+
+  private determineFileType(): void {
+    const extension = this.filePath.toLowerCase().split(".").pop();
+    switch (extension) {
+      case "md":
+      case "markdown":
+        this.fileType = "markdown";
+        break;
+      case "txt":
+      case "log":
+      case "yml":
+      case "yaml":
+        this.fileType = "text";
+        break;
+      default:
+        this.fileType = "unsupported";
+    }
+  }
+
+  private loadFile(): void {
+    if (!this.did || !this.dvid || !this.datasetName || !this.selectedVersion 
|| !this.filePath) return;
+
+    this.isLoading = true;
+
+    this.datasetService
+      .retrieveDatasetVersionSingleFile(this.filePath, this.isLogin)
+      .pipe(
+        switchMap(blob => {
+          return new Promise<string>((resolve, reject) => {
+            const reader = new FileReader();

Review Comment:
   Replace Promise style with rxjs style



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts:
##########
@@ -81,6 +82,9 @@ export class DatasetDetailComponent implements OnInit {
   chunkSizeMB: number = 50;
   maxConcurrentChunks: number = 10;
 
+  public isCreatingReadme: boolean = false;

Review Comment:
   Remove this



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.ts:
##########
@@ -0,0 +1,306 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  Component,
+  ElementRef,
+  EventEmitter,
+  Input,
+  OnChanges,
+  OnInit,
+  Output,
+  SimpleChanges,
+  ViewChild,
+  ViewEncapsulation,
+} from "@angular/core";
+import { DatasetService } from 
"../../../../../service/user/dataset/dataset.service";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { NotificationService } from 
"../../../../../../common/service/notification/notification.service";
+import { switchMap } from "rxjs/operators";
+import { of } from "rxjs";
+import { EditorInstance, EditorOption } from "angular-markdown-editor";
+import { MarkdownService } from "ngx-markdown";
+
+@UntilDestroy()
+@Component({
+  selector: "texera-user-dataset-file-editor",
+  templateUrl: "./user-dataset-file-editor.component.html",
+  styleUrls: ["./user-dataset-file-editor.component.scss"],
+  encapsulation: ViewEncapsulation.None,
+})
+export class UserDatasetFileEditorComponent implements OnInit, OnChanges {
+  @Input() did: number | undefined;
+  @Input() dvid: number | undefined;
+  @Input() selectedVersion: any | undefined;
+  @Input() datasetName: string = "";
+  @Input() filePath: string = "";
+  @Input() isMaximized: boolean = false;
+  @Input() userHasWriteAccess: boolean = false;
+  @Input() isLogin: boolean = true;
+  @Input() chunkSizeMB!: number;
+  @Input() maxConcurrentChunks!: number;
+  @Input() isEditMode: boolean = true;
+  @Output() userMakeChanges = new EventEmitter<void>();
+  @Output() editCanceled = new EventEmitter<void>();
+
+  @ViewChild("fileTextarea") fileTextarea!: ElementRef<HTMLTextAreaElement>;
+
+  public fileContent: string = "";
+  public fileExists: boolean = false;
+  public isLoading: boolean = false;
+  public editingContent: string = "";
+  public fileType: "markdown" | "text" | "unsupported" = "unsupported";
+  public showFileContent: boolean = false;
+
+  // Angular Markdown Editor properties
+  public bsEditorInstance!: EditorInstance;
+  public editorOptions!: EditorOption;
+
+  constructor(
+    private datasetService: DatasetService,
+    private notificationService: NotificationService,
+    private markdownService: MarkdownService
+  ) {}
+
+  ngOnInit(): void {
+    this.initializeEditorOptions();
+
+    if (this.dvid && this.datasetName && this.selectedVersion && 
this.filePath) {
+      this.determineFileType();
+      this.loadFile();
+      this.isEditMode = true;
+    }
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (
+      (changes["dvid"] || changes["datasetName"] || changes["selectedVersion"] 
|| changes["filePath"]) &&
+      this.dvid &&
+      this.datasetName &&
+      this.selectedVersion &&
+      this.filePath
+    ) {
+      this.isEditMode = false;
+      this.showFileContent = false;
+      this.isLoading = false;
+      this.fileExists = false;
+      this.fileContent = "";
+      this.editingContent = "";
+
+      this.determineFileType();
+      this.loadFile();
+    }
+  }
+
+  private initializeEditorOptions(): void {
+    this.editorOptions = {
+      autofocus: false,
+      iconlibrary: "fa",
+      savable: false,
+      onShow: (e: EditorInstance) => {
+        this.bsEditorInstance = e;
+        console.log("Markdown editor initialized");
+      },
+      onChange: (e: EditorInstance) => {
+        this.editingContent = e.getContent();
+      },
+      parser: (val: string) => this.parseMarkdown(val),
+    };
+  }
+
+  private determineFileType(): void {
+    const extension = this.filePath.toLowerCase().split(".").pop();
+    switch (extension) {
+      case "md":
+      case "markdown":
+        this.fileType = "markdown";
+        break;
+      case "txt":
+      case "log":
+      case "yml":
+      case "yaml":
+        this.fileType = "text";
+        break;
+      default:
+        this.fileType = "unsupported";
+    }
+  }
+
+  private loadFile(): void {
+    if (!this.did || !this.dvid || !this.datasetName || !this.selectedVersion 
|| !this.filePath) return;
+
+    this.isLoading = true;
+
+    this.datasetService
+      .retrieveDatasetVersionSingleFile(this.filePath, this.isLogin)
+      .pipe(
+        switchMap(blob => {
+          return new Promise<string>((resolve, reject) => {
+            const reader = new FileReader();
+            reader.onload = () => resolve(reader.result as string);
+            reader.onerror = () => reject(reader.error);
+            reader.readAsText(blob);
+          });
+        }),
+        untilDestroyed(this)
+      )
+      .subscribe({
+        next: content => {
+          this.isLoading = false;
+          this.fileExists = true;
+          this.fileContent = content;
+          this.editingContent = content;
+        },
+        error: () => {
+          this.isLoading = false;
+          this.fileExists = false;
+          this.fileContent = "";
+          this.editingContent = "";
+          console.log("File not found or error loading");
+        },
+      });
+  }
+
+  public cancelEditing(): void {
+    this.editingContent = this.fileContent;
+    this.isEditMode = false;
+    this.editCanceled.emit();
+  }
+
+  public onMarkdownEditorChange(event: any): void {
+    if (event && event.detail && event.detail.eventData) {
+      this.editingContent = event.detail.eventData.getContent();
+    } else {
+      // Handle direct content change
+      this.editingContent = event;
+    }
+  }
+
+  public onEditorKeydown(event: KeyboardEvent): void {
+    if ((event.ctrlKey || event.metaKey) && event.key === "s") {
+      event.preventDefault();
+      this.saveFile();
+    }
+
+    if (event.key === "Tab") {
+      event.preventDefault();
+      const textarea = event.target as HTMLTextAreaElement;
+      const start = textarea.selectionStart;
+      const end = textarea.selectionEnd;
+
+      const value = textarea.value;
+      textarea.value = value.substring(0, start) + "  " + value.substring(end);
+
+      textarea.selectionStart = textarea.selectionEnd = start + 2;
+
+      this.editingContent = textarea.value;
+    }
+  }
+
+  public saveFile(): void {
+    if (!this.did || !this.userHasWriteAccess) return;
+
+    if (this.editingContent === this.fileContent) {
+      this.notificationService.warning("No changes detected in file content");
+      return;
+    }
+
+    this.uploadFileContent(this.editingContent, `${this.getFileName()} updated 
successfully`);
+  }
+
+  private uploadFileContent(content: string, successMessage: string): void {
+    if (!this.did) return;
+
+    this.datasetService
+      .getDataset(this.did, this.isLogin)
+      .pipe(
+        switchMap(dashboardDataset => {
+          const datasetName = dashboardDataset.dataset.name;
+          const fileName = this.getFileName();
+
+          const mimeType = this.getMimeType();
+          const fileBlob = new Blob([content], { type: mimeType });
+          const file = new File([fileBlob], fileName, { type: mimeType });
+
+          return this.datasetService.multipartUpload(datasetName, fileName, 
file, 50 * 1024 * 1024, 10);
+        }),
+        switchMap(progress => {
+          if (progress.status === "finished") {
+            const fileName = this.getFileName();
+            const versionMessage = successMessage.includes("created") ? 
`Created ${fileName}` : `Updated ${fileName}`;
+
+            return this.datasetService.createDatasetVersion(this.did!, 
versionMessage);
+          }
+          return of(progress);
+        }),
+        untilDestroyed(this)
+      )
+      .subscribe({
+        next: result => {
+          if (result && typeof result === "object" && "dvid" in result) {
+            this.fileExists = true;
+            this.fileContent = content;
+            this.isEditMode = false;
+            this.notificationService.success(successMessage);
+            this.userMakeChanges.emit();
+          }
+        },
+        error: (error: unknown) => {
+          console.error("Error uploading file:", error);
+          this.notificationService.error(`Failed to save 
${this.getFileName()}`);
+        },
+      });
+  }
+
+  private getMimeType(): string {
+    switch (this.fileType) {
+      case "markdown":
+        return "text/markdown";
+      case "text":
+        return "text/plain";
+      default:
+        return "text/plain";
+    }
+  }
+
+  public getFileName(): string {

Review Comment:
   remove this, and test the editing of a file in folders



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts:
##########
@@ -205,6 +209,149 @@ export class DatasetDetailComponent implements OnInit {
     }
   }
 
+  public onFileChanged(): void {
+    this.userMakeChanges.emit();
+
+    // Get the current filename to re-select after refresh
+    const currentFileName = this.currentDisplayedFileName;
+
+    this.retrieveDatasetVersionList();

Review Comment:
   Use the subscribe style to achieve the call handling



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts:
##########
@@ -205,6 +209,149 @@ export class DatasetDetailComponent implements OnInit {
     }
   }
 
+  public onFileChanged(): void {
+    this.userMakeChanges.emit();
+
+    // Get the current filename to re-select after refresh
+    const currentFileName = this.currentDisplayedFileName;
+
+    this.retrieveDatasetVersionList();
+
+    // Wait a bit for the version list to update, then refresh the current 
version
+    setTimeout(() => {
+      if (this.versions.length > 0) {
+        // Select the latest version (newly created)
+        this.selectedVersion = this.versions[0];
+
+        // Refresh the file tree for the new version
+        if (this.did && this.selectedVersion.dvid) {
+          this.datasetService
+            .retrieveDatasetVersionFileTree(this.did, 
this.selectedVersion.dvid, this.isLogin)
+            .pipe(untilDestroyed(this))
+            .subscribe(data => {
+              this.fileTreeNodeList = data.fileNodes;
+              this.currentDatasetVersionSize = data.size;
+
+              // Try to find and re-select the same file we were editing
+              const fileNode = this.findFileInTree(currentFileName);
+              if (fileNode) {
+                this.loadFileContent(fileNode);
+              } else {
+                // Fallback to first file if our file isn't found
+                let currentNode = this.fileTreeNodeList[0];
+                while (currentNode && currentNode.type === "directory" && 
currentNode.children) {
+                  currentNode = currentNode.children[0];
+                }
+                if (currentNode) {
+                  this.loadFileContent(currentNode);
+                }
+              }
+            });
+        }
+      }
+    }, 500); // Small delay to ensure backend has processed the new version
+
+    this.exitEditMode();
+  }
+
+  private findFileInTree(fileName: string, nodes: DatasetFileNode[] = 
this.fileTreeNodeList): DatasetFileNode | null {
+    for (const node of nodes) {
+      if (node.name === fileName && node.type === "file") {
+        return node;
+      }
+      if (node.children) {
+        const found = this.findFileInTree(fileName, node.children);
+        if (found) {
+          return found;
+        }
+      }
+    }
+    return null;
+  }
+
+  public onClickCreateReadme(): void {
+    this.modalService.confirm({
+      nzTitle: "Create README.md",
+      nzContent: "Are you sure you want to create a README.md file for this 
dataset?",
+      nzOkText: "Yes, Create",
+      nzCancelText: "Cancel",
+      nzOnOk: () => {
+        this.createReadmeFile();
+      },
+    });
+  }
+
+  private createReadmeFile(): void {
+    if (!this.did) return;
+
+    this.isCreatingReadme = true;
+    const defaultReadmeContent = "# Dataset README\n\nDescribe your dataset 
here...";

Review Comment:
   Replace 'Dataset' with actual dataset name



##########
core/gui/package.json:
##########
@@ -111,11 +116,13 @@
     "@nrwl/nx-cloud": "19.1.0",
     "@nx/angular": "20.0.3",
     "@types/backbone": "1.4.15",
+    "@types/bootstrap": "^5",
     "@types/content-disposition": "0",
     "@types/dagre": "0.7.47",
     "@types/file-saver": "2.0.5",
     "@types/graphlib": "2.1.8",
     "@types/jasmine": "4.6.4",
+    "@types/jquery": "^3",
     "@types/json-schema": "7.0.9",

Review Comment:
   @aglinxinyuan Please take a look to see if both devDependencies & 
dependencies need to have these packages



##########
core/gui/package.json:
##########
@@ -46,16 +46,21 @@
     "@types/lodash-es": "4.17.4",
     "@types/plotly.js-basic-dist-min": "2.12.4",
     "ajv": "8.10.0",
+    "angular-markdown-editor": "^3.1.1",
     "backbone": "1.4.1",
+    "bootstrap": "^5.3.7",
+    "bootstrap-markdown": "^2.10.0",
     "content-disposition": "0.5.4",
     "dagre": "0.8.5",
     "deep-map": "2.0.0",
     "edit-distance": "1.0.4",
     "es6-weak-map": "2.0.3",
     "file-saver": "2.0.5",
+    "font-awesome": "^4.7.0",
     "fuse.js": "6.5.3",
     "html2canvas": "1.4.1",
     "jointjs": "3.5.4",
+    "jquery": "^3.7.1",
     "js-abbreviation-number": "1.4.0",

Review Comment:
   Move it to the dependee package, and leave a comment



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts:
##########
@@ -205,6 +209,149 @@ export class DatasetDetailComponent implements OnInit {
     }
   }
 
+  public onFileChanged(): void {
+    this.userMakeChanges.emit();
+
+    // Get the current filename to re-select after refresh
+    const currentFileName = this.currentDisplayedFileName;
+
+    this.retrieveDatasetVersionList();
+
+    // Wait a bit for the version list to update, then refresh the current 
version
+    setTimeout(() => {
+      if (this.versions.length > 0) {
+        // Select the latest version (newly created)
+        this.selectedVersion = this.versions[0];
+
+        // Refresh the file tree for the new version
+        if (this.did && this.selectedVersion.dvid) {
+          this.datasetService
+            .retrieveDatasetVersionFileTree(this.did, 
this.selectedVersion.dvid, this.isLogin)
+            .pipe(untilDestroyed(this))
+            .subscribe(data => {
+              this.fileTreeNodeList = data.fileNodes;
+              this.currentDatasetVersionSize = data.size;
+
+              // Try to find and re-select the same file we were editing
+              const fileNode = this.findFileInTree(currentFileName);
+              if (fileNode) {
+                this.loadFileContent(fileNode);
+              } else {
+                // Fallback to first file if our file isn't found
+                let currentNode = this.fileTreeNodeList[0];
+                while (currentNode && currentNode.type === "directory" && 
currentNode.children) {
+                  currentNode = currentNode.children[0];
+                }
+                if (currentNode) {
+                  this.loadFileContent(currentNode);
+                }
+              }
+            });
+        }
+      }
+    }, 500); // Small delay to ensure backend has processed the new version
+
+    this.exitEditMode();
+  }
+
+  private findFileInTree(fileName: string, nodes: DatasetFileNode[] = 
this.fileTreeNodeList): DatasetFileNode | null {
+    for (const node of nodes) {
+      if (node.name === fileName && node.type === "file") {
+        return node;
+      }
+      if (node.children) {
+        const found = this.findFileInTree(fileName, node.children);
+        if (found) {
+          return found;
+        }
+      }
+    }
+    return null;
+  }
+
+  public onClickCreateReadme(): void {
+    this.modalService.confirm({
+      nzTitle: "Create README.md",
+      nzContent: "Are you sure you want to create a README.md file for this 
dataset?",
+      nzOkText: "Yes, Create",
+      nzCancelText: "Cancel",
+      nzOnOk: () => {
+        this.createReadmeFile();
+      },
+    });
+  }
+
+  private createReadmeFile(): void {
+    if (!this.did) return;
+
+    this.isCreatingReadme = true;
+    const defaultReadmeContent = "# Dataset README\n\nDescribe your dataset 
here...";
+
+    this.datasetService
+      .getDataset(this.did, this.isLogin)

Review Comment:
   see if you can avoid this additional request



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts:
##########
@@ -205,6 +209,149 @@ export class DatasetDetailComponent implements OnInit {
     }
   }
 
+  public onFileChanged(): void {
+    this.userMakeChanges.emit();
+
+    // Get the current filename to re-select after refresh
+    const currentFileName = this.currentDisplayedFileName;
+
+    this.retrieveDatasetVersionList();
+
+    // Wait a bit for the version list to update, then refresh the current 
version
+    setTimeout(() => {
+      if (this.versions.length > 0) {
+        // Select the latest version (newly created)
+        this.selectedVersion = this.versions[0];
+
+        // Refresh the file tree for the new version
+        if (this.did && this.selectedVersion.dvid) {
+          this.datasetService
+            .retrieveDatasetVersionFileTree(this.did, 
this.selectedVersion.dvid, this.isLogin)
+            .pipe(untilDestroyed(this))
+            .subscribe(data => {
+              this.fileTreeNodeList = data.fileNodes;
+              this.currentDatasetVersionSize = data.size;
+
+              // Try to find and re-select the same file we were editing
+              const fileNode = this.findFileInTree(currentFileName);
+              if (fileNode) {
+                this.loadFileContent(fileNode);
+              } else {
+                // Fallback to first file if our file isn't found
+                let currentNode = this.fileTreeNodeList[0];
+                while (currentNode && currentNode.type === "directory" && 
currentNode.children) {
+                  currentNode = currentNode.children[0];
+                }
+                if (currentNode) {
+                  this.loadFileContent(currentNode);
+                }
+              }
+            });
+        }
+      }
+    }, 500); // Small delay to ensure backend has processed the new version
+
+    this.exitEditMode();
+  }
+
+  private findFileInTree(fileName: string, nodes: DatasetFileNode[] = 
this.fileTreeNodeList): DatasetFileNode | null {
+    for (const node of nodes) {
+      if (node.name === fileName && node.type === "file") {
+        return node;
+      }
+      if (node.children) {
+        const found = this.findFileInTree(fileName, node.children);
+        if (found) {
+          return found;
+        }
+      }
+    }
+    return null;
+  }
+
+  public onClickCreateReadme(): void {
+    this.modalService.confirm({
+      nzTitle: "Create README.md",
+      nzContent: "Are you sure you want to create a README.md file for this 
dataset?",
+      nzOkText: "Yes, Create",
+      nzCancelText: "Cancel",
+      nzOnOk: () => {
+        this.createReadmeFile();
+      },
+    });
+  }
+
+  private createReadmeFile(): void {
+    if (!this.did) return;
+
+    this.isCreatingReadme = true;
+    const defaultReadmeContent = "# Dataset README\n\nDescribe your dataset 
here...";
+
+    this.datasetService
+      .getDataset(this.did, this.isLogin)
+      .pipe(
+        switchMap(dashboardDataset => {
+          const datasetName = dashboardDataset.dataset.name;
+          const readmeBlob = new Blob([defaultReadmeContent], { type: 
"text/markdown" });
+          const readmeFile = new File([readmeBlob], "README.md", { type: 
"text/markdown" });
+          return this.datasetService.multipartUpload(
+            datasetName,
+            "README.md",
+            readmeFile,
+            this.chunkSizeMB * 1024 * 1024,
+            this.maxConcurrentChunks
+          );
+        }),
+        switchMap(progress => {
+          if (progress.status === "finished") {
+            return this.datasetService.createDatasetVersion(this.did!, 
"Created README.md");

Review Comment:
   "Created" => "Create"; "README.md" => "README"



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.html:
##########
@@ -0,0 +1,120 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+<!-- Loading State -->
+<div
+  *ngIf="isLoading"
+  class="file-loading">
+  <nz-spin nzTip="Loading file...">
+    <nz-alert
+      nzType="info"
+      nzMessage="Loading file content"></nz-alert>
+  </nz-spin>
+</div>
+
+<!-- Show file content if it exists and is editable -->
+<div
+  *ngIf="!isLoading && fileExists && isEditable()"
+  class="file-content">
+  <div
+    *ngIf="isEditMode"
+    class="file-expanded">
+    <div class="file-header">
+      <div class="file-title-section">
+        <h4 class="file-title">
+          <i
+            nz-icon
+            [nzType]="fileType === 'markdown' ? 'file-markdown' : 
'file-text'"></i>

Review Comment:
   Remove this to simplify the logic. Using text icon is good enough



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts:
##########
@@ -205,6 +209,149 @@ export class DatasetDetailComponent implements OnInit {
     }
   }
 
+  public onFileChanged(): void {
+    this.userMakeChanges.emit();
+
+    // Get the current filename to re-select after refresh
+    const currentFileName = this.currentDisplayedFileName;
+
+    this.retrieveDatasetVersionList();
+
+    // Wait a bit for the version list to update, then refresh the current 
version
+    setTimeout(() => {
+      if (this.versions.length > 0) {
+        // Select the latest version (newly created)
+        this.selectedVersion = this.versions[0];
+
+        // Refresh the file tree for the new version
+        if (this.did && this.selectedVersion.dvid) {
+          this.datasetService
+            .retrieveDatasetVersionFileTree(this.did, 
this.selectedVersion.dvid, this.isLogin)
+            .pipe(untilDestroyed(this))
+            .subscribe(data => {
+              this.fileTreeNodeList = data.fileNodes;
+              this.currentDatasetVersionSize = data.size;
+
+              // Try to find and re-select the same file we were editing
+              const fileNode = this.findFileInTree(currentFileName);
+              if (fileNode) {
+                this.loadFileContent(fileNode);
+              } else {
+                // Fallback to first file if our file isn't found
+                let currentNode = this.fileTreeNodeList[0];
+                while (currentNode && currentNode.type === "directory" && 
currentNode.children) {
+                  currentNode = currentNode.children[0];
+                }
+                if (currentNode) {
+                  this.loadFileContent(currentNode);
+                }
+              }
+            });
+        }
+      }
+    }, 500); // Small delay to ensure backend has processed the new version
+
+    this.exitEditMode();
+  }
+
+  private findFileInTree(fileName: string, nodes: DatasetFileNode[] = 
this.fileTreeNodeList): DatasetFileNode | null {
+    for (const node of nodes) {
+      if (node.name === fileName && node.type === "file") {
+        return node;
+      }
+      if (node.children) {
+        const found = this.findFileInTree(fileName, node.children);
+        if (found) {
+          return found;
+        }
+      }
+    }
+    return null;
+  }
+
+  public onClickCreateReadme(): void {
+    this.modalService.confirm({
+      nzTitle: "Create README.md",
+      nzContent: "Are you sure you want to create a README.md file for this 
dataset?",
+      nzOkText: "Yes, Create",
+      nzCancelText: "Cancel",
+      nzOnOk: () => {
+        this.createReadmeFile();
+      },
+    });
+  }
+
+  private createReadmeFile(): void {
+    if (!this.did) return;
+
+    this.isCreatingReadme = true;
+    const defaultReadmeContent = "# Dataset README\n\nDescribe your dataset 
here...";
+
+    this.datasetService
+      .getDataset(this.did, this.isLogin)
+      .pipe(
+        switchMap(dashboardDataset => {
+          const datasetName = dashboardDataset.dataset.name;
+          const readmeBlob = new Blob([defaultReadmeContent], { type: 
"text/markdown" });
+          const readmeFile = new File([readmeBlob], "README.md", { type: 
"text/markdown" });
+          return this.datasetService.multipartUpload(
+            datasetName,
+            "README.md",
+            readmeFile,
+            this.chunkSizeMB * 1024 * 1024,
+            this.maxConcurrentChunks
+          );
+        }),
+        switchMap(progress => {
+          if (progress.status === "finished") {
+            return this.datasetService.createDatasetVersion(this.did!, 
"Created README.md");
+          }
+          return of(progress);
+        }),
+        untilDestroyed(this)
+      )
+      .subscribe({
+        next: result => {
+          if (result && typeof result === "object" && "dvid" in result) {
+            this.isCreatingReadme = false;
+            this.notificationService.success("README created successfully!");
+
+            this.currentDisplayedFileName = "README.md";
+            this.onFileChanged();
+
+            setTimeout(() => {

Review Comment:
   remove this



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts:
##########
@@ -205,6 +209,149 @@ export class DatasetDetailComponent implements OnInit {
     }
   }
 
+  public onFileChanged(): void {
+    this.userMakeChanges.emit();
+
+    // Get the current filename to re-select after refresh
+    const currentFileName = this.currentDisplayedFileName;
+
+    this.retrieveDatasetVersionList();
+
+    // Wait a bit for the version list to update, then refresh the current 
version
+    setTimeout(() => {
+      if (this.versions.length > 0) {

Review Comment:
   Remove this



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts:
##########
@@ -205,6 +209,149 @@ export class DatasetDetailComponent implements OnInit {
     }
   }
 
+  public onFileChanged(): void {
+    this.userMakeChanges.emit();

Review Comment:
   Remove this, simply reload the dataset to show the latest version



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.ts:
##########
@@ -0,0 +1,306 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  Component,
+  ElementRef,
+  EventEmitter,
+  Input,
+  OnChanges,
+  OnInit,
+  Output,
+  SimpleChanges,
+  ViewChild,
+  ViewEncapsulation,
+} from "@angular/core";
+import { DatasetService } from 
"../../../../../service/user/dataset/dataset.service";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { NotificationService } from 
"../../../../../../common/service/notification/notification.service";
+import { switchMap } from "rxjs/operators";
+import { of } from "rxjs";
+import { EditorInstance, EditorOption } from "angular-markdown-editor";
+import { MarkdownService } from "ngx-markdown";
+
+@UntilDestroy()
+@Component({
+  selector: "texera-user-dataset-file-editor",
+  templateUrl: "./user-dataset-file-editor.component.html",
+  styleUrls: ["./user-dataset-file-editor.component.scss"],
+  encapsulation: ViewEncapsulation.None,
+})
+export class UserDatasetFileEditorComponent implements OnInit, OnChanges {
+  @Input() did: number | undefined;
+  @Input() dvid: number | undefined;
+  @Input() selectedVersion: any | undefined;
+  @Input() datasetName: string = "";
+  @Input() filePath: string = "";
+  @Input() isMaximized: boolean = false;
+  @Input() userHasWriteAccess: boolean = false;
+  @Input() isLogin: boolean = true;
+  @Input() chunkSizeMB!: number;
+  @Input() maxConcurrentChunks!: number;
+  @Input() isEditMode: boolean = true;
+  @Output() userMakeChanges = new EventEmitter<void>();
+  @Output() editCanceled = new EventEmitter<void>();
+
+  @ViewChild("fileTextarea") fileTextarea!: ElementRef<HTMLTextAreaElement>;
+
+  public fileContent: string = "";
+  public fileExists: boolean = false;
+  public isLoading: boolean = false;

Review Comment:
   A general comment: see if you can remove some of the state variables



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.ts:
##########
@@ -0,0 +1,306 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  Component,
+  ElementRef,
+  EventEmitter,
+  Input,
+  OnChanges,
+  OnInit,
+  Output,
+  SimpleChanges,
+  ViewChild,
+  ViewEncapsulation,
+} from "@angular/core";
+import { DatasetService } from 
"../../../../../service/user/dataset/dataset.service";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { NotificationService } from 
"../../../../../../common/service/notification/notification.service";
+import { switchMap } from "rxjs/operators";
+import { of } from "rxjs";
+import { EditorInstance, EditorOption } from "angular-markdown-editor";
+import { MarkdownService } from "ngx-markdown";
+
+@UntilDestroy()
+@Component({
+  selector: "texera-user-dataset-file-editor",
+  templateUrl: "./user-dataset-file-editor.component.html",
+  styleUrls: ["./user-dataset-file-editor.component.scss"],
+  encapsulation: ViewEncapsulation.None,
+})
+export class UserDatasetFileEditorComponent implements OnInit, OnChanges {
+  @Input() did: number | undefined;
+  @Input() dvid: number | undefined;
+  @Input() selectedVersion: any | undefined;
+  @Input() datasetName: string = "";
+  @Input() filePath: string = "";
+  @Input() isMaximized: boolean = false;
+  @Input() userHasWriteAccess: boolean = false;
+  @Input() isLogin: boolean = true;
+  @Input() chunkSizeMB!: number;
+  @Input() maxConcurrentChunks!: number;
+  @Input() isEditMode: boolean = true;
+  @Output() userMakeChanges = new EventEmitter<void>();
+  @Output() editCanceled = new EventEmitter<void>();
+
+  @ViewChild("fileTextarea") fileTextarea!: ElementRef<HTMLTextAreaElement>;
+
+  public fileContent: string = "";
+  public fileExists: boolean = false;
+  public isLoading: boolean = false;
+  public editingContent: string = "";
+  public fileType: "markdown" | "text" | "unsupported" = "unsupported";
+  public showFileContent: boolean = false;
+
+  // Angular Markdown Editor properties
+  public bsEditorInstance!: EditorInstance;
+  public editorOptions!: EditorOption;
+
+  constructor(
+    private datasetService: DatasetService,
+    private notificationService: NotificationService,
+    private markdownService: MarkdownService
+  ) {}
+
+  ngOnInit(): void {
+    this.initializeEditorOptions();
+
+    if (this.dvid && this.datasetName && this.selectedVersion && 
this.filePath) {
+      this.determineFileType();
+      this.loadFile();
+      this.isEditMode = true;
+    }
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (
+      (changes["dvid"] || changes["datasetName"] || changes["selectedVersion"] 
|| changes["filePath"]) &&
+      this.dvid &&
+      this.datasetName &&
+      this.selectedVersion &&
+      this.filePath
+    ) {
+      this.isEditMode = false;
+      this.showFileContent = false;
+      this.isLoading = false;
+      this.fileExists = false;
+      this.fileContent = "";
+      this.editingContent = "";
+
+      this.determineFileType();
+      this.loadFile();
+    }
+  }
+
+  private initializeEditorOptions(): void {
+    this.editorOptions = {
+      autofocus: false,
+      iconlibrary: "fa",
+      savable: false,
+      onShow: (e: EditorInstance) => {
+        this.bsEditorInstance = e;
+        console.log("Markdown editor initialized");
+      },
+      onChange: (e: EditorInstance) => {
+        this.editingContent = e.getContent();
+      },
+      parser: (val: string) => this.parseMarkdown(val),
+    };
+  }
+
+  private determineFileType(): void {
+    const extension = this.filePath.toLowerCase().split(".").pop();
+    switch (extension) {
+      case "md":
+      case "markdown":
+        this.fileType = "markdown";
+        break;
+      case "txt":
+      case "log":
+      case "yml":
+      case "yaml":
+        this.fileType = "text";
+        break;
+      default:
+        this.fileType = "unsupported";
+    }
+  }
+
+  private loadFile(): void {
+    if (!this.did || !this.dvid || !this.datasetName || !this.selectedVersion 
|| !this.filePath) return;
+
+    this.isLoading = true;
+
+    this.datasetService
+      .retrieveDatasetVersionSingleFile(this.filePath, this.isLogin)
+      .pipe(
+        switchMap(blob => {
+          return new Promise<string>((resolve, reject) => {
+            const reader = new FileReader();
+            reader.onload = () => resolve(reader.result as string);
+            reader.onerror = () => reject(reader.error);
+            reader.readAsText(blob);
+          });
+        }),
+        untilDestroyed(this)
+      )
+      .subscribe({
+        next: content => {
+          this.isLoading = false;
+          this.fileExists = true;
+          this.fileContent = content;
+          this.editingContent = content;
+        },
+        error: () => {
+          this.isLoading = false;
+          this.fileExists = false;
+          this.fileContent = "";
+          this.editingContent = "";
+          console.log("File not found or error loading");

Review Comment:
   remove this log



##########
core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.ts:
##########
@@ -0,0 +1,306 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  Component,
+  ElementRef,
+  EventEmitter,
+  Input,
+  OnChanges,
+  OnInit,
+  Output,
+  SimpleChanges,
+  ViewChild,
+  ViewEncapsulation,
+} from "@angular/core";
+import { DatasetService } from 
"../../../../../service/user/dataset/dataset.service";
+import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
+import { NotificationService } from 
"../../../../../../common/service/notification/notification.service";
+import { switchMap } from "rxjs/operators";
+import { of } from "rxjs";
+import { EditorInstance, EditorOption } from "angular-markdown-editor";
+import { MarkdownService } from "ngx-markdown";
+
+@UntilDestroy()
+@Component({
+  selector: "texera-user-dataset-file-editor",
+  templateUrl: "./user-dataset-file-editor.component.html",
+  styleUrls: ["./user-dataset-file-editor.component.scss"],
+  encapsulation: ViewEncapsulation.None,
+})
+export class UserDatasetFileEditorComponent implements OnInit, OnChanges {
+  @Input() did: number | undefined;
+  @Input() dvid: number | undefined;
+  @Input() selectedVersion: any | undefined;
+  @Input() datasetName: string = "";
+  @Input() filePath: string = "";
+  @Input() isMaximized: boolean = false;
+  @Input() userHasWriteAccess: boolean = false;
+  @Input() isLogin: boolean = true;
+  @Input() chunkSizeMB!: number;
+  @Input() maxConcurrentChunks!: number;
+  @Input() isEditMode: boolean = true;
+  @Output() userMakeChanges = new EventEmitter<void>();
+  @Output() editCanceled = new EventEmitter<void>();
+
+  @ViewChild("fileTextarea") fileTextarea!: ElementRef<HTMLTextAreaElement>;
+
+  public fileContent: string = "";
+  public fileExists: boolean = false;
+  public isLoading: boolean = false;
+  public editingContent: string = "";
+  public fileType: "markdown" | "text" | "unsupported" = "unsupported";
+  public showFileContent: boolean = false;
+
+  // Angular Markdown Editor properties
+  public bsEditorInstance!: EditorInstance;
+  public editorOptions!: EditorOption;
+
+  constructor(
+    private datasetService: DatasetService,
+    private notificationService: NotificationService,
+    private markdownService: MarkdownService
+  ) {}
+
+  ngOnInit(): void {
+    this.initializeEditorOptions();
+
+    if (this.dvid && this.datasetName && this.selectedVersion && 
this.filePath) {
+      this.determineFileType();
+      this.loadFile();
+      this.isEditMode = true;
+    }
+  }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (
+      (changes["dvid"] || changes["datasetName"] || changes["selectedVersion"] 
|| changes["filePath"]) &&
+      this.dvid &&
+      this.datasetName &&
+      this.selectedVersion &&
+      this.filePath
+    ) {
+      this.isEditMode = false;
+      this.showFileContent = false;
+      this.isLoading = false;
+      this.fileExists = false;
+      this.fileContent = "";
+      this.editingContent = "";
+
+      this.determineFileType();
+      this.loadFile();
+    }
+  }
+
+  private initializeEditorOptions(): void {
+    this.editorOptions = {
+      autofocus: false,
+      iconlibrary: "fa",
+      savable: false,
+      onShow: (e: EditorInstance) => {
+        this.bsEditorInstance = e;
+        console.log("Markdown editor initialized");
+      },
+      onChange: (e: EditorInstance) => {
+        this.editingContent = e.getContent();
+      },
+      parser: (val: string) => this.parseMarkdown(val),
+    };
+  }
+
+  private determineFileType(): void {
+    const extension = this.filePath.toLowerCase().split(".").pop();
+    switch (extension) {
+      case "md":
+      case "markdown":
+        this.fileType = "markdown";
+        break;
+      case "txt":
+      case "log":
+      case "yml":
+      case "yaml":
+        this.fileType = "text";
+        break;
+      default:
+        this.fileType = "unsupported";
+    }
+  }
+
+  private loadFile(): void {
+    if (!this.did || !this.dvid || !this.datasetName || !this.selectedVersion 
|| !this.filePath) return;
+
+    this.isLoading = true;
+
+    this.datasetService
+      .retrieveDatasetVersionSingleFile(this.filePath, this.isLogin)
+      .pipe(
+        switchMap(blob => {
+          return new Promise<string>((resolve, reject) => {
+            const reader = new FileReader();
+            reader.onload = () => resolve(reader.result as string);
+            reader.onerror = () => reject(reader.error);
+            reader.readAsText(blob);
+          });
+        }),
+        untilDestroyed(this)
+      )
+      .subscribe({
+        next: content => {
+          this.isLoading = false;
+          this.fileExists = true;
+          this.fileContent = content;
+          this.editingContent = content;
+        },
+        error: () => {
+          this.isLoading = false;
+          this.fileExists = false;
+          this.fileContent = "";
+          this.editingContent = "";
+          console.log("File not found or error loading");
+        },
+      });
+  }
+
+  public cancelEditing(): void {
+    this.editingContent = this.fileContent;
+    this.isEditMode = false;
+    this.editCanceled.emit();
+  }
+
+  public onMarkdownEditorChange(event: any): void {
+    if (event && event.detail && event.detail.eventData) {
+      this.editingContent = event.detail.eventData.getContent();
+    } else {
+      // Handle direct content change
+      this.editingContent = event;
+    }
+  }
+
+  public onEditorKeydown(event: KeyboardEvent): void {
+    if ((event.ctrlKey || event.metaKey) && event.key === "s") {
+      event.preventDefault();
+      this.saveFile();
+    }
+
+    if (event.key === "Tab") {
+      event.preventDefault();
+      const textarea = event.target as HTMLTextAreaElement;
+      const start = textarea.selectionStart;
+      const end = textarea.selectionEnd;
+
+      const value = textarea.value;
+      textarea.value = value.substring(0, start) + "  " + value.substring(end);
+
+      textarea.selectionStart = textarea.selectionEnd = start + 2;
+
+      this.editingContent = textarea.value;
+    }
+  }
+
+  public saveFile(): void {
+    if (!this.did || !this.userHasWriteAccess) return;
+
+    if (this.editingContent === this.fileContent) {
+      this.notificationService.warning("No changes detected in file content");
+      return;
+    }
+
+    this.uploadFileContent(this.editingContent, `${this.getFileName()} updated 
successfully`);
+  }
+
+  private uploadFileContent(content: string, successMessage: string): void {
+    if (!this.did) return;
+
+    this.datasetService
+      .getDataset(this.did, this.isLogin)
+      .pipe(
+        switchMap(dashboardDataset => {
+          const datasetName = dashboardDataset.dataset.name;
+          const fileName = this.getFileName();
+
+          const mimeType = this.getMimeType();
+          const fileBlob = new Blob([content], { type: mimeType });
+          const file = new File([fileBlob], fileName, { type: mimeType });
+
+          return this.datasetService.multipartUpload(datasetName, fileName, 
file, 50 * 1024 * 1024, 10);
+        }),
+        switchMap(progress => {
+          if (progress.status === "finished") {
+            const fileName = this.getFileName();
+            const versionMessage = successMessage.includes("created") ? 
`Created ${fileName}` : `Updated ${fileName}`;
+
+            return this.datasetService.createDatasetVersion(this.did!, 
versionMessage);
+          }
+          return of(progress);
+        }),
+        untilDestroyed(this)
+      )
+      .subscribe({
+        next: result => {
+          if (result && typeof result === "object" && "dvid" in result) {
+            this.fileExists = true;
+            this.fileContent = content;
+            this.isEditMode = false;
+            this.notificationService.success(successMessage);
+            this.userMakeChanges.emit();
+          }
+        },
+        error: (error: unknown) => {
+          console.error("Error uploading file:", error);
+          this.notificationService.error(`Failed to save 
${this.getFileName()}`);
+        },
+      });
+  }
+
+  private getMimeType(): string {
+    switch (this.fileType) {
+      case "markdown":
+        return "text/markdown";
+      case "text":
+        return "text/plain";
+      default:
+        return "text/plain";
+    }
+  }
+
+  public getFileName(): string {
+    if (!this.filePath) return "";
+    return this.filePath.split("/").pop() || this.filePath;
+  }
+
+  public isEditable(): boolean {
+    return this.fileType === "markdown" || this.fileType === "text";

Review Comment:
   remove this



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to