From a22b6943aa4522a266ec5d738dd8985b850655eb Mon Sep 17 00:00:00 2001 From: vato007 Date: Fri, 5 Jul 2024 17:45:43 +0930 Subject: [PATCH] Tidy up editor, file tree, get list field working, allow creating new items --- src/app/app.component.html | 24 +----- src/app/app.component.ts | 85 +++--------------- src/app/editor/editor.component.html | 2 + src/app/editor/editor.component.scss | 3 + src/app/editor/editor.component.ts | 9 +- src/app/file-tree/file-tree.component.html | 10 ++- src/app/file-tree/file-tree.component.ts | 86 +++++++++++++++++-- src/app/list-field/list-field.component.ts | 32 ++++--- .../proto-definition-selector.component.ts | 11 +-- src/app/proto-field/proto-field.component.ts | 34 ++++++-- src/styles.scss | 2 + 11 files changed, 171 insertions(+), 127 deletions(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index 28231fc..d7cbcc0 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -18,18 +18,13 @@ - @if(selectedDirectory()) { - + - } - @if (!selectedDirectory()) { -
- -
- }@else { @if(selectedMessage()) { + @if(selectedMessage()) { - } } + }
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index f292bd7..0da4441 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,6 +1,6 @@ import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { CommonModule } from '@angular/common'; -import { Component, computed, signal } from '@angular/core'; +import { Component, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @@ -8,17 +8,14 @@ import { MatSidenavModule } from '@angular/material/sidenav'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTreeModule } from '@angular/material/tree'; import { RouterOutlet } from '@angular/router'; -import { message, open } from '@tauri-apps/api/dialog'; -import { UnlistenFn, listen } from '@tauri-apps/api/event'; -import { FileEntry, readDir } from '@tauri-apps/api/fs'; import { EditorComponent } from './editor/editor.component'; import { FileOrFolder, FileTreeComponent, } from './file-tree/file-tree.component'; -import { OpenFolderMessage } from './messages/openfolder.message'; import { ProtoMessage } from './model/proto-message.model'; import { ProtoDefinitionSelectorComponent } from './proto-definition-selector/proto-definition-selector.component'; +import { readTextFile } from '@tauri-apps/api/fs'; const mobileBreakpoints = [Breakpoints.Handset, Breakpoints.TabletPortrait]; @Component({ @@ -40,27 +37,15 @@ const mobileBreakpoints = [Breakpoints.Handset, Breakpoints.TabletPortrait]; ], }) export class AppComponent { - protected selectedDirectory = signal(null); - protected files = signal([]); + protected selectedFileContents = signal(undefined); protected selectedMessage = signal(undefined); protected rightSideOpen = signal(true); protected leftSideOpen = signal(true); - private unlisten?: UnlistenFn; - protected isMobile = signal( this.breakpointObserver.isMatched(mobileBreakpoints) ); - protected directoryName = computed(() => { - const directory = this.selectedDirectory(); - if (directory) { - const directorySplit = directory.split('/'); - return directorySplit[directorySplit.length - 1]; - } - return null; - }); - constructor(private breakpointObserver: BreakpointObserver) { breakpointObserver .observe(mobileBreakpoints) @@ -68,60 +53,18 @@ export class AppComponent { .subscribe((matches) => this.isMobile.set(matches.matches)); } - async ngOnInit() { - this.unlisten = await listen( - 'openfolder', - async (event: OpenFolderMessage) => { - await this.selectDirectory(); - } - ); - } - - async ngOnDestroy() { - if (this.unlisten) { - this.unlisten(); - } - } - - async selectDirectory() { - const selectedDirectory = await open({ - directory: true, - multiple: false, - }); - if (Array.isArray(selectedDirectory)) { - message('Only a single folder can be selected at a time'); - return; - } else { - this.selectedDirectory.set(selectedDirectory); - } - if (selectedDirectory) { - const entries = await readDir(selectedDirectory, { - recursive: true, - }); - const splitNumbers = /(\d)+|(\D)+/; - this.files.set( - entries - .sort(this.sortFiles(splitNumbers)) - .map((entry) => this.mapEntry(entry, splitNumbers)) - ); - } - } - - private mapEntry(entry: FileEntry, splitNumbers: RegExp): FileOrFolder { - return { - isDirectory: entry.children != null, - name: entry.name || '', - children: entry.children - ?.sort(this.sortFiles(splitNumbers)) - .map((entry) => this.mapEntry(entry, splitNumbers)), - path: entry.path, - }; - } - - private sortFiles = (splitNumbers: RegExp) => (a: FileEntry, b: FileEntry) => - a.name?.localeCompare(b.name ?? '') ?? 0; - protected selectMessage(message: ProtoMessage) { this.selectedMessage.set(message); } + + protected async fileSelected(file: FileOrFolder) { + try { + this.selectedFileContents.set(await readTextFile(file.path)); + } catch (err) { + console.error(err); + alert( + 'Failed to read selected file, please ensure you have appropriate permissions and try again.' + ); + } + } } diff --git a/src/app/editor/editor.component.html b/src/app/editor/editor.component.html index 7fc713b..f832d7f 100644 --- a/src/app/editor/editor.component.html +++ b/src/app/editor/editor.component.html @@ -1,6 +1,8 @@ +

{{ selectedMessage().name }}

@for (item of selectedMessage().values; track $index) { diff --git a/src/app/editor/editor.component.scss b/src/app/editor/editor.component.scss index e69de29..83fd8be 100644 --- a/src/app/editor/editor.component.scss +++ b/src/app/editor/editor.component.scss @@ -0,0 +1,3 @@ +:host { + padding: var(--mat-sidenav-container-shape); +} diff --git a/src/app/editor/editor.component.ts b/src/app/editor/editor.component.ts index 1dcc59e..f3a9b6b 100644 --- a/src/app/editor/editor.component.ts +++ b/src/app/editor/editor.component.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { Component, ElementRef, + computed, effect, input, signal, @@ -22,8 +23,12 @@ export class EditorComponent { // TODO: This needs to be reworked so we have a local property and implement some kind of auto-save fileContents = input(); selectedMessage = input.required(); - // TODO: This needs to start with the parsed file contents, and get updated when the code value changes - protected values = signal([]); + + protected values = computed(() => { + const message = this.selectedMessage(); + return message.values.map((value) => null); + }); + private code = viewChild>('code'); constructor() { diff --git a/src/app/file-tree/file-tree.component.html b/src/app/file-tree/file-tree.component.html index 99097b9..07c8ec5 100644 --- a/src/app/file-tree/file-tree.component.html +++ b/src/app/file-tree/file-tree.component.html @@ -1,4 +1,11 @@ -

{{ workspaceName() }}

+

@if(workspaceName()) {workspaceName()} @else { No Worspace Selected}

+@if(!selectedDirectory()) { +
+ +
+}@else { +} diff --git a/src/app/file-tree/file-tree.component.ts b/src/app/file-tree/file-tree.component.ts index a3d7014..a821863 100644 --- a/src/app/file-tree/file-tree.component.ts +++ b/src/app/file-tree/file-tree.component.ts @@ -1,6 +1,13 @@ import { FlatTreeControl } from '@angular/cdk/tree'; import { CommonModule } from '@angular/common'; -import { Component, computed, input, output } from '@angular/core'; +import { + Component, + OnDestroy, + OnInit, + computed, + output, + signal, +} from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { @@ -8,6 +15,10 @@ import { MatTreeFlattener, MatTreeModule, } from '@angular/material/tree'; +import { message, open } from '@tauri-apps/api/dialog'; +import { UnlistenFn, listen } from '@tauri-apps/api/event'; +import { FileEntry, readDir } from '@tauri-apps/api/fs'; +import { OpenFolderMessage } from '../messages/openfolder.message'; export interface FileOrFolder { isDirectory: boolean; @@ -29,12 +40,10 @@ interface FileNode { templateUrl: './file-tree.component.html', styleUrl: './file-tree.component.scss', }) -export class FileTreeComponent { - workspaceName = input(); - files = input([]); - +export class FileTreeComponent implements OnInit, OnDestroy { fileSelected = output(); + // File tree protected hasChild = (_: number, node: FileNode) => node.expandable; private _transformer = (node: FileOrFolder, level: number) => ({ @@ -64,4 +73,71 @@ export class FileTreeComponent { dataSource.data = this.files(); return dataSource; }); + + // Folder selection + protected selectedDirectory = signal(null); + protected files = signal([]); + protected workspaceName = computed(() => { + const directory = this.selectedDirectory(); + if (directory) { + const directorySplit = directory.split('/'); + return directorySplit[directorySplit.length - 1]; + } + return null; + }); + + private unlisten?: UnlistenFn; + + async ngOnInit() { + this.unlisten = await listen( + 'openfolder', + async (event: OpenFolderMessage) => { + await this.selectDirectory(); + } + ); + } + + async ngOnDestroy() { + if (this.unlisten) { + this.unlisten(); + } + } + + async selectDirectory() { + const selectedDirectory = await open({ + directory: true, + multiple: false, + }); + if (Array.isArray(selectedDirectory)) { + message('Only a single folder can be selected at a time'); + return; + } else { + this.selectedDirectory.set(selectedDirectory); + } + if (selectedDirectory) { + const entries = await readDir(selectedDirectory, { + recursive: true, + }); + const splitNumbers = /(\d)+|(\D)+/; + this.files.set( + entries + .sort(this.sortFiles(splitNumbers)) + .map((entry) => this.mapEntry(entry, splitNumbers)) + ); + } + } + + private mapEntry(entry: FileEntry, splitNumbers: RegExp): FileOrFolder { + return { + isDirectory: entry.children != null, + name: entry.name || '', + children: entry.children + ?.sort(this.sortFiles(splitNumbers)) + .map((entry) => this.mapEntry(entry, splitNumbers)), + path: entry.path, + }; + } + + private sortFiles = (splitNumbers: RegExp) => (a: FileEntry, b: FileEntry) => + a.name?.localeCompare(b.name ?? '') ?? 0; } diff --git a/src/app/list-field/list-field.component.ts b/src/app/list-field/list-field.component.ts index a9bb0bb..17f68e6 100644 --- a/src/app/list-field/list-field.component.ts +++ b/src/app/list-field/list-field.component.ts @@ -1,24 +1,29 @@ import { CommonModule } from '@angular/common'; import { + AfterViewInit, ChangeDetectionStrategy, Component, + OnInit, + forwardRef, input, model, } from '@angular/core'; -import { MatButton } from '@angular/material/button'; +import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; +import { ListMessage } from '../model/proto-message.model'; import { ProtoFieldComponent } from '../proto-field/proto-field.component'; -import { - EnumMessage, - ListMessage, - ProtoMessageField, -} from '../model/proto-message.model'; @Component({ selector: 'app-list-field', standalone: true, - imports: [CommonModule, MatButton, MatIconModule, ProtoFieldComponent], - template: ` @for(value of values(); track $index) { + imports: [ + CommonModule, + MatButtonModule, + MatIconModule, + forwardRef(() => ProtoFieldComponent), + ], + template: `

{{ label() }}

+ @if(values()) { @for(value of values(); track $index) {
remove
- } + } } `, styleUrl: './list-field.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ListFieldComponent { + label = input(); configuration = input.required(); values = model(); add() { const newValues = this.values(); - newValues?.push(null); - this.values.set(newValues); + if (newValues) { + newValues.push(null); + this.values.set(newValues); + } else { + this.values.set([null]); + } } remove(index: number) { diff --git a/src/app/proto-definition-selector/proto-definition-selector.component.ts b/src/app/proto-definition-selector/proto-definition-selector.component.ts index cc0e5da..6d9c439 100644 --- a/src/app/proto-definition-selector/proto-definition-selector.component.ts +++ b/src/app/proto-definition-selector/proto-definition-selector.component.ts @@ -20,6 +20,9 @@ import { ProtoDefinitionService } from '../proto-definition.service'; imports: [CommonModule, MatButtonModule, MatListModule], template: `

Protobuf Definitions

+ @for (item of definitionFiles(); track $index) { } - @if(selectedProtoFile()) {

Messages in {{ selectedProtoFile()?.name }}

@@ -67,10 +67,7 @@ export class ProtoDefinitionSelectorComponent { return this.isDragging(); } - constructor( - private protoDefinitionService: ProtoDefinitionService, - private elementRef: ElementRef - ) {} + constructor(private protoDefinitionService: ProtoDefinitionService) {} protected async addDefinitionFiles() { const files = this.protoSelector()?.nativeElement.files; diff --git a/src/app/proto-field/proto-field.component.ts b/src/app/proto-field/proto-field.component.ts index e3815c8..92a5121 100644 --- a/src/app/proto-field/proto-field.component.ts +++ b/src/app/proto-field/proto-field.component.ts @@ -17,6 +17,7 @@ import { MessageConfiguration, MessageTypeEnum, } from '../model/proto-message.model'; +import { MatInputModule } from '@angular/material/input'; @Component({ selector: 'app-proto-field', @@ -28,22 +29,36 @@ import { MatCheckboxModule, MatFormFieldModule, MatSelectModule, + MatInputModule, ], template: `@switch (configuration().type) { @case (MessageTypeEnum.String) { - + + {{ label() }} + + } @case (MessageTypeEnum.Numeric) { - + + {{ label() }} + + } @case (MessageTypeEnum.Boolean) { - +

+ {{ label() }} +

} @case(MessageTypeEnum.Enum) { - - @for(option of enumConfiguration()!.options; track - enumConfiguration()!.options) { - - } - + + {{ label() }} + + @for(option of enumConfiguration()!.options; track + enumConfiguration()!.options) { + None + {{ option }} + } + + } @case (MessageTypeEnum.List) { @@ -53,6 +68,7 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProtoFieldComponent { + label = input(); configuration = input.required(); value = model(); diff --git a/src/styles.scss b/src/styles.scss index 82ae672..7da354a 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -41,4 +41,6 @@ body { --mat-tree-node-min-height: 24px; --mat-tree-node-text-size: 14px; --mdc-icon-button-state-layer-size: 24px; + --mat-icon-button-touch-target-display: none; + --mdc-list-list-item-one-line-container-height: 24px; }