Compare commits

1 Commits

Author SHA1 Message Date
c8d0ac80f3 Start adding dockview project
All checks were successful
build / build (push) Successful in 2m53s
2025-12-15 21:28:44 +10:30
20 changed files with 733 additions and 11225 deletions

View File

@@ -2,8 +2,7 @@
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1, "version": 1,
"cli": { "cli": {
"packageManager": "bun", "packageManager": "bun"
"analytics": false
}, },
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
@@ -92,6 +91,32 @@
} }
} }
} }
},
"dockview-angular": {
"projectType": "library",
"root": "projects/dockview-angular",
"sourceRoot": "projects/dockview-angular/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular/build:ng-packagr",
"configurations": {
"production": {
"tsConfig": "projects/dockview-angular/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "projects/dockview-angular/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "projects/dockview-angular/tsconfig.spec.json"
}
}
}
} }
}, },
"schematics": { "schematics": {

846
bun.lock

File diff suppressed because it is too large Load Diff

10520
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,41 +10,41 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^21.2.4", "@angular/animations": "^20.3.6",
"@angular/common": "^21.2.4", "@angular/common": "^20.3.6",
"@angular/compiler": "^21.2.4", "@angular/compiler": "^20.3.6",
"@angular/core": "^21.2.4", "@angular/core": "^20.3.6",
"@angular/forms": "^21.2.4", "@angular/forms": "^20.3.6",
"@angular/platform-browser": "^21.2.4", "@angular/platform-browser": "^20.3.6",
"@angular/platform-browser-dynamic": "^21.2.4", "@angular/platform-browser-dynamic": "^20.3.6",
"@angular/router": "^21.2.4", "@angular/router": "^20.3.6",
"@duckdb/duckdb-wasm": "^1.32.0", "@duckdb/duckdb-wasm": "^1.30.0",
"@primeng/themes": "^21.0.4", "@primeng/themes": "20.2.0",
"@tailwindcss/postcss": "^4.2.1", "@tailwindcss/postcss": "^4.1.15",
"dockview-core": "^4.9.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.8.1", "prettier": "^3.6.2",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primeng": "^21.1.3", "primeng": "20.2.0",
"rxjs": "~7.8.2", "rxjs": "~7.8.2",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.1.15",
"tailwindcss-primeui": "^0.6.1", "tailwindcss-primeui": "0.6.1",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"zod": "^4.3.6" "zod": "4.1.12"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^21.2.2", "@angular/build": "^20.3.6",
"@angular/cli": "^21.2.2", "@angular/cli": "^20.3.6",
"@angular/compiler-cli": "^21.2.4", "@angular/compiler-cli": "^20.3.6",
"@tauri-apps/cli": "^2.10.1", "@tauri-apps/cli": "^2.9.0",
"@types/jasmine": "~5.1.15", "@types/jasmine": "~5.1.12",
"baseline-browser-mapping": "^2.10.24", "jasmine-core": "~5.12.0",
"jasmine-core": "~5.12.1",
"karma": "~6.4.4", "karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1", "karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.5.8", "ng-packagr": "^20.3.0",
"typescript": "~5.9.3" "typescript": "~5.9.3"
} }
} }

View File

@@ -0,0 +1,63 @@
# DockviewAngular
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.0.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the library, run:
```bash
ng build dockview-angular
```
This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
### Publishing the Library
Once the project is built, you can publish your library by following these steps:
1. Navigate to the `dist` directory:
```bash
cd dist/dockview-angular
```
2. Run the `npm publish` command to publish your library to the npm registry:
```bash
npm publish
```
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/dockview-angular",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,12 @@
{
"name": "dockview-angular",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^20.3.0",
"@angular/core": "^20.3.0"
},
"dependencies": {
"tslib": "^2.3.0"
},
"sideEffects": false
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DockviewAngular } from './dockview-angular';
describe('DockviewAngular', () => {
let component: DockviewAngular;
let fixture: ComponentFixture<DockviewAngular>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DockviewAngular]
})
.compileComponents();
fixture = TestBed.createComponent(DockviewAngular);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,26 @@
import {
Component,
ElementRef,
contentChildren,
viewChild,
} from '@angular/core';
import { DockviewApi, createDockview } from 'dockview-core';
@Component({
selector: 'lib-dockview-angular',
imports: [],
template: ` <div #dockingElement></div> `,
styles: ``,
})
export class DockviewAngular {
// Child html elements that will be shown in separate dockview panels
// TODO: Maybe better as a directive so that we can provide the group api to each panel
// It would also allow us to set properties for each panel, like its starting position
private readonly childElements = contentChildren<ElementRef>('panel');
private readonly dockingElement = viewChild<ElementRef>('dockingElement');
private readonly api: DockviewApi;
constructor() {
this.api = createDockview(this.dockingElement()?.nativeElement, {});
}
}

View File

@@ -0,0 +1,5 @@
/*
* Public API Surface of dockview-angular
*/
export * from './lib/dockview-angular';

View File

@@ -0,0 +1,18 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/lib",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"include": [
"src/**/*.ts"
],
"exclude": [
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,11 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

View File

@@ -0,0 +1,14 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.ts"
]
}

View File

@@ -16,9 +16,7 @@
> >
<ng-template #panel> <ng-template #panel>
<div class="h-full w-full overflow-auto"> <div class="h-full w-full overflow-auto">
<app-file-tree <app-file-tree (selectFile)="selectedFile.set($event)"></app-file-tree>
(selectFile)="selectedSheet.set({ file: $event, history: [] })"
></app-file-tree>
</div> </div>
</ng-template> </ng-template>
<ng-template #panel> <ng-template #panel>
@@ -33,12 +31,12 @@
[class.border-dashed]="dragging()" [class.border-dashed]="dragging()"
[class.border-2]="dragging()" [class.border-2]="dragging()"
> >
@if (sheets().length > 0) { @if (tabs().length > 0) {
<p-tabs [(value)]="selectedTab" class="w-full" scrollable> <p-tabs [(value)]="selectedTab" class="w-full" scrollable>
<p-tablist> <p-tablist>
@for (tab of sheets(); track $index) { @for (tab of tabs(); track $index) {
<p-tab [value]="$index" (auxclick)="removeTab($index)"> <p-tab [value]="$index" (auxclick)="removeTab($index)">
<span>{{ tab.file.name }}</span> <span>{{ tab.name }}</span>
<span <span
(click)="removeTab($index)" (click)="removeTab($index)"
class="material-symbols-outlined" class="material-symbols-outlined"
@@ -50,10 +48,10 @@
</p-tablist> </p-tablist>
</p-tabs> </p-tabs>
} }
@if (selectedSheet()) { @if (selectedFile()) {
<app-file-viewer <app-file-viewer
class="flex w-full flex-1" class="flex w-full flex-1"
[sheet]="selectedSheet()" [file]="selectedFile()"
[(columns)]="selectedFileColumns" [(columns)]="selectedFileColumns"
></app-file-viewer> ></app-file-viewer>
} @else { } @else {
@@ -69,21 +67,13 @@
</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">
@let selectedSheet = this.selectedSheet(); Panel 3
@if (selectedSheet) { </div></ng-template
@for (state of selectedSheet.history; track $index) { >
<!-- TODO: Make a diff compared to the initial state. -->
<!-- Alternative is to make state a list of patches, but probably more complicated -->
<div>
</div>
}
}</div
></ng-template>
</p-splitter> </p-splitter>
</ng-template> </ng-template>
<ng-template #panel> <ng-template #panel>
@if (selectedSheet()) { @if (selectedFile()) {
<app-column-editor <app-column-editor
class="w-full overflow-auto" class="w-full overflow-auto"
[(columns)]="selectedFileColumns" [(columns)]="selectedFileColumns"

View File

@@ -1,7 +1,6 @@
import { import {
Component, Component,
ElementRef, ElementRef,
HostListener,
effect, effect,
signal, signal,
viewChild, viewChild,
@@ -17,7 +16,7 @@ import { InputTextModule } from 'primeng/inputtext';
import { FileTreeComponent } from './file-tree/file-tree.component'; import { FileTreeComponent } from './file-tree/file-tree.component';
import { FileViewerComponent } from './file-viewer/file-viewer.component'; import { FileViewerComponent } from './file-viewer/file-viewer.component';
import { ColumnEditorComponent } from './column-editor/column-editor.component'; import { ColumnEditorComponent } from './column-editor/column-editor.component';
import { Column, Sheet } from './duckdb.service'; import { Column } from './duckdb.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -38,9 +37,9 @@ import { Column, Sheet } from './duckdb.service';
styleUrl: './app.component.scss', styleUrl: './app.component.scss',
}) })
export class AppComponent { export class AppComponent {
protected selectedSheet = signal<Sheet | undefined>(undefined); protected selectedFile = signal<File | undefined>(undefined);
protected selectedFileColumns = signal<Column[]>([]); protected selectedFileColumns = signal<Column[]>([]);
protected sheets = signal<Sheet[]>([]); protected tabs = signal<File[]>([]);
protected selectedTab = signal(0); protected selectedTab = signal(0);
protected dragging = signal(false); protected dragging = signal(false);
@@ -48,25 +47,25 @@ export class AppComponent {
constructor() { constructor() {
effect(() => { effect(() => {
const selectedFile = this.selectedSheet(); const selectedFile = this.selectedFile();
if (selectedFile) { if (selectedFile) {
this.addSheet(selectedFile); this.addFile(selectedFile);
} }
}); });
effect(() => { effect(() => {
if (this.selectedSheet() !== this.sheets()[this.selectedTab()]) { if (this.selectedFile() !== this.tabs()[this.selectedTab()]) {
if (this.sheets().length > 0) { if (this.tabs().length > 0) {
this.selectedSheet.set(this.sheets()[Number(this.selectedTab())]); this.selectedFile.set(this.tabs()[Number(this.selectedTab())]);
} else { } else {
this.selectedSheet.set(undefined); this.selectedFile.set(undefined);
} }
} }
}); });
} }
protected removeTab(index: number) { protected removeTab(index: number) {
this.sheets.update((tabs) => { this.tabs.update((tabs) => {
const copy = tabs.slice(); const copy = tabs.slice();
copy.splice(index, 1); copy.splice(index, 1);
return copy; return copy;
@@ -74,12 +73,12 @@ export class AppComponent {
if (this.selectedTab() === index) { if (this.selectedTab() === index) {
if (this.selectedTab() > 0) { if (this.selectedTab() > 0) {
this.selectedTab.update((tab) => tab - 1); this.selectedTab.update((tab) => tab - 1);
this.selectedSheet.set(this.sheets()[this.selectedTab()]); this.selectedFile.set(this.tabs()[this.selectedTab()]);
} else if (this.sheets().length > 1) { } else if (this.tabs().length > 1) {
this.selectedTab.update((tab) => tab + 1); this.selectedTab.update((tab) => tab + 1);
this.selectedSheet.set(this.sheets()[this.selectedTab()]); this.selectedFile.set(this.tabs()[this.selectedTab()]);
} else { } else {
this.selectedSheet.set(undefined); this.selectedFile.set(undefined);
} }
} }
} }
@@ -90,7 +89,7 @@ export class AppComponent {
if (files) { if (files) {
for (const file of files) { for (const file of files) {
if (file.type === 'text/csv') { if (file.type === 'text/csv') {
this.addSheet({ file, history: [] }); this.addFile(file);
} }
} }
} }
@@ -120,43 +119,31 @@ export class AppComponent {
if (files) { if (files) {
for (const file of files) { for (const file of files) {
if (file.type === 'text/csv') { if (file.type === 'text/csv') {
this.addSheet({ file, history: [] }); this.addFile(file);
} }
} }
this.selectedTab.set(this.sheets().length - 1); this.selectedTab.set(this.tabs().length - 1);
} }
} }
private addSheet(newSheet: Sheet) { private addFile(file: File) {
if ( if (
this.sheets().find( this.tabs().find(
(sheet) => (tab) =>
sheet.file.webkitRelativePath === newSheet.file.webkitRelativePath && tab.webkitRelativePath === file.webkitRelativePath &&
sheet.file.name === newSheet.file.name, tab.name === file.name,
) )
) { ) {
this.selectedTab.set( this.selectedTab.set(
this.sheets().findIndex( this.tabs().findIndex(
(sheet) => (tab) =>
sheet.file.webkitRelativePath === tab.webkitRelativePath === file.webkitRelativePath &&
newSheet.file.webkitRelativePath && tab.name === file.name,
sheet.file.name === newSheet.file.name,
), ),
); );
} else { } else {
this.sheets.update((tabs) => [...tabs, newSheet]); this.tabs.update((tabs) => [...tabs, file]);
this.selectedTab.set(this.sheets().length - 1); this.selectedTab.set(this.tabs().length - 1);
}
}
@HostListener('window:keydown', [
'$event.key',
'$event.ctrlKey',
'$event.metaKey',
])
keydown(key: string, hasCtrl: boolean, hasMeta: boolean) {
const sheets = this.sheets();
if (sheets.length > 1 && key === 'z' && (hasCtrl || hasMeta)) {
this.sheets()[0].history.pop();
} }
} }
} }

View File

@@ -1,36 +1,15 @@
import { import { ChangeDetectionStrategy, Component, model } from '@angular/core';
ChangeDetectionStrategy,
Component,
model,
signal,
} from '@angular/core';
import { AccordionModule } from 'primeng/accordion'; import { AccordionModule } from 'primeng/accordion';
import { Column } from '../duckdb.service'; import { Column } from '../duckdb.service';
import { Checkbox } from 'primeng/checkbox'; import { Checkbox, CheckboxChangeEvent } from 'primeng/checkbox';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { Button } from 'primeng/button'; import { Button } from 'primeng/button';
import { Tooltip } from 'primeng/tooltip'; import { Tooltip } from 'primeng/tooltip';
import { Dialog } from 'primeng/dialog';
import { FloatLabel } from 'primeng/floatlabel';
import { InputText } from 'primeng/inputtext';
import { Select } from 'primeng/select';
import { Field, form, FormField } from '@angular/forms/signals';
@Component({ @Component({
selector: 'app-column-editor', selector: 'app-column-editor',
standalone: true, standalone: true,
imports: [ imports: [AccordionModule, Checkbox, FormsModule, Button, Tooltip],
AccordionModule,
Checkbox,
FormsModule,
Button,
Tooltip,
Dialog,
FloatLabel,
InputText,
Select,
FormField,
],
template: ` template: `
<p-accordion [value]="0"> <p-accordion [value]="0">
<p-accordion-panel [value]="0"> <p-accordion-panel [value]="0">
@@ -66,65 +45,12 @@ import { Field, form, FormField } from '@angular/forms/signals';
</p-accordion-content> </p-accordion-content>
</p-accordion-panel> </p-accordion-panel>
</p-accordion> </p-accordion>
<p-dialog
[(visible)]="editDialogVisible"
header="Edit Column"
[draggable]="false"
[resizable]="false"
[modal]="true"
[dismissableMask]="true"
>
<div class="flex flex-col gap-2">
<p-float-label variant="in">
<input
pInputText
id="column-edit-name"
[formField]="editingColumn.name"
/>
<label for="column-edit-name">Name</label>
</p-float-label>
<p-float-label variant="in">
<p-select
fluid
inputId="column-edit-type"
[formField]="$any(editingColumn.type)"
[options]="columnTypes"
appendTo="body"
></p-select>
<label for="column-edit-type">Type</label>
</p-float-label>
<div class="flex items-center gap-2">
<p-check-box
[formField]="editingColumn.enabled"
[binary]="true"
inputId="column-edit-enabled"
></p-check-box>
<label for="column-edit-enabled">Enabled</label>
</div>
</div>
<ng-template #footer>
<p-button (click)="saveColumn()">Save</p-button>
</ng-template>
</p-dialog>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ColumnEditorComponent { export class ColumnEditorComponent {
columns = model<Column[]>(); columns = model<Column[]>();
protected readonly editDialogVisible = signal(false);
protected readonly columnTypes = ['string', 'number', 'boolean'];
protected readonly currentColumn = signal<Column>({
name: '',
enabled: true,
type: 'string',
});
protected readonly editIndex = signal<number | undefined>(undefined);
protected readonly editingColumn = form(this.currentColumn);
protected checkboxChanged(index: number) { protected checkboxChanged(index: number) {
this.columns.update((columns) => { this.columns.update((columns) => {
if (columns) { if (columns) {
@@ -139,27 +65,5 @@ export class ColumnEditorComponent {
}); });
} }
protected editColumn(index: number) { protected editColumn(index: number) {}
const columns = this.columns();
if (columns) {
this.editIndex.set(index);
this.currentColumn.set(columns[index]);
this.editDialogVisible.set(true);
}
}
protected saveColumn() {
const editIndex = this.editIndex();
if (editIndex == null) {
return;
}
this.columns.update((columns) => {
const oldColumns = columns?.slice();
if (oldColumns) {
oldColumns[editIndex] = this.editingColumn().value();
}
return oldColumns;
});
this.editDialogVisible.set(false);
}
} }

View File

@@ -48,18 +48,6 @@ export const RowsResponse = z.object({
aggregateValues: AggregateValue.array(), aggregateValues: AggregateValue.array(),
}); });
export const TableState = z.object({
columns: z.array(Column),
sorts: z.array(SortColumn),
filters: z.array(Filter),
aggregates: z.array(Aggregate)
});
export const Sheet = z.object({
file: z.file(),
history: z.array(TableState),
})
export type Column = z.infer<typeof Column>; export type Column = z.infer<typeof Column>;
export type SortColumn = z.infer<typeof SortColumn>; export type SortColumn = z.infer<typeof SortColumn>;
export type FilterValue = z.infer<typeof FilterValue>; export type FilterValue = z.infer<typeof FilterValue>;
@@ -69,8 +57,6 @@ export type AggregateType = z.infer<typeof AggregateType>;
export type Aggregate = z.infer<typeof Aggregate>; export type Aggregate = z.infer<typeof Aggregate>;
export type AggregateValue = z.infer<typeof AggregateValue>; export type AggregateValue = z.infer<typeof AggregateValue>;
export type RowsResponse = z.infer<typeof RowsResponse>; export type RowsResponse = z.infer<typeof RowsResponse>;
export type TableState = z.infer<typeof TableState>;
export type Sheet = z.infer<typeof Sheet>;
const sanitisedFileName = (file: File) => const sanitisedFileName = (file: File) =>
file.name.toLowerCase().replaceAll("'", '').replaceAll(/\s*/g, ''); file.name.toLowerCase().replaceAll("'", '').replaceAll(/\s*/g, '');
@@ -190,12 +176,15 @@ export class DuckdbService {
file: File, file: File,
start: number, start: number,
numRows: number, numRows: number,
state: TableState, columns: Column[],
sorts: SortColumn[],
filters: Filter[],
aggregates: Aggregate[],
): Promise<RowsResponse> { ): Promise<RowsResponse> {
const conn = await this.db.connect(); const conn = await this.db.connect();
try { try {
const whereClause = this.getWhereClause(state.filters); const whereClause = this.getWhereClause(filters);
const mappedFilterValues = state.filters.flatMap((filter) => const mappedFilterValues = filters.flatMap((filter) =>
filter.value filter.value
.filter((value) => value?.value) .filter((value) => value?.value)
.map( .map(
@@ -204,8 +193,8 @@ export class DuckdbService {
), ),
); );
let aggregatesQuery = 'SELECT COUNT(1) totalRows'; let aggregatesQuery = 'SELECT COUNT(1) totalRows';
if (state.aggregates.length > 0) { if (aggregates.length > 0) {
for (const aggregate of state.aggregates) { for (const aggregate of aggregates) {
aggregatesQuery += `, ${aggregate.type}("${aggregate.column}") "${aggregate.column}"`; aggregatesQuery += `, ${aggregate.type}("${aggregate.column}") "${aggregate.column}"`;
} }
} }
@@ -217,9 +206,9 @@ export class DuckdbService {
const aggregateValues: AggregateValue[] = Object.entries(aggregatesJson) const aggregateValues: AggregateValue[] = Object.entries(aggregatesJson)
.filter(([key]) => key !== 'totalRows') .filter(([key]) => key !== 'totalRows')
.map(([key, value]) => AggregateValue.parse({ column: key, value })); .map(([key, value]) => AggregateValue.parse({ column: key, value }));
let query = `SELECT ${state.columns.map((column) => `"${column.name}"`).join(', ')} FROM ${sanitisedFileName(file)} ${whereClause}`; let query = `SELECT ${columns.map((column) => `"${column.name}"`).join(', ')} FROM ${sanitisedFileName(file)} ${whereClause}`;
if (state.sorts.length > 0) { if (sorts.length > 0) {
query += ` ORDER BY ${state.sorts.map((sort) => `"${sort.name}" ${sort.sortType}`).join(', ')}`; query += ` ORDER BY ${sorts.map((sort) => `"${sort.name}" ${sort.sortType}`).join(', ')}`;
} }
query += ` LIMIT ${numRows} OFFSET ${start}`; query += ` LIMIT ${numRows} OFFSET ${start}`;
const stmt = await conn.prepare(query); const stmt = await conn.prepare(query);

View File

@@ -2,7 +2,3 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
p-table tr > th {
background: var(--p-datatable-header-cell-background);
}

View File

@@ -18,7 +18,6 @@ import {
DuckdbService, DuckdbService,
Filter, Filter,
FilterValue, FilterValue,
Sheet,
aggregateTypes, aggregateTypes,
} from '../duckdb.service'; } from '../duckdb.service';
import { ButtonModule } from 'primeng/button'; import { ButtonModule } from 'primeng/button';
@@ -45,7 +44,7 @@ import { PaginatorModule, PaginatorState } from 'primeng/paginator';
PaginatorModule, PaginatorModule,
], ],
template: ` template: `
@if (sheet() && enabledColumns().length > 0) { @if (file() && enabledColumns().length > 0) {
<div class="h-full"> <div class="h-full">
<p-table <p-table
#table #table
@@ -171,37 +170,37 @@ import { PaginatorModule, PaginatorState } from 'primeng/paginator';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class FileViewerComponent { export class FileViewerComponent {
sheet = input<Sheet | undefined>(); file = input<File | undefined>();
private duckdbService = inject(DuckdbService); private duckdbService = inject(DuckdbService);
// Can't be computed since effect has to run first so file exists in duckdb // Can't be computed since effect has to run first so file exists in duckdb
columns = model<Column[]>([]); columns = model<Column[]>([]);
protected readonly aggregates = signal<(Aggregate | undefined)[]>([]); protected aggregates = signal<(Aggregate | undefined)[]>([]);
protected readonly currentAggregateColumn = signal<number | undefined>(undefined); protected currentAggregateColumn = signal<number | undefined>(undefined);
protected readonly aggregateValues = signal<(number | undefined)[]>([]); protected aggregateValues = signal<(number | undefined)[]>([]);
protected readonly NUM_ROWS = 200; protected readonly NUM_ROWS = 200;
protected readonly currentPage = signal(0); protected currentPage = signal(0);
// This is required since once the number of rows exceeds ~300000, the footer gets blocked // This is required since once the number of rows exceeds ~300000, the footer gets blocked
// and the user can't even scroll to those bottom rows. // and the user can't even scroll to those bottom rows.
protected readonly PAGE_SIZE = 300_000; protected readonly PAGE_SIZE = 300_000;
protected readonly currentValue = signal<any[]>([]); protected currentValue = signal<any[]>([]);
protected readonly appliedFilters = signal<Filter[]>([]); protected appliedFilters = signal<Filter[]>([]);
protected readonly currentRowCount = signal(0n); protected currentRowCount = signal(0n);
private readonly table = viewChild(Table); private table = viewChild(Table);
protected readonly hasAggregates = computed( protected hasAggregates = computed(
() => !!this.columns().find((col) => this.isAggregateColumn(col)), () => !!this.columns().find((col) => this.isAggregateColumn(col)),
); );
protected readonly enabledColumns = computed(() => protected enabledColumns = computed(() =>
this.columns().filter((col) => col.enabled), this.columns().filter((col) => col.enabled),
); );
protected readonly aggregateItems: MenuItem[] = aggregateTypes.map((type) => ({ protected aggregateItems: MenuItem[] = aggregateTypes.map((type) => ({
label: type, label: type,
command: () => this.updateAggregateValue(type), command: () => this.updateAggregateValue(type),
})); }));
@@ -219,8 +218,8 @@ export class FileViewerComponent {
} }
protected async onLazyLoad(event: TableLazyLoadEvent) { protected async onLazyLoad(event: TableLazyLoadEvent) {
const sheet = this.sheet(); const file = this.file();
if (sheet) { if (file) {
this.appliedFilters.set( this.appliedFilters.set(
event.filters event.filters
? Object.entries(event.filters).flatMap(([column, filter]) => { ? Object.entries(event.filters).flatMap(([column, filter]) => {
@@ -255,20 +254,16 @@ export class FileViewerComponent {
: [], : [],
); );
const rows = await this.duckdbService.getRows( const rows = await this.duckdbService.getRows(
sheet.file, file,
(event.first ?? 0) + this.currentPage() * this.PAGE_SIZE, (event.first ?? 0) + this.currentPage() * this.PAGE_SIZE,
event.rows ?? 0, event.rows ?? 0,
{ this.columns(),
columns: this.columns(),
// TODO: We should be maintaining sorts separately so we can store them
sorts:
event.multiSortMeta?.map((meta) => ({ event.multiSortMeta?.map((meta) => ({
name: meta.field, name: meta.field,
sortType: meta.order < 0 ? 'desc' : 'asc', sortType: meta.order < 0 ? 'desc' : 'asc',
})) ?? [], })) ?? [],
filters: this.appliedFilters(), this.appliedFilters(),
aggregates: this.aggregates().filter((agg) => !!agg) as Aggregate[], this.aggregates().filter((agg) => !!agg) as Aggregate[],
},
); );
// First clear out existing data, don't want to risk loading entire file into memory // First clear out existing data, don't want to risk loading entire file into memory
// Keep data in previous/next page so when user scrolls between 2 pages they don't see missing data // Keep data in previous/next page so when user scrolls between 2 pages they don't see missing data
@@ -290,18 +285,20 @@ export class FileViewerComponent {
} }
private async loadEmpty() { private async loadEmpty() {
// TODO: Add to history, if the history is empty. If not empty, then load from that history const file = this.file();
const sheet = this.sheet(); if (file) {
if (sheet) { await this.duckdbService.addFile(file);
await this.duckdbService.addFile(sheet.file); this.columns.set(await this.duckdbService.getColumns(file));
this.columns.set(await this.duckdbService.getColumns(sheet.file));
this.aggregates.set(new Array(this.columns().length)); this.aggregates.set(new Array(this.columns().length));
const rows = await this.duckdbService.getRows(sheet.file, 0, this.NUM_ROWS, { const rows = await this.duckdbService.getRows(
columns: this.columns(), file,
sorts: [], 0,
filters: [], this.NUM_ROWS,
aggregates: [], this.columns(),
}); [],
[],
[],
);
this.currentRowCount.set(rows.totalRows); this.currentRowCount.set(rows.totalRows);
this.currentPage.set(0); this.currentPage.set(0);
const newValue = Array.from({ const newValue = Array.from({
@@ -321,7 +318,6 @@ export class FileViewerComponent {
} }
private async updateAggregateValue(type: AggregateType) { private async updateAggregateValue(type: AggregateType) {
// Update history
this.aggregates.update((aggregates) => { this.aggregates.update((aggregates) => {
const copy = aggregates.slice(); const copy = aggregates.slice();
copy[this.currentAggregateColumn()!] = { copy[this.currentAggregateColumn()!] = {
@@ -331,9 +327,8 @@ export class FileViewerComponent {
return copy; return copy;
}); });
const aggregateValue = await this.duckdbService.getAggregateValue( const aggregateValue = await this.duckdbService.getAggregateValue(
this.sheet()!.file, this.file()!,
this.appliedFilters(), this.appliedFilters(),
this.aggregates()[this.currentAggregateColumn()!]!, this.aggregates()[this.currentAggregateColumn()!]!,
); );

View File

@@ -3,6 +3,11 @@
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"paths": {
"dockview-angular": [
"./dist/dockview-angular"
]
},
"outDir": "./dist/out-tsc", "outDir": "./dist/out-tsc",
"strict": true, "strict": true,
"noImplicitOverride": true, "noImplicitOverride": true,
@@ -23,5 +28,13 @@
"strictInjectionParameters": true, "strictInjectionParameters": true,
"strictInputAccessModifiers": true, "strictInputAccessModifiers": true,
"strictTemplates": true "strictTemplates": true
},
"references": [
{
"path": "./projects/dockview-angular/tsconfig.lib.json"
},
{
"path": "./projects/dockview-angular/tsconfig.spec.json"
} }
]
} }