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" /> <p-button icon="pi pi-save" class="mr-2" text severity="secondary" />
</ng-template> </ng-template>
</p-toolbar> </p-toolbar>
<p-splitter <p-splitter [panelSizes]="[15, 70, 15]" class="h-full" styleClass="mb-8 h-full">
[panelSizes]="[15, 70, 15]"
class="h-full"
[style]="{ height: '100%' }"
styleClass="mb-8"
>
<ng-template #panel> <ng-template #panel>
<div class="h-full w-full"> <div class="h-full w-full">
<app-file-tree (selectFile)="selectedFile.set($event)"></app-file-tree> <app-file-tree (selectFile)="selectedFile.set($event)"></app-file-tree>
@@ -25,18 +20,31 @@
<ng-template #panel> <ng-template #panel>
<p-splitter layout="vertical" [panelSizes]="[70, 30]"> <p-splitter layout="vertical" [panelSizes]="[70, 30]">
<ng-template #panel> <ng-template #panel>
<p-tabs value="0" class="w-full" <div class="flex flex-col w-full">
><p-tablist> @if (tabs().length > 0) {
<p-tab value="0">Tab 1</p-tab> <p-tabs [(value)]="selectedTab" class="w-full" scrollable>
<p-tab value="1">Tab 2</p-tab> <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-tablist>
<p-tabpanels class="h-full">
<p-tabpanel value="0">
<app-file-viewer [file]="selectedFile()"></app-file-viewer>
</p-tabpanel>
</p-tabpanels>
</p-tabs> </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>
<ng-template #panel> <ng-template #panel>
<div class="col flex items-center justify-center"> <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 { ButtonModule } from 'primeng/button';
import { SplitterModule } from 'primeng/splitter'; import { SplitterModule } from 'primeng/splitter';
import { TabsModule } from 'primeng/tabs'; import { TabsModule } from 'primeng/tabs';
@@ -28,7 +28,68 @@ import { FileViewerComponent } from './file-viewer/file-viewer.component';
styleUrl: './app.component.scss', styleUrl: './app.component.scss',
}) })
export class AppComponent { export class AppComponent {
// TODO: When a file is selected, we add a new sheet with that file as the current file? protected selectedFile = signal<File | undefined>(undefined);
// For now we'll just store the current file only protected tabs = signal<File[]>([]);
selectedFile = signal<File | undefined>(undefined); 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 { import {
ApplicationConfig, ApplicationConfig,
inject,
provideAppInitializer,
provideZonelessChangeDetection, provideZonelessChangeDetection,
} from '@angular/core'; } from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
@@ -8,6 +10,7 @@ import { providePrimeNG } from 'primeng/config';
import Aura from '@primeng/themes/aura'; import Aura from '@primeng/themes/aura';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { DuckdbService } from './duckdb.service';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
@@ -19,5 +22,9 @@ export const appConfig: ApplicationConfig = {
preset: Aura, preset: Aura,
}, },
}), }),
provideAppInitializer(async () => {
const duckDbService = inject(DuckdbService);
await duckDbService.init();
}),
], ],
}; };

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed,
effect, effect,
inject, inject,
input, input,
@@ -123,6 +124,7 @@ import { PaginatorModule, PaginatorState } from 'primeng/paginator';
} }
</tr> </tr>
</ng-template> </ng-template>
@if (hasAggregates()) {
<ng-template #footer> <ng-template #footer>
<tr> <tr>
@for (col of columns(); track $index) { @for (col of columns(); track $index) {
@@ -151,6 +153,7 @@ import { PaginatorModule, PaginatorState } from 'primeng/paginator';
} }
</tr> </tr>
</ng-template> </ng-template>
}
</p-table> </p-table>
</div> </div>
@if (currentRowCount() > PAGE_SIZE) { @if (currentRowCount() > PAGE_SIZE) {
@@ -191,6 +194,10 @@ export class FileViewerComponent {
private table = viewChild(Table); private table = viewChild(Table);
protected hasAggregates = computed(
() => !!this.columns().find((col) => this.isAggregateColumn(col)),
);
protected aggregateItems: MenuItem[] = aggregateTypes.map((type) => ({ protected aggregateItems: MenuItem[] = aggregateTypes.map((type) => ({
label: type, label: type,
command: () => this.updateAggregateValue(type), command: () => this.updateAggregateValue(type),
@@ -198,7 +205,7 @@ export class FileViewerComponent {
constructor() { constructor() {
effect(async () => { 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) // 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]; this.currentValue()[event.first! + i] = rows.rows![i];
} }
event.forceUpdate!(); 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="icon" type="image/x-icon" href="favicon.ico" />
<link <link
rel="stylesheet" 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> <style>
.material-symbols-outlined { .material-symbols-outlined {