This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new 8bfe034af1 refactor(frontend): extract shared VersionsFilesBrowser 
component (#8942)
8bfe034af1 is described below

commit 8bfe034af17f88e8396b4961714658a48f79cff2
Author: Matthew B. <[email protected]>
AuthorDate: Sat Oct 10 08:50:21 2026 +0000

    refactor(frontend): extract shared VersionsFilesBrowser component (#8942)
    
    ### What changes were proposed in this PR?
    - Extracted the duplicated "Versions & Files" tab from `dataset-detail`
    and `model-detail` into one shared presentational component,
    `texera-versions-files-browser` (template, SCSS, toolbar view-state,
    sider resize, and copy-path logic).
    - Both detail pages now render `<texera-versions-files-browser>` with
    inputs/outputs and keep their own data loading and version-selection
    logic.
    - Kept `isMaximized` and `isRightBarCollapsed` parent-owned as two-way
    bindings, since `isMaximized` also gates the page header outside the
    tab; the child reports toggles rather than owning the flags.
    - Moved the view-state unit tests (sider resize, animation-frame
    scheduling, copy-path, toggle emits) into the new component's spec;
    parent specs keep their integration coverage by rendering the real
    child.
    ### Any related issues, documentation, discussions?
    Closes: #8941
    ### How was this PR tested?
    - Run `cd frontend && npx ng test
    --include='**/versions-files-browser.component.spec.ts'
    --include='**/dataset-detail.component.spec.ts'
    --include='**/model-detail.component.spec.ts'`, expect all 231 tests to
    pass.
    - Open a dataset detail page, switch to the "Versions & Files" tab,
    confirm the version picker, file tree, file renderer, resize handle,
    maximize/minimize, hide/show right bar, copy-path, and zip download all
    behave as before.
    - Repeat on a model detail page and confirm identical behavior, and
    confirm the model Settings tab (which still uses nz-select) is
    unaffected.
    ### Was this PR authored or co-authored using generative AI tooling?
    Co-authored with Claude Opus 4.8 in compliance with ASF
    πŸ€– Generated with [Claude Code](https://claude.com/claude-code)
---
 .../dataset-detail.component.html                  | 242 +++------------------
 .../dataset-detail.component.spec.ts               | 126 -----------
 .../dataset-detail.component.ts                    |  64 +-----
 .../model-detail.component.html                    | 242 +++------------------
 .../model-detail.component.spec.ts                 |  63 +-----
 .../user-model-explorer/model-detail.component.ts  |  61 +-----
 .../versions-files-browser.component.html          | 231 ++++++++++++++++++++
 .../versions-files-browser.component.scss          | 136 ++++++++++++
 .../versions-files-browser.component.spec.ts       | 182 ++++++++++++++++
 .../versions-files-browser.component.ts            | 177 +++++++++++++++
 10 files changed, 806 insertions(+), 718 deletions(-)

diff --git 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html
 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html
index acaf1ec5e8..7ef75951fa 100644
--- 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html
+++ 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html
@@ -251,218 +251,36 @@
     </div>
   </nz-tab>
   <nz-tab nzTitle="Versions & Files">
-    <div class="versions-tab-content">
-      <nz-layout>
-        <nz-content
-          [ngClass]="{'grayed-out': false, 'disabled-click': false}"
-          style="background-color: white">
-          <nz-card>
-            <div style="display: flex; justify-content: space-between; 
align-items: center">
-              <div class="file-info">
-                <h3 class="file-title">
-                  <span class="file-title-main">
-                    <b>{{ currentDisplayedFileName }}</b>
-
-                    <button
-                      nz-button
-                      nzType="text"
-                      nzSize="small"
-                      class="copy-path-btn"
-                      nz-tooltip
-                      nzTooltipTitle="Copy file path"
-                      *ngIf="currentDisplayedFileName"
-                      (click)="copyCurrentFilePath()">
-                      <i
-                        nz-icon
-                        nzType="copy"
-                        nzTheme="twotone">
-                      </i>
-                    </button>
-                  </span>
-
-                  <span
-                    *ngIf="currentFileSize"
-                    class="file-size">
-                    <i
-                      nz-icon
-                      nzType="file"
-                      nzTheme="outline"
-                      class="icon-file"></i>
-                    {{ formatSize(currentFileSize) }}
-                  </span>
-                </h3>
-              </div>
-              <div style="display: flex">
-                <button
-                  nz-button
-                  *ngIf="selectedVersion"
-                  nz-tooltip="Download the file"
-                  [disabled]="!isLogin || !isDownloadAllowed()"
-                  (click)="onClickDownloadCurrentFile()">
-                  <i
-                    nz-icon
-                    nzTheme="outline"
-                    nzType="download">
-                  </i>
-                </button>
-                <button
-                  nz-button
-                  *ngIf="!isMaximized && selectedVersion"
-                  nz-tooltip="Maximize View"
-                  (click)="onClickScaleTheView()">
-                  <i
-                    nz-icon
-                    nzTheme="outline"
-                    nzType="expand">
-                  </i>
-                </button>
-                <button
-                  nz-button
-                  *ngIf="isMaximized && selectedVersion"
-                  nz-tooltip="Minimize View"
-                  (click)="onClickScaleTheView()">
-                  <i
-                    nz-icon
-                    nzTheme="outline"
-                    nzType="compress">
-                  </i>
-                </button>
-                <button
-                  *ngIf="!isRightBarCollapsed"
-                  nz-button
-                  nz-tooltip="Hide the right bar"
-                  (click)="onClickHideRightBar()">
-                  <i
-                    nz-icon
-                    nzTheme="outline"
-                    nzType="right">
-                  </i>
-                </button>
-                <button
-                  *ngIf="isRightBarCollapsed"
-                  nz-button
-                  nz-tooltip="Show Tree"
-                  (click)="onClickHideRightBar()">
-                  <i
-                    nz-icon
-                    nzTheme="outline"
-                    nzType="left">
-                  </i>
-                </button>
-              </div>
-            </div>
-          </nz-card>
-          <nz-empty
-            class="empty-version-indicator"
-            *ngIf="!selectedVersion"
-            nzNotFoundContent="No version is selected"></nz-empty>
-
-          <texera-user-dataset-file-renderer
-            *ngIf="selectedVersion"
-            [isMaximized]="isMaximized"
-            [resourceId]="did"
-            [versionId]="selectedVersion.dvid"
-            [filePath]="currentDisplayedFileName"
-            [fileSize]="currentFileSize"
-            [isLogin]="isLogin"
-            class="file-renderer">
-          </texera-user-dataset-file-renderer>
-        </nz-content>
-        <nz-sider
-          *ngIf="!isRightBarCollapsed"
-          nzTheme="light"
-          [nzWidth]="siderWidth"
-          nz-resizable
-          [nzMinWidth]="MIN_SIDER_WIDTH"
-          [nzMaxWidth]="MAX_SIDER_WIDTH"
-          (nzResize)="onSideResize($event)">
-          <nz-resize-handle nzDirection="left">
-            <div class="sider-resize-line">
-              <i
-                class="sider-resize-handle"
-                nz-icon
-                nzType="more"
-                nzTheme="outline"></i>
-            </div>
-          </nz-resize-handle>
-          <div class="right-sider">
-            <nz-collapse nzGhost>
-              <nz-collapse-panel
-                nzHeader="Current Versions"
-                nzActive="true">
-                <div class="version-storage">
-                  <h6 style="font-weight: lighter; font-size: 0.9em">Choose a 
Version:</h6>
-                  <div class="select-and-button-container">
-                    <nz-select
-                      nzShowSearch
-                      nzPlaceHolder="Select a version"
-                      (ngModelChange)="onVersionSelected($event)"
-                      [(ngModel)]="selectedVersion">
-                      <nz-option
-                        *ngFor="let version of versions"
-                        [nzValue]="version"
-                        [nzLabel]="version.name"></nz-option>
-                    </nz-select>
-                    <button
-                      nz-button
-                      nz-tooltip="Download Dataset"
-                      (click)="onClickDownloadVersionAsZip()"
-                      *ngIf="selectedVersion"
-                      [disabled]="!isLogin || !isDownloadAllowed()"
-                      class="spaced-button">
-                      <i
-                        nz-icon
-                        nzType="download"
-                        nzTheme="outline"></i>
-                    </button>
-                  </div>
-                  <ng-container *ngIf="selectedVersion">
-                    <div class="version-size">
-                      <i
-                        nz-icon
-                        nzType="database"
-                        nzTheme="outline"
-                        class="icon-database"></i>
-                      Version Size: {{ formatSize(currentDatasetVersionSize) }}
-                    </div>
-                    <div
-                      *ngIf="selectedVersionCreationTime"
-                      class="version-date">
-                      <i
-                        nz-icon
-                        nzType="calendar"
-                        nzTheme="outline"
-                        class="icon-database"></i>
-                      Created at: {{ selectedVersionCreationTime }}
-                    </div>
-                  </ng-container>
-                </div>
-                <texera-user-dataset-version-filetree
-                  [fileTreeNodes]="fileTreeNodeList"
-                  [isTreeNodeDeletable]="userHasWriteAccess()"
-                  [isCoverSettable]="userHasWriteAccess()"
-                  (selectedTreeNode)="onVersionFileTreeNodeSelected($event)"
-                  (deletedTreeNode)="onPreviouslyUploadedFileDeleted($event)"
-                  (setCoverImage)="onSetCoverImage($event)">
-                </texera-user-dataset-version-filetree>
-              </nz-collapse-panel>
-            </nz-collapse>
-
-            <nz-divider *ngIf="userDatasetAccessLevel === 
'WRITE'"></nz-divider>
-            <texera-version-uploader
-              *ngIf="userDatasetAccessLevel === 'WRITE'"
-              [resourceId]="did"
-              [ownerEmail]="ownerEmail"
-              [resourceName]="datasetName"
-              [endpoint]="datasetEndpoint"
-              [createVersion]="createDatasetVersion"
-              (versionCreated)="onVersionCreated()"
-              (uploadsInFlightChange)="uploadsInFlight = $event">
-            </texera-version-uploader>
-          </div>
-        </nz-sider>
-      </nz-layout>
-    </div>
+    <texera-versions-files-browser
+      [(isMaximized)]="isMaximized"
+      [(isRightBarCollapsed)]="isRightBarCollapsed"
+      [currentDisplayedFileName]="currentDisplayedFileName"
+      [currentFileSize]="currentFileSize"
+      [currentVersionSize]="currentDatasetVersionSize"
+      [selectedVersionCreationTime]="selectedVersionCreationTime"
+      [versions]="versions"
+      [selectedVersion]="selectedVersion"
+      (versionSelected)="onVersionSelected($event)"
+      [fileTreeNodeList]="fileTreeNodeList"
+      (fileTreeNodeSelected)="onVersionFileTreeNodeSelected($event)"
+      (fileDeleted)="onPreviouslyUploadedFileDeleted($event)"
+      (setCoverImage)="onSetCoverImage($event)"
+      [resourceId]="did"
+      [versionId]="selectedVersion?.dvid"
+      [ownerEmail]="ownerEmail"
+      [resourceName]="datasetName"
+      [endpoint]="datasetEndpoint"
+      [createVersion]="createDatasetVersion"
+      (versionCreated)="onVersionCreated()"
+      (uploadsInFlightChange)="uploadsInFlight = $event"
+      [isLogin]="isLogin"
+      [writeAccess]="userHasWriteAccess()"
+      [downloadAllowed]="isDownloadAllowed()"
+      emptyMessage="No version is selected"
+      downloadZipTooltip="Download Dataset"
+      (downloadCurrentFile)="onClickDownloadCurrentFile()"
+      (downloadVersionAsZip)="onClickDownloadVersionAsZip()">
+    </texera-versions-files-browser>
   </nz-tab>
   <nz-tab
     nzTitle="Settings"
diff --git 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
index 0aa5c93707..5074cfad32 100644
--- 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
@@ -24,7 +24,6 @@ import { NoopAnimationsModule } from 
"@angular/platform-browser/animations";
 import { ActivatedRoute, Router } from "@angular/router";
 import { concat, of, Subject, throwError } from "rxjs";
 import { NzModalService } from "ng-zorro-antd/modal";
-import { NzResizableDirective } from "ng-zorro-antd/resizable";
 import { NzTooltipDirective } from "ng-zorro-antd/tooltip";
 import { MarkdownService } from "ngx-markdown";
 import { DatasetDetailComponent } from "./dataset-detail.component";
@@ -43,7 +42,6 @@ import { commonTestImports, commonTestProviders } from 
"../../../../../common/te
 import { Contributor, Dataset, DatasetVersion } from 
"../../../../../common/type/dataset";
 import { DashboardDataset } from 
"../../../../type/dashboard-dataset.interface";
 import { HttpErrorResponse, HttpStatusCode } from "@angular/common/http";
-import { NzResizeEvent } from "ng-zorro-antd/resizable";
 import { format } from "date-fns";
 import { USER_DATASET } from "../../../../../app-routing.constant";
 
@@ -232,15 +230,6 @@ describe("DatasetDetailComponent rendered explorer", () => 
{
         expect((fixture.nativeElement as 
HTMLElement).querySelector(".copy-path-btn")).not.toBeNull();
       });
 
-      it("copies the path of the file being shown", () => {
-        const spy = vi.spyOn(component, 
"copyCurrentFilePath").mockResolvedValue(undefined);
-        const el = render(c => (c.currentDisplayedFileName = "a/b.csv"));
-
-        el.querySelector<HTMLElement>(".copy-path-btn")!.click();
-
-        expect(spy).toHaveBeenCalledTimes(1);
-      });
-
       it("shows the file size in human units, and nothing when it is unknown", 
() => {
         const el = render(c => {
           c.currentDisplayedFileName = "a/b.csv";
@@ -1107,20 +1096,6 @@ describe("DatasetDetailComponent behavior", () => {
     });
   });
 
-  describe("view flags", () => {
-    beforeEach(() => createComponent());
-
-    it("toggles the maximize and right-bar flags", () => {
-      expect(component.isMaximized).toBe(false);
-      component.onClickScaleTheView();
-      expect(component.isMaximized).toBe(true);
-
-      expect(component.isRightBarCollapsed).toBe(false);
-      component.onClickHideRightBar();
-      expect(component.isRightBarCollapsed).toBe(true);
-    });
-  });
-
   describe("toggleLike", () => {
     it("unlikes and decrements the like count when currently liked", () => {
       hubServiceStub.postUnlike.mockReturnValue(of(true));
@@ -1371,54 +1346,6 @@ describe("DatasetDetailComponent behavior", () => {
     });
   });
 
-  describe("copyCurrentFilePath", () => {
-    let originalClipboardDescriptor: PropertyDescriptor | undefined;
-    let writeText: ReturnType<typeof vi.fn>;
-
-    beforeEach(() => {
-      // Capture the original own-property descriptor (undefined if navigator 
has no own
-      // `clipboard`, e.g. under jsdom) so afterEach can restore the exact 
shape.
-      originalClipboardDescriptor = Object.getOwnPropertyDescriptor(navigator, 
"clipboard");
-      writeText = vi.fn().mockResolvedValue(undefined);
-      Object.defineProperty(navigator, "clipboard", { value: { writeText }, 
configurable: true });
-      createComponent();
-    });
-
-    afterEach(() => {
-      if (originalClipboardDescriptor) {
-        Object.defineProperty(navigator, "clipboard", 
originalClipboardDescriptor);
-      } else {
-        delete (navigator as any).clipboard;
-      }
-    });
-
-    it("writes the displayed path to the clipboard and toasts success", async 
() => {
-      component.currentDisplayedFileName = "/a/b/c.txt";
-
-      await component.copyCurrentFilePath();
-
-      expect(writeText).toHaveBeenCalledWith("/a/b/c.txt");
-      expect(notificationServiceStub.success).toHaveBeenCalledWith("File path 
copied to clipboard");
-    });
-
-    it("does nothing when no file is displayed", async () => {
-      component.currentDisplayedFileName = "";
-
-      await component.copyCurrentFilePath();
-
-      expect(writeText).not.toHaveBeenCalled();
-    });
-
-    it("toasts an error when the clipboard write rejects", async () => {
-      writeText.mockRejectedValue(new Error("denied"));
-      component.currentDisplayedFileName = "/a/b/c.txt";
-
-      await component.copyCurrentFilePath();
-
-      expect(notificationServiceStub.error).toHaveBeenCalledWith("Failed to 
copy file path");
-    });
-  });
-
   describe("version-node selection", () => {
     it("onVersionFileTreeNodeSelected loads the selected node's content", () 
=> {
       const node = { name: "file.csv", type: "file" } as unknown as Parameters<
@@ -1971,38 +1898,6 @@ describe("DatasetDetailComponent behavior", () => {
 
       expect(component.isMaximized).toBe(true);
     });
-
-    // ─── sider resize ───────────────────────────────────────────────────────
-
-    it("applies the dragged sider width on the next animation frame", async () 
=> {
-      renderWith({ did: 5 });
-
-      component.onSideResize({ width: 321 } as NzResizeEvent);
-      // the handler defers to requestAnimationFrame; let that frame run
-      await new Promise(resolve => requestAnimationFrame(() => resolve(null)));
-
-      expect(component.siderWidth).toBe(321);
-    });
-
-    it("cancels the frame the previous resize scheduled", () => {
-      renderWith({ did: 5 });
-      // Hand out a known frame id so the assertion below pins down *which* 
frame is
-      // cancelled: the component starts with id = -1, so merely asserting that
-      // cancelAnimationFrame was called would pass even if the id were never 
tracked.
-      const request = vi.spyOn(globalThis, 
"requestAnimationFrame").mockReturnValue(100);
-      const cancel = vi.spyOn(globalThis, "cancelAnimationFrame");
-      try {
-        component.onSideResize({ width: 100 } as NzResizeEvent);
-        cancel.mockClear(); // drop the initial cancel(-1)
-
-        component.onSideResize({ width: 200 } as NzResizeEvent);
-
-        expect(cancel).toHaveBeenCalledWith(100);
-      } finally {
-        cancel.mockRestore();
-        request.mockRestore();
-      }
-    });
   });
 });
 
@@ -2443,27 +2338,6 @@ describe("DatasetDetailComponent rendered template", () 
=> {
       expect(el.querySelector(".dataset-header")).not.toBeNull();
       expect(byTooltip("Minimize View")).toBeUndefined();
     });
-
-    it("applies a width the resize handle reports, between the bounds it 
declares", async () => {
-      const sider = fixture.debugElement.query(By.css("nz-sider"));
-      expect(sider.nativeElement.style.width).toBe("400px");
-
-      // The drag itself belongs to NzResizableDirective; what this component 
owns
-      // is the bounds it hands the directive and what it does with the 
reported
-      // width. Both have to be pinned, and in the right order β€” swapped bounds
-      // would let the handle collapse the sider past its minimum.
-      const resizable = sider.injector.get(NzResizableDirective);
-      expect(resizable.nzMinWidth).toBe(component.MIN_SIDER_WIDTH);
-      expect(resizable.nzMaxWidth).toBe(component.MAX_SIDER_WIDTH);
-      expect(resizable.nzMinWidth).toBeLessThan(resizable.nzMaxWidth as 
number);
-
-      sider.triggerEventHandler("nzResize", { width: 520 });
-      // The new width is applied on the next animation frame.
-      await new Promise(resolve => requestAnimationFrame(() => resolve(null)));
-      fixture.detectChanges();
-
-      expect(sider.nativeElement.style.width).toBe("520px");
-    });
   });
 
   describe("version picker", () => {
diff --git 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
index bb9521aa67..f9d8f70c16 100644
--- 
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
+++ 
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts
@@ -23,7 +23,6 @@ import { USER_DATASET } from 
"../../../../../app-routing.constant";
 import { extractErrorMessage } from "../../../../../common/util/error";
 import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
 import { DatasetService, validateDatasetName } from 
"../../../../service/user/dataset/dataset.service";
-import { NzResizeEvent, NzResizableDirective, NzResizeHandleComponent } from 
"ng-zorro-antd/resizable";
 import {
   DatasetFileNode,
   getFullPathFromDatasetFileNode,
@@ -58,18 +57,13 @@ import { NzPopconfirmDirective } from 
"ng-zorro-antd/popconfirm";
 import { NzSwitchComponent } from "ng-zorro-antd/switch";
 import { FormsModule } from "@angular/forms";
 import { MarkdownDescriptionComponent } from 
"../../markdown-description/markdown-description.component";
-import { NzLayoutComponent, NzContentComponent, NzSiderComponent } from 
"ng-zorro-antd/layout";
 import { NzWaveDirective } from "ng-zorro-antd/core/wave";
-import { NzEmptyComponent } from "ng-zorro-antd/empty";
 import { NzTabsComponent, NzTabComponent } from "ng-zorro-antd/tabs";
-import { UserDatasetFileRendererComponent } from 
"./user-dataset-file-renderer/user-dataset-file-renderer.component";
 import { NzCollapseComponent, NzCollapsePanelComponent } from 
"ng-zorro-antd/collapse";
-import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select";
-import { UserDatasetVersionFiletreeComponent } from 
"./user-dataset-version-filetree/user-dataset-version-filetree.component";
 import { NzDividerComponent } from "ng-zorro-antd/divider";
-import { VersionUploaderComponent } from 
"../../version-uploader/version-uploader.component";
 import { DATASET_FILE_RESOURCE_ENDPOINT } from 
"../../../../service/user/file-resource/file-resource-endpoint";
 import { NzInputDirective } from "ng-zorro-antd/input";
+import { VersionsFilesBrowserComponent } from 
"../../versions-files-browser/versions-files-browser.component";
 
 export const THROTTLE_TIME_MS = 1000;
 
@@ -92,29 +86,19 @@ export const THROTTLE_TIME_MS = 1000;
     NzSwitchComponent,
     FormsModule,
     MarkdownDescriptionComponent,
-    NzLayoutComponent,
-    NzContentComponent,
     NzWaveDirective,
-    NzEmptyComponent,
     NzTabsComponent,
     NzTabComponent,
-    UserDatasetFileRendererComponent,
-    NzSiderComponent,
-    NzResizableDirective,
-    NzResizeHandleComponent,
     NzCollapseComponent,
     NzCollapsePanelComponent,
-    NzSelectComponent,
-    NgFor,
-    NzOptionComponent,
-    UserDatasetVersionFiletreeComponent,
     NzDividerComponent,
-    VersionUploaderComponent,
+    NgFor,
     NzInputDirective,
     NzDropdownDirective,
     NzDropdownMenuComponent,
     NzMenuDirective,
     NzMenuItemComponent,
+    VersionsFilesBrowserComponent,
   ],
 })
 export class DatasetDetailComponent implements OnInit {
@@ -136,8 +120,10 @@ export class DatasetDetailComponent implements OnInit {
   public currentFileSize: number | undefined;
   public currentDatasetVersionSize: number | undefined;
 
-  public isRightBarCollapsed = false;
+  // The versions/files browser is presentational; its two toolbar flags live 
here (maximizing
+  // the file view also hides the page header) and are shared through two-way 
bindings.
   public isMaximized = false;
+  public isRightBarCollapsed = false;
 
   public versions: ReadonlyArray<DatasetVersion> = [];
   public selectedVersion: DatasetVersion | undefined;
@@ -162,7 +148,8 @@ export class DatasetDetailComponent implements OnInit {
 
   readonly datasetEndpoint = DATASET_FILE_RESOURCE_ENDPOINT;
 
-  @ViewChild(VersionUploaderComponent) private versionUploader?: 
VersionUploaderComponent;
+  @ViewChild(VersionsFilesBrowserComponent)
+  private versionsFilesBrowser?: VersionsFilesBrowserComponent<DatasetVersion>;
 
   constructor(
     private route: ActivatedRoute,
@@ -183,18 +170,6 @@ export class DatasetDetailComponent implements OnInit {
       });
   }
 
-  // item for control the resizeable sider
-  MAX_SIDER_WIDTH = 600;
-  MIN_SIDER_WIDTH = 150;
-  siderWidth = 400;
-  id = -1;
-  onSideResize({ width }: NzResizeEvent): void {
-    cancelAnimationFrame(this.id);
-    this.id = requestAnimationFrame(() => {
-      this.siderWidth = width!;
-    });
-  }
-
   ngOnInit(): void {
     this.route.params
       .pipe(
@@ -415,14 +390,6 @@ export class DatasetDetailComponent implements OnInit {
       .subscribe();
   };
 
-  onClickScaleTheView() {
-    this.isMaximized = !this.isMaximized;
-  }
-
-  onClickHideRightBar() {
-    this.isRightBarCollapsed = !this.isRightBarCollapsed;
-  }
-
   onVersionSelected(version: DatasetVersion | undefined): void {
     this.selectedVersion = version;
     if (this.did && version?.dvid)
@@ -483,7 +450,7 @@ export class DatasetDetailComponent implements OnInit {
             this.notificationService.success(
               `File ${node.name} is successfully deleted. You may finalize it 
or revert it at the "Create Version" panel`
             );
-            this.versionUploader?.notePathStaged(relativePath);
+            this.versionsFilesBrowser?.notePathStaged(relativePath);
           },
           error: (err: unknown) => {
             this.notificationService.error("Failed to delete the file");
@@ -644,19 +611,6 @@ export class DatasetDetailComponent implements OnInit {
       });
   }
 
-  async copyCurrentFilePath(): Promise<void> {
-    if (!this.currentDisplayedFileName) {
-      return;
-    }
-
-    try {
-      await navigator.clipboard.writeText(this.currentDisplayedFileName);
-      this.notificationService.success("File path copied to clipboard");
-    } catch (error) {
-      this.notificationService.error("Failed to copy file path");
-    }
-  }
-
   onAddContributor(): void {
     this.openContributorEditor("Add Contributor", null, newContributor => [
       ...this.datasetContributors,
diff --git 
a/frontend/src/app/dashboard/component/user/user-model/user-model-explorer/model-detail.component.html
 
b/frontend/src/app/dashboard/component/user/user-model/user-model-explorer/model-detail.component.html
index ca772155c4..cd40389898 100644
--- 
a/frontend/src/app/dashboard/component/user/user-model/user-model-explorer/model-detail.component.html
+++ 
b/frontend/src/app/dashboard/component/user/user-model/user-model-explorer/model-detail.component.html
@@ -159,217 +159,37 @@
     </div>
   </nz-tab>
   <nz-tab nzTitle="Versions &amp; Files">
-    <div class="versions-tab-content">
-      <nz-layout>
-        <nz-content style="background-color: white">
-          <nz-card>
-            <div style="display: flex; justify-content: space-between; 
align-items: center">
-              <div class="file-info">
-                <h3 class="file-title">
-                  <span class="file-title-main">
-                    <b>{{ currentDisplayedFileName }}</b>
-
-                    <button
-                      nz-button
-                      nzType="text"
-                      nzSize="small"
-                      class="copy-path-btn"
-                      nz-tooltip
-                      nzTooltipTitle="Copy file path"
-                      *ngIf="currentDisplayedFileName"
-                      (click)="copyCurrentFilePath()">
-                      <i
-                        nz-icon
-                        nzType="copy"
-                        nzTheme="twotone">
-                      </i>
-                    </button>
-                  </span>
-
-                  <span
-                    *ngIf="currentFileSize"
-                    class="file-size">
-                    <i
-                      nz-icon
-                      nzType="file"
-                      nzTheme="outline"
-                      class="icon-file"></i>
-                    {{ formatSize(currentFileSize) }}
-                  </span>
-                </h3>
-              </div>
-              <div style="display: flex">
-                <button
-                  nz-button
-                  *ngIf="selectedVersion"
-                  nz-tooltip="Download the file"
-                  [disabled]="!isLogin || !isDownloadAllowed()"
-                  (click)="onClickDownloadCurrentFile()">
-                  <i
-                    nz-icon
-                    nzTheme="outline"
-                    nzType="download">
-                  </i>
-                </button>
-                <button
-                  nz-button
-                  *ngIf="!isMaximized && selectedVersion"
-                  nz-tooltip="Maximize View"
-                  (click)="onClickScaleTheView()">
-                  <i
-                    nz-icon
-                    nzTheme="outline"
-                    nzType="expand">
-                  </i>
-                </button>
-                <button
-                  nz-button
-                  *ngIf="isMaximized && selectedVersion"
-                  nz-tooltip="Minimize View"
-                  (click)="onClickScaleTheView()">
-                  <i
-                    nz-icon
-                    nzTheme="outline"
-                    nzType="compress">
-                  </i>
-                </button>
-                <button
-                  *ngIf="!isRightBarCollapsed"
-                  nz-button
-                  nz-tooltip="Hide the right bar"
-                  (click)="onClickHideRightBar()">
-                  <i
-                    nz-icon
-                    nzTheme="outline"
-                    nzType="right">
-                  </i>
-                </button>
-                <button
-                  *ngIf="isRightBarCollapsed"
-                  nz-button
-                  nz-tooltip="Show Tree"
-                  (click)="onClickHideRightBar()">
-                  <i
-                    nz-icon
-                    nzTheme="outline"
-                    nzType="left">
-                  </i>
-                </button>
-              </div>
-            </div>
-          </nz-card>
-          <nz-empty
-            class="empty-version-indicator"
-            *ngIf="!selectedVersion"
-            nzNotFoundContent="This model has no version yet"></nz-empty>
-
-          <texera-user-dataset-file-renderer
-            *ngIf="selectedVersion"
-            [isMaximized]="isMaximized"
-            [resourceType]="modelEntityType"
-            [resourceId]="mid"
-            [versionId]="selectedVersion.mvid"
-            [filePath]="currentDisplayedFileName"
-            [fileSize]="currentFileSize"
-            [isLogin]="isLogin"
-            class="file-renderer">
-          </texera-user-dataset-file-renderer>
-        </nz-content>
-        <nz-sider
-          *ngIf="!isRightBarCollapsed"
-          nzTheme="light"
-          [nzWidth]="siderWidth"
-          nz-resizable
-          [nzMinWidth]="MIN_SIDER_WIDTH"
-          [nzMaxWidth]="MAX_SIDER_WIDTH"
-          (nzResize)="onSideResize($event)">
-          <nz-resize-handle nzDirection="left">
-            <div class="sider-resize-line">
-              <i
-                class="sider-resize-handle"
-                nz-icon
-                nzType="more"
-                nzTheme="outline"></i>
-            </div>
-          </nz-resize-handle>
-          <div class="right-sider">
-            <nz-collapse nzGhost>
-              <nz-collapse-panel
-                nzHeader="Current Versions"
-                nzActive="true">
-                <div class="version-storage">
-                  <h6 style="font-weight: lighter; font-size: 0.9em">Choose a 
Version:</h6>
-                  <div class="select-and-button-container">
-                    <nz-select
-                      nzShowSearch
-                      nzPlaceHolder="Select a version"
-                      (ngModelChange)="onVersionSelected($event)"
-                      [(ngModel)]="selectedVersion">
-                      <nz-option
-                        *ngFor="let version of versions"
-                        [nzValue]="version"
-                        [nzLabel]="version.name"></nz-option>
-                    </nz-select>
-                    <button
-                      nz-button
-                      nz-tooltip="Download this version as ZIP"
-                      (click)="onClickDownloadVersionAsZip()"
-                      *ngIf="selectedVersion"
-                      [disabled]="!isLogin || !isDownloadAllowed()"
-                      class="spaced-button">
-                      <i
-                        nz-icon
-                        nzType="download"
-                        nzTheme="outline"></i>
-                    </button>
-                  </div>
-                  <ng-container *ngIf="selectedVersion">
-                    <div class="version-size">
-                      <i
-                        nz-icon
-                        nzType="database"
-                        nzTheme="outline"
-                        class="icon-database"></i>
-                      Version Size: {{ formatSize(currentModelVersionSize) }}
-                    </div>
-                    <div
-                      *ngIf="selectedVersionCreationTime"
-                      class="version-date">
-                      <i
-                        nz-icon
-                        nzType="calendar"
-                        nzTheme="outline"
-                        class="icon-database"></i>
-                      Created at: {{ selectedVersionCreationTime }}
-                    </div>
-                  </ng-container>
-                </div>
-                <texera-user-dataset-version-filetree
-                  [fileTreeNodes]="fileTreeNodeList"
-                  [isTreeNodeDeletable]="userHasWriteAccess()"
-                  [isCoverSettable]="userHasWriteAccess()"
-                  (selectedTreeNode)="onVersionFileTreeNodeSelected($event)"
-                  (deletedTreeNode)="onPreviouslyUploadedFileDeleted($event)"
-                  (setCoverImage)="onSetCoverImage($event)">
-                </texera-user-dataset-version-filetree>
-              </nz-collapse-panel>
-            </nz-collapse>
-
-            <nz-divider *ngIf="userHasWriteAccess()"></nz-divider>
-            <texera-version-uploader
-              *ngIf="userHasWriteAccess()"
-              [resourceId]="mid"
-              [ownerEmail]="ownerEmail"
-              [resourceName]="modelName"
-              [endpoint]="modelEndpoint"
-              [createVersion]="createModelVersion"
-              (versionCreated)="onVersionCreated()"
-              (uploadsInFlightChange)="uploadsInFlight = $event">
-            </texera-version-uploader>
-          </div>
-        </nz-sider>
-      </nz-layout>
-    </div>
+    <texera-versions-files-browser
+      [(isMaximized)]="isMaximized"
+      [(isRightBarCollapsed)]="isRightBarCollapsed"
+      [currentDisplayedFileName]="currentDisplayedFileName"
+      [currentFileSize]="currentFileSize"
+      [currentVersionSize]="currentModelVersionSize"
+      [selectedVersionCreationTime]="selectedVersionCreationTime"
+      [versions]="versions"
+      [selectedVersion]="selectedVersion"
+      (versionSelected)="onVersionSelected($event)"
+      [fileTreeNodeList]="fileTreeNodeList"
+      (fileTreeNodeSelected)="onVersionFileTreeNodeSelected($event)"
+      (fileDeleted)="onPreviouslyUploadedFileDeleted($event)"
+      (setCoverImage)="onSetCoverImage($event)"
+      [resourceId]="mid"
+      [resourceType]="modelEntityType"
+      [versionId]="selectedVersion?.mvid"
+      [ownerEmail]="ownerEmail"
+      [resourceName]="modelName"
+      [endpoint]="modelEndpoint"
+      [createVersion]="createModelVersion"
+      (versionCreated)="onVersionCreated()"
+      (uploadsInFlightChange)="uploadsInFlight = $event"
+      [isLogin]="isLogin"
+      [writeAccess]="userHasWriteAccess()"
+      [downloadAllowed]="isDownloadAllowed()"
+      emptyMessage="This model has no version yet"
+      downloadZipTooltip="Download this version as ZIP"
+      (downloadCurrentFile)="onClickDownloadCurrentFile()"
+      (downloadVersionAsZip)="onClickDownloadVersionAsZip()">
+    </texera-versions-files-browser>
   </nz-tab>
   <nz-tab
     nzTitle="Settings"
diff --git 
a/frontend/src/app/dashboard/component/user/user-model/user-model-explorer/model-detail.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/user-model/user-model-explorer/model-detail.component.spec.ts
index bb9d0e2900..970bd352f3 100644
--- 
a/frontend/src/app/dashboard/component/user/user-model/user-model-explorer/model-detail.component.spec.ts
+++ 
b/frontend/src/app/dashboard/component/user/user-model/user-model-explorer/model-detail.component.spec.ts
@@ -24,7 +24,6 @@ import { of, Subject, throwError } from "rxjs";
 import { MarkdownService } from "ngx-markdown";
 import { NzModalService } from "ng-zorro-antd/modal";
 import { NgModel } from "@angular/forms";
-import { NzResizableDirective } from "ng-zorro-antd/resizable";
 import { By } from "@angular/platform-browser";
 import { commonTestImports, commonTestProviders } from 
"../../../../../common/testing/test-utils";
 import { NotificationService } from 
"../../../../../common/service/notification/notification.service";
@@ -195,9 +194,6 @@ describe("ModelDetailComponent", () => {
     return el as unknown as E;
   };
 
-  // The sider coalesces resize events into one write per animation frame.
-  const nextFrame = (): Promise<void> => new Promise<void>(resolve => 
requestAnimationFrame(() => resolve()));
-
   // ─── loading the model 
──────────────────────────────────────────────────────
 
   it("reads the mid off the route as a number and loads the model", () => {
@@ -459,29 +455,6 @@ describe("ModelDetailComponent", () => {
     expect(component.isDownloadAllowed()).toBe(true);
   });
 
-  // ─── the file path clipboard 
────────────────────────────────────────────────
-
-  it("copies the open file's path and reports failure", async () => {
-    create();
-    const writeText = vi.fn(() => Promise.resolve());
-    Object.defineProperty(navigator, "clipboard", { value: { writeText }, 
configurable: true });
-
-    render({ currentDisplayedFileName: "/model/a/b/v1/model.pt" });
-    await component.copyCurrentFilePath();
-    expect(writeText).toHaveBeenCalledWith("/model/a/b/v1/model.pt");
-    expect(notificationService["success"]).toHaveBeenCalled();
-
-    writeText.mockImplementationOnce(() => Promise.reject(new 
Error("denied")));
-    await component.copyCurrentFilePath();
-    expect(notificationService["error"]).toHaveBeenCalled();
-
-    // Nothing open: no clipboard call at all.
-    writeText.mockClear();
-    render({ currentDisplayedFileName: "" });
-    await component.copyCurrentFilePath();
-    expect(writeText).not.toHaveBeenCalled();
-  });
-
   // ─── template 
───────────────────────────────────────────────────────────────
 
   it("renders the model's name, tags and stats", () => {
@@ -1021,13 +994,13 @@ describe("ModelDetailComponent", () => {
     const root = openTab("Versions & Files");
 
     expect(root.querySelector("nz-sider")).not.toBeNull();
-    component.onClickHideRightBar();
+    // The browser owns the toolbar controls; the page keeps the two flags and 
feeds them back in.
+    component.isRightBarCollapsed = true;
     fixture.detectChanges();
     expect(fixture.nativeElement.querySelector("nz-sider")).toBeNull();
 
-    component.onClickScaleTheView();
+    component.isMaximized = true;
     fixture.detectChanges();
-    expect(component.isMaximized).toBe(true);
     expect(fixture.nativeElement.querySelector(".model-header")).toBeNull();
   });
 
@@ -1054,23 +1027,6 @@ describe("ModelDetailComponent", () => {
     expect(component.currentUid).toBe(42);
   });
 
-  // ─── the resizable sider 
────────────────────────────────────────────────────
-
-  it("stores the dragged sider width on the next animation frame", async () => 
{
-    create();
-    expect(component.siderWidth).toBe(400);
-
-    // A drag emits continuously; the component coalesces to one write per 
frame.
-    component.onSideResize({ width: 250, height: 999 });
-
-    // The write is deferred, not immediate β€” that deferral is the whole point 
of the
-    // requestAnimationFrame hop, and a synchronous assignment would land here.
-    expect(component.siderWidth).toBe(400);
-    await nextFrame();
-
-    expect(component.siderWidth).toBe(250);
-  });
-
   // ─── guards against an absent model id 
──────────────────────────────────────
 
   it("fetches nothing without a model id", () => {
@@ -1369,19 +1325,6 @@ describe("ModelDetailComponent", () => {
     expect(q<HTMLButtonElement>(fixture.nativeElement, 
".spaced-button").disabled).toBe(true);
   });
 
-  it("wires the sider's resize handle to the width the page keeps", async () 
=> {
-    oneVersionWithOneFile();
-    create();
-    openTab("Versions & Files");
-
-    const sider = 
fixture.debugElement.query(By.directive(NzResizableDirective));
-    expect(sider).not.toBeNull();
-    sider.injector.get(NzResizableDirective).nzResize.emit({ width: 275, 
height: 0 });
-    await nextFrame();
-
-    expect(component.siderWidth).toBe(275);
-  });
-
   it("wires the version picker to both the selection and the file tree", () => 
{
     const versions = [aVersion(2, "v2"), aVersion(1, "v1")];
     modelService["retrieveModelVersionList"] = vi.fn(() => of(versions));
diff --git 
a/frontend/src/app/dashboard/component/user/user-model/user-model-explorer/model-detail.component.ts
 
b/frontend/src/app/dashboard/component/user/user-model/user-model-explorer/model-detail.component.ts
index 37bab19df4..2b11761d93 100644
--- 
a/frontend/src/app/dashboard/component/user/user-model/user-model-explorer/model-detail.component.ts
+++ 
b/frontend/src/app/dashboard/component/user/user-model/user-model-explorer/model-detail.component.ts
@@ -25,7 +25,6 @@ import { Observable, of } from "rxjs";
 import { format } from "date-fns";
 import { NgIf, NgClass, NgFor } from "@angular/common";
 import { FormsModule } from "@angular/forms";
-import { NzResizeEvent, NzResizableDirective, NzResizeHandleComponent } from 
"ng-zorro-antd/resizable";
 import { NzCardComponent, NzCardMetaComponent } from "ng-zorro-antd/card";
 import { NzTooltipDirective } from "ng-zorro-antd/tooltip";
 import { NzTagComponent } from "ng-zorro-antd/tag";
@@ -33,9 +32,6 @@ import { Ι΅NzTransitionPatchDirective } from 
"ng-zorro-antd/core/transition-patc
 import { NzIconDirective } from "ng-zorro-antd/icon";
 import { NzButtonComponent } from "ng-zorro-antd/button";
 import { NzWaveDirective } from "ng-zorro-antd/core/wave";
-import { NzLayoutComponent, NzContentComponent, NzSiderComponent } from 
"ng-zorro-antd/layout";
-import { NzEmptyComponent } from "ng-zorro-antd/empty";
-import { NzCollapseComponent, NzCollapsePanelComponent } from 
"ng-zorro-antd/collapse";
 import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select";
 import { NzTabsComponent, NzTabComponent } from "ng-zorro-antd/tabs";
 import { NzDividerComponent } from "ng-zorro-antd/divider";
@@ -64,9 +60,7 @@ import {
   getRelativePathFromDatasetFileNode,
 } from "../../../../../common/type/datasetVersionFileTree";
 import { MarkdownDescriptionComponent } from 
"../../markdown-description/markdown-description.component";
-import { VersionUploaderComponent } from 
"../../version-uploader/version-uploader.component";
-import { UserDatasetFileRendererComponent } from 
"../../user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component";
-import { UserDatasetVersionFiletreeComponent } from 
"../../user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component";
+import { VersionsFilesBrowserComponent } from 
"../../versions-files-browser/versions-files-browser.component";
 
 @UntilDestroy()
 @Component({
@@ -85,14 +79,6 @@ import { UserDatasetVersionFiletreeComponent } from 
"../../user-dataset/user-dat
     NzIconDirective,
     NzButtonComponent,
     NzWaveDirective,
-    NzLayoutComponent,
-    NzContentComponent,
-    NzSiderComponent,
-    NzResizableDirective,
-    NzResizeHandleComponent,
-    NzEmptyComponent,
-    NzCollapseComponent,
-    NzCollapsePanelComponent,
     NzSelectComponent,
     NzOptionComponent,
     NzTabsComponent,
@@ -101,9 +87,7 @@ import { UserDatasetVersionFiletreeComponent } from 
"../../user-dataset/user-dat
     NzInputDirective,
     NzSwitchComponent,
     MarkdownDescriptionComponent,
-    VersionUploaderComponent,
-    UserDatasetFileRendererComponent,
-    UserDatasetVersionFiletreeComponent,
+    VersionsFilesBrowserComponent,
   ],
 })
 export class ModelDetailComponent implements OnInit {
@@ -142,8 +126,10 @@ export class ModelDetailComponent implements OnInit {
   public likeCount: number = 0;
   public isLiked: boolean = false;
 
-  public isRightBarCollapsed = false;
+  // The versions/files browser is presentational; its two toolbar flags live 
here (maximizing
+  // the file view also hides the page header) and are shared through two-way 
bindings.
   public isMaximized = false;
+  public isRightBarCollapsed = false;
 
   public isLogin: boolean = this.userService.isLogin();
   public currentUid: number | undefined = 
this.userService.getCurrentUser()?.uid;
@@ -153,7 +139,7 @@ export class ModelDetailComponent implements OnInit {
   public readonly frameworks = MODEL_FRAMEWORKS;
   public readonly formats = MODEL_FORMATS;
 
-  @ViewChild(VersionUploaderComponent) private versionUploader?: 
VersionUploaderComponent;
+  @ViewChild(VersionsFilesBrowserComponent) private versionsFilesBrowser?: 
VersionsFilesBrowserComponent<ModelVersion>;
 
   // Renaming mid-upload strands the in-flight parts under the old name, so 
the Settings tab
   // blocks it until the panel is idle.
@@ -180,19 +166,6 @@ export class ModelDetailComponent implements OnInit {
       });
   }
 
-  // Resizable sider holding the version picker and the file tree.
-  MAX_SIDER_WIDTH = 600;
-  MIN_SIDER_WIDTH = 150;
-  siderWidth = 400;
-  id = -1;
-
-  onSideResize({ width }: NzResizeEvent): void {
-    cancelAnimationFrame(this.id);
-    this.id = requestAnimationFrame(() => {
-      this.siderWidth = width!;
-    });
-  }
-
   ngOnInit(): void {
     this.route.params
       .pipe(
@@ -448,26 +421,6 @@ export class ModelDetailComponent implements OnInit {
       .subscribe();
   }
 
-  async copyCurrentFilePath(): Promise<void> {
-    if (!this.currentDisplayedFileName) {
-      return;
-    }
-    try {
-      await navigator.clipboard.writeText(this.currentDisplayedFileName);
-      this.notificationService.success("File path copied to clipboard");
-    } catch {
-      this.notificationService.error("Failed to copy file path");
-    }
-  }
-
-  onClickScaleTheView(): void {
-    this.isMaximized = !this.isMaximized;
-  }
-
-  onClickHideRightBar(): void {
-    this.isRightBarCollapsed = !this.isRightBarCollapsed;
-  }
-
   isDownloadAllowed(): boolean {
     if (this.isOwner) {
       return true;
@@ -494,7 +447,7 @@ export class ModelDetailComponent implements OnInit {
           );
           // Undefined only when the panel is not rendered, which is the same 
write-access
           // condition that gates the tree's delete control.
-          this.versionUploader?.notePathStaged(relativePath);
+          this.versionsFilesBrowser?.notePathStaged(relativePath);
         },
         error: () => this.notificationService.error("Failed to delete the 
file"),
       });
diff --git 
a/frontend/src/app/dashboard/component/user/versions-files-browser/versions-files-browser.component.html
 
b/frontend/src/app/dashboard/component/user/versions-files-browser/versions-files-browser.component.html
new file mode 100644
index 0000000000..9926d0b394
--- /dev/null
+++ 
b/frontend/src/app/dashboard/component/user/versions-files-browser/versions-files-browser.component.html
@@ -0,0 +1,231 @@
+<!--
+  ~ 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.
+  -->
+
+<div class="versions-tab-content">
+  <nz-layout>
+    <nz-content style="background-color: white">
+      <nz-card>
+        <div style="display: flex; justify-content: space-between; 
align-items: center">
+          <div class="file-info">
+            <h3 class="file-title">
+              <span class="file-title-main">
+                <b>{{ currentDisplayedFileName }}</b>
+
+                <button
+                  nz-button
+                  nzType="text"
+                  nzSize="small"
+                  class="copy-path-btn"
+                  nz-tooltip
+                  nzTooltipTitle="Copy file path"
+                  *ngIf="currentDisplayedFileName"
+                  (click)="copyCurrentFilePath()">
+                  <i
+                    nz-icon
+                    nzType="copy"
+                    nzTheme="twotone">
+                  </i>
+                </button>
+              </span>
+
+              <span
+                *ngIf="currentFileSize"
+                class="file-size">
+                <i
+                  nz-icon
+                  nzType="file"
+                  nzTheme="outline"
+                  class="icon-file"></i>
+                {{ formatSize(currentFileSize) }}
+              </span>
+            </h3>
+          </div>
+          <div style="display: flex">
+            <button
+              nz-button
+              *ngIf="selectedVersion"
+              nz-tooltip="Download the file"
+              [disabled]="!isLogin || !downloadAllowed"
+              (click)="downloadCurrentFile.emit()">
+              <i
+                nz-icon
+                nzTheme="outline"
+                nzType="download">
+              </i>
+            </button>
+            <button
+              nz-button
+              *ngIf="!isMaximized && selectedVersion"
+              nz-tooltip="Maximize View"
+              (click)="onClickScaleTheView()">
+              <i
+                nz-icon
+                nzTheme="outline"
+                nzType="expand">
+              </i>
+            </button>
+            <button
+              nz-button
+              *ngIf="isMaximized && selectedVersion"
+              nz-tooltip="Minimize View"
+              (click)="onClickScaleTheView()">
+              <i
+                nz-icon
+                nzTheme="outline"
+                nzType="compress">
+              </i>
+            </button>
+            <button
+              *ngIf="!isRightBarCollapsed"
+              nz-button
+              nz-tooltip="Hide the right bar"
+              (click)="onClickHideRightBar()">
+              <i
+                nz-icon
+                nzTheme="outline"
+                nzType="right">
+              </i>
+            </button>
+            <button
+              *ngIf="isRightBarCollapsed"
+              nz-button
+              nz-tooltip="Show Tree"
+              (click)="onClickHideRightBar()">
+              <i
+                nz-icon
+                nzTheme="outline"
+                nzType="left">
+              </i>
+            </button>
+          </div>
+        </div>
+      </nz-card>
+      <nz-empty
+        class="empty-version-indicator"
+        *ngIf="!selectedVersion"
+        [nzNotFoundContent]="emptyMessage"></nz-empty>
+
+      <texera-user-dataset-file-renderer
+        *ngIf="selectedVersion"
+        [isMaximized]="isMaximized"
+        [resourceType]="resourceType"
+        [resourceId]="resourceId"
+        [versionId]="versionId"
+        [filePath]="currentDisplayedFileName"
+        [fileSize]="currentFileSize"
+        [isLogin]="isLogin"
+        class="file-renderer">
+      </texera-user-dataset-file-renderer>
+    </nz-content>
+    <nz-sider
+      *ngIf="!isRightBarCollapsed"
+      nzTheme="light"
+      [nzWidth]="siderWidth"
+      nz-resizable
+      [nzMinWidth]="MIN_SIDER_WIDTH"
+      [nzMaxWidth]="MAX_SIDER_WIDTH"
+      (nzResize)="onSideResize($event)">
+      <nz-resize-handle nzDirection="left">
+        <div class="sider-resize-line">
+          <i
+            class="sider-resize-handle"
+            nz-icon
+            nzType="more"
+            nzTheme="outline"></i>
+        </div>
+      </nz-resize-handle>
+      <div class="right-sider">
+        <nz-collapse nzGhost>
+          <nz-collapse-panel
+            nzHeader="Current Versions"
+            nzActive="true">
+            <div class="version-storage">
+              <h6 style="font-weight: lighter; font-size: 0.9em">Choose a 
Version:</h6>
+              <div class="select-and-button-container">
+                <nz-select
+                  nzShowSearch
+                  nzPlaceHolder="Select a version"
+                  [ngModel]="selectedVersion"
+                  (ngModelChange)="versionSelected.emit($event)">
+                  <nz-option
+                    *ngFor="let version of versions"
+                    [nzValue]="version"
+                    [nzLabel]="version.name"></nz-option>
+                </nz-select>
+                <button
+                  nz-button
+                  [nz-tooltip]="downloadZipTooltip"
+                  [attr.nz-tooltip]="downloadZipTooltip"
+                  (click)="downloadVersionAsZip.emit()"
+                  *ngIf="selectedVersion"
+                  [disabled]="!isLogin || !downloadAllowed"
+                  class="spaced-button">
+                  <i
+                    nz-icon
+                    nzType="download"
+                    nzTheme="outline"></i>
+                </button>
+              </div>
+              <ng-container *ngIf="selectedVersion">
+                <div class="version-size">
+                  <i
+                    nz-icon
+                    nzType="database"
+                    nzTheme="outline"
+                    class="icon-database"></i>
+                  Version Size: {{ formatSize(currentVersionSize) }}
+                </div>
+                <div
+                  *ngIf="selectedVersionCreationTime"
+                  class="version-date">
+                  <i
+                    nz-icon
+                    nzType="calendar"
+                    nzTheme="outline"
+                    class="icon-database"></i>
+                  Created at: {{ selectedVersionCreationTime }}
+                </div>
+              </ng-container>
+            </div>
+            <texera-user-dataset-version-filetree
+              [fileTreeNodes]="fileTreeNodeList"
+              [isTreeNodeDeletable]="writeAccess"
+              [isCoverSettable]="writeAccess"
+              (selectedTreeNode)="fileTreeNodeSelected.emit($event)"
+              (deletedTreeNode)="fileDeleted.emit($event)"
+              (setCoverImage)="setCoverImage.emit($event)">
+            </texera-user-dataset-version-filetree>
+          </nz-collapse-panel>
+        </nz-collapse>
+
+        <nz-divider *ngIf="writeAccess"></nz-divider>
+        <texera-version-uploader
+          *ngIf="writeAccess"
+          [resourceId]="resourceId"
+          [ownerEmail]="ownerEmail"
+          [resourceName]="resourceName"
+          [endpoint]="endpoint"
+          [createVersion]="createVersion"
+          (versionCreated)="versionCreated.emit()"
+          (uploadsInFlightChange)="uploadsInFlightChange.emit($event)">
+        </texera-version-uploader>
+      </div>
+    </nz-sider>
+  </nz-layout>
+</div>
diff --git 
a/frontend/src/app/dashboard/component/user/versions-files-browser/versions-files-browser.component.scss
 
b/frontend/src/app/dashboard/component/user/versions-files-browser/versions-files-browser.component.scss
new file mode 100644
index 0000000000..b1e2c96b26
--- /dev/null
+++ 
b/frontend/src/app/dashboard/component/user/versions-files-browser/versions-files-browser.component.scss
@@ -0,0 +1,136 @@
+/**
+ * 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.
+ */
+
+.versions-tab-content {
+  height: 100%;
+  overflow-y: auto;
+  padding-bottom: 24px;
+}
+
+nz-layout {
+  height: 100%;
+}
+
+.version-storage {
+  padding: 0 15px;
+  margin-bottom: 25px;
+}
+
+.version-storage nz-select {
+  width: 100%;
+  margin: 0;
+}
+
+nz-select {
+  flex-grow: 1;
+}
+
+.right-sider {
+  height: 100%;
+  overflow-y: auto;
+  position: relative;
+  z-index: 0;
+}
+
+.sider-resize-line {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  width: 5px;
+  border-right: 1px solid #e8e8e8;
+}
+
+.sider-resize-handle {
+  background: #fff;
+  border: 1px solid #ddd;
+  text-align: center;
+  font-size: 12px;
+  height: 20px;
+  line-height: 20px;
+}
+
+.file-renderer {
+  width: 95%;
+  height: 80%;
+  margin: auto;
+}
+
+.select-and-button-container {
+  display: flex;
+  align-items: stretch;
+  gap: 10px;
+}
+
+.spaced-button {
+  flex-shrink: 0;
+  width: 30px;
+  height: 24px;
+}
+
+.file-title {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-bottom: 0;
+}
+
+.file-title-main {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  min-width: 0;
+}
+
+.copy-path-btn {
+  padding: 0 4px;
+  height: auto;
+  line-height: 1;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.file-size,
+.version-size,
+.version-date {
+  font-size: 12px;
+  color: #8c8c8c;
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.version-date {
+  display: flex;
+}
+
+.version-size {
+  margin-top: 8px;
+}
+
+.icon-database,
+.icon-file {
+  font-size: 14px;
+}
+
+.empty-version-indicator {
+  margin-top: 15%;
+}
diff --git 
a/frontend/src/app/dashboard/component/user/versions-files-browser/versions-files-browser.component.spec.ts
 
b/frontend/src/app/dashboard/component/user/versions-files-browser/versions-files-browser.component.spec.ts
new file mode 100644
index 0000000000..7b558dede6
--- /dev/null
+++ 
b/frontend/src/app/dashboard/component/user/versions-files-browser/versions-files-browser.component.spec.ts
@@ -0,0 +1,182 @@
+/**
+ * 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 { ComponentFixture, TestBed } from "@angular/core/testing";
+import { By } from "@angular/platform-browser";
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { NzResizeEvent } from "ng-zorro-antd/resizable";
+import { NzSelectComponent } from "ng-zorro-antd/select";
+import { VersionsFilesBrowserComponent } from 
"./versions-files-browser.component";
+import { NotificationService } from 
"../../../../common/service/notification/notification.service";
+import { commonTestImports, commonTestProviders } from 
"../../../../common/testing/test-utils";
+
+describe("VersionsFilesBrowserComponent", () => {
+  let fixture: ComponentFixture<VersionsFilesBrowserComponent>;
+  let component: VersionsFilesBrowserComponent;
+  let notification: {
+    success: ReturnType<typeof vi.fn>;
+    error: ReturnType<typeof vi.fn>;
+    info: ReturnType<typeof vi.fn>;
+  };
+
+  beforeEach(() => {
+    notification = { success: vi.fn(), error: vi.fn(), info: vi.fn() };
+    TestBed.configureTestingModule({
+      imports: [VersionsFilesBrowserComponent, NoopAnimationsModule, 
...commonTestImports],
+      providers: [{ provide: NotificationService, useValue: notification }, 
...commonTestProviders],
+    });
+    fixture = TestBed.createComponent(VersionsFilesBrowserComponent);
+    component = fixture.componentInstance;
+  });
+
+  it("starts with the default sider bounds and width", () => {
+    expect(component.siderWidth).toBe(400);
+    expect(component.MIN_SIDER_WIDTH).toBe(150);
+    expect(component.MAX_SIDER_WIDTH).toBe(600);
+    expect(component.MIN_SIDER_WIDTH).toBeLessThan(component.MAX_SIDER_WIDTH);
+  });
+
+  // The parent owns both toolbar flags: the browser reports a toggle and 
never mutates its input.
+  it("reports a maximize toggle instead of owning the flag", () => {
+    const emitted: boolean[] = [];
+    component.isMaximizedChange.subscribe(v => emitted.push(v));
+
+    component.isMaximized = false;
+    component.onClickScaleTheView();
+    component.isMaximized = true;
+    component.onClickScaleTheView();
+
+    expect(emitted).toEqual([true, false]);
+    expect(component.isMaximized).toBe(true); // input left untouched
+  });
+
+  it("reports a right-bar toggle instead of owning the flag", () => {
+    const emitted: boolean[] = [];
+    component.isRightBarCollapsedChange.subscribe(v => emitted.push(v));
+
+    component.isRightBarCollapsed = false;
+    component.onClickHideRightBar();
+
+    expect(emitted).toEqual([true]);
+    expect(component.isRightBarCollapsed).toBe(false);
+  });
+
+  it("applies the dragged sider width on the next animation frame", async () 
=> {
+    component.onSideResize({ width: 321 } as NzResizeEvent);
+    // the handler defers to requestAnimationFrame; a synchronous write would 
land here
+    expect(component.siderWidth).toBe(400);
+    await new Promise(resolve => requestAnimationFrame(() => resolve(null)));
+
+    expect(component.siderWidth).toBe(321);
+  });
+
+  it("cancels the frame the previous resize scheduled", () => {
+    const request = vi.spyOn(globalThis, 
"requestAnimationFrame").mockReturnValue(100);
+    const cancel = vi.spyOn(globalThis, "cancelAnimationFrame");
+    try {
+      component.onSideResize({ width: 100 } as NzResizeEvent);
+      cancel.mockClear(); // drop the initial cancel of the sentinel id
+
+      component.onSideResize({ width: 200 } as NzResizeEvent);
+
+      expect(cancel).toHaveBeenCalledWith(100);
+    } finally {
+      cancel.mockRestore();
+      request.mockRestore();
+    }
+  });
+
+  describe("copyCurrentFilePath", () => {
+    let originalClipboardDescriptor: PropertyDescriptor | undefined;
+    let writeText: ReturnType<typeof vi.fn>;
+
+    beforeEach(() => {
+      originalClipboardDescriptor = Object.getOwnPropertyDescriptor(navigator, 
"clipboard");
+      writeText = vi.fn().mockResolvedValue(undefined);
+      Object.defineProperty(navigator, "clipboard", { value: { writeText }, 
configurable: true });
+    });
+
+    afterEach(() => {
+      if (originalClipboardDescriptor) {
+        Object.defineProperty(navigator, "clipboard", 
originalClipboardDescriptor);
+      } else {
+        delete (navigator as any).clipboard;
+      }
+    });
+
+    it("writes the displayed path to the clipboard and toasts success", async 
() => {
+      component.currentDisplayedFileName = "/a/b/c.txt";
+
+      await component.copyCurrentFilePath();
+
+      expect(writeText).toHaveBeenCalledWith("/a/b/c.txt");
+      expect(notification.success).toHaveBeenCalledWith("File path copied to 
clipboard");
+    });
+
+    it("does nothing when no file is displayed", async () => {
+      component.currentDisplayedFileName = "";
+
+      await component.copyCurrentFilePath();
+
+      expect(writeText).not.toHaveBeenCalled();
+    });
+
+    it("toasts an error when the clipboard write rejects", async () => {
+      writeText.mockRejectedValue(new Error("denied"));
+      component.currentDisplayedFileName = "/a/b/c.txt";
+
+      await component.copyCurrentFilePath();
+
+      expect(notification.error).toHaveBeenCalledWith("Failed to copy file 
path");
+    });
+  });
+
+  it("delegates notePathStaged to the embedded uploader", () => {
+    const uploader = { notePathStaged: vi.fn() };
+    (component as any).versionUploader = uploader;
+
+    component.notePathStaged("owner/ds/v1/a.txt");
+
+    expect(uploader.notePathStaged).toHaveBeenCalledWith("owner/ds/v1/a.txt");
+  });
+
+  it("emits the version chosen in the picker", () => {
+    const v1 = { name: "v1" };
+    const v2 = { name: "v2" };
+    const picked: Array<{ name: string } | undefined> = [];
+    component.versions = [v1, v2];
+    component.versionSelected.subscribe(v => picked.push(v));
+    fixture.detectChanges();
+
+    const select = fixture.debugElement.query(By.directive(NzSelectComponent));
+    select.triggerEventHandler("ngModelChange", v2);
+
+    expect(picked).toEqual([v2]);
+  });
+
+  it("shows the empty-state message until a version is selected", () => {
+    component.emptyMessage = "No version is selected";
+    component.selectedVersion = undefined;
+    fixture.detectChanges();
+
+    const empty = (fixture.nativeElement as 
HTMLElement).querySelector(".empty-version-indicator");
+    expect(empty).not.toBeNull();
+    expect((fixture.nativeElement as HTMLElement).textContent ?? 
"").toContain("No version is selected");
+  });
+});
diff --git 
a/frontend/src/app/dashboard/component/user/versions-files-browser/versions-files-browser.component.ts
 
b/frontend/src/app/dashboard/component/user/versions-files-browser/versions-files-browser.component.ts
new file mode 100644
index 0000000000..7acc71532e
--- /dev/null
+++ 
b/frontend/src/app/dashboard/component/user/versions-files-browser/versions-files-browser.component.ts
@@ -0,0 +1,177 @@
+/**
+ * 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, EventEmitter, Input, Output, ViewChild } from 
"@angular/core";
+import { Observable } from "rxjs";
+import { NgIf, NgFor } from "@angular/common";
+import { FormsModule } from "@angular/forms";
+import { NzResizeEvent, NzResizableDirective, NzResizeHandleComponent } from 
"ng-zorro-antd/resizable";
+import { NzCardComponent } from "ng-zorro-antd/card";
+import { NzTooltipDirective } from "ng-zorro-antd/tooltip";
+import { NzIconDirective } from "ng-zorro-antd/icon";
+import { NzButtonComponent } from "ng-zorro-antd/button";
+import { NzLayoutComponent, NzContentComponent, NzSiderComponent } from 
"ng-zorro-antd/layout";
+import { NzEmptyComponent } from "ng-zorro-antd/empty";
+import { NzCollapseComponent, NzCollapsePanelComponent } from 
"ng-zorro-antd/collapse";
+import { NzSelectComponent, NzOptionComponent } from "ng-zorro-antd/select";
+import { NzDividerComponent } from "ng-zorro-antd/divider";
+import { formatSize } from "src/app/common/util/size-formatter.util";
+import { NotificationService } from 
"../../../../common/service/notification/notification.service";
+import { EntityType } from "../../../../hub/service/hub.service";
+import { DatasetFileNode } from 
"../../../../common/type/datasetVersionFileTree";
+import {
+  FileResourceEndpoint,
+  DATASET_FILE_RESOURCE_ENDPOINT,
+} from "../../../service/user/file-resource/file-resource-endpoint";
+import { UserDatasetFileRendererComponent } from 
"../user-dataset/user-dataset-explorer/user-dataset-file-renderer/user-dataset-file-renderer.component";
+import { UserDatasetVersionFiletreeComponent } from 
"../user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component";
+import { VersionUploaderComponent } from 
"../version-uploader/version-uploader.component";
+
+/** The only field the version picker needs from a dataset/model version. */
+export interface VersionListEntry {
+  readonly name: string;
+}
+
+/**
+ * Presentational "Versions & Files" tab shared by the dataset and model 
detail pages: a file
+ * header/toolbar, the file renderer, and a resizable sider holding the 
version picker, file tree,
+ * and version uploader. It owns only its local toolbar view state; every 
server call and the
+ * version-selection flow stay in the parent, reached through the inputs and 
outputs below.
+ */
+@Component({
+  selector: "texera-versions-files-browser",
+  templateUrl: "./versions-files-browser.component.html",
+  styleUrls: ["./versions-files-browser.component.scss"],
+  imports: [
+    NgIf,
+    NgFor,
+    FormsModule,
+    NzCardComponent,
+    NzTooltipDirective,
+    NzIconDirective,
+    NzButtonComponent,
+    NzLayoutComponent,
+    NzContentComponent,
+    NzSiderComponent,
+    NzResizableDirective,
+    NzResizeHandleComponent,
+    NzEmptyComponent,
+    NzCollapseComponent,
+    NzCollapsePanelComponent,
+    NzSelectComponent,
+    NzOptionComponent,
+    NzDividerComponent,
+    UserDatasetFileRendererComponent,
+    UserDatasetVersionFiletreeComponent,
+    VersionUploaderComponent,
+  ],
+})
+export class VersionsFilesBrowserComponent<V extends VersionListEntry = 
VersionListEntry> {
+  // File currently shown in the renderer (resolved by the parent).
+  @Input() currentDisplayedFileName: string = "";
+  @Input() currentFileSize: number | undefined;
+  @Input() currentVersionSize: number | undefined;
+  @Input() selectedVersionCreationTime: string = "";
+
+  // Version picker.
+  @Input() versions: ReadonlyArray<V> = [];
+  @Input() selectedVersion: V | undefined;
+  @Output() versionSelected = new EventEmitter<V | undefined>();
+
+  // File tree.
+  @Input() fileTreeNodeList: DatasetFileNode[] = [];
+  @Output() fileTreeNodeSelected = new EventEmitter<DatasetFileNode>();
+  @Output() fileDeleted = new EventEmitter<DatasetFileNode>();
+  @Output() setCoverImage = new EventEmitter<string>();
+
+  // File renderer. resourceType defaults to Dataset to match the renderer's 
own default.
+  @Input() resourceId: number | undefined;
+  @Input() resourceType: EntityType = EntityType.Dataset;
+  @Input() versionId: number | undefined;
+
+  // Version uploader.
+  @Input() ownerEmail: string = "";
+  @Input() resourceName: string = "";
+  @Input() endpoint: FileResourceEndpoint = DATASET_FILE_RESOURCE_ENDPOINT;
+  @Input() createVersion!: (versionName: string) => Observable<unknown>;
+  @Output() versionCreated = new EventEmitter<void>();
+  @Output() uploadsInFlightChange = new EventEmitter<boolean>();
+
+  // Flags and per-resource labels.
+  @Input() isLogin: boolean = false;
+  @Input() writeAccess: boolean = false;
+  @Input() downloadAllowed: boolean = false;
+  @Input() emptyMessage: string = "";
+  @Input() downloadZipTooltip: string = "";
+
+  // Toolbar actions handled by the parent (per-resource download endpoints).
+  @Output() downloadCurrentFile = new EventEmitter<void>();
+  @Output() downloadVersionAsZip = new EventEmitter<void>();
+
+  // The parent owns the two toolbar view flags (maximizing also hides the 
parent page
+  // header); the browser reads them and reports toggles back through the 
two-way bindings.
+  @Input() isMaximized = false;
+  @Output() isMaximizedChange = new EventEmitter<boolean>();
+  @Input() isRightBarCollapsed = false;
+  @Output() isRightBarCollapsedChange = new EventEmitter<boolean>();
+
+  // Local sider width state.
+  readonly MAX_SIDER_WIDTH = 600;
+  readonly MIN_SIDER_WIDTH = 150;
+  siderWidth = 400;
+  private rafId = -1;
+
+  formatSize = formatSize;
+
+  @ViewChild(VersionUploaderComponent) private versionUploader?: 
VersionUploaderComponent;
+
+  constructor(private notificationService: NotificationService) {}
+
+  onSideResize({ width }: NzResizeEvent): void {
+    cancelAnimationFrame(this.rafId);
+    this.rafId = requestAnimationFrame(() => {
+      this.siderWidth = width!;
+    });
+  }
+
+  onClickScaleTheView(): void {
+    this.isMaximizedChange.emit(!this.isMaximized);
+  }
+
+  onClickHideRightBar(): void {
+    this.isRightBarCollapsedChange.emit(!this.isRightBarCollapsed);
+  }
+
+  async copyCurrentFilePath(): Promise<void> {
+    if (!this.currentDisplayedFileName) {
+      return;
+    }
+    try {
+      await navigator.clipboard.writeText(this.currentDisplayedFileName);
+      this.notificationService.success("File path copied to clipboard");
+    } catch {
+      this.notificationService.error("Failed to copy file path");
+    }
+  }
+
+  /** Lets the parent stage a path on the embedded uploader after it deletes 
the file. */
+  notePathStaged(relativePath: string): void {
+    this.versionUploader?.notePathStaged(relativePath);
+  }
+}

Reply via email to