Add support for multiple tabs
All checks were successful
build / build (push) Successful in 1m29s

This commit is contained in:
2025-06-01 16:21:42 +09:30
parent 91c322c1d6
commit 8c661f2533
7 changed files with 140 additions and 57 deletions

View File

@@ -11,12 +11,7 @@
<p-button icon="pi pi-save" class="mr-2" text severity="secondary" />
</ng-template>
</p-toolbar>
<p-splitter
[panelSizes]="[15, 70, 15]"
class="h-full"
[style]="{ height: '100%' }"
styleClass="mb-8"
>
<p-splitter [panelSizes]="[15, 70, 15]" class="h-full" styleClass="mb-8 h-full">
<ng-template #panel>
<div class="h-full w-full">
<app-file-tree (selectFile)="selectedFile.set($event)"></app-file-tree>
@@ -25,18 +20,31 @@
<ng-template #panel>
<p-splitter layout="vertical" [panelSizes]="[70, 30]">
<ng-template #panel>
<p-tabs value="0" class="w-full"
><p-tablist>
<p-tab value="0">Tab 1</p-tab>
<p-tab value="1">Tab 2</p-tab>
<div class="flex flex-col w-full">
@if (tabs().length > 0) {
<p-tabs [(value)]="selectedTab" class="w-full" scrollable>
<p-tablist>
@for (tab of tabs(); track $index) {
<p-tab [value]="$index">
<span>{{ tab.name }}</span>
<span
(click)="removeTab($index)"
class="material-symbols-outlined"
>
close
</span>
</p-tab>
}
</p-tablist>
<p-tabpanels class="h-full">
<p-tabpanel value="0">
<app-file-viewer [file]="selectedFile()"></app-file-viewer>
</p-tabpanel>
</p-tabpanels>
</p-tabs>
<!-- <div class="col flex items-center justify-center">Panel 2</div> -->
}
@if (selectedFile()) {
<app-file-viewer
class="flex w-full flex-1"
[file]="selectedFile()"
></app-file-viewer>
}
</div>
</ng-template>
<ng-template #panel>
<div class="col flex items-center justify-center">

View File

@@ -1,4 +1,4 @@
import { Component, signal, viewChild } from '@angular/core';
import { Component, effect, signal, viewChild } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { SplitterModule } from 'primeng/splitter';
import { TabsModule } from 'primeng/tabs';
@@ -28,7 +28,68 @@ import { FileViewerComponent } from './file-viewer/file-viewer.component';
styleUrl: './app.component.scss',
})
export class AppComponent {
// TODO: When a file is selected, we add a new sheet with that file as the current file?
// For now we'll just store the current file only
selectedFile = signal<File | undefined>(undefined);
protected selectedFile = signal<File | undefined>(undefined);
protected tabs = signal<File[]>([]);
protected selectedTab = signal(0);
constructor() {
effect(() => {
const selectedFile = this.selectedFile();
if (selectedFile) {
if (
this.tabs().find(
(tab) => tab.webkitRelativePath === selectedFile.webkitRelativePath,
)
) {
this.selectedTab.set(
this.tabs().findIndex(
(tab) =>
tab.webkitRelativePath === selectedFile.webkitRelativePath,
),
);
} else {
this.tabs.update((tabs) => [...tabs, selectedFile]);
this.selectedTab.set(this.tabs().length - 1);
}
}
});
effect(() => {
if (this.selectedFile() !== this.tabs()[this.selectedTab()]) {
if (this.tabs().length > 0) {
this.selectedFile.set(this.tabs()[Number(this.selectedTab())]);
} else {
this.selectedFile.set(undefined);
}
}
});
}
protected removeTab(index: number) {
this.tabs.update((tabs) => {
const copy = tabs.slice();
copy.splice(index);
return copy;
});
if (this.selectedTab() === index) {
if (this.selectedTab() > 0) {
this.selectedTab.update((tab) => tab - 1);
this.selectedFile.set(this.tabs()[this.selectedTab()]);
} else if (this.tabs().length > 1) {
this.selectedTab.update((tab) => tab + 1);
this.selectedFile.set(this.tabs()[this.selectedTab()]);
} else {
this.selectedFile.set(undefined);
}
}
}
// TODO: Drop files over viewport
protected fileDropped(event: DragEvent) {
if (event.dataTransfer?.items.length ?? 0 > 1) {
// Open in the tabs
} else if (event.dataTransfer?.items.length ?? 0 > 0) {
// Open in current tab
}
}
}

View File

@@ -1,5 +1,7 @@
import {
ApplicationConfig,
inject,
provideAppInitializer,
provideZonelessChangeDetection,
} from '@angular/core';
import { provideRouter } from '@angular/router';
@@ -8,6 +10,7 @@ import { providePrimeNG } from 'primeng/config';
import Aura from '@primeng/themes/aura';
import { routes } from './app.routes';
import { DuckdbService } from './duckdb.service';
export const appConfig: ApplicationConfig = {
providers: [
@@ -19,5 +22,9 @@ export const appConfig: ApplicationConfig = {
preset: Aura,
},
}),
provideAppInitializer(async () => {
const duckDbService = inject(DuckdbService);
await duckDbService.init();
}),
],
};

View File

@@ -102,9 +102,6 @@ const suffix = (operator: FilterOperator) => {
export class DuckdbService {
private db!: duckdb.AsyncDuckDB;
constructor() {
this.init();
}
async init() {
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();

View File

@@ -1,5 +1,4 @@
:host {
display: flex;
flex-direction: column;
height: 100%;
}

View File

@@ -2,6 +2,7 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
effect,
inject,
input,
@@ -123,6 +124,7 @@ import { PaginatorModule, PaginatorState } from 'primeng/paginator';
}
</tr>
</ng-template>
@if (hasAggregates()) {
<ng-template #footer>
<tr>
@for (col of columns(); track $index) {
@@ -151,6 +153,7 @@ import { PaginatorModule, PaginatorState } from 'primeng/paginator';
}
</tr>
</ng-template>
}
</p-table>
</div>
@if (currentRowCount() > PAGE_SIZE) {
@@ -191,6 +194,10 @@ export class FileViewerComponent {
private table = viewChild(Table);
protected hasAggregates = computed(
() => !!this.columns().find((col) => this.isAggregateColumn(col)),
);
protected aggregateItems: MenuItem[] = aggregateTypes.map((type) => ({
label: type,
command: () => this.updateAggregateValue(type),
@@ -198,7 +205,7 @@ export class FileViewerComponent {
constructor() {
effect(async () => {
this.loadEmpty();
await this.loadEmpty();
});
}
@@ -268,7 +275,7 @@ export class FileViewerComponent {
}
// Can't update the current value, otherwise we get an infinite loop due to primeng change detection rerunning (plus it's faster to mutate)
for (let i = 0; i < event.rows!; i++) {
for (let i = 0; i < rows.rows!.length; i++) {
this.currentValue()[event.first! + i] = rows.rows![i];
}
event.forceUpdate!();
@@ -340,4 +347,8 @@ export class FileViewerComponent {
}),
);
}
protected isAggregateColumn(col: Column) {
return col.type === 'DOUBLE' || col.type === 'BIGINT';
}
}

View File

@@ -8,7 +8,7 @@
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=mediation"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=close,mediation"
/>
<style>
.material-symbols-outlined {