From 186a6fb456598382752e3a085a395cb3b9acc2ae Mon Sep 17 00:00:00 2001 From: R0tenur Date: Wed, 23 Mar 2022 11:50:43 +0100 Subject: [PATCH 01/55] keep theme tags in index --- frontend/src/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/index.html b/frontend/src/index.html index b83de47..a8df16d 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -7,7 +7,8 @@ - + + From 025eed6061cf0547aeb1e9a6ceaca9af2d604208 Mon Sep 17 00:00:00 2001 From: R0tenur Date: Wed, 23 Mar 2022 11:51:10 +0100 Subject: [PATCH 02/55] add helper css --- frontend/src/styles.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index e984c35..9c83a8c 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -19,3 +19,12 @@ body { justify-content: center; align-items: center; } +.flex { + display: flex; +} +.justify-content-between { + justify-content: space-between; +} +.muted { + color: rgb(115, 115, 115); +} From 821a826a101108c259d02536a25b421589700ff1 Mon Sep 17 00:00:00 2001 From: R0tenur Date: Wed, 23 Mar 2022 11:52:58 +0100 Subject: [PATCH 03/55] ignore fake service from code coverage --- frontend/src/app/services/fake-db.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/services/fake-db.service.ts b/frontend/src/app/services/fake-db.service.ts index 8a94979..ffcca2e 100644 --- a/frontend/src/app/services/fake-db.service.ts +++ b/frontend/src/app/services/fake-db.service.ts @@ -1,4 +1,4 @@ -/* istanbul ignore */ +/* istanbul ignore file */ import { Injectable } from '@angular/core'; @Injectable({ From 04901660bc0e12ad16ff273523f0f2cc1e1ccf16 Mon Sep 17 00:00:00 2001 From: R0tenur Date: Wed, 23 Mar 2022 12:41:05 +0100 Subject: [PATCH 04/55] add basic state handling with history --- frontend/src/app/state/state.spec.ts | 126 +++++++++++++++++++++++++++ frontend/src/app/state/state.ts | 38 ++++++++ 2 files changed, 164 insertions(+) create mode 100644 frontend/src/app/state/state.spec.ts create mode 100644 frontend/src/app/state/state.ts diff --git a/frontend/src/app/state/state.spec.ts b/frontend/src/app/state/state.spec.ts new file mode 100644 index 0000000..58e6cae --- /dev/null +++ b/frontend/src/app/state/state.spec.ts @@ -0,0 +1,126 @@ +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { State } from './state'; + +describe('State', () => { + let state: State; + const value: Dummy = { prop: 'dummy' }; + beforeEach(() => { + TestBed.configureTestingModule({}); + state = new State(); + }); + + it('should be created', () => { + expect(state).toBeTruthy(); + }); + + it('should return set value', done => { + + // Act + state.set(value); + + state.select$.subscribe(v => { + // Assert + expect(v?.prop).toEqual(value.prop); + done(); + }); + + }); + + it('clear should clear state', fakeAsync(() => { + // Arrange + let returned: Dummy | undefined; + let set = false; + state.select$.subscribe(v => { + returned = v; + set = true; + }); + + // Act + state.set(value); + + tick(); + state.clear(); + tick(); + + // Assert + expect(set).toBeTrue(); + expect(returned).toBe(undefined as any as Dummy); + + })); + + it('undo sets previous value', () => { + // Arrange + const first: Dummy = { prop: 'first' }; + const second: Dummy = { prop: 'second' }; + + let returned: Dummy | undefined; + let set = false; + state.select$.subscribe(v => { + returned = v; + set = true; + }); + + // Act + state.set(first); + state.set(second); + state.undo(); + + // Assert + expect(set).toBeTrue(); + expect(returned?.prop).toBe(first.prop); + + }); + + it('redo sets next value', () => { + // Arrange + const first: Dummy = { prop: 'first' }; + const second: Dummy = { prop: 'second' }; + + let returned: Dummy | undefined; + let set = false; + state.select$.subscribe(v => { + returned = v; + set = true; + }); + + // Act + state.set(first); + state.set(second); + state.undo(); + state.redo(); + + // Assert + expect(set).toBeTrue(); + expect(returned?.prop).toBe(second.prop); + + }); + it('set purges the history ahead', () => { + // Arrange + const first: Dummy = { prop: 'first' }; + const second: Dummy = { prop: 'second' }; + const third: Dummy = { prop: 'third' }; + + let returned: Dummy | undefined; + let set = false; + state.select$.subscribe(v => { + returned = v; + set = true; + }); + + // Act + state.set(first); + state.set(second); + state.undo(); + state.set(third); + state.redo(); + + // Assert + expect(set).toBeTrue(); + expect(returned?.prop).toBe(third.prop); + + }); +}); + +interface Dummy { + prop: string; +} diff --git a/frontend/src/app/state/state.ts b/frontend/src/app/state/state.ts new file mode 100644 index 0000000..d00d350 --- /dev/null +++ b/frontend/src/app/state/state.ts @@ -0,0 +1,38 @@ +import { Observable, BehaviorSubject } from 'rxjs'; + +export class State { + public get select$(): Observable { + return this.state.asObservable(); + } + private currentHistoryIndex = 0; + private history: T[] = [undefined as any as T]; + private readonly state: BehaviorSubject = new BehaviorSubject(undefined as any as T); + constructor() { + } + public set(newState: T): void { + if (this.currentHistoryIndex < this.history.length - 1) { + this.history.slice(0, this.currentHistoryIndex); + } + + this.history.push(newState); + this.currentHistoryIndex = this.history.length - 1; + this.state.next(newState); + } + + public clear(): void { + this.state.next(undefined as any as T); + } + public undo(): void { + if (this.currentHistoryIndex > 0) { + this.currentHistoryIndex--; + this.state.next(this.history[this.currentHistoryIndex]); + } + } + public redo(): void { + + if (this.currentHistoryIndex < this.history.length - 1) { + this.currentHistoryIndex++; + this.state.next(this.history[this.currentHistoryIndex]); + } + } +} From 5c6bdaaae1a83324d5e1bcd6e0d232b29616c42e Mon Sep 17 00:00:00 2001 From: R0tenur Date: Wed, 23 Mar 2022 12:41:36 +0100 Subject: [PATCH 05/55] add basic state handling with history --- frontend/src/app/services/state.token.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 frontend/src/app/services/state.token.ts diff --git a/frontend/src/app/services/state.token.ts b/frontend/src/app/services/state.token.ts new file mode 100644 index 0000000..8038046 --- /dev/null +++ b/frontend/src/app/services/state.token.ts @@ -0,0 +1,22 @@ +/* istanbul ignore file */ +import { InjectionToken } from '@angular/core'; +import { State } from '../state/state'; + +const states: Record = {}; + +export const StateInjector = (key: string) => { + return new InjectionToken>( + key, + { + providedIn: 'root', + factory: () => { + if (states.hasOwnProperty(key)) { + return states[key]; + } + states[key] = new State(); + + return states[key]; + }, + } + ); +}; From f0d78d7510999455e26c3c81fcb0ded6c5a36df4 Mon Sep 17 00:00:00 2001 From: R0tenur Date: Wed, 23 Mar 2022 12:42:02 +0100 Subject: [PATCH 06/55] ignore tokens in code coverage --- frontend/src/app/services/mermaid.token.ts | 1 + frontend/src/app/services/window.token.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/frontend/src/app/services/mermaid.token.ts b/frontend/src/app/services/mermaid.token.ts index 7d69125..9cf063f 100644 --- a/frontend/src/app/services/mermaid.token.ts +++ b/frontend/src/app/services/mermaid.token.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import { InjectionToken } from '@angular/core'; import { Mermaid } from 'mermaid'; import mermaid from 'mermaid'; diff --git a/frontend/src/app/services/window.token.ts b/frontend/src/app/services/window.token.ts index 2a35b51..9e9f00b 100644 --- a/frontend/src/app/services/window.token.ts +++ b/frontend/src/app/services/window.token.ts @@ -1,3 +1,4 @@ +/* istanbul ignore file */ import { InjectionToken } from '@angular/core'; export const WINDOW = new InjectionToken( From 0b80ac22361e1bdfafcf1475ae586127a4763a5e Mon Sep 17 00:00:00 2001 From: R0tenur Date: Wed, 23 Mar 2022 12:44:45 +0100 Subject: [PATCH 07/55] throw away alertservice and use generic state instead --- frontend/src/app/app-testing.module.ts | 4 +- .../app/components/alert/alert.component.html | 4 +- .../components/alert/alert.component.spec.ts | 12 ++-- .../app/components/alert/alert.component.ts | 12 ++-- frontend/src/app/models/error.model.ts | 1 + .../src/app/services/alert.service.spec.ts | 56 ------------------- frontend/src/app/services/alert.service.ts | 23 -------- .../app/services/data-studio.service.spec.ts | 24 ++++---- .../src/app/services/data-studio.service.ts | 10 ++-- 9 files changed, 41 insertions(+), 105 deletions(-) delete mode 100644 frontend/src/app/services/alert.service.spec.ts delete mode 100644 frontend/src/app/services/alert.service.ts diff --git a/frontend/src/app/app-testing.module.ts b/frontend/src/app/app-testing.module.ts index b90e8db..0299392 100644 --- a/frontend/src/app/app-testing.module.ts +++ b/frontend/src/app/app-testing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; import { ButtonComponent } from './components/button/button.component'; import { MERMAID } from './services/mermaid.token'; @@ -41,9 +42,10 @@ const fakeMermaidProvider = ({ ], imports: [ FormsModule, + RouterTestingModule.withRoutes([]), ], providers: [fakeWindowProvider], - exports: [ButtonComponent, FormsModule] + exports: [ButtonComponent, FormsModule, RouterTestingModule] }) export class AppTestingModule { } diff --git a/frontend/src/app/components/alert/alert.component.html b/frontend/src/app/components/alert/alert.component.html index 7c8f445..2a9ea59 100644 --- a/frontend/src/app/components/alert/alert.component.html +++ b/frontend/src/app/components/alert/alert.component.html @@ -1,4 +1,4 @@ -
+

An error occured!

@@ -34,5 +34,5 @@

Report it here: https://github.com/R0tenur/visualization/issues

- Dismiss + Dismiss

diff --git a/frontend/src/app/components/alert/alert.component.spec.ts b/frontend/src/app/components/alert/alert.component.spec.ts index 6be7d52..fb31e5f 100644 --- a/frontend/src/app/components/alert/alert.component.spec.ts +++ b/frontend/src/app/components/alert/alert.component.spec.ts @@ -3,16 +3,17 @@ import { By } from '@angular/platform-browser'; import { BehaviorSubject, Subject } from 'rxjs'; import { Status } from '../../../../../shared/models/status.enum'; import { AppTestingModule } from '../../app-testing.module'; -import { ChartError } from '../../models/error.model'; -import { AlertService } from '../../services/alert.service'; +import { ChartError, ChartErrorKey } from '../../models/error.model'; import { DataStudioService } from '../../services/data-studio.service'; +import { StateInjector } from '../../services/state.token'; +import { State } from '../../state/state'; import { AlertComponent } from './alert.component'; describe('AlertComponent', () => { let component: AlertComponent; let fixture: ComponentFixture; - let alertService: AlertService; + let alertService: State; let dataStudioService: DataStudioService; const alertSubject: Subject = new Subject(); const markdownSubject: BehaviorSubject = new BehaviorSubject(undefined as any as string); @@ -20,6 +21,7 @@ describe('AlertComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [AlertComponent], + providers: [{ provide: StateInjector(ChartErrorKey), useValue: new State()}], imports: [AppTestingModule], }) .compileComponents(); @@ -27,8 +29,8 @@ describe('AlertComponent', () => { afterEach(() => alertSubject.next(undefined)); beforeEach(() => { - alertService = TestBed.inject(AlertService); - spyOnProperty(alertService, 'Alert$').and.returnValue(alertSubject.asObservable()); + alertService = TestBed.inject>(StateInjector(ChartErrorKey)); + spyOnProperty(alertService, 'select$').and.returnValue(alertSubject.asObservable()); dataStudioService = TestBed.inject(DataStudioService); spyOnProperty(dataStudioService, 'Markdown$').and.returnValue(markdownSubject.asObservable()); diff --git a/frontend/src/app/components/alert/alert.component.ts b/frontend/src/app/components/alert/alert.component.ts index f4499f8..e6dee6d 100644 --- a/frontend/src/app/components/alert/alert.component.ts +++ b/frontend/src/app/components/alert/alert.component.ts @@ -1,7 +1,8 @@ -import { Component } from '@angular/core'; -import { ChartError } from '../../models/error.model'; -import { AlertService } from '../../services/alert.service'; +import { Component, Inject } from '@angular/core'; +import { ChartError, ChartErrorKey } from '../../models/error.model'; import { DataStudioService } from '../../services/data-studio.service'; +import { StateInjector } from '../../services/state.token'; +import { State} from '../../state/state'; @Component({ selector: 'app-alert', @@ -9,7 +10,10 @@ import { DataStudioService } from '../../services/data-studio.service'; styleUrls: ['./alert.component.scss'] }) export class AlertComponent { - constructor(public readonly alertService: AlertService, public readonly az: DataStudioService) {} + constructor( + @Inject(StateInjector(ChartErrorKey)) public readonly state: State, + public readonly az: DataStudioService) { } + public exportMarkdown(markdown: string): void { this.az.saveCommand({ mermaid: markdown }); } diff --git a/frontend/src/app/models/error.model.ts b/frontend/src/app/models/error.model.ts index 8499491..e2da7a7 100644 --- a/frontend/src/app/models/error.model.ts +++ b/frontend/src/app/models/error.model.ts @@ -1,5 +1,6 @@ import { Status } from '../../../../shared/models/status.enum'; +export const ChartErrorKey = 'ChartErrorKey'; export interface ChartError { status: Status; errors: string[]; diff --git a/frontend/src/app/services/alert.service.spec.ts b/frontend/src/app/services/alert.service.spec.ts deleted file mode 100644 index 40d8e4d..0000000 --- a/frontend/src/app/services/alert.service.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { take } from 'rxjs/operators'; -import { Status } from '../../../../shared/models/status.enum'; -import { ChartError } from '../models/error.model'; - -import { AlertService } from './alert.service'; - -describe('AlertService', () => { - let alertService: AlertService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - alertService = TestBed.inject(AlertService); - }); - - it('should be created', () => { - expect(alertService).toBeTruthy(); - }); - describe('showError', () => { - it('should trigger alert', done => { - // Arrange - const chartError = createChartError(); - - // Act - alertService.showError(chartError); - alertService.Alert$.pipe(take(1)).subscribe(alert => { - // Assert - expect(alert).toBe(chartError); - done(); - }); - }); - }); - - describe('dismissError', () => { - it('should dismiss alert', (done) => { - // Arrange - const chartError = createChartError(); - // Act - alertService.showError(chartError); - alertService.dismissError(); - alertService.Alert$.pipe(take(1)).subscribe(alert => { - // Assert - expect(alert).toBeUndefined(); - done(); - }); - }); - }); - - const createChartError = (errorMessage: string = 'err') => { - const errors = [errorMessage]; - const rawData = 'rawData'; - const status = Status.Error; - const chartError: ChartError = { errors, rawData, status }; - return chartError; - }; -}); diff --git a/frontend/src/app/services/alert.service.ts b/frontend/src/app/services/alert.service.ts deleted file mode 100644 index 41ae772..0000000 --- a/frontend/src/app/services/alert.service.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Observable, BehaviorSubject } from 'rxjs'; -import { ChartError } from '../models/error.model'; - -@Injectable({ - providedIn: 'root' -}) -export class AlertService { - - public get Alert$(): Observable { - return this.alert$.asObservable(); - } - - private alert$: BehaviorSubject = new BehaviorSubject(undefined as any as ChartError); - - public showError(error: ChartError): void { - this.alert$.next(error); - } - - public dismissError(): void { - this.alert$.next(undefined as any as ChartError); - } -} diff --git a/frontend/src/app/services/data-studio.service.spec.ts b/frontend/src/app/services/data-studio.service.spec.ts index 1418534..477043e 100644 --- a/frontend/src/app/services/data-studio.service.spec.ts +++ b/frontend/src/app/services/data-studio.service.spec.ts @@ -1,7 +1,6 @@ import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { Status } from '../../../../shared/models/status.enum'; import { AppTestingModule } from '../app-testing.module'; -import { AlertService } from './alert.service'; import { DataStudioService } from './data-studio.service'; import { WINDOW } from './window.token'; @@ -9,18 +8,23 @@ import { Subject, Subscription } from 'rxjs'; import { Mermaid } from 'mermaid'; import { MERMAID } from './mermaid.token'; import { Exportable } from '../models/exportable.model'; +import { StateInjector } from './state.token'; +import { ChartError, ChartErrorKey } from '../models/error.model'; +import { State } from '../state/state'; describe('DataStudioService', () => { let dataStudioService: DataStudioService; - let alert: AlertService; + let alert: State; let windowRef: Window; let mermaid: Mermaid; beforeEach(() => { TestBed.configureTestingModule({ - imports: [AppTestingModule] + imports: [AppTestingModule], + providers: [{ provide: StateInjector(ChartErrorKey), useValue: new State() }], + }); - alert = TestBed.inject(AlertService); + alert = TestBed.inject(StateInjector(ChartErrorKey)); windowRef = TestBed.inject(WINDOW); mermaid = TestBed.inject(MERMAID); dataStudioService = TestBed.inject(DataStudioService); @@ -97,7 +101,7 @@ describe('DataStudioService', () => { }); it('should trigger error alert when status error', fakeAsync(() => { // Arrange - spyOn(alert, 'showError'); + spyOn(alert, 'set'); const statusSubscription = dataStudioService.Status$.subscribe(); const status = Status.Error; @@ -115,7 +119,7 @@ describe('DataStudioService', () => { tick(); // Assert - expect(alert.showError).toHaveBeenCalledWith({ status, errors, rawData: JSON.stringify(rawData) }); + expect(alert.set).toHaveBeenCalledWith({ status, errors, rawData: JSON.stringify(rawData) }); statusSubscription.unsubscribe(); })); describe('Database$', () => { @@ -131,7 +135,7 @@ describe('DataStudioService', () => { }); it('should render mermaid to svg', fakeAsync(() => { // Arrange - spyOn(alert, 'showError'); + spyOn(alert, 'set'); const markdown = 'dummyMarkdown'; const svg = 'dummySVG'; @@ -147,13 +151,13 @@ describe('DataStudioService', () => { tick(); // Assert - expect(alert.showError).not.toHaveBeenCalled(); + expect(alert.set).not.toHaveBeenCalled(); expect(database).toEqual({ svg, mermaid: markdown }); })); it('should propagate mermaid errors to alert', fakeAsync(() => { // Arrange - spyOn(alert, 'showError'); + spyOn(alert, 'set'); const markdown = 'dummyMarkdown'; const errorMsg = 'errorMessage'; @@ -170,7 +174,7 @@ describe('DataStudioService', () => { tick(); // Assert - expect(alert.showError).toHaveBeenCalled(); + expect(alert.set).toHaveBeenCalled(); })); }); diff --git a/frontend/src/app/services/data-studio.service.ts b/frontend/src/app/services/data-studio.service.ts index 061d97f..18fabf8 100644 --- a/frontend/src/app/services/data-studio.service.ts +++ b/frontend/src/app/services/data-studio.service.ts @@ -2,11 +2,13 @@ import { Inject, Injectable } from '@angular/core'; import { concat, fromEvent, Observable, ReplaySubject, Subject } from 'rxjs'; import { filter, map, switchMap, tap } from 'rxjs/operators'; import { Status } from '../../../../shared/models/status.enum'; -import { AlertService } from './alert.service'; import { Exportable } from '../models/exportable.model'; import { WINDOW } from './window.token'; import { MERMAID } from './mermaid.token'; import { Mermaid } from 'mermaid'; +import { State } from '../state/state'; +import { ChartError, ChartErrorKey } from '../models/error.model'; +import { StateInjector } from './state.token'; declare const acquireVsCodeApi: () => ({ postMessage: (message: any) => void; }); @@ -50,13 +52,13 @@ export class DataStudioService { constructor( @Inject(WINDOW) private readonly window: Window, @Inject(MERMAID) private readonly mermaid: Mermaid, - private readonly alert: AlertService) { + @Inject(StateInjector(ChartErrorKey)) public readonly alert: State) { this.initializeMermaid(); const azEvent$ = fromEvent(this.window, 'message').pipe( tap(event => { if (event.data.status === Status.Error) { - this.alert.showError({ + this.alert.set({ status: event.data.status, errors: event.data.errors, rawData: JSON.stringify(event.data.rawData) @@ -122,7 +124,7 @@ export class DataStudioService { this.clientStatus$.next(Status.GeneratingSvg); } catch (e: any) { this.clientStatus$.next(Status.Error); - this.alert.showError({ + this.alert.set({ status: Status.Complete, errors: [ e.message, From 7141437b11629d7f421940cc19695f0bbaa87cac Mon Sep 17 00:00:00 2001 From: R0tenur Date: Wed, 23 Mar 2022 20:34:08 +0100 Subject: [PATCH 08/55] add support for context menu --- .../context-menu/context-menu.component.html | 7 ++ .../context-menu/context-menu.component.scss | 29 ++++++++ .../context-menu.component.spec.ts | 68 +++++++++++++++++++ .../context-menu/context-menu.component.ts | 18 +++++ frontend/src/app/models/context-menu.model.ts | 16 +++++ 5 files changed, 138 insertions(+) create mode 100644 frontend/src/app/components/context-menu/context-menu.component.html create mode 100644 frontend/src/app/components/context-menu/context-menu.component.scss create mode 100644 frontend/src/app/components/context-menu/context-menu.component.spec.ts create mode 100644 frontend/src/app/components/context-menu/context-menu.component.ts create mode 100644 frontend/src/app/models/context-menu.model.ts diff --git a/frontend/src/app/components/context-menu/context-menu.component.html b/frontend/src/app/components/context-menu/context-menu.component.html new file mode 100644 index 0000000..e659fe8 --- /dev/null +++ b/frontend/src/app/components/context-menu/context-menu.component.html @@ -0,0 +1,7 @@ +
+
    + +
+
diff --git a/frontend/src/app/components/context-menu/context-menu.component.scss b/frontend/src/app/components/context-menu/context-menu.component.scss new file mode 100644 index 0000000..eaed054 --- /dev/null +++ b/frontend/src/app/components/context-menu/context-menu.component.scss @@ -0,0 +1,29 @@ +.context-menu { + position: absolute; + width: 200px; + z-index: 9999999; + border: 1px solid rgb(146, 143, 143); + border-radius: .5rem; + font-size: .8em; + padding-top: .4rem; + padding-bottom: .4rem; + cursor: grab; +} + +ul { + list-style-type: none; + line-height: 150%; + padding-left: 0; + margin-top: 0; + margin-bottom: 0; +} +li { + margin-left: .2rem; + margin-right: .2rem; + border-radius: .3rem; + text-indent: -2em; + padding-left: 2.5em; + &:hover { + background-color: gray; + } +} diff --git a/frontend/src/app/components/context-menu/context-menu.component.spec.ts b/frontend/src/app/components/context-menu/context-menu.component.spec.ts new file mode 100644 index 0000000..12a44b5 --- /dev/null +++ b/frontend/src/app/components/context-menu/context-menu.component.spec.ts @@ -0,0 +1,68 @@ +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { BehaviorSubject } from 'rxjs'; +import { ContextMenu, contextMenuStateKey } from '../../models/context-menu.model'; +import { ContextMenuService } from '../../services/context-menu.service'; +import { StateInjector } from '../../services/state.token'; +import { State } from '../../state/state'; + +import { ContextMenuComponent } from './context-menu.component'; + +describe('ContextMenuComponent', () => { + let component: ContextMenuComponent; + let fixture: ComponentFixture; + let state: State; + let contextMenuSubject: BehaviorSubject; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ContextMenuComponent], + providers: [{ provide: StateInjector(contextMenuStateKey), useValue: new State() }], + + }) + .compileComponents(); + }); + + beforeEach(() => { + contextMenuSubject = new BehaviorSubject(undefined as any as ContextMenu); + state = TestBed.inject(StateInjector(contextMenuStateKey)); + spyOnProperty(state, 'select$').and.returnValue(contextMenuSubject.asObservable()); + fixture = TestBed.createComponent(ContextMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should not show menu when not open', () => { + expect(fixture.nativeElement.querySelector('.context-menu')).toBeFalsy(); + }); + + it('should show menu when value', () => { + // Act + contextMenuSubject.next({items: [], position: { x: 1, y: 1 }}); + fixture.detectChanges(); + + // Assert + expect(fixture.nativeElement.querySelector('.context-menu')).toBeTruthy(); + }); + + it('should trigger event when clicked', fakeAsync(() => { + // Arragne + const menuItem = { label: 'dummy', event: jasmine.createSpy() }; + spyOn(state, 'clear'); + + // Act + contextMenuSubject.next({ items: [menuItem], position: { x: 1, y: 1 } }); + fixture.detectChanges(); + tick(); + const listItem = fixture.nativeElement.querySelector('li'); + listItem.click(); + tick(); + + // Assert + expect(menuItem.event).toHaveBeenCalled(); + expect(state.clear).toHaveBeenCalled(); + })); +}); diff --git a/frontend/src/app/components/context-menu/context-menu.component.ts b/frontend/src/app/components/context-menu/context-menu.component.ts new file mode 100644 index 0000000..7185b98 --- /dev/null +++ b/frontend/src/app/components/context-menu/context-menu.component.ts @@ -0,0 +1,18 @@ +import { Component, Inject } from '@angular/core'; +import { ContextMenu, ContextMenuItem, contextMenuStateKey } from '../../models/context-menu.model'; +import { StateInjector } from '../../services/state.token'; +import { State } from '../../state/state'; + +@Component({ + selector: 'app-context-menu', + templateUrl: './context-menu.component.html', + styleUrls: ['./context-menu.component.scss'] +}) +export class ContextMenuComponent { + + constructor(@Inject(StateInjector(contextMenuStateKey)) public readonly state: State) { } + public handleEvent(item: ContextMenuItem): void { + item.event(); + this.state.clear(); + } +} diff --git a/frontend/src/app/models/context-menu.model.ts b/frontend/src/app/models/context-menu.model.ts new file mode 100644 index 0000000..afbaf41 --- /dev/null +++ b/frontend/src/app/models/context-menu.model.ts @@ -0,0 +1,16 @@ +import { Position } from './position.model'; + +export const contextMenuStateKey = 'ContextMenuState'; + +export interface ContextMenu { + items: ContextMenuItem[]; + position: Position; +} + +export interface ContextMenuItem { + label: string; + shortcut?: string; + event: () => void; + subMenu?: ContextMenu; +} + From 143e81b4fed1429597a6f1c0554b64dee1b29670 Mon Sep 17 00:00:00 2001 From: R0tenur Date: Thu, 24 Mar 2022 12:07:03 +0100 Subject: [PATCH 09/55] add position --- frontend/src/app/models/position.model.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 frontend/src/app/models/position.model.ts diff --git a/frontend/src/app/models/position.model.ts b/frontend/src/app/models/position.model.ts new file mode 100644 index 0000000..09614ba --- /dev/null +++ b/frontend/src/app/models/position.model.ts @@ -0,0 +1,4 @@ +export interface Position { + x: number; + y: number; +} From 627d707721219664d86b3f182c722cd3063c0dd6 Mon Sep 17 00:00:00 2001 From: R0tenur Date: Mon, 28 Mar 2022 21:30:03 +0200 Subject: [PATCH 10/55] add basic and probably temporary menu to reach builder --- .../src/app/components/bar/bar.component.html | 2 + .../app/components/bar/bar.component.spec.ts | 37 +++++++++++++++++++ .../src/app/components/bar/bar.component.ts | 16 ++++++++ 3 files changed, 55 insertions(+) create mode 100644 frontend/src/app/components/bar/bar.component.html create mode 100644 frontend/src/app/components/bar/bar.component.spec.ts create mode 100644 frontend/src/app/components/bar/bar.component.ts diff --git a/frontend/src/app/components/bar/bar.component.html b/frontend/src/app/components/bar/bar.component.html new file mode 100644 index 0000000..33cd2e5 --- /dev/null +++ b/frontend/src/app/components/bar/bar.component.html @@ -0,0 +1,2 @@ +OverviewBuilder (Beta) +
diff --git a/frontend/src/app/components/bar/bar.component.spec.ts b/frontend/src/app/components/bar/bar.component.spec.ts new file mode 100644 index 0000000..42fdc91 --- /dev/null +++ b/frontend/src/app/components/bar/bar.component.spec.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { AppTestingModule } from '../../app-testing.module'; + +import { BarComponent } from './bar.component'; + +describe('BarComponent', () => { + let component: BarComponent; + let fixture: ComponentFixture; + let router: Router; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppTestingModule], + declarations: [BarComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + router = TestBed.inject(Router); + spyOn(router, 'navigate'); + fixture = TestBed.createComponent(BarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + it('should navigate to path', () => { + // Act + component.navigate('/builder'); + // Assert + expect(router.navigate).toHaveBeenCalledWith(['/builder']); + }); +}); diff --git a/frontend/src/app/components/bar/bar.component.ts b/frontend/src/app/components/bar/bar.component.ts new file mode 100644 index 0000000..ef2be80 --- /dev/null +++ b/frontend/src/app/components/bar/bar.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-bar', + templateUrl: './bar.component.html', +}) +export class BarComponent { + + constructor(private readonly router: Router) { } + + public navigate(route: string): void { + this.router.navigate([route]); + } + +} From 264d0b0d3ce7dd4b3dbac926ad83a3b8831cc52d Mon Sep 17 00:00:00 2001 From: R0tenur Date: Mon, 28 Mar 2022 21:33:05 +0200 Subject: [PATCH 11/55] get rid of input from mermaid viewer and make it self sustained --- .../mermaid-viewer.component.html | 15 +-- .../mermaid-viewer.component.spec.ts | 3 +- .../mermaid-viewer.component.ts | 101 ++++++++++++------ 3 files changed, 76 insertions(+), 43 deletions(-) diff --git a/frontend/src/app/components/mermaid-viewer/mermaid-viewer.component.html b/frontend/src/app/components/mermaid-viewer/mermaid-viewer.component.html index 43c1bcd..e04b8c5 100644 --- a/frontend/src/app/components/mermaid-viewer/mermaid-viewer.component.html +++ b/frontend/src/app/components/mermaid-viewer/mermaid-viewer.component.html @@ -1,8 +1,11 @@ -Reset zoom -
-
- -
+
+ Export + Reset zoom +
+
+ +
-
+
+
diff --git a/frontend/src/app/components/mermaid-viewer/mermaid-viewer.component.spec.ts b/frontend/src/app/components/mermaid-viewer/mermaid-viewer.component.spec.ts index 573396b..ad10bf7 100644 --- a/frontend/src/app/components/mermaid-viewer/mermaid-viewer.component.spec.ts +++ b/frontend/src/app/components/mermaid-viewer/mermaid-viewer.component.spec.ts @@ -18,8 +18,7 @@ describe('MermaidViewerComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(MermaidViewerComponent); component = fixture.componentInstance; - component.darkmode = true; - component.svg = sampleSvg; + fixture.detectChanges(); }); diff --git a/frontend/src/app/components/mermaid-viewer/mermaid-viewer.component.ts b/frontend/src/app/components/mermaid-viewer/mermaid-viewer.component.ts index 9a0c731..bb5b0b0 100644 --- a/frontend/src/app/components/mermaid-viewer/mermaid-viewer.component.ts +++ b/frontend/src/app/components/mermaid-viewer/mermaid-viewer.component.ts @@ -1,23 +1,24 @@ -import { AfterViewInit, Component, ElementRef, HostListener, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, HostListener, OnDestroy } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { Exportable } from '../../models/exportable.model'; +import { DataStudioService } from '../../services/data-studio.service'; @Component({ selector: 'app-mermaid-viewer', templateUrl: './mermaid-viewer.component.html', styleUrls: ['./mermaid-viewer.component.scss'] }) -export class MermaidViewerComponent implements OnInit, AfterViewInit, OnDestroy { +export class MermaidViewerComponent implements OnDestroy { private readonly mermaidSvgId: string = 'mermaidSvgChart'; - @Input() public darkmode!: boolean; - @Input() public svg!: string; - @Input() public safeSvg!: SafeHtml; - @ViewChild('svgContainer') - svgContainer!: ElementRef; public get ViewBoxString(): string { return `${this.viewBox.value.x / this.scale} ${this.viewBox.value.y / this.scale} ${this.viewBox.value.w / this.scale} ${this.viewBox.value.h / this.scale}`; } + public get Database$(): Observable { + return this.dataStudioService.Database$; + } - private viewBoxSubscription!: Subscription; + public safeSvg!: SafeHtml; private svgElement!: SVGElement; private isPanning = false; private viewBox: BehaviorSubject = new BehaviorSubject(null); @@ -25,17 +26,22 @@ export class MermaidViewerComponent implements OnInit, AfterViewInit, OnDestroy public scale = 0.8; private startPoint: { x: number, y: number } = { x: 0, y: 0 }; private endPoint: { x: number, y: number } = { x: 0, y: 0 }; - public get Svg$(): Observable<(SafeHtml | null)> { - return this.svg$.asObservable(); - } - public svg$: BehaviorSubject = new BehaviorSubject(null); - constructor(public readonly sanitizer: DomSanitizer) { } - ngOnInit(): void { - this.safeSvg = this.sanitizer.bypassSecurityTrustHtml(this.svg); - } - public ngAfterViewInit(): void { - this.setupSvgHandling(); + private loaded = false; + public database: Exportable | undefined; + private viewBoxSubscription!: Subscription; + + + constructor(private readonly sanitizer: DomSanitizer, private readonly dataStudioService: DataStudioService) { + this.dataStudioService + .Database$.pipe(take(1)).subscribe(d => { + if (!this.database) { + this.database = d; + this.safeSvg = this.safeHtml(d.svg); + + } + }); } + public ngOnDestroy(): void { if (this.viewBoxSubscription) { this.viewBoxSubscription.unsubscribe(); @@ -49,10 +55,16 @@ export class MermaidViewerComponent implements OnInit, AfterViewInit, OnDestroy public refreshZoom(): void { this.viewBox.next(this.viewBox.value); } + public exportSvg(svg: string, markdown: string): void { + this.dataStudioService.saveCommand({ chart: svg, mermaid: markdown }); + } @HostListener('body:wheel', ['$event']) public refreshZoom2(e: WheelEvent): void { + if (!this.loaded) { + return; + } this.scale += (-e.deltaY / 100); if (this.scale < 0.1) { @@ -65,23 +77,11 @@ export class MermaidViewerComponent implements OnInit, AfterViewInit, OnDestroy this.viewBox.next(this.viewBox.value); } - private setupSvgHandling(): void { - this.svgElement = document.querySelector('#' + this.mermaidSvgId) as any as SVGElement; - this.svgSize = { w: this.svgElement.clientWidth, h: this.svgElement.clientHeight }; - this.viewBox.next({ x: 0, y: 0, ...this.svgSize }); - this.startPoint = { x: 0, y: 0 }; - this.endPoint = { x: 0, y: 0 }; - document.getElementById(this.mermaidSvgId)?.removeAttribute('style'); - document.getElementById(this.mermaidSvgId)?.removeAttribute('height'); - document.getElementById(this.mermaidSvgId)?.removeAttribute('width'); - setTimeout(() => { - this.viewBoxSubscription = this.viewBox.asObservable().subscribe(() => { - document.getElementById(this.mermaidSvgId)?.setAttribute('viewBox', this.ViewBoxString); - }); - }, 60); - } @HostListener('body:mousedown', ['$event']) private startPan(e: MouseEvent): boolean { + if (!this.loaded) { + return true; + } if (!(e.target as any).classList.contains('zoom')) { this.isPanning = true; this.startPoint = { x: e.x, y: e.y }; @@ -90,7 +90,7 @@ export class MermaidViewerComponent implements OnInit, AfterViewInit, OnDestroy } @HostListener('body:mousemove', ['$event']) private panning(e: MouseEvent): boolean { - if (this.isPanning) { + if (this.isPanning && this.loaded) { this.endPoint = { x: e.x, y: e.y }; let dx = (this.startPoint.x - this.endPoint.x - 5) / (10); let dy = (this.startPoint.y - this.endPoint.y - 5) / (10); @@ -112,9 +112,40 @@ export class MermaidViewerComponent implements OnInit, AfterViewInit, OnDestroy } @HostListener('body:mouseup', ['$event']) private panEnd(): boolean { - if (this.isPanning) { + if (this.isPanning && this.loaded) { + this.isPanning = false; } return true; } + + private setupSvgHandling(): void { + setTimeout(() => { + this.svgElement = document.querySelector('#' + this.mermaidSvgId) as any as SVGElement; + this.removeMermaidAttributes(); + this.svgSize = { w: this.svgElement.clientWidth, h: this.svgElement.clientHeight }; + this.viewBox.next({ x: 0, y: 0, ...this.svgSize }); + this.startPoint = { x: 0, y: 0 }; + this.endPoint = { x: 0, y: 0 }; + this.loaded = true; + }, 40); + + setTimeout(() => { + this.viewBoxSubscription = this.viewBox.asObservable().subscribe(() => { + document.getElementById(this.mermaidSvgId)?.setAttribute('viewBox', this.ViewBoxString); + }); + + }, 200); + } + private safeHtml(svg: string): SafeHtml { + this.setupSvgHandling(); + return this.sanitizer.bypassSecurityTrustHtml(svg); + } + + private removeMermaidAttributes(): void { + document.getElementById(this.mermaidSvgId)?.removeAttribute('style'); + document.getElementById(this.mermaidSvgId)?.removeAttribute('height'); + document.getElementById(this.mermaidSvgId)?.removeAttribute('width'); + document.getElementById(this.mermaidSvgId)?.removeAttribute('viewBox'); + } } From 393da17f0af0e6a2f9e75a924efecc167b04bc9b Mon Sep 17 00:00:00 2001 From: R0tenur Date: Mon, 28 Mar 2022 21:37:56 +0200 Subject: [PATCH 12/55] add border to prevent button rezise on hover --- frontend/src/app/components/button/button.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/button/button.component.scss b/frontend/src/app/components/button/button.component.scss index 4797985..8927ceb 100644 --- a/frontend/src/app/components/button/button.component.scss +++ b/frontend/src/app/components/button/button.component.scss @@ -4,7 +4,7 @@ text-decoration: none; background: none; padding: 10px; - + border: 2px solid var(--vscode-background); display: inline-block; transition: all 0.4s ease 0s; border: none; From 661fd57f708d2107da1ea32bfa2173838ed582cd Mon Sep 17 00:00:00 2001 From: R0tenur Date: Wed, 30 Mar 2022 12:42:40 +0200 Subject: [PATCH 13/55] add basic add and rename features --- .../src/app/services/builder.service.spec.ts | 110 ++++++++++++++++++ frontend/src/app/services/builder.service.ts | 68 +++++++++++ 2 files changed, 178 insertions(+) create mode 100644 frontend/src/app/services/builder.service.spec.ts create mode 100644 frontend/src/app/services/builder.service.ts diff --git a/frontend/src/app/services/builder.service.spec.ts b/frontend/src/app/services/builder.service.spec.ts new file mode 100644 index 0000000..595af12 --- /dev/null +++ b/frontend/src/app/services/builder.service.spec.ts @@ -0,0 +1,110 @@ +import { TestBed } from '@angular/core/testing'; +import { BehaviorSubject } from 'rxjs'; +import { builderKey } from '../models/builder.model'; +import { Column } from '../models/column.model'; +import { Table } from '../models/table-svg.model'; +import { State } from '../state/state'; + +import { BuilderService } from './builder.service'; +import { StateInjector } from './state.token'; + +describe('BuilderService', () => { + let service: BuilderService; + let state: State; + let stateSubject: BehaviorSubject; + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [{ provide: StateInjector(builderKey), useValue: new State() }], + }); + state = TestBed.inject>(StateInjector(builderKey)); + spyOn(state, 'set'); + stateSubject = new BehaviorSubject(undefined as any as Table[]); + spyOnProperty(state, 'select$').and.returnValue(stateSubject.asObservable()); + service = TestBed.inject(BuilderService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('setTable', () => { + it('should set as new array', () => { + // Arrange + const arr = [new Table('table', 'dummy')]; + // Act + service.setTables(arr); + expect(state.set).toHaveBeenCalledWith(arr); + }); + }); + + describe('addTable', () => { + it('should add table', async () => { + // Arrange + const arr = [new Table('table', 'dummy')]; + stateSubject.next(arr); + const newTable = new Table('new table', 'dummy'); + + // Act + await service.addTable(newTable); + + // Assert + expect(state.set).toHaveBeenCalledWith([...arr, newTable]); + }); + }); + + describe('renameTable', () => { + it('should rename table', async () => { + // Arrange + const table = new Table('table', 'dummy'); + const arr = [table]; + stateSubject.next(arr); + const newName = 'new name'; + + // Act + await service.renameTable(table, newName); + + // Assert + expect(state.set).toHaveBeenCalledWith([new Table(newName, 'dummy')]); + }); + }); + + describe('addColumn', () => { + it('should add column', async () => { + // Arrange + const table = new Table('table', 'dummy'); + const arr = [table]; + stateSubject.next(arr); + const newName = 'new name'; + + // Act + await service.addColumn(table, newName); + + // Assert + const expected = new Table('table', 'dummy'); + expected.columns.push(new Column(0, table, newName)); + expect(state.set).toHaveBeenCalledWith([expected]); + }); + }); + + describe('renameColumn', () => { + it('should rename column', async () => { + // Arrange + const table = new Table('table', 'dummy'); + const arr = [table]; + stateSubject.next(arr); + const oldName = 'old name'; + const newName = 'new name'; + await service.addColumn(table, oldName); + + const column = new Column(0, table, oldName); + // Act + await service.renameColumn(table, column, newName); + + // Assert + const expected = new Table('table', 'dummy'); + expected.columns.push(new Column(0, table, newName)); + expect(state.set).toHaveBeenCalledWith([expected]); + }); + }); + +}); diff --git a/frontend/src/app/services/builder.service.ts b/frontend/src/app/services/builder.service.ts new file mode 100644 index 0000000..a806efe --- /dev/null +++ b/frontend/src/app/services/builder.service.ts @@ -0,0 +1,68 @@ +import { Inject, Injectable } from '@angular/core'; +import { take } from 'rxjs/operators'; +import { builderKey } from '../models/builder.model'; +import { Column } from '../models/column.model'; +import { Table } from '../models/table-svg.model'; +import { State } from '../state/state'; +import { StateInjector } from './state.token'; + +@Injectable({ + providedIn: 'root' +}) +export class BuilderService { + + constructor(@Inject(StateInjector(builderKey)) public readonly state: State) { + this.state.set([]); + } + public setTables(tables: Table[]): void { + this.state.set([...tables]); + } + public async addTable(table: Table): Promise { + const tables = await this.getTables(); + if (tables) { + this.state.set([...tables, table]); + } + } + + public async renameTable(table: Table, newName: string): Promise { + const tables = await this.getTables(); + if (tables) { + const tableIndex = tables.findIndex(t => t.name === table.name && t.schema === table.schema); + tables[tableIndex].name = newName; + this.state.set(tables); + } + } + public async addColumn(table: Table, columnName: string): Promise { + const tables = await this.getTables(); + if (tables) { + const tableRef = tables.find(t => t.name === table.name && t.schema === table.schema); + if (!tableRef) { return; } + + const column = new Column( + tableRef.columns.length, + tableRef, + columnName); + + tableRef.columns.push(column); + + this.state.set(tables); + } + } + + public async renameColumn(table: Table, column: Column, newName: string): Promise { + const tables = await this.getTables(); + if (tables) { + const tableRef = tables.find(t => t.name === table.name && t.schema === table.schema); + if (!tableRef) { return; } + + tableRef.columns[column.index].name = newName; + + this.state.set(tables); + } + } + private async getTables(): Promise { + const tables = await this.state.select$.pipe(take(1)).toPromise(); + + return [...tables]; + } +} From 11c9706e516c06e244a89cbe1e5df1a131f73f18 Mon Sep 17 00:00:00 2001 From: R0tenur Date: Wed, 30 Mar 2022 12:43:08 +0200 Subject: [PATCH 14/55] remove unsused test file for dev stuff --- .../dev-bar/dev-bar.component.spec.ts | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 frontend/src/app/components/dev-bar/dev-bar.component.spec.ts diff --git a/frontend/src/app/components/dev-bar/dev-bar.component.spec.ts b/frontend/src/app/components/dev-bar/dev-bar.component.spec.ts deleted file mode 100644 index 3f43eda..0000000 --- a/frontend/src/app/components/dev-bar/dev-bar.component.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { AppTestingModule } from '../../app-testing.module'; - -import { DevBarComponent } from './dev-bar.component'; - -describe('DevBarComponent', () => { - let component: DevBarComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [DevBarComponent], - imports: [AppTestingModule], - }) - .compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DevBarComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); From 985df8ecb3990e4ec593b9c3c100d3c8511c693d Mon Sep 17 00:00:00 2001 From: R0tenur Date: Wed, 30 Mar 2022 12:44:42 +0200 Subject: [PATCH 15/55] ignore testing module from coverage --- frontend/src/app/app-testing.module.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/app-testing.module.ts b/frontend/src/app/app-testing.module.ts index 0299392..77677d8 100644 --- a/frontend/src/app/app-testing.module.ts +++ b/frontend/src/app/app-testing.module.ts @@ -1,5 +1,6 @@ +/* istanbul ignore file */ import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterTestingModule } from '@angular/router/testing'; import { ButtonComponent } from './components/button/button.component'; @@ -41,11 +42,11 @@ const fakeMermaidProvider = ({ ButtonComponent, ], imports: [ - FormsModule, RouterTestingModule.withRoutes([]), + ReactiveFormsModule ], providers: [fakeWindowProvider], - exports: [ButtonComponent, FormsModule, RouterTestingModule] + exports: [ButtonComponent, RouterTestingModule, ReactiveFormsModule] }) export class AppTestingModule { } From 4bc248fb66dc166ac38ebff1503ed5c3feeeeaa9 Mon Sep 17 00:00:00 2001 From: R0tenur Date: Wed, 30 Mar 2022 12:50:35 +0200 Subject: [PATCH 16/55] set more suitable constants for the chart --- frontend/src/app/settings/constants.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/settings/constants.ts b/frontend/src/app/settings/constants.ts index 581aed6..7f8a69c 100644 --- a/frontend/src/app/settings/constants.ts +++ b/frontend/src/app/settings/constants.ts @@ -4,25 +4,29 @@ const border = '--vscode-editorWidget-border'; const getCssVariable = (variable: string) => getComputedStyle(document.documentElement) .getPropertyValue(variable); + export interface Constants { tableWidth: number; padding: number; - margin: number; - headerHeight: number; + titleMargin: number; + titleHeight: number; + columnHeight: number; + columnMargin: number; fontSize: number; - tablesOnRow: number; + borderWidth: number; backgroundColor: string; fontColor: string; borderColor: string; } export default { tableWidth: 200, - padding: 25, - margin: 30, - headerHeight: 60, - fontSize: 12, - tablesOnRow: 4, - backgroundColor: getCssVariable(background), - fontColor: getCssVariable(forground), - borderColor: getCssVariable(border), + padding: 18, + titleMargin: 30, + titleHeight: 25, + borderWidth: 2, + columnHeight: 20, + fontSize: 16, + backgroundColor: getCssVariable(background) || getComputedStyle(document.body).backgroundColor, + fontColor: getCssVariable(forground) || getComputedStyle(document.body).color, + borderColor: getCssVariable(border) || getComputedStyle(document.body).color, } as Constants; From 8461c4fa279923fccdf7df6b10d891c3f59e140b Mon Sep 17 00:00:00 2001 From: R0tenur Date: Thu, 31 Mar 2022 15:38:08 +0200 Subject: [PATCH 17/55] add models for new builder --- frontend/src/app/models/add-relation.model.ts | 8 ++ frontend/src/app/models/builder.model.ts | 1 + frontend/src/app/models/column.model.ts | 24 ++++++ frontend/src/app/models/relation.model.ts | 73 +++++++++++++++++++ frontend/src/app/models/rename.model.ts | 11 +++ frontend/src/app/models/svg-position.model.ts | 6 ++ frontend/src/app/models/table-svg.model.ts | 57 +++++++++++++++ 7 files changed, 180 insertions(+) create mode 100644 frontend/src/app/models/add-relation.model.ts create mode 100644 frontend/src/app/models/builder.model.ts create mode 100644 frontend/src/app/models/column.model.ts create mode 100644 frontend/src/app/models/relation.model.ts create mode 100644 frontend/src/app/models/rename.model.ts create mode 100644 frontend/src/app/models/svg-position.model.ts create mode 100644 frontend/src/app/models/table-svg.model.ts diff --git a/frontend/src/app/models/add-relation.model.ts b/frontend/src/app/models/add-relation.model.ts new file mode 100644 index 0000000..a75e229 --- /dev/null +++ b/frontend/src/app/models/add-relation.model.ts @@ -0,0 +1,8 @@ +import { Column } from './column.model'; +import { Position } from './position.model'; +export const addRelationKey = 'columnRelationKey'; + +export interface AddRelation { + column: Column; + position: Position; +} diff --git a/frontend/src/app/models/builder.model.ts b/frontend/src/app/models/builder.model.ts new file mode 100644 index 0000000..ae253cb --- /dev/null +++ b/frontend/src/app/models/builder.model.ts @@ -0,0 +1 @@ +export const builderKey = 'builderKey'; diff --git a/frontend/src/app/models/column.model.ts b/frontend/src/app/models/column.model.ts new file mode 100644 index 0000000..f107d59 --- /dev/null +++ b/frontend/src/app/models/column.model.ts @@ -0,0 +1,24 @@ +import constants from '../settings/constants'; +import { SvgPosition } from './svg-position.model'; +import { Table } from './table-svg.model'; + +export class Column implements SvgPosition { + public readonly width = constants.tableWidth; + public readonly height = constants.columnHeight; + public get xBox(): number { + return this.table.x; + } + public get xText(): number { + return this.table.x + constants.padding; + } + public get yBox(): number { + return this.table.y + this.table.titleHeight + (this.height * (this.index)); + } + public get yText(): number { + return this.table.y + this.table.titleHeight + (this.height * (this.index) + constants.padding); + } + constructor( + public readonly index: number, + public readonly table: Table, + public name: string) { } +} diff --git a/frontend/src/app/models/relation.model.ts b/frontend/src/app/models/relation.model.ts new file mode 100644 index 0000000..da0d3a7 --- /dev/null +++ b/frontend/src/app/models/relation.model.ts @@ -0,0 +1,73 @@ +import { Table } from './table-svg.model'; +import { Column } from './column.model'; +export const relationStateKey = 'relationStateKey'; +export class Relation { + public get tablesHasPositions(): boolean { + return this.toTable.hasPosition && this.fromTable.hasPosition; + } + public get fromArrowXStart(): number { + return this.fromToTheRight ? this.from.xBox - 14 : this.from.xBox + this.from.width + 14; + } + public get fromArrowXEnd(): number { + return this.fromToTheRight ? this.from.xBox - 6 : this.from.xBox + this.from.width + 6; + } + public get fromArrowYUpEnd(): number { + return this.fromToTheRight ? this.from.yBox + 5 : this.from.yBox + 15; + } + public get fromArrowYDownEnd(): number { + return this.fromToTheRight ? this.from.yBox + 15 : this.from.yBox + 5; + } + + public get toArrowXStart(): number { + return this.fromToTheRight ? this.to.xBox + this.to.width + 2 : this.to.xBox - 2; + } + public get toArrowXEnd(): number { + return this.fromToTheRight ? this.to.xBox + this.to.width + 10 : this.to.xBox - 10; + } + public get toArrowYUpEnd(): number { + return this.fromToTheRight ? this.to.yBox + 15 : this.to.yBox + 5; + } + public get toArrowYDownEnd(): number { + return this.fromToTheRight ? this.to.yBox + 5 : this.to.yBox + 15; + } + + public get fromXStart(): number { + return this.fromToTheRight ? this.from.xBox - 20 : this.from.xBox + this.from.width + 20; + } + public get fromXEnd(): number { + return this.fromToTheRight ? this.from.xBox : this.from.xBox + this.from.width; + } + public get fromX(): number { + return this.fromXStart; + } + public get fromY(): number { + return this.from.yBox + 10; + + } + public get toX(): number { + return this.toXStart; + } + public get toXStart(): number { + return this.fromToTheRight ? this.toXEnd + 20 : this.toXEnd - 20; + } + public get toXEnd(): number { + return this.fromToTheRight ? this.to.xBox + this.to.width : this.to.xBox; + } + public get toY(): number { + return this.to.yBox + 10; + } + public get fromTable(): Table { + return this.from.table; + } + private get fromToTheRight(): boolean { + return this.from.table.x > this.to.table.x + this.to.width / 2; + } + + constructor(private readonly from: Column, private readonly to: Column) { + from.table.fromRelations.push(this); + to.table.toRelations.push(this); + } + public get toTable(): Table { + return this.from.table; + } +} diff --git a/frontend/src/app/models/rename.model.ts b/frontend/src/app/models/rename.model.ts new file mode 100644 index 0000000..b51e66c --- /dev/null +++ b/frontend/src/app/models/rename.model.ts @@ -0,0 +1,11 @@ + +import { Column } from './column.model'; +import { Position } from './position.model'; +import { Table } from './table-svg.model'; + +export const renameKey = 'renameModel'; +export interface Rename { + position: Position; + table?: Table; + column?: Column; +} diff --git a/frontend/src/app/models/svg-position.model.ts b/frontend/src/app/models/svg-position.model.ts new file mode 100644 index 0000000..459928e --- /dev/null +++ b/frontend/src/app/models/svg-position.model.ts @@ -0,0 +1,6 @@ +export interface SvgPosition { + xBox: number; + xText: number; + yBox: number; + yText: number; +} diff --git a/frontend/src/app/models/table-svg.model.ts b/frontend/src/app/models/table-svg.model.ts new file mode 100644 index 0000000..fbf5c96 --- /dev/null +++ b/frontend/src/app/models/table-svg.model.ts @@ -0,0 +1,57 @@ +import constants from '../settings/constants'; +import { Column } from './column.model'; +import { Relation } from './relation.model'; + +export class Table { + public get x(): number { + return this.xPosition ?? 0; + } + public set x(value: number) { + this.xPosition = value; + } + public get xText(): number { + return this.x + constants.padding; + } + public get y(): number { + return this.yPosition ?? 0; + } + public set y(value: number) { + this.yPosition = value; + } + public get yText(): number { + return this.y + constants.padding; + } + public get yBox(): number { + return this.y; + } + public get height(): number { + return this.titleHeight + this.columns.length * constants.columnHeight; + } + public get bottomPosition(): number { + return this.y + this.height; + } + public get rightXPosition(): number { + return this.x + this.width; + } + public get hasPosition(): boolean { + return this.xPosition !== undefined && this.yPosition !== undefined; + } + + public readonly fromRelations: Relation[] = []; + public readonly toRelations: Relation[] = []; + public readonly columns: Column[] = []; + public readonly width = constants.tableWidth; + public readonly titleHeight = constants.titleHeight; + + private xPosition: number | undefined; + private yPosition: number | undefined; + constructor( + public name: string, + public readonly schema: string) { + } + public setPosition(x: number, y: number): void { + this.x = x; + this.y = y; + } +} + From 0c064ced7b2a68e4c10db5cca4b14d6682952ed8 Mon Sep 17 00:00:00 2001 From: R0tenur Date: Thu, 31 Mar 2022 15:39:09 +0200 Subject: [PATCH 18/55] add component for basic context menu --- .../app/components/context-menu/context-menu.component.html | 2 +- .../src/app/components/context-menu/context-menu.component.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/context-menu/context-menu.component.html b/frontend/src/app/components/context-menu/context-menu.component.html index e659fe8..02a38c5 100644 --- a/frontend/src/app/components/context-menu/context-menu.component.html +++ b/frontend/src/app/components/context-menu/context-menu.component.html @@ -1,4 +1,4 @@ -
+