Compare commits
18 Commits
e1747ce20a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e0acfcc27e | |||
| 906c13fde2 | |||
| 7bda79db00 | |||
| 2eca7fc22d | |||
| 200f76780b | |||
| cf89834fe9 | |||
| acbb4c9e2c | |||
| 1e18b9e768 | |||
| 53a9ce3a45 | |||
| 885ed5892e | |||
| 1c4536878e | |||
| 3897c1841e | |||
| 8003a8fee9 | |||
| cdeccf33b3 | |||
| af613c3d37 | |||
| ec0f21cf16 | |||
| dd8d1374ab | |||
| 451877667a |
@@ -32,6 +32,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ^22.12.0
|
||||||
- uses: browser-actions/setup-chrome@v1
|
- uses: browser-actions/setup-chrome@v1
|
||||||
id: setup-chrome
|
id: setup-chrome
|
||||||
with:
|
with:
|
||||||
@@ -49,9 +52,8 @@ jobs:
|
|||||||
run: bun run build
|
run: bun run build
|
||||||
|
|
||||||
- name: Publish to Cloudflare Pages
|
- name: Publish to Cloudflare Pages
|
||||||
uses: cloudflare/pages-action@v1
|
uses: cloudflare/wrangler-action@v3
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
projectName: buf
|
command: pages deploy dist/proto-creator/browser --project-name=buf
|
||||||
directory: dist/proto-creator/browser
|
|
||||||
|
|||||||
33
angular.json
33
angular.json
@@ -58,7 +58,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"serviceWorker": "ngsw-config.json"
|
"serviceWorker": "ngsw-config.json",
|
||||||
|
"security": { "autoCsp": true }
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
@@ -84,10 +85,10 @@
|
|||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
"builder": "@angular/build:extract-i18n"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
"builder": "@angular/build:karma",
|
||||||
"options": {
|
"options": {
|
||||||
"polyfills": [],
|
"polyfills": [],
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
@@ -107,5 +108,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"type": "component"
|
||||||
|
},
|
||||||
|
"@schematics/angular:directive": {
|
||||||
|
"type": "directive"
|
||||||
|
},
|
||||||
|
"@schematics/angular:service": {
|
||||||
|
"type": "service"
|
||||||
|
},
|
||||||
|
"@schematics/angular:guard": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
},
|
||||||
|
"@schematics/angular:interceptor": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
},
|
||||||
|
"@schematics/angular:module": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
},
|
||||||
|
"@schematics/angular:pipe": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
},
|
||||||
|
"@schematics/angular:resolver": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,12 @@
|
|||||||
module.exports = function (config) {
|
module.exports = function (config) {
|
||||||
config.set({
|
config.set({
|
||||||
basePath: '',
|
basePath: '',
|
||||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
frameworks: ['jasmine'],
|
||||||
plugins: [
|
plugins: [
|
||||||
require('karma-jasmine'),
|
require('karma-jasmine'),
|
||||||
require('karma-chrome-launcher'),
|
require('karma-chrome-launcher'),
|
||||||
require('karma-jasmine-html-reporter'),
|
require('karma-jasmine-html-reporter'),
|
||||||
require('karma-coverage'),
|
require('karma-coverage'),
|
||||||
require('@angular-devkit/build-angular/plugins/karma')
|
|
||||||
],
|
],
|
||||||
client: {
|
client: {
|
||||||
jasmine: {
|
jasmine: {
|
||||||
|
|||||||
51
package.json
51
package.json
@@ -11,40 +11,39 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular-devkit/build-angular": "^18.1.1",
|
"@angular/animations": "^20.0.0",
|
||||||
"@angular/animations": "^18.1.1",
|
"@angular/cdk": "^20.0.1",
|
||||||
"@angular/cdk": "^18.1.1",
|
"@angular/common": "^20.0.0",
|
||||||
"@angular/common": "^18.1.1",
|
"@angular/compiler": "^20.0.0",
|
||||||
"@angular/compiler": "^18.1.1",
|
"@angular/core": "^20.0.0",
|
||||||
"@angular/core": "^18.1.1",
|
"@angular/forms": "^20.0.0",
|
||||||
"@angular/forms": "^18.1.1",
|
"@angular/material": "^20.0.1",
|
||||||
"@angular/material": "^18.1.1",
|
"@angular/platform-browser": "^20.0.0",
|
||||||
"@angular/platform-browser": "^18.1.1",
|
"@angular/platform-browser-dynamic": "^20.0.0",
|
||||||
"@angular/platform-browser-dynamic": "^18.1.1",
|
"@angular/router": "^20.0.0",
|
||||||
"@angular/router": "^18.1.1",
|
"@angular/service-worker": "^20.0.0",
|
||||||
"@angular/service-worker": "^18.1.1",
|
|
||||||
"@tauri-apps/api": "^1.6.0",
|
"@tauri-apps/api": "^1.6.0",
|
||||||
"highlight.js": "^11.10.0",
|
"@types/json-schema": "^7.0.15",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"monaco-editor": "^0.50.0",
|
"monaco-editor": "^0.50.0",
|
||||||
"ngx-monaco-editor-v2": "^18.0.1",
|
"ngx-monaco-editor-v2": "^19.0.2",
|
||||||
"protobufjs": "^7.3.2",
|
"protobufjs": "^7.4.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.2",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.8.1"
|
||||||
"zone.js": "^0.14.8"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^18.1.1",
|
"@angular/build": "^20.0.0",
|
||||||
"@angular/cli": "^18.1.1",
|
"@angular/cli": "^20.0.0",
|
||||||
"@angular/compiler-cli": "^18.1.1",
|
"@angular/compiler-cli": "^20.0.0",
|
||||||
"@tauri-apps/cli": "^1.6.0",
|
"@tauri-apps/cli": "^1.6.3",
|
||||||
"@types/jasmine": "^5.1.4",
|
"@types/jasmine": "^5.1.7",
|
||||||
"@types/protobufjs": "^6.0.0",
|
"@types/protobufjs": "^6.0.0",
|
||||||
"jasmine-core": "^5.1.2",
|
"jasmine-core": "^5.6.0",
|
||||||
"karma": "^6.4.3",
|
"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",
|
||||||
"typescript": "^5.5.3"
|
"typescript": "^5.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,10 @@
|
|||||||
[opened]="leftSideOpen()"
|
[opened]="leftSideOpen()"
|
||||||
(closed)="leftSideOpen.set(false)"
|
(closed)="leftSideOpen.set(false)"
|
||||||
>
|
>
|
||||||
<app-file-tree (fileSelected)="fileSelected($event)"></app-file-tree>
|
<app-file-tree
|
||||||
|
class="bubble-sidenav"
|
||||||
|
(fileSelected)="fileSelected($event)"
|
||||||
|
></app-file-tree>
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
<mat-sidenav
|
<mat-sidenav
|
||||||
[opened]="rightSideOpen()"
|
[opened]="rightSideOpen()"
|
||||||
@@ -40,6 +43,7 @@
|
|||||||
(closed)="rightSideOpen.set(false)"
|
(closed)="rightSideOpen.set(false)"
|
||||||
>
|
>
|
||||||
<app-proto-definition-selector
|
<app-proto-definition-selector
|
||||||
|
class="bubble-sidenav"
|
||||||
(messageSelected)="selectMessage($event)"
|
(messageSelected)="selectMessage($event)"
|
||||||
></app-proto-definition-selector>
|
></app-proto-definition-selector>
|
||||||
</mat-sidenav>
|
</mat-sidenav>
|
||||||
|
|||||||
@@ -33,3 +33,9 @@ app-editor {
|
|||||||
.mat-icon.filled {
|
.mat-icon.filled {
|
||||||
font-variation-settings: "FILL" 1, "wght" 400, "GRAD" 0, "opsz" 24;
|
font-variation-settings: "FILL" 1, "wght" 400, "GRAD" 0, "opsz" 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bubble-sidenav {
|
||||||
|
background-color: var(--mat-sys-secondary-container);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||||
import { CommonModule } from '@angular/common';
|
import { Component, signal, inject } from '@angular/core';
|
||||||
import { Component, computed, signal } from '@angular/core';
|
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
import { MatTreeModule } from '@angular/material/tree';
|
import { MatTreeModule } from '@angular/material/tree';
|
||||||
import { RouterOutlet } from '@angular/router';
|
|
||||||
import { EditorComponent } from './editor/editor.component';
|
import { EditorComponent } from './editor/editor.component';
|
||||||
import {
|
import {
|
||||||
FileOrFolder,
|
FileOrFolder,
|
||||||
@@ -15,15 +13,14 @@ import {
|
|||||||
} from './file-tree/file-tree.component';
|
} from './file-tree/file-tree.component';
|
||||||
import { ProtoMessage } from './model/proto-message.model';
|
import { ProtoMessage } from './model/proto-message.model';
|
||||||
import { ProtoDefinitionSelectorComponent } from './proto-definition-selector/proto-definition-selector.component';
|
import { ProtoDefinitionSelectorComponent } from './proto-definition-selector/proto-definition-selector.component';
|
||||||
|
|
||||||
const mobileBreakpoints = [Breakpoints.Handset, Breakpoints.TabletPortrait];
|
const mobileBreakpoints = [Breakpoints.Handset, Breakpoints.TabletPortrait];
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
EditorComponent,
|
EditorComponent,
|
||||||
FileTreeComponent,
|
FileTreeComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
@@ -32,10 +29,11 @@ const mobileBreakpoints = [Breakpoints.Handset, Breakpoints.TabletPortrait];
|
|||||||
MatToolbarModule,
|
MatToolbarModule,
|
||||||
MatTreeModule,
|
MatTreeModule,
|
||||||
ProtoDefinitionSelectorComponent,
|
ProtoDefinitionSelectorComponent,
|
||||||
RouterOutlet,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
|
private breakpointObserver = inject(BreakpointObserver);
|
||||||
|
|
||||||
protected selectedFile = signal<FileOrFolder | undefined>(undefined);
|
protected selectedFile = signal<FileOrFolder | undefined>(undefined);
|
||||||
protected selectedMessage = signal<ProtoMessage | undefined>(undefined);
|
protected selectedMessage = signal<ProtoMessage | undefined>(undefined);
|
||||||
protected rightSideOpen = signal(true);
|
protected rightSideOpen = signal(true);
|
||||||
@@ -46,7 +44,9 @@ export class AppComponent {
|
|||||||
this.breakpointObserver.isMatched(mobileBreakpoints)
|
this.breakpointObserver.isMatched(mobileBreakpoints)
|
||||||
);
|
);
|
||||||
|
|
||||||
constructor(private breakpointObserver: BreakpointObserver) {
|
constructor() {
|
||||||
|
const breakpointObserver = this.breakpointObserver;
|
||||||
|
|
||||||
breakpointObserver
|
breakpointObserver
|
||||||
.observe(mobileBreakpoints)
|
.observe(mobileBreakpoints)
|
||||||
.pipe(takeUntilDestroyed())
|
.pipe(takeUntilDestroyed())
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
APP_INITIALIZER,
|
|
||||||
ApplicationConfig,
|
ApplicationConfig,
|
||||||
isDevMode,
|
isDevMode,
|
||||||
provideExperimentalZonelessChangeDetection,
|
provideZonelessChangeDetection,
|
||||||
|
provideAppInitializer,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
@@ -16,30 +17,25 @@ import hljs from 'highlight.js/lib/core';
|
|||||||
import json from 'highlight.js/lib/languages/json';
|
import json from 'highlight.js/lib/languages/json';
|
||||||
import { provideMonacoEditor } from 'ngx-monaco-editor-v2';
|
import { provideMonacoEditor } from 'ngx-monaco-editor-v2';
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
|
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideExperimentalZonelessChangeDetection(),
|
provideZonelessChangeDetection(),
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
provideAnimationsAsync(),
|
provideAnimationsAsync(),
|
||||||
{
|
provideAppInitializer(() => {
|
||||||
provide: APP_INITIALIZER,
|
hljs.registerLanguage('json', json);
|
||||||
useValue: () => {
|
}),
|
||||||
hljs.registerLanguage('json', json);
|
provideAppInitializer(() => {
|
||||||
},
|
const initializerFn = ((iconRegistry: MatIconRegistry) => () => {
|
||||||
multi: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: APP_INITIALIZER,
|
|
||||||
useFactory: (iconRegistry: MatIconRegistry) => () => {
|
|
||||||
iconRegistry.registerFontClassAlias(
|
iconRegistry.registerFontClassAlias(
|
||||||
'material-symbols-rounded',
|
'material-symbols-rounded',
|
||||||
'material-symbols-rounded'
|
'material-symbols-rounded'
|
||||||
);
|
);
|
||||||
},
|
})(inject(MatIconRegistry));
|
||||||
deps: [MatIconRegistry],
|
return initializerFn();
|
||||||
multi: true,
|
}),
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: MAT_ICON_DEFAULT_OPTIONS,
|
provide: MAT_ICON_DEFAULT_OPTIONS,
|
||||||
useValue: { fontSet: 'material-symbols-rounded' },
|
useValue: { fontSet: 'material-symbols-rounded' },
|
||||||
@@ -49,5 +45,11 @@ export const appConfig: ApplicationConfig = {
|
|||||||
enabled: !isDevMode(),
|
enabled: !isDevMode(),
|
||||||
registrationStrategy: 'registerWhenStable:30000',
|
registrationStrategy: 'registerWhenStable:30000',
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
||||||
|
// Filled doesn't look great with low density, by default the label is lost, and adding
|
||||||
|
// it back makes it look cramped
|
||||||
|
useValue: { appearance: 'outline' },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="editor-items">
|
<div class="editor-items">
|
||||||
@if(values()) { @for (item of selectedMessage().values; track $index) {
|
@if(values()) { @for (item of selectedMessage().values; track $index) {
|
||||||
<app-proto-field
|
<app-proto-field
|
||||||
[label]="item.name"
|
[label]="item.friendlyName || item.name"
|
||||||
[configuration]="item.configuration"
|
[configuration]="item.configuration"
|
||||||
[value]="values()[item.name]"
|
[value]="values()[item.name]"
|
||||||
(valueChange)="updateValue(item.name, $event)"
|
(valueChange)="updateValue(item.name, $event)"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
@@ -8,6 +7,7 @@ import {
|
|||||||
input,
|
input,
|
||||||
signal,
|
signal,
|
||||||
viewChild,
|
viewChild,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
@@ -29,9 +29,7 @@ type PreviewType = 'raw' | 'edit' | 'diff';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-editor',
|
selector: 'app-editor',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
ProtoFieldComponent,
|
ProtoFieldComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatButtonToggleModule,
|
MatButtonToggleModule,
|
||||||
@@ -43,6 +41,8 @@ type PreviewType = 'raw' | 'edit' | 'diff';
|
|||||||
styleUrl: './editor.component.scss',
|
styleUrl: './editor.component.scss',
|
||||||
})
|
})
|
||||||
export class EditorComponent {
|
export class EditorComponent {
|
||||||
|
private snackBar = inject(MatSnackBar);
|
||||||
|
|
||||||
selectedFile = input<FileOrFolder>();
|
selectedFile = input<FileOrFolder>();
|
||||||
selectedMessage = input.required<ProtoMessage>();
|
selectedMessage = input.required<ProtoMessage>();
|
||||||
indentSize = input<number>(2);
|
indentSize = input<number>(2);
|
||||||
@@ -80,7 +80,9 @@ export class EditorComponent {
|
|||||||
|
|
||||||
private code = viewChild<ElementRef<HTMLElement>>('code');
|
private code = viewChild<ElementRef<HTMLElement>>('code');
|
||||||
|
|
||||||
constructor(sanitizer: DomSanitizer, private snackBar: MatSnackBar) {
|
constructor() {
|
||||||
|
const sanitizer = inject(DomSanitizer);
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const element = this.code()?.nativeElement;
|
const element = this.code()?.nativeElement;
|
||||||
if (element) {
|
if (element) {
|
||||||
@@ -98,17 +100,14 @@ export class EditorComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
effect(
|
effect(() => {
|
||||||
() => {
|
const message = this.selectedMessage();
|
||||||
const message = this.selectedMessage();
|
this.values.set(
|
||||||
this.values.set(
|
Object.fromEntries(
|
||||||
Object.fromEntries(
|
message.values.map((value) => [[value.name, undefined]])
|
||||||
message.values.map((value) => [[value.name, undefined]])
|
)
|
||||||
)
|
);
|
||||||
);
|
});
|
||||||
},
|
|
||||||
{ allowSignalWrites: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
effect(async () => {
|
effect(async () => {
|
||||||
const selectedFile = this.selectedFile();
|
const selectedFile = this.selectedFile();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-wrapper {
|
.row-wrapper {
|
||||||
@@ -11,7 +12,3 @@
|
|||||||
app-proto-field {
|
app-proto-field {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-button {
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -12,32 +11,40 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { ListMessage } from '../../model/proto-message.model';
|
import { ListMessage } from '../../model/proto-message.model';
|
||||||
import { ProtoFieldComponent } from '../proto-field/proto-field.component';
|
import { ProtoFieldComponent } from '../proto-field/proto-field.component';
|
||||||
|
import { MatExpansionModule } from '@angular/material/expansion';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-list-field',
|
selector: 'app-list-field',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatExpansionModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
forwardRef(() => ProtoFieldComponent),
|
forwardRef(() => ProtoFieldComponent),
|
||||||
],
|
],
|
||||||
template: `<h3>{{ label() }}</h3>
|
template: `
|
||||||
@if(values()) { @for(value of values(); track $index) {
|
<mat-accordion>
|
||||||
<div class="row-wrapper">
|
<mat-expansion-panel [expanded]="true">
|
||||||
<app-proto-field
|
<mat-expansion-panel-header>
|
||||||
[configuration]="configuration().subConfiguration"
|
<mat-panel-title>{{ label() }}</mat-panel-title>
|
||||||
[value]="value"
|
</mat-expansion-panel-header>
|
||||||
(valueChange)="updateValue($index, $event)"
|
@if(values()) { @for(value of values(); track $index) {
|
||||||
></app-proto-field>
|
<div class="row-wrapper">
|
||||||
<button mat-icon-button (click)="remove($index)">
|
<app-proto-field
|
||||||
<mat-icon>remove</mat-icon>
|
[configuration]="configuration().subConfiguration"
|
||||||
</button>
|
[value]="value"
|
||||||
</div>
|
(valueChange)="updateValue($index, $event)"
|
||||||
} }
|
></app-proto-field>
|
||||||
<button mat-icon-button class="add-button" (click)="add()">
|
<button mat-icon-button (click)="remove($index)">
|
||||||
<mat-icon>add</mat-icon>
|
<mat-icon>remove</mat-icon>
|
||||||
</button>`,
|
</button>
|
||||||
|
</div>
|
||||||
|
} }
|
||||||
|
<button mat-icon-button class="add-button" (click)="add()">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
</mat-accordion>
|
||||||
|
`,
|
||||||
styleUrl: './list-field.component.scss',
|
styleUrl: './list-field.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-wrapper {
|
.row-wrapper {
|
||||||
@@ -12,10 +13,6 @@ app-proto-field {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-right: 10px;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-button {
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -12,39 +11,47 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MapMessage } from '../../model/proto-message.model';
|
import { MapMessage } from '../../model/proto-message.model';
|
||||||
import { ProtoFieldComponent } from '../proto-field/proto-field.component';
|
import { ProtoFieldComponent } from '../proto-field/proto-field.component';
|
||||||
|
import { MatExpansionModule } from '@angular/material/expansion';
|
||||||
|
|
||||||
const keyIsEmpty = (key: string | number) => key == null || key === '';
|
const keyIsEmpty = (key: string | number) => key == null || key === '';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-map-field',
|
selector: 'app-map-field',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
forwardRef(() => ProtoFieldComponent),
|
forwardRef(() => ProtoFieldComponent),
|
||||||
|
MatExpansionModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
],
|
],
|
||||||
template: `<h3>{{ label() }}</h3>
|
template: `
|
||||||
@if(valuePairs()) { @for(value of valuePairs(); track $index) {
|
<mat-accordion>
|
||||||
<div class="row-wrapper">
|
<mat-expansion-panel [expanded]="true">
|
||||||
<app-proto-field
|
<mat-expansion-panel-header>
|
||||||
[configuration]="configuration().keyConfiguration"
|
<mat-panel-title>{{ label() }}</mat-panel-title>
|
||||||
[value]="value[0]"
|
</mat-expansion-panel-header>
|
||||||
(valueChange)="updateKey($index, $event)"
|
@if(valuePairs()) { @for(value of valuePairs(); track $index) {
|
||||||
></app-proto-field>
|
<div class="row-wrapper">
|
||||||
<app-proto-field
|
<app-proto-field
|
||||||
[configuration]="configuration().valueConfiguration"
|
[configuration]="configuration().keyConfiguration"
|
||||||
[value]="value[1]"
|
[value]="value[0]"
|
||||||
(valueChange)="updateValue($index, $event)"
|
(valueChange)="updateKey($index, $event)"
|
||||||
></app-proto-field>
|
></app-proto-field>
|
||||||
<button mat-icon-button (click)="remove($index)">
|
<app-proto-field
|
||||||
<mat-icon>remove</mat-icon>
|
[configuration]="configuration().valueConfiguration"
|
||||||
</button>
|
[value]="value[1]"
|
||||||
</div>
|
(valueChange)="updateValue($index, $event)"
|
||||||
} }
|
></app-proto-field>
|
||||||
<button mat-icon-button class="add-button" (click)="add()">
|
<button mat-icon-button (click)="remove($index)">
|
||||||
<mat-icon>add</mat-icon>
|
<mat-icon>remove</mat-icon>
|
||||||
</button>`,
|
</button>
|
||||||
|
</div>
|
||||||
|
} }
|
||||||
|
<button mat-icon-button class="add-button" (click)="add()">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
</mat-accordion>
|
||||||
|
`,
|
||||||
styleUrl: './map-field.component.scss',
|
styleUrl: './map-field.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
@@ -58,22 +65,19 @@ export class MapFieldComponent {
|
|||||||
private changedInternal = false;
|
private changedInternal = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(
|
effect(() => {
|
||||||
() => {
|
// TODO: Super hacky but can't really think of another way to keep these in sync
|
||||||
// TODO: Super hacky but can't really think of another way to keep these in sync
|
// without removing an entry when the key gets blanked. Would need an alternate
|
||||||
// without removing an entry when the key gets blanked. Would need an alternate
|
// design that updated on blur only perhaps
|
||||||
// design that updated on blur only perhaps
|
if (this.changedInternal) {
|
||||||
if (this.changedInternal) {
|
this.changedInternal = false;
|
||||||
this.changedInternal = false;
|
return;
|
||||||
return;
|
}
|
||||||
}
|
const values = this.values();
|
||||||
const values = this.values();
|
if (values) {
|
||||||
if (values) {
|
this.valuePairs.set(Object.entries(values));
|
||||||
this.valuePairs.set(Object.entries(values));
|
}
|
||||||
}
|
});
|
||||||
},
|
|
||||||
{ allowSignalWrites: true }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add() {
|
add() {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -9,24 +8,35 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ObjectMessage } from '../../model/proto-message.model';
|
import { ObjectMessage } from '../../model/proto-message.model';
|
||||||
import { ProtoFieldComponent } from '../proto-field/proto-field.component';
|
import { ProtoFieldComponent } from '../proto-field/proto-field.component';
|
||||||
|
import { MatExpansionModule } from '@angular/material/expansion';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-object-field',
|
selector: 'app-object-field',
|
||||||
standalone: true,
|
imports: [MatExpansionModule, forwardRef(() => ProtoFieldComponent)],
|
||||||
imports: [CommonModule, forwardRef(() => ProtoFieldComponent)],
|
template: `
|
||||||
template: `<h3>
|
<mat-accordion>
|
||||||
{{ label() }} ({{ configuration().messageDefinition.name }})
|
<mat-expansion-panel [expanded]="true">
|
||||||
</h3>
|
<mat-expansion-panel-header>
|
||||||
<div>
|
<mat-panel-title>
|
||||||
@for (item of configuration().messageDefinition.values; track $index) {
|
{{ label() ?? configuration().messageDefinition.name }}
|
||||||
<app-proto-field
|
</mat-panel-title>
|
||||||
[label]="item.name"
|
@if(label()) {
|
||||||
[configuration]="item.configuration"
|
<mat-panel-description>
|
||||||
[value]="currentValue()[item.name]"
|
{{ configuration().messageDefinition.name }}
|
||||||
(valueChange)="updateValue(item.name, $event)"
|
</mat-panel-description>
|
||||||
></app-proto-field>
|
}
|
||||||
}
|
</mat-expansion-panel-header>
|
||||||
</div>`,
|
@for (item of configuration().messageDefinition.values; track $index) {
|
||||||
|
<app-proto-field
|
||||||
|
[label]="item.friendlyName || item.name"
|
||||||
|
[configuration]="item.configuration"
|
||||||
|
[value]="currentValue()[item.name]"
|
||||||
|
(valueChange)="updateValue(item.name, $event)"
|
||||||
|
></app-proto-field>
|
||||||
|
}
|
||||||
|
</mat-expansion-panel>
|
||||||
|
</mat-accordion>
|
||||||
|
`,
|
||||||
styleUrl: './object-field.component.scss',
|
styleUrl: './object-field.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -28,9 +27,7 @@ import { StringFieldComponent } from '../string-field/string-field.component';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-proto-field',
|
selector: 'app-proto-field',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ListFieldComponent,
|
ListFieldComponent,
|
||||||
MapFieldComponent,
|
MapFieldComponent,
|
||||||
@@ -58,14 +55,17 @@ import { StringFieldComponent } from '../string-field/string-field.component';
|
|||||||
[min]="numericConfiguration().min ?? null"
|
[min]="numericConfiguration().min ?? null"
|
||||||
[max]="numericConfiguration().max ?? null"
|
[max]="numericConfiguration().max ?? null"
|
||||||
/>
|
/>
|
||||||
<mat-hint *ngIf="number.hasError('min')"
|
@if(number.hasError('min')) {
|
||||||
|
<mat-hint
|
||||||
>Number should not be less than
|
>Number should not be less than
|
||||||
{{ numericConfiguration().min }}</mat-hint
|
{{ numericConfiguration().min }}</mat-hint
|
||||||
>
|
>
|
||||||
<mat-hint *ngIf="number.hasError('max')"
|
} @if(number.hasError('max')) {
|
||||||
|
<mat-hint
|
||||||
>Number should not greater than
|
>Number should not greater than
|
||||||
{{ numericConfiguration().max }}</mat-hint
|
{{ numericConfiguration().max }}</mat-hint
|
||||||
>
|
>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
} @case (MessageTypeEnum.Boolean) {
|
} @case (MessageTypeEnum.Boolean) {
|
||||||
<p>
|
<p>
|
||||||
@@ -75,10 +75,11 @@ import { StringFieldComponent } from '../string-field/string-field.component';
|
|||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>{{ label() }}</mat-label>
|
<mat-label>{{ label() }}</mat-label>
|
||||||
<mat-select [(value)]="value">
|
<mat-select [(value)]="value">
|
||||||
@for(option of enumConfiguration().options; track
|
|
||||||
enumConfiguration()!.options) {
|
|
||||||
<mat-option>None</mat-option>
|
<mat-option>None</mat-option>
|
||||||
<mat-option [value]="option">{{ option }}</mat-option>
|
@for(option of enumConfiguration().options; track option) {
|
||||||
|
<mat-option [value]="option.protoName">{{
|
||||||
|
option.friendlyName || option.protoName
|
||||||
|
}}</mat-option>
|
||||||
}
|
}
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -13,9 +12,7 @@ import { MonacoEditorModule } from 'ngx-monaco-editor-v2';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-string-field',
|
selector: 'app-string-field',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
FormsModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: var(--mat-sidenav-container-shape);
|
|
||||||
padding: var(--mat-sidenav-container-shape);
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: background-color 100ms linear;
|
transition: background-color 100ms linear;
|
||||||
overflow-x: hidden;
|
overflow-x: auto;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-tree {
|
.mat-tree {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
min-width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-tree-node {
|
.mat-tree-node {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
@@ -40,8 +39,7 @@ const collator = new Intl.Collator(undefined, { numeric: true });
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-file-tree',
|
selector: 'app-file-tree',
|
||||||
standalone: true,
|
imports: [MatButtonModule, MatIconModule, MatTreeModule],
|
||||||
imports: [CommonModule, MatButtonModule, MatIconModule, MatTreeModule],
|
|
||||||
templateUrl: './file-tree.component.html',
|
templateUrl: './file-tree.component.html',
|
||||||
styleUrl: './file-tree.component.scss',
|
styleUrl: './file-tree.component.scss',
|
||||||
})
|
})
|
||||||
@@ -151,7 +149,10 @@ export class FileTreeComponent implements OnInit, OnDestroy {
|
|||||||
const file = files[i];
|
const file = files[i];
|
||||||
if (file.webkitRelativePath?.includes('/')) {
|
if (file.webkitRelativePath?.includes('/')) {
|
||||||
// Got a file in a folder, so put it into the appropriate folder in the tree
|
// Got a file in a folder, so put it into the appropriate folder in the tree
|
||||||
const splitFilePath = file.webkitRelativePath.split('/');
|
if (i === 0) {
|
||||||
|
this.selectedDirectory.set(file.webkitRelativePath.split('/')[0]);
|
||||||
|
}
|
||||||
|
const splitFilePath = file.webkitRelativePath.split('/').slice(1);
|
||||||
let currentChildren: FileOrFolder[] | undefined = mappedFiles;
|
let currentChildren: FileOrFolder[] | undefined = mappedFiles;
|
||||||
for (let j = 0; j < splitFilePath.length - 1; j++) {
|
for (let j = 0; j < splitFilePath.length - 1; j++) {
|
||||||
const relativePath = splitFilePath[j];
|
const relativePath = splitFilePath[j];
|
||||||
|
|||||||
@@ -38,8 +38,13 @@ export interface ObjectMessage extends MessageConfiguration {
|
|||||||
|
|
||||||
export interface RawMessage extends MessageConfiguration {}
|
export interface RawMessage extends MessageConfiguration {}
|
||||||
|
|
||||||
|
export interface EnumMessageOption {
|
||||||
|
protoName: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EnumMessage extends MessageConfiguration {
|
export interface EnumMessage extends MessageConfiguration {
|
||||||
options: string[];
|
options: EnumMessageOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageConfiguration {
|
export interface MessageConfiguration {
|
||||||
@@ -90,11 +95,12 @@ export const RawMessage = (): RawMessage => ({ type: MessageTypeEnum.Raw });
|
|||||||
|
|
||||||
export const EnumMessage = (options: string[]) => ({
|
export const EnumMessage = (options: string[]) => ({
|
||||||
type: MessageTypeEnum.Enum,
|
type: MessageTypeEnum.Enum,
|
||||||
options,
|
options: options.map((option) => ({ protoName: option })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export interface ProtoMessageField<T extends MessageConfiguration> {
|
export interface ProtoMessageField<T extends MessageConfiguration> {
|
||||||
name: string;
|
name: string;
|
||||||
|
friendlyName?: string;
|
||||||
configuration: T;
|
configuration: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +125,9 @@ export const UnknownProto = (
|
|||||||
): ProtoMessage => ({
|
): ProtoMessage => ({
|
||||||
name,
|
name,
|
||||||
fullName,
|
fullName,
|
||||||
values: [{ name: 'Raw JSON', configuration: RawMessage() }],
|
values: [
|
||||||
|
{ name: 'Raw JSON', friendlyName: 'Raw JSON', configuration: RawMessage() },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EDITABLE_MESSAGE_TYPES = [
|
export const EDITABLE_MESSAGE_TYPES = [
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
|
EnumMessage,
|
||||||
ListMessage,
|
ListMessage,
|
||||||
MapMessage,
|
MapMessage,
|
||||||
MessageConfiguration,
|
MessageConfiguration,
|
||||||
MessageTypeEnum,
|
MessageTypeEnum,
|
||||||
NumericMessage,
|
NumericMessage,
|
||||||
ObjectMessage,
|
ObjectMessage,
|
||||||
ProtoMessageField,
|
|
||||||
StringMessage,
|
StringMessage,
|
||||||
} from '../../../model/proto-message.model';
|
} from '../../../model/proto-message.model';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
@@ -18,12 +17,11 @@ import { ListEditorFieldComponent } from '../list-editor-field/list-editor-field
|
|||||||
import { MapEditorFieldComponent } from '../map-editor-field/map-editor-field.component';
|
import { MapEditorFieldComponent } from '../map-editor-field/map-editor-field.component';
|
||||||
import { ObjectEditorFieldComponent } from '../object-editor-field/object-editor-field.component';
|
import { ObjectEditorFieldComponent } from '../object-editor-field/object-editor-field.component';
|
||||||
import { StringEditorFieldComponent } from '../string-editor-field/string-editor-field.component';
|
import { StringEditorFieldComponent } from '../string-editor-field/string-editor-field.component';
|
||||||
|
import { EnumEditorFieldComponent } from '../enum-editor-field/enum-editor-field.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-definition-editor-field',
|
selector: 'app-definition-editor-field',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ListEditorFieldComponent,
|
ListEditorFieldComponent,
|
||||||
MapEditorFieldComponent,
|
MapEditorFieldComponent,
|
||||||
@@ -32,6 +30,7 @@ import { StringEditorFieldComponent } from '../string-editor-field/string-editor
|
|||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
ObjectEditorFieldComponent,
|
ObjectEditorFieldComponent,
|
||||||
StringEditorFieldComponent,
|
StringEditorFieldComponent,
|
||||||
|
EnumEditorFieldComponent,
|
||||||
],
|
],
|
||||||
template: ` @switch (fieldConfiguration().type) {
|
template: ` @switch (fieldConfiguration().type) {
|
||||||
@case(MessageTypeEnum.String) {
|
@case(MessageTypeEnum.String) {
|
||||||
@@ -49,9 +48,11 @@ import { StringEditorFieldComponent } from '../string-editor-field/string-editor
|
|||||||
[(ngModel)]="configuration.min"
|
[(ngModel)]="configuration.min"
|
||||||
[max]="configuration.max ?? null"
|
[max]="configuration.max ?? null"
|
||||||
/>
|
/>
|
||||||
<mat-hint *ngIf="min.hasError('max')"
|
@if(min.hasError('max')) {
|
||||||
|
<mat-hint
|
||||||
>Min should not be greater than {{ configuration.max }}</mat-hint
|
>Min should not be greater than {{ configuration.max }}</mat-hint
|
||||||
>
|
>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>Max</mat-label>
|
<mat-label>Max</mat-label>
|
||||||
@@ -62,9 +63,9 @@ import { StringEditorFieldComponent } from '../string-editor-field/string-editor
|
|||||||
[(ngModel)]="configuration.max"
|
[(ngModel)]="configuration.max"
|
||||||
[min]="configuration.min ?? null"
|
[min]="configuration.min ?? null"
|
||||||
/>
|
/>
|
||||||
<mat-hint *ngIf="max.hasError('min')"
|
@if(max.hasError('min')) {
|
||||||
>Max should not be less than {{ configuration.min }}</mat-hint
|
<mat-hint>Max should not be less than {{ configuration.min }}</mat-hint>
|
||||||
>
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
} @case (MessageTypeEnum.List) {
|
} @case (MessageTypeEnum.List) {
|
||||||
<app-list-editor-field
|
<app-list-editor-field
|
||||||
@@ -78,6 +79,10 @@ import { StringEditorFieldComponent } from '../string-editor-field/string-editor
|
|||||||
<app-object-editor-field
|
<app-object-editor-field
|
||||||
[field]="objectConfiguration(fieldConfiguration())"
|
[field]="objectConfiguration(fieldConfiguration())"
|
||||||
></app-object-editor-field>
|
></app-object-editor-field>
|
||||||
|
} @case(MessageTypeEnum.Enum) {
|
||||||
|
<app-enum-editor-field
|
||||||
|
[configuration]="enumConfiguration(fieldConfiguration())"
|
||||||
|
></app-enum-editor-field>
|
||||||
} }`,
|
} }`,
|
||||||
styleUrl: './definition-editor-field.component.css',
|
styleUrl: './definition-editor-field.component.css',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -105,5 +110,9 @@ export class DefinitionEditorFieldComponent {
|
|||||||
return configuration as ObjectMessage;
|
return configuration as ObjectMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected enumConfiguration(configuration: MessageConfiguration) {
|
||||||
|
return configuration as EnumMessage;
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly MessageTypeEnum = MessageTypeEnum;
|
protected readonly MessageTypeEnum = MessageTypeEnum;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mat-form-field {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||||
import {
|
import {
|
||||||
EDITABLE_MESSAGE_TYPES,
|
|
||||||
ListMessage,
|
ListMessage,
|
||||||
MessageConfiguration,
|
MessageConfiguration,
|
||||||
MessageTypeEnum,
|
MessageTypeEnum,
|
||||||
@@ -11,14 +9,18 @@ import {
|
|||||||
ProtoMessage,
|
ProtoMessage,
|
||||||
} from '../../model/proto-message.model';
|
} from '../../model/proto-message.model';
|
||||||
import { DefinitionEditorFieldComponent } from './definition-editor-field/definition-editor-field.component';
|
import { DefinitionEditorFieldComponent } from './definition-editor-field/definition-editor-field.component';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-definition-editor',
|
selector: 'app-definition-editor',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
FormsModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
DefinitionEditorFieldComponent,
|
DefinitionEditorFieldComponent,
|
||||||
],
|
],
|
||||||
template: `
|
template: `
|
||||||
@@ -26,6 +28,14 @@ import { DefinitionEditorFieldComponent } from './definition-editor-field/defini
|
|||||||
<mat-dialog-content>
|
<mat-dialog-content>
|
||||||
@for (field of editableMessages; track $index) {
|
@for (field of editableMessages; track $index) {
|
||||||
<h3>{{ field.name }}</h3>
|
<h3>{{ field.name }}</h3>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Friendly Name</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[(ngModel)]="field.friendlyName"
|
||||||
|
placeholder="Optional"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
<app-definition-editor-field
|
<app-definition-editor-field
|
||||||
[fieldConfiguration]="field.configuration"
|
[fieldConfiguration]="field.configuration"
|
||||||
></app-definition-editor-field>
|
></app-definition-editor-field>
|
||||||
@@ -39,28 +49,7 @@ import { DefinitionEditorFieldComponent } from './definition-editor-field/defini
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class DefinitionEditorComponent {
|
export class DefinitionEditorComponent {
|
||||||
protected editableMessages = this.protoMessage.values.filter((message) =>
|
protected protoMessage = inject<ProtoMessage>(MAT_DIALOG_DATA);
|
||||||
this.filterMessageConfiguration(message.configuration)
|
|
||||||
);
|
|
||||||
|
|
||||||
private filterMessageConfiguration(
|
protected editableMessages = this.protoMessage.values;
|
||||||
configuration: MessageConfiguration
|
|
||||||
): boolean {
|
|
||||||
if (configuration.type === MessageTypeEnum.List) {
|
|
||||||
return this.filterMessageConfiguration(
|
|
||||||
(configuration as ListMessage).subConfiguration
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (configuration.type === MessageTypeEnum.Object) {
|
|
||||||
// Ensure at least one nested message can be configured
|
|
||||||
return !!(configuration as ObjectMessage).messageDefinition.values.find(
|
|
||||||
(message) => this.filterMessageConfiguration(message.configuration)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Map can always be configured, as key needs to be a string or numeric type
|
|
||||||
return EDITABLE_MESSAGE_TYPES.includes(configuration.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(@Inject(MAT_DIALOG_DATA) protected protoMessage: ProtoMessage) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mat-form-field {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||||
|
import { EnumMessage } from '../../../model/proto-message.model';
|
||||||
|
import { MatFormFieldModule, MatLabel } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-enum-editor-field',
|
||||||
|
standalone: true,
|
||||||
|
imports: [FormsModule, MatFormFieldModule, MatInputModule, MatLabel],
|
||||||
|
template: `
|
||||||
|
@for (option of configuration().options; track $index) {
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>{{ option.protoName }}</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[(ngModel)]="option.friendlyName"
|
||||||
|
placeholder="Friendly Name"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
styleUrl: './enum-editor-field.component.css',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class EnumEditorFieldComponent {
|
||||||
|
configuration = input.required<EnumMessage>();
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -10,8 +9,7 @@ import { ListMessage } from '../../../model/proto-message.model';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-list-editor-field',
|
selector: 'app-list-editor-field',
|
||||||
standalone: true,
|
imports: [forwardRef(() => DefinitionEditorFieldComponent)],
|
||||||
imports: [CommonModule, forwardRef(() => DefinitionEditorFieldComponent)],
|
|
||||||
template: `<app-definition-editor-field
|
template: `<app-definition-editor-field
|
||||||
[fieldConfiguration]="field().subConfiguration"
|
[fieldConfiguration]="field().subConfiguration"
|
||||||
></app-definition-editor-field>`,
|
></app-definition-editor-field>`,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -14,8 +13,7 @@ import {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-map-editor-field',
|
selector: 'app-map-editor-field',
|
||||||
standalone: true,
|
imports: [forwardRef(() => DefinitionEditorFieldComponent)],
|
||||||
imports: [CommonModule, forwardRef(() => DefinitionEditorFieldComponent)],
|
|
||||||
template: `
|
template: `
|
||||||
<h4>Key Configuration</h4>
|
<h4>Key Configuration</h4>
|
||||||
<app-definition-editor-field
|
<app-definition-editor-field
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
@@ -6,32 +5,43 @@ import {
|
|||||||
forwardRef,
|
forwardRef,
|
||||||
input,
|
input,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import { ObjectMessage } from '../../../model/proto-message.model';
|
||||||
EDITABLE_MESSAGE_TYPES,
|
|
||||||
ObjectMessage,
|
|
||||||
} from '../../../model/proto-message.model';
|
|
||||||
import { DefinitionEditorFieldComponent } from '../definition-editor-field/definition-editor-field.component';
|
import { DefinitionEditorFieldComponent } from '../definition-editor-field/definition-editor-field.component';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-object-editor-field',
|
selector: 'app-object-editor-field',
|
||||||
standalone: true,
|
imports: [
|
||||||
imports: [CommonModule, forwardRef(() => DefinitionEditorFieldComponent)],
|
FormsModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
forwardRef(() => DefinitionEditorFieldComponent),
|
||||||
|
],
|
||||||
template: `
|
template: `
|
||||||
@for (field of editableFields(); track $index) {
|
@for (field of editableFields(); track $index) {
|
||||||
<h4>{{ field.name }}</h4>
|
<h4>{{ field.name }}</h4>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Friendly Name</mat-label>
|
||||||
|
<input matInput [(ngModel)]="field.friendlyName" placeholder="Optional" />
|
||||||
|
</mat-form-field>
|
||||||
<app-definition-editor-field
|
<app-definition-editor-field
|
||||||
[fieldConfiguration]="field.configuration"
|
[fieldConfiguration]="field.configuration"
|
||||||
></app-definition-editor-field>
|
></app-definition-editor-field>
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
styles: `
|
||||||
|
mat-form-field {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ObjectEditorFieldComponent {
|
export class ObjectEditorFieldComponent {
|
||||||
field = input.required<ObjectMessage>();
|
field = input.required<ObjectMessage>();
|
||||||
|
|
||||||
protected editableFields = computed(() =>
|
protected editableFields = computed(
|
||||||
this.field().messageDefinition.values.filter((field) =>
|
() => this.field().messageDefinition.values
|
||||||
EDITABLE_MESSAGE_TYPES.includes(field.configuration.type)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
@@ -8,15 +7,9 @@ import { MatInputModule } from '@angular/material/input';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-string-editor-field',
|
selector: 'app-string-editor-field',
|
||||||
standalone: true,
|
imports: [FormsModule, MatFormFieldModule, MatInputModule, MatSelectModule],
|
||||||
imports: [
|
template: `
|
||||||
CommonModule,
|
<mat-form-field>
|
||||||
FormsModule,
|
|
||||||
MatFormFieldModule,
|
|
||||||
MatInputModule,
|
|
||||||
MatSelectModule,
|
|
||||||
],
|
|
||||||
template: ` <mat-form-field>
|
|
||||||
<mat-label>Max Length</mat-label>
|
<mat-label>Max Length</mat-label>
|
||||||
<input matInput type="number" [(ngModel)]="configuration().maxLength" />
|
<input matInput type="number" [(ngModel)]="configuration().maxLength" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@@ -26,7 +19,8 @@ import { MatInputModule } from '@angular/material/input';
|
|||||||
<mat-option value="text">Text</mat-option>
|
<mat-option value="text">Text</mat-option>
|
||||||
<mat-option value="sql">SQL</mat-option>
|
<mat-option value="sql">SQL</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>`,
|
</mat-form-field>
|
||||||
|
`,
|
||||||
styleUrl: './string-editor-field.component.scss',
|
styleUrl: './string-editor-field.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
BooleanMessage,
|
||||||
|
EnumMessage,
|
||||||
|
ListMessage,
|
||||||
|
MessageConfiguration,
|
||||||
|
NumericMessage,
|
||||||
|
ObjectMessage,
|
||||||
|
ProtoMessage,
|
||||||
|
ProtoMessageField,
|
||||||
|
StringMessage,
|
||||||
|
UnknownProto,
|
||||||
|
} from '../model/proto-message.model';
|
||||||
|
import { JSONSchema7 } from 'json-schema';
|
||||||
|
|
||||||
|
// jSON Schema types: https://json-schema.org/understanding-json-schema/reference/type
|
||||||
|
// Note: There are specific keywords for each type that should be considered.
|
||||||
|
|
||||||
|
// Ignore required properties for now, we don't care about this yet (in protobuf everything is optional
|
||||||
|
// anyway)
|
||||||
|
// Also ignore references/don't follow them, they can be unknown/raw json. Later can add an option to follow
|
||||||
|
// trusted/untrusted references. Only use $ref that starts with # as it's a local ref/defined in the file.
|
||||||
|
// Also ignore pattern properties: https://tour.json-schema.org/content/03-Objects/01-Pattern-Properties
|
||||||
|
// Also ignore conditional validation: https://tour.json-schema.org/content/05-Conditional-Validation/01-Ensuring-Conditional-Property-Presence
|
||||||
|
|
||||||
|
// Bare minimum to support is anything required by ingey
|
||||||
|
|
||||||
|
// Definitely do want to consider reusable subschemas though, already have a similar check to this
|
||||||
|
// in protobuf parsing as well when a nested message is used: https://tour.json-schema.org/content/06-Combining-Subschemas/01-Reusing-and-Referencing-with-defs-and-ref
|
||||||
|
// Simple example schema (create a test for this):
|
||||||
|
// {
|
||||||
|
// "$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
// "$id": "https://example.com/product.schema.json",
|
||||||
|
// "title": "Product",
|
||||||
|
// "description": "A product from Acme's catalog",
|
||||||
|
// "type": "object",
|
||||||
|
// "properties": {
|
||||||
|
// "productId": {
|
||||||
|
// "description": "The unique identifier for a product",
|
||||||
|
// "type": "integer"
|
||||||
|
// },
|
||||||
|
// "productName": {
|
||||||
|
// "description": "Name of the product",
|
||||||
|
// "type": "string"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class JsonSchemaImporterService {
|
||||||
|
async parseJsonSchema(
|
||||||
|
fileName: string,
|
||||||
|
schema: JSONSchema7
|
||||||
|
): Promise<ProtoMessage[]> {
|
||||||
|
const rootElements: ProtoMessage[] = [];
|
||||||
|
if (schema['$defs']) {
|
||||||
|
// Parse all definitions first, they're referenced later
|
||||||
|
for (const [name, value] of Object.entries(schema.$defs)) {
|
||||||
|
if (value instanceof Boolean) {
|
||||||
|
throw new Error('Cannot use boolean for definition value');
|
||||||
|
}
|
||||||
|
const jsonValue = value as JSONSchema7;
|
||||||
|
const objectMessage = ObjectMessage({ name, fileName, values: [] });
|
||||||
|
const properties = jsonValue.properties;
|
||||||
|
if (!properties) {
|
||||||
|
throw new Error('Malformed JOSN Schema, no properties present');
|
||||||
|
}
|
||||||
|
for (const [name, value] of Object.entries(properties)) {
|
||||||
|
objectMessage.messageDefinition.values.push({
|
||||||
|
name,
|
||||||
|
configuration: this.convertMessageRecursive(
|
||||||
|
value as JSONSchema7,
|
||||||
|
rootElements,
|
||||||
|
name
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!schema.title || schema.type !== 'object') {
|
||||||
|
throw new Error('Unsupported JSON Schema, root must be an object');
|
||||||
|
}
|
||||||
|
const properties = schema.properties;
|
||||||
|
if (!properties) {
|
||||||
|
throw new Error('Malformed JOSN Schema, no properties present');
|
||||||
|
}
|
||||||
|
const objectMessage: ObjectMessage = ObjectMessage({
|
||||||
|
name: schema.title!,
|
||||||
|
fileName,
|
||||||
|
values: [],
|
||||||
|
});
|
||||||
|
for (const [name, value] of Object.entries(properties)) {
|
||||||
|
objectMessage.messageDefinition.values.push({
|
||||||
|
name,
|
||||||
|
configuration: this.convertMessageRecursive(
|
||||||
|
value as JSONSchema7,
|
||||||
|
rootElements,
|
||||||
|
name
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: schema.title,
|
||||||
|
fileName,
|
||||||
|
values: [{ name: schema.title, configuration: objectMessage }],
|
||||||
|
},
|
||||||
|
...rootElements,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert each type here?
|
||||||
|
private convertMessageRecursive(
|
||||||
|
value: JSONSchema7,
|
||||||
|
parsedMessages: ProtoMessage[],
|
||||||
|
name?: string
|
||||||
|
): MessageConfiguration {
|
||||||
|
if (value.$ref) {
|
||||||
|
const refMessageName = value.$ref.substring('#/definitions/'.length);
|
||||||
|
const foundMessage = parsedMessages.find(
|
||||||
|
(message) => message.name === refMessageName
|
||||||
|
);
|
||||||
|
return ObjectMessage(foundMessage!);
|
||||||
|
}
|
||||||
|
const type = Array.isArray(value.type) ? value.type[0] : value.type;
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
if (value.enum) {
|
||||||
|
// TODO: Validate we have strings
|
||||||
|
return EnumMessage(value.enum.map((value) => value as string));
|
||||||
|
}
|
||||||
|
return StringMessage();
|
||||||
|
case 'integer':
|
||||||
|
case 'number':
|
||||||
|
return NumericMessage();
|
||||||
|
case 'array':
|
||||||
|
if (Array.isArray(value.items)) {
|
||||||
|
throw new Error('Cannot parse array type');
|
||||||
|
}
|
||||||
|
return ListMessage(
|
||||||
|
this.convertMessageRecursive(
|
||||||
|
value.items as JSONSchema7,
|
||||||
|
parsedMessages
|
||||||
|
)
|
||||||
|
);
|
||||||
|
case 'boolean':
|
||||||
|
return BooleanMessage();
|
||||||
|
case 'object':
|
||||||
|
return ObjectMessage({
|
||||||
|
name: name!,
|
||||||
|
values: Object.entries(value.properties!).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
configuration: this.convertMessageRecursive(
|
||||||
|
// TODO: How can this be a boolean???
|
||||||
|
value as JSONSchema7,
|
||||||
|
parsedMessages,
|
||||||
|
name
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new Error('Invalid message type found');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: var(--mat-sidenav-container-shape);
|
|
||||||
padding: var(--mat-sidenav-container-shape);
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: background-color 100ms linear;
|
transition: background-color 100ms linear;
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
HostListener,
|
HostListener,
|
||||||
computed,
|
computed,
|
||||||
|
inject,
|
||||||
output,
|
output,
|
||||||
signal,
|
signal,
|
||||||
viewChild,
|
viewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatListModule, MatSelectionList } from '@angular/material/list';
|
|
||||||
import { ProtoMessage } from '../model/proto-message.model';
|
|
||||||
import { ProtoDefinitionService } from './proto-definition.service';
|
|
||||||
import { MatTreeModule } from '@angular/material/tree';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
import { writeTextFile } from '@tauri-apps/api/fs';
|
|
||||||
import { save } from '@tauri-apps/api/dialog';
|
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatListModule, MatSelectionList } from '@angular/material/list';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { MatTreeModule } from '@angular/material/tree';
|
||||||
|
import { save } from '@tauri-apps/api/dialog';
|
||||||
|
import { writeTextFile } from '@tauri-apps/api/fs';
|
||||||
|
import { ProtoMessage } from '../model/proto-message.model';
|
||||||
import { DefinitionEditorComponent } from './definition-editor/definition-editor.component';
|
import { DefinitionEditorComponent } from './definition-editor/definition-editor.component';
|
||||||
|
import { ProtoDefinitionService } from './proto-definition.service';
|
||||||
|
import { JSONSchema7 } from 'json-schema';
|
||||||
|
import { JsonSchemaImporterService } from './json-schema-importer.service';
|
||||||
|
|
||||||
declare const __TAURI__: any;
|
declare const __TAURI__: any;
|
||||||
|
|
||||||
@@ -29,27 +30,23 @@ const collator = new Intl.Collator(undefined, { numeric: true });
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-proto-definition-selector',
|
selector: 'app-proto-definition-selector',
|
||||||
standalone: true,
|
imports: [MatButtonModule, MatIconModule, MatListModule, MatTreeModule],
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
MatButtonModule,
|
|
||||||
MatIconModule,
|
|
||||||
MatListModule,
|
|
||||||
MatTreeModule,
|
|
||||||
],
|
|
||||||
template: `
|
template: `
|
||||||
<h2>Protobuf Definitions</h2>
|
<h2>Schemas</h2>
|
||||||
<button mat-button (click)="protoSelector.click()">
|
<button mat-button (click)="protoSelector.click()">
|
||||||
Select definitions
|
Import Protobuf Definition
|
||||||
|
</button>
|
||||||
|
<button mat-button (click)="jsonSchemaSelector.click()">
|
||||||
|
Import JSON Schema
|
||||||
|
</button>
|
||||||
|
<button mat-button (click)="configurationSelector.click()">
|
||||||
|
Import Configuration
|
||||||
</button>
|
</button>
|
||||||
@if(currentFiles().length > 0) {
|
@if(currentFiles().length > 0) {
|
||||||
<button mat-button (click)="exportConfiguration()">
|
<button mat-button (click)="exportConfiguration()">
|
||||||
Export Configuration
|
Export Configuration
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<button mat-button (click)="configurationSelector.click()">
|
|
||||||
Import Configuration
|
|
||||||
</button>
|
|
||||||
<mat-selection-list [multiple]="false">
|
<mat-selection-list [multiple]="false">
|
||||||
@for (item of currentFiles(); track $index) {
|
@for (item of currentFiles(); track $index) {
|
||||||
<mat-list-option
|
<mat-list-option
|
||||||
@@ -78,7 +75,7 @@ const collator = new Intl.Collator(undefined, { numeric: true });
|
|||||||
>
|
>
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<div matListItemLine>{{ item.name }}</div>
|
<div>{{ item.name }}</div>
|
||||||
</mat-list-option>
|
</mat-list-option>
|
||||||
}
|
}
|
||||||
</mat-selection-list>
|
</mat-selection-list>
|
||||||
@@ -90,6 +87,12 @@ const collator = new Intl.Collator(undefined, { numeric: true });
|
|||||||
(change)="addDefinitionFiles()"
|
(change)="addDefinitionFiles()"
|
||||||
accept=".proto"
|
accept=".proto"
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
#jsonSchemaSelector
|
||||||
|
type="file"
|
||||||
|
(change)="importJsonSchema()"
|
||||||
|
accept=".json"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
#configurationSelector
|
#configurationSelector
|
||||||
type="file"
|
type="file"
|
||||||
@@ -102,10 +105,17 @@ const collator = new Intl.Collator(undefined, { numeric: true });
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ProtoDefinitionSelectorComponent {
|
export class ProtoDefinitionSelectorComponent {
|
||||||
|
private jsonSchemaImporterService = inject(JsonSchemaImporterService);
|
||||||
|
private protoDefinitionService = inject(ProtoDefinitionService);
|
||||||
|
private snackBar = inject(MatSnackBar);
|
||||||
|
private dialog = inject(MatDialog);
|
||||||
|
|
||||||
messageSelected = output<ProtoMessage>();
|
messageSelected = output<ProtoMessage>();
|
||||||
|
|
||||||
protected protoSelector =
|
protected protoSelector =
|
||||||
viewChild<ElementRef<HTMLInputElement>>('protoSelector');
|
viewChild<ElementRef<HTMLInputElement>>('protoSelector');
|
||||||
|
protected jsonSchemaSelector =
|
||||||
|
viewChild<ElementRef<HTMLInputElement>>('jsonSchemaSelector');
|
||||||
protected configurationSelector = viewChild<ElementRef<HTMLInputElement>>(
|
protected configurationSelector = viewChild<ElementRef<HTMLInputElement>>(
|
||||||
'configurationSelector'
|
'configurationSelector'
|
||||||
);
|
);
|
||||||
@@ -135,12 +145,6 @@ export class ProtoDefinitionSelectorComponent {
|
|||||||
return this.isDragging();
|
return this.isDragging();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
|
||||||
private protoDefinitionService: ProtoDefinitionService,
|
|
||||||
private snackBar: MatSnackBar,
|
|
||||||
private dialog: MatDialog
|
|
||||||
) {}
|
|
||||||
|
|
||||||
protected async addDefinitionFiles() {
|
protected async addDefinitionFiles() {
|
||||||
const files = this.protoSelector()?.nativeElement.files;
|
const files = this.protoSelector()?.nativeElement.files;
|
||||||
if (files) {
|
if (files) {
|
||||||
@@ -185,6 +189,29 @@ export class ProtoDefinitionSelectorComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async importJsonSchema() {
|
||||||
|
const jsonSchemaFile = this.jsonSchemaSelector()?.nativeElement.files;
|
||||||
|
try {
|
||||||
|
const fileContents = await jsonSchemaFile?.item(0)?.text();
|
||||||
|
if (fileContents) {
|
||||||
|
const schema: JSONSchema7 = JSON.parse(fileContents);
|
||||||
|
const messageObjects =
|
||||||
|
await this.jsonSchemaImporterService.parseJsonSchema(
|
||||||
|
jsonSchemaFile?.item(0)?.name!,
|
||||||
|
schema
|
||||||
|
);
|
||||||
|
this.allProtoFiles.set(messageObjects);
|
||||||
|
this.selectedDefinition.set(messageObjects);
|
||||||
|
this.selectedProtoFile.set(null);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.snackBar.open(
|
||||||
|
"Could not parse json schema file, please ensure it's valid. Note: This feature is still unstable and is likely to have errors with valid schemas."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected async importConfiguration() {
|
protected async importConfiguration() {
|
||||||
const configurationFile = this.configurationSelector()?.nativeElement.files;
|
const configurationFile = this.configurationSelector()?.nativeElement.files;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
|
import { provideZonelessChangeDetection } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
EnumMessage,
|
EnumMessage,
|
||||||
ListMessage,
|
ListMessage,
|
||||||
@@ -58,7 +58,7 @@ describe('TestService', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [provideExperimentalZonelessChangeDetection()],
|
providers: [provideZonelessChangeDetection()],
|
||||||
});
|
});
|
||||||
service = TestBed.inject(ProtoDefinitionService);
|
service = TestBed.inject(ProtoDefinitionService);
|
||||||
});
|
});
|
||||||
@@ -142,7 +142,7 @@ describe('TestService', () => {
|
|||||||
|
|
||||||
const enumMessage = converted.values[10].configuration as EnumMessage;
|
const enumMessage = converted.values[10].configuration as EnumMessage;
|
||||||
expect(enumMessage.options.length).toBe(1);
|
expect(enumMessage.options.length).toBe(1);
|
||||||
expect(enumMessage.options[0]).toBe('Hello');
|
expect(enumMessage.options[0].protoName).toBe('Hello');
|
||||||
|
|
||||||
const objectListMessage = converted.values[12].configuration as ListMessage;
|
const objectListMessage = converted.values[12].configuration as ListMessage;
|
||||||
expect(objectListMessage.type).toBe(MessageTypeEnum.List);
|
expect(objectListMessage.type).toBe(MessageTypeEnum.List);
|
||||||
|
|||||||
@@ -383,7 +383,6 @@ export class ProtoDefinitionService {
|
|||||||
switch (fieldType) {
|
switch (fieldType) {
|
||||||
case 'string':
|
case 'string':
|
||||||
return StringMessage();
|
return StringMessage();
|
||||||
break;
|
|
||||||
case 'int32':
|
case 'int32':
|
||||||
case 'int64':
|
case 'int64':
|
||||||
case 'float':
|
case 'float':
|
||||||
|
|||||||
@@ -1,50 +1,34 @@
|
|||||||
@use "@angular/material" as mat;
|
@use "@angular/material" as mat;
|
||||||
@use "./theme.scss";
|
|
||||||
|
|
||||||
@include mat.core();
|
|
||||||
|
|
||||||
@mixin custom-colours($theme) {
|
|
||||||
.mat-toolbar {
|
|
||||||
background-color: mat.get-theme-color($theme, tertiary-container);
|
|
||||||
}
|
|
||||||
|
|
||||||
app-proto-definition-selector,
|
|
||||||
app-file-tree {
|
|
||||||
background-color: mat.get-theme-color($theme, secondary-container);
|
|
||||||
transition: background-color 100ms linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.droppable {
|
|
||||||
background-color: mat.get-theme-color($theme, secondary, 40);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@include mat.core-theme(theme.$rose-theme);
|
color-scheme: dark light;
|
||||||
@include mat.toolbar-theme(theme.$rose-theme);
|
height: 100%;
|
||||||
@include mat.button-theme(theme.$rose-theme);
|
|
||||||
@include mat.tree-theme(theme.$rose-theme);
|
@include mat.theme(
|
||||||
@include mat.sidenav-theme(theme.$rose-theme);
|
(
|
||||||
@include mat.icon-theme(theme.$rose-theme);
|
color: mat.$rose-palette,
|
||||||
@include mat.form-field-theme(theme.$rose-theme);
|
typography: Roboto,
|
||||||
@include mat.list-theme(theme.$rose-theme);
|
density: -5,
|
||||||
@include mat.select-theme(theme.$rose-theme);
|
)
|
||||||
@include mat.snack-bar-theme(theme.$rose-theme);
|
);
|
||||||
@include mat.button-toggle-theme(theme.$rose-theme);
|
@include mat.toolbar-overrides(
|
||||||
@include mat.dialog-theme(theme.$rose-theme);
|
(
|
||||||
@include custom-colours(theme.$rose-theme);
|
container-background-color: var(--mat-sys-primary-container),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
@include mat.sidenav-overrides(
|
||||||
|
(
|
||||||
|
container-shape: 1rem,
|
||||||
|
container-background-color: transparent,
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font: mat.get-theme-typography(theme.$rose-theme, body-medium, font);
|
background: var(--mat-sys-surface);
|
||||||
--mat-tree-node-min-height: 24px;
|
color: var(--mat-sys-on-surface);
|
||||||
--mat-tree-node-text-size: 14px;
|
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||||
--mdc-icon-button-state-layer-size: 24px;
|
|
||||||
--mat-icon-button-touch-target-display: none;
|
|
||||||
--mdc-list-list-item-one-line-container-height: 24px;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
@use "@angular/material" as mat;
|
|
||||||
|
|
||||||
$rose-theme: mat.define-theme(
|
|
||||||
(
|
|
||||||
color: (
|
|
||||||
theme-type: dark,
|
|
||||||
primary: mat.$rose-palette,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
Reference in New Issue
Block a user