From ae25df52d02062fa69a77c28b5536827b4774627 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Mon, 17 Dec 2018 17:17:35 +0900 Subject: [PATCH 01/10] Change to --- ...e-code-snippet-action-dialog.component.html | 2 +- .../note-code-snippet-editor.component.html | 2 +- src/browser/stack/stack-shared/index.ts | 2 ++ .../stack-item.component.html} | 2 +- .../stack-item.component.scss} | 4 +++- .../stack-item.component.spec.ts} | 18 +++++++++--------- .../stack-item.component.ts} | 9 +++++---- .../stack-shared.module.ts} | 8 ++++---- src/browser/stack/stack-ship/index.ts | 2 -- src/browser/stack/stack.module.ts | 6 +++--- 10 files changed, 29 insertions(+), 26 deletions(-) create mode 100644 src/browser/stack/stack-shared/index.ts rename src/browser/stack/{stack-ship/stack-chip.component.html => stack-shared/stack-item.component.html} (82%) rename src/browser/stack/{stack-ship/stack-chip.component.scss => stack-shared/stack-item.component.scss} (74%) rename src/browser/stack/{stack-ship/stack-chip.component.spec.ts => stack-shared/stack-item.component.spec.ts} (83%) rename src/browser/stack/{stack-ship/stack-chip.component.ts => stack-shared/stack-item.component.ts} (85%) rename src/browser/stack/{stack-ship/stack-chip.module.ts => stack-shared/stack-shared.module.ts} (56%) delete mode 100644 src/browser/stack/stack-ship/index.ts diff --git a/src/browser/note/note-editor/note-code-snippet-action-dialog/note-code-snippet-action-dialog.component.html b/src/browser/note/note-editor/note-code-snippet-action-dialog/note-code-snippet-action-dialog.component.html index 6749f8d6..99fef949 100644 --- a/src/browser/note/note-editor/note-code-snippet-action-dialog/note-code-snippet-action-dialog.component.html +++ b/src/browser/note/note-editor/note-code-snippet-action-dialog/note-code-snippet-action-dialog.component.html @@ -32,7 +32,7 @@

{{ dialogTitle }}

- + {{ stack.name }} diff --git a/src/browser/note/note-editor/note-code-snippet-editor/note-code-snippet-editor.component.html b/src/browser/note/note-editor/note-code-snippet-editor/note-code-snippet-editor.component.html index 4c830753..ad4b49c4 100644 --- a/src/browser/note/note-editor/note-code-snippet-editor/note-code-snippet-editor.component.html +++ b/src/browser/note/note-editor/note-code-snippet-editor/note-code-snippet-editor.component.html @@ -1,5 +1,5 @@
- + {{ _config.codeFileName }}
diff --git a/src/browser/stack/stack-shared/index.ts b/src/browser/stack/stack-shared/index.ts new file mode 100644 index 00000000..3a4e7d20 --- /dev/null +++ b/src/browser/stack/stack-shared/index.ts @@ -0,0 +1,2 @@ +export * from './stack-shared.module'; +export * from './stack-item.component'; diff --git a/src/browser/stack/stack-ship/stack-chip.component.html b/src/browser/stack/stack-shared/stack-item.component.html similarity index 82% rename from src/browser/stack/stack-ship/stack-chip.component.html rename to src/browser/stack/stack-shared/stack-item.component.html index 13a7d59d..bcfa422c 100644 --- a/src/browser/stack/stack-ship/stack-chip.component.html +++ b/src/browser/stack/stack-shared/stack-item.component.html @@ -1,5 +1,5 @@
+ class="StackItem__wrapper">
diff --git a/src/browser/stack/stack-ship/stack-chip.component.scss b/src/browser/stack/stack-shared/stack-item.component.scss similarity index 74% rename from src/browser/stack/stack-ship/stack-chip.component.scss rename to src/browser/stack/stack-shared/stack-item.component.scss index b4713745..a5e95aee 100644 --- a/src/browser/stack/stack-ship/stack-chip.component.scss +++ b/src/browser/stack/stack-shared/stack-item.component.scss @@ -1,4 +1,4 @@ -.StackChip { +.StackItem { display: inline-flex; cursor: inherit; @@ -8,7 +8,9 @@ height: 16px; > img { + margin: 0 auto; display: block; + height: 100%; } } } diff --git a/src/browser/stack/stack-ship/stack-chip.component.spec.ts b/src/browser/stack/stack-shared/stack-item.component.spec.ts similarity index 83% rename from src/browser/stack/stack-ship/stack-chip.component.spec.ts rename to src/browser/stack/stack-shared/stack-item.component.spec.ts index 0c20cf78..6ac25a41 100644 --- a/src/browser/stack/stack-ship/stack-chip.component.spec.ts +++ b/src/browser/stack/stack-shared/stack-item.component.spec.ts @@ -4,17 +4,17 @@ import { expectDom, fastTestSetup } from '../../../../test/helpers'; import { UiModule } from '../../ui/ui.module'; import { StackDummy } from '../dummies'; import { Stack } from '../stack.model'; -import { StackChipComponent } from './stack-chip.component'; +import { StackItemComponent } from './stack-item.component'; -describe('browser.stack.stackChip.StackChipComponent', () => { - let fixture: ComponentFixture; - let component: StackChipComponent; +describe('browser.stack.stackShared.StackItemComponent', () => { + let fixture: ComponentFixture; + let component: StackItemComponent; const stackDummy = new StackDummy(); const getIconEl = (): HTMLImageElement => - fixture.debugElement.query(By.css('.StackChip__wrapper > img')).nativeElement as HTMLImageElement; + (fixture.debugElement.nativeElement as HTMLElement).querySelector('.StackItem__wrapper > img'); fastTestSetup(); @@ -25,14 +25,14 @@ describe('browser.stack.stackChip.StackChipComponent', () => { UiModule, ], declarations: [ - StackChipComponent, + StackItemComponent, ], }) .compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(StackChipComponent); + fixture = TestBed.createComponent(StackItemComponent); component = fixture.componentInstance; }); @@ -43,7 +43,7 @@ describe('browser.stack.stackChip.StackChipComponent', () => { }); it('should icon not exists.', () => { - expect(fixture.debugElement.query(By.css('.StackChip__wrapper > img'))).toBeNull(); + expect(fixture.debugElement.query(By.css('.StackItem__wrapper > img'))).toBeNull(); }); it('should tooltip are not enabled because tooltip message is empty.', fakeAsync(() => { @@ -76,7 +76,7 @@ describe('browser.stack.stackChip.StackChipComponent', () => { }); it('should icon not exists.', () => { - expect(fixture.debugElement.query(By.css('.StackChip__wrapper > img'))).toBeNull(); + expect(fixture.debugElement.query(By.css('.StackItem__wrapper > img'))).toBeNull(); }); it('should now show tooltip when input \'disableTooltip\' to true.', fakeAsync(() => { diff --git a/src/browser/stack/stack-ship/stack-chip.component.ts b/src/browser/stack/stack-shared/stack-item.component.ts similarity index 85% rename from src/browser/stack/stack-ship/stack-chip.component.ts rename to src/browser/stack/stack-shared/stack-item.component.ts index d90720e1..09ca2209 100644 --- a/src/browser/stack/stack-ship/stack-chip.component.ts +++ b/src/browser/stack/stack-shared/stack-item.component.ts @@ -13,16 +13,17 @@ import { Stack } from '../stack.model'; @Component({ - selector: 'gd-stack-chip', - templateUrl: './stack-chip.component.html', - styleUrls: ['./stack-chip.component.scss'], + selector: 'gd-stack-item', + templateUrl: './stack-item.component.html', + styleUrls: ['./stack-item.component.scss'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, host: { + 'class': 'StackItem', '[attr.aria-label]': 'stack?.name', }, }) -export class StackChipComponent implements DoCheck { +export class StackItemComponent implements DoCheck { @Input() stack: Stack; @Input() disableTooltip = true; diff --git a/src/browser/stack/stack-ship/stack-chip.module.ts b/src/browser/stack/stack-shared/stack-shared.module.ts similarity index 56% rename from src/browser/stack/stack-ship/stack-chip.module.ts rename to src/browser/stack/stack-shared/stack-shared.module.ts index a3f58cfe..0863dea2 100644 --- a/src/browser/stack/stack-ship/stack-chip.module.ts +++ b/src/browser/stack/stack-shared/stack-shared.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { UiModule } from '../../ui/ui.module'; -import { StackChipComponent } from './stack-chip.component'; +import { StackItemComponent } from './stack-item.component'; @NgModule({ @@ -8,12 +8,12 @@ import { StackChipComponent } from './stack-chip.component'; UiModule, ], declarations: [ - StackChipComponent, + StackItemComponent, ], exports: [ - StackChipComponent, + StackItemComponent, ], }) -export class StackChipModule { +export class StackSharedModule { } diff --git a/src/browser/stack/stack-ship/index.ts b/src/browser/stack/stack-ship/index.ts deleted file mode 100644 index 7d7d24df..00000000 --- a/src/browser/stack/stack-ship/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './stack-chip.module'; -export * from './stack-chip.component'; diff --git a/src/browser/stack/stack.module.ts b/src/browser/stack/stack.module.ts index e69e4b65..48ec89b3 100644 --- a/src/browser/stack/stack.module.ts +++ b/src/browser/stack/stack.module.ts @@ -1,17 +1,17 @@ import { NgModule } from '@angular/core'; -import { StackChipModule } from './stack-ship'; +import { StackSharedModule } from './stack-shared'; import { StackViewer } from './stack-viewer'; @NgModule({ imports: [ - StackChipModule, + StackSharedModule, ], providers: [ StackViewer, ], exports: [ - StackChipModule, + StackSharedModule, ], }) export class StackModule { From 2e4b3d4cafd8118d043d42edd684a768ef627730 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Mon, 17 Dec 2018 17:48:54 +0900 Subject: [PATCH 02/10] Keep 'src/assets' directory --- src/assets/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/assets/.gitkeep diff --git a/src/assets/.gitkeep b/src/assets/.gitkeep new file mode 100644 index 00000000..e69de29b From 1eff826a7a4c71a0c1e304321ad19c5f95a44468 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Wed, 19 Dec 2018 17:34:22 +0900 Subject: [PATCH 03/10] Add 'getStackWithSafe' --- src/browser/stack/stack-viewer.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/browser/stack/stack-viewer.ts b/src/browser/stack/stack-viewer.ts index b88e30fd..5f141ec7 100644 --- a/src/browser/stack/stack-viewer.ts +++ b/src/browser/stack/stack-viewer.ts @@ -28,6 +28,16 @@ export class StackViewer { return this.stacks.find(stack => stack.name === name) || null; } + getStackWithSafe(name: string): Stack { + let stack = this.getStack(name); + + if (stack === null) { + stack = { name } as Stack; + } + + return stack; + } + search(query: string): Stack[] { return new SearchModel() .registerScoringStrategy(3, (stack, _query) => From 45196700a827ae12edd31787ae9d468a9a1e0294 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Wed, 19 Dec 2018 17:34:59 +0900 Subject: [PATCH 04/10] Fix 'rename' -> 'move': Rename overwrite file by default --- src/browser/shared/fs.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/browser/shared/fs.service.ts b/src/browser/shared/fs.service.ts index b6c38a76..76cfcd76 100644 --- a/src/browser/shared/fs.service.ts +++ b/src/browser/shared/fs.service.ts @@ -3,11 +3,11 @@ import { copy, CopyOptions, ensureDir, + move, pathExists, readdir, readFile, readJson, - rename, writeFile, writeJson, } from 'fs-extra'; @@ -61,6 +61,7 @@ export class FsService { } renameFile(oldFileName: string, newFileName: string): Observable { - return from(rename(oldFileName, newFileName)).pipe(enterZone(this.ngZone)); + return from(move(oldFileName, newFileName, { overwrite: false })) + .pipe(enterZone(this.ngZone)); } } From 72e62f310a4746dff82ebd084c523d1fbe64b3ee Mon Sep 17 00:00:00 2001 From: seokju-na Date: Wed, 19 Dec 2018 17:40:44 +0900 Subject: [PATCH 05/10] Add 'changeNoteStacks', Refactor 'changeNoteTitle' Add suffix for duplicated note content file name. --- .../note-collection.service.spec.ts | 49 +++++++------ .../note-collection.service.ts | 68 +++++++++++-------- 2 files changed, 66 insertions(+), 51 deletions(-) diff --git a/src/browser/note/note-collection/note-collection.service.spec.ts b/src/browser/note/note-collection/note-collection.service.spec.ts index 07d649ec..f4efd2bf 100644 --- a/src/browser/note/note-collection/note-collection.service.spec.ts +++ b/src/browser/note/note-collection/note-collection.service.spec.ts @@ -22,6 +22,7 @@ import { NoteStateWithRoot } from '../note.state'; import { NoteDummy, NoteItemDummy } from './dummies'; import { AddNoteAction, + ChangeNoteStacksAction, ChangeNoteTitleAction, DeleteNoteAction, DeselectNoteAction, @@ -318,13 +319,17 @@ describe('browser.note.noteCollection.NoteCollectionService', () => { const callback = jasmine.createSpy('change note title spy'); collection.changeNoteTitle(note, newTitle).then(callback).catch(err => error = err); + flush(); mockFs .expect({ - methodName: 'isPathExists', - args: [newContentFilePath], + methodName: 'renameFile', + args: [ + note.contentFilePath, // Old Path + newContentFilePath, // New Path + ], }) - .flush(true); + .error(new Error()); expect(error instanceof NoteContentFileAlreadyExistsError).toBe(true); expect((error as NoteError).code).toEqual(NoteErrorCodes.CONTENT_FILE_ALREADY_EXISTS); @@ -345,31 +350,17 @@ describe('browser.note.noteCollection.NoteCollectionService', () => { const callback = jasmine.createSpy('change note title spy'); collection.changeNoteTitle(note, newTitle).then(callback); + flush(); - // Pass file duplication. mockFs - .expect({ - methodName: 'isPathExists', - args: [newContentFilePath], - }) - .flush(false); - - // 2 stubs - const stubs = mockFs.expectMany([ - { - methodName: 'writeJsonFile', - args: [note.filePath, FsMatchLiterals.ANY], - }, - { + .expect({ methodName: 'renameFile', args: [ note.contentFilePath, // Old Path newContentFilePath, // New Path ], - }, - ]); - - stubs.forEach(stub => stub.flush()); + }) + .flush(); expect(store.dispatch).toHaveBeenCalledWith(new ChangeNoteTitleAction({ note, @@ -380,6 +371,22 @@ describe('browser.note.noteCollection.NoteCollectionService', () => { })); }); + describe('changeNoteStacks', () => { + it('should dispatch \"CHANGE_NOTE_STACKS\' action.', () => { + spyOn(store, 'dispatch'); + + const note = noteItemDummy.create(); + const stacks = ['a', 'b', 'c']; + + collection.changeNoteStacks(note, stacks); + + expect(store.dispatch).toHaveBeenCalledWith(new ChangeNoteStacksAction({ + note, + stacks, + })); + }); + }); + describe('getNoteVcsFileChangeStatus', () => { it('should return status of note.', () => { const vcsFileChanges = new Subject(); diff --git a/src/browser/note/note-collection/note-collection.service.ts b/src/browser/note/note-collection/note-collection.service.ts index 72a676f0..50636cc5 100644 --- a/src/browser/note/note-collection/note-collection.service.ts +++ b/src/browser/note/note-collection/note-collection.service.ts @@ -4,7 +4,7 @@ import { select, Store } from '@ngrx/store'; import { shell } from 'electron'; import * as path from 'path'; import { Observable, Subject, Subscription, zip } from 'rxjs'; -import { filter, map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap, take } from 'rxjs/operators'; import { makeNoteContentFileName, Note, NoteSnippetTypes } from '../../../core/note'; import { VcsFileChange, VcsFileChangeStatusTypes } from '../../../core/vcs'; import { isOutsidePath } from '../../../libs/path'; @@ -16,7 +16,9 @@ import { convertToNoteSnippets, NoteParser } from '../note-shared'; import { NoteStateWithRoot } from '../note.state'; import { AddNoteAction, - ChangeNoteTitleAction, DeleteNoteAction, + ChangeNoteStacksAction, + ChangeNoteTitleAction, + DeleteNoteAction, DeselectNoteAction, LoadNoteCollectionAction, LoadNoteCollectionCompleteAction, @@ -97,19 +99,15 @@ export class NoteCollectionService implements OnDestroy { })); } - getFilteredAndSortedNoteList(waitForInitial: boolean = true): Observable { + getFilteredAndSortedNoteList(): Observable { return this.store.pipe( - select(state => state.note.collection), - filter(state => waitForInitial ? state.loaded : true), - select(state => state.filteredAndSortedNotes), + select(state => state.note.collection.filteredAndSortedNotes), ); } - getSelectedNote(waitForInitial: boolean = true): Observable { + getSelectedNote(): Observable { return this.store.pipe( - select(state => state.note.collection), - filter(state => waitForInitial ? state.loaded : true), - select(state => state.selectedNote), + select(state => state.note.collection.selectedNote), ); } @@ -215,33 +213,39 @@ export class NoteCollectionService implements OnDestroy { async changeNoteTitle(noteItem: NoteItem, newTitle: string): Promise { const dirName = path.dirname(noteItem.contentFilePath); - const newContentFileName = makeNoteContentFileName(noteItem.createdDatetime, newTitle); - const newContentFilePath = path.resolve(dirName, newContentFileName); + let newContentFileName: string; + let newContentFilePath: string; + + const allNotes = await toPromise(this.store.pipe( + select(state => state.note.collection.notes), + take(1), + )); + const allNoteContentFilePaths = allNotes.map(item => item.contentFilePath); + + let index = 0; + const isNoteTitleDuplicated = title => allNoteContentFilePaths.includes(title); + + // Check title duplication. + do { + const title = index === 0 ? newTitle : `${newTitle}(${index})`; + + newContentFileName = makeNoteContentFileName(noteItem.createdDatetime, title); + newContentFilePath = path.resolve(dirName, newContentFileName); + index++; + } while (isNoteTitleDuplicated(newContentFilePath)); // If content file name is same, just ignore. if (newContentFileName === noteItem.contentFileName) { return; } - if (await toPromise(this.fs.isPathExists(newContentFilePath))) { + // Rename file. + try { + await toPromise(this.fs.renameFile(noteItem.contentFilePath, newContentFilePath)); + } catch (error) { throw new NoteContentFileAlreadyExistsError(); } - const note: Note = { - id: noteItem.id, - title: newTitle, - snippets: noteItem.snippets, - stackIds: noteItem.stackIds, - contentFileName: newContentFileName, - contentFilePath: newContentFilePath, - createdDatetime: noteItem.createdDatetime, - }; - - await toPromise(zip( - this.fs.writeJsonFile(noteItem.filePath, note), - this.fs.renameFile(noteItem.contentFilePath, newContentFilePath), - )); - this.store.dispatch(new ChangeNoteTitleAction({ note: noteItem, title: newTitle, @@ -250,6 +254,10 @@ export class NoteCollectionService implements OnDestroy { })); } + changeNoteStacks(noteItem: NoteItem, stacks: string[]): void { + this.store.dispatch(new ChangeNoteStacksAction({ note: noteItem, stacks })); + } + deleteNote(noteItem: NoteItem): void { const allRemoved = shell.moveItemToTrash(noteItem.filePath) && shell.moveItemToTrash(noteItem.contentFilePath); @@ -259,7 +267,7 @@ export class NoteCollectionService implements OnDestroy { return; } - this.getSelectedNote(false).pipe(take(1)).subscribe((selectedNote: NoteItem) => { + this.getSelectedNote().pipe(take(1)).subscribe((selectedNote: NoteItem) => { if (selectedNote && selectedNote.id === noteItem.id) { this.store.dispatch(new DeselectNoteAction()); } @@ -272,7 +280,7 @@ export class NoteCollectionService implements OnDestroy { this.toggleNoteSelectionSubscription = this._toggleNoteSelection.asObservable().pipe( switchMap(note => - this.getSelectedNote(false).pipe( + this.getSelectedNote().pipe( take(1), map(selectedNote => ([selectedNote, note])), ), From a4d1b39425678d48c5a0ec591b38017625cef006 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Wed, 19 Dec 2018 17:41:36 +0900 Subject: [PATCH 06/10] Change display type: 'block' -> 'inline-block' --- src/browser/ui/chips/chip-list.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/ui/chips/chip-list.component.scss b/src/browser/ui/chips/chip-list.component.scss index 4026a711..90ebf294 100644 --- a/src/browser/ui/chips/chip-list.component.scss +++ b/src/browser/ui/chips/chip-list.component.scss @@ -1,7 +1,7 @@ @import "./chip-sizes"; .ChipList { - display: block; + display: inline-flex; overflow: hidden; &__wrapper { From 47a4026e3b822789f0d7721fb2d4d7fde29d385a Mon Sep 17 00:00:00 2001 From: seokju-na Date: Wed, 19 Dec 2018 17:43:10 +0900 Subject: [PATCH 07/10] Refactor for changed method --- src/browser/app/app.component.ts | 2 +- .../note/note-collection/note-list/note-list.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/app/app.component.ts b/src/browser/app/app.component.ts index b39ecb0b..04edd113 100644 --- a/src/browser/app/app.component.ts +++ b/src/browser/app/app.component.ts @@ -51,7 +51,7 @@ export class AppComponent implements OnInit { ); readonly noSelectedNote: Observable = this.collection - .getSelectedNote(false) + .getSelectedNote() .pipe(map(selectedNote => selectedNote === null)); constructor( diff --git a/src/browser/note/note-collection/note-list/note-list.component.ts b/src/browser/note/note-collection/note-list/note-list.component.ts index 18e34239..788cbe7a 100644 --- a/src/browser/note/note-collection/note-list/note-list.component.ts +++ b/src/browser/note/note-collection/note-list/note-list.component.ts @@ -59,7 +59,7 @@ export class NoteListComponent implements OnInit, OnDestroy, AfterViewInit { ngOnInit(): void { this.selectedNoteSubscription = this.collection - .getSelectedNote(true) + .getSelectedNote() .subscribe(selectedNote => this._selectedNote = selectedNote); } From 6db89b9c09a4f0665294ab0637761685e9b674c0 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Wed, 19 Dec 2018 17:43:55 +0900 Subject: [PATCH 08/10] Fix 'ExpressionChangedAfterItHasBeenCheckedError' --- .../note/note-collection/note-list/note-list.component.html | 4 +++- .../note/note-collection/note-list/note-list.component.scss | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/browser/note/note-collection/note-list/note-list.component.html b/src/browser/note/note-collection/note-list/note-list.component.html index df8342fc..3321a7fa 100644 --- a/src/browser/note/note-collection/note-list/note-list.component.html +++ b/src/browser/note/note-collection/note-list/note-list.component.html @@ -1,5 +1,7 @@
-
+
No Notes
diff --git a/src/browser/note/note-collection/note-list/note-list.component.scss b/src/browser/note/note-collection/note-list/note-list.component.scss index 4fad1673..119a4627 100644 --- a/src/browser/note/note-collection/note-list/note-list.component.scss +++ b/src/browser/note/note-collection/note-list/note-list.component.scss @@ -7,10 +7,16 @@ min-height: 1px; &__emptyState { + visibility: visible; padding: $spacing-triple $spacing; font: { size: $font-size-md; weight: $font-weight-semiBold; }; + + &--hide { + display: none !important; + visibility: hidden; + } } } From 7884091790c09958554644ffb43526a38ee96088 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Wed, 19 Dec 2018 17:44:51 +0900 Subject: [PATCH 09/10] #109 Stack input in note --- .../note-collection.actions.ts | 15 +- .../note-collection/note-collection.module.ts | 2 + .../note-collection.reducer.spec.ts | 48 ++++ .../note-collection.reducer.ts | 7 + .../note/note-editor/_note-editor-theme.scss | 22 ++ .../note/note-editor/note-content.effects.ts | 5 +- .../note-editor/note-editor.component.html | 35 ++- .../note-editor/note-editor.component.scss | 31 +++ .../note-editor/note-editor.component.spec.ts | 190 +++++++++++++-- .../note/note-editor/note-editor.component.ts | 222 ++++++++++++------ 10 files changed, 487 insertions(+), 90 deletions(-) diff --git a/src/browser/note/note-collection/note-collection.actions.ts b/src/browser/note/note-collection/note-collection.actions.ts index 86608bf4..7b6a3d01 100644 --- a/src/browser/note/note-collection/note-collection.actions.ts +++ b/src/browser/note/note-collection/note-collection.actions.ts @@ -20,6 +20,7 @@ export enum NoteCollectionActionTypes { UPDATE_CONTRIBUTION_FAIL = '[NoteCollection] Update contribution fail', CHANGE_NOTE_TITLE = '[NoteCollection] Change note title', DELETE_NOTE = '[NoteCollection] Delete note', + CHANGE_NOTE_STACKS = '[NoteCollection] Change note stacks', } @@ -142,6 +143,17 @@ export class DeleteNoteAction implements Action { } +export class ChangeNoteStacksAction implements Action { + readonly type = NoteCollectionActionTypes.CHANGE_NOTE_STACKS; + + constructor(public readonly payload: { + note: NoteItem, + stacks: string[], + }) { + } +} + + export type NoteCollectionAction = LoadNoteCollectionAction | LoadNoteCollectionCompleteAction @@ -157,4 +169,5 @@ export type NoteCollectionAction = | UpdateNoteContributionAction | UpdateNoteContributionFailAction | ChangeNoteTitleAction - | DeleteNoteAction; + | DeleteNoteAction + | ChangeNoteStacksAction; diff --git a/src/browser/note/note-collection/note-collection.module.ts b/src/browser/note/note-collection/note-collection.module.ts index 254e647a..13362dcf 100644 --- a/src/browser/note/note-collection/note-collection.module.ts +++ b/src/browser/note/note-collection/note-collection.module.ts @@ -1,6 +1,7 @@ import { DatePipe } from '@angular/common'; import { NgModule } from '@angular/core'; import { SharedModule } from '../../shared'; +import { StackSharedModule } from '../../stack/stack-shared'; import { UiModule } from '../../ui/ui.module'; import { NoteSharedModule } from '../note-shared'; import { CreateNewNoteDialogComponent } from './create-new-note-dialog/create-new-note-dialog.component'; @@ -20,6 +21,7 @@ import { NoteListToolsComponent } from './note-list-tools/note-list-tools.compon UiModule, SharedModule, NoteSharedModule, + StackSharedModule, ], declarations: [ NoteCalendarComponent, diff --git a/src/browser/note/note-collection/note-collection.reducer.spec.ts b/src/browser/note/note-collection/note-collection.reducer.spec.ts index b9f2122f..97d82310 100644 --- a/src/browser/note/note-collection/note-collection.reducer.spec.ts +++ b/src/browser/note/note-collection/note-collection.reducer.spec.ts @@ -4,6 +4,7 @@ import { datetime, DateUnits } from '../../../libs/datetime'; import { NoteItemDummy, prepareForFilteringNotes, prepareForSortingNotes } from './dummies'; import { AddNoteAction, + ChangeNoteStacksAction, ChangeNoteTitleAction, ChangeSortDirectionAction, ChangeSortOrderAction, @@ -524,4 +525,51 @@ describe('browser.note.noteCollection.noteCollectionReducer', () => { expect(result.selectedNote).toBeNull(); }); }); + + describe('CHANGE_NOTE_STACKS', () => { + const dummy = new NoteItemDummy(); + let notes: NoteItem[]; + let beforeState: NoteCollectionState; + + beforeEach(() => { + notes = createDummies(dummy, 10); + beforeState = noteCollectionReducer( + undefined, + new LoadNoteCollectionCompleteAction({ notes }), + ); + }); + + it('should change note stack ids at index.', () => { + const targetNote = notes[3]; + const result = noteCollectionReducer( + beforeState, + new ChangeNoteStacksAction({ + note: targetNote, + stacks: ['a', 'b', 'c'], + }), + ); + + expect(result.notes[3].stackIds).toEqual(['a', 'b', 'c']); + }); + + it('should change note stack ids at index and selected note if index of note ' + + 'is currently selected.', () => { + const targetNote = notes[7]; + beforeState = noteCollectionReducer( + beforeState, + new SelectNoteAction({ note: targetNote }), + ); + + const result = noteCollectionReducer( + beforeState, + new ChangeNoteStacksAction({ + note: targetNote, + stacks: ['a', 'b', 'c'], + }), + ); + + expect(result.selectedNote.stackIds).toEqual(['a', 'b', 'c']); + expect(result.notes[7].stackIds).toEqual(['a', 'b', 'c']); + }); + }); }); diff --git a/src/browser/note/note-collection/note-collection.reducer.ts b/src/browser/note/note-collection/note-collection.reducer.ts index d5626b64..56fb77e4 100644 --- a/src/browser/note/note-collection/note-collection.reducer.ts +++ b/src/browser/note/note-collection/note-collection.reducer.ts @@ -243,6 +243,13 @@ export function noteCollectionReducer( withNoteDelete(state, getIndexOfNote(state, action.payload.note)), ); + case NoteCollectionActionTypes.CHANGE_NOTE_STACKS: + return withFilteredAndSortedNotes(withNoteUpdate( + state, + getIndexOfNote(state, action.payload.note), + { stackIds: action.payload.stacks }, + )); + default: return state; } diff --git a/src/browser/note/note-editor/_note-editor-theme.scss b/src/browser/note/note-editor/_note-editor-theme.scss index b4232938..94a03cfe 100644 --- a/src/browser/note/note-editor/_note-editor-theme.scss +++ b/src/browser/note/note-editor/_note-editor-theme.scss @@ -2,10 +2,32 @@ @import "../../ui/style/theming"; @mixin gd-note-editor-theme($theme) { + $is-dark: map-get($theme, is-dark); $background: map-get($theme, background); $foreground: map-get($theme, foreground); .NoteEditor { + $bg-color: if($is-dark, gd-color($background, background), gd-color($background, background-highlight)); + background-color: $bg-color; + + &__stacks { + background-color: gd-color($background, background-highlight); + border-bottom: 1px solid gd-color($foreground, divider); + + gd-form-field .FormField__content { + display: flex; + flex-wrap: wrap; + } + + input.ChipInput { + color: gd-color($foreground, text); + + &::placeholder { + color: gd-color($foreground, disabled-text); + } + } + } + &__titleTextarea { border-bottom: 1px solid gd-color($foreground, divider); diff --git a/src/browser/note/note-editor/note-content.effects.ts b/src/browser/note/note-editor/note-content.effects.ts index 8440147f..e0e179d4 100644 --- a/src/browser/note/note-editor/note-content.effects.ts +++ b/src/browser/note/note-editor/note-content.effects.ts @@ -3,6 +3,7 @@ import { Actions, Effect, ofType } from '@ngrx/effects'; import { select, Store } from '@ngrx/store'; import { Observable, of } from 'rxjs'; import { catchError, debounceTime, map, mergeMap, switchMap, take, takeUntil } from 'rxjs/operators'; +import { NoteCollectionAction, NoteCollectionActionTypes } from '../note-collection'; import { NoteState, NoteStateWithRoot } from '../note.state'; import { LoadNoteContentAction, @@ -49,12 +50,14 @@ export class NoteContentEffects { ); @Effect() - saveCurrentNote: Observable = this.actions.pipe( + saveCurrentNote: Observable = this.actions.pipe( ofType( NoteEditorActionTypes.APPEND_SNIPPET, NoteEditorActionTypes.UPDATE_SNIPPET, NoteEditorActionTypes.INSERT_SNIPPET, NoteEditorActionTypes.REMOVE_SNIPPET, + NoteCollectionActionTypes.CHANGE_NOTE_TITLE, + NoteCollectionActionTypes.CHANGE_NOTE_STACKS, ), debounceTime(NOTE_EDITOR_SAVE_NOTE_CONTENT_THROTTLE_TIME), switchMap(() => diff --git a/src/browser/note/note-editor/note-editor.component.html b/src/browser/note/note-editor/note-editor.component.html index 56a556dd..ee2cd85f 100644 --- a/src/browser/note/note-editor/note-editor.component.html +++ b/src/browser/note/note-editor/note-editor.component.html @@ -1,4 +1,37 @@ -
+
+
+ + + + + + {{ stack.name }} + + + + + + + + + + {{ item.name }} + + + +
+
diff --git a/src/browser/note/note-editor/note-editor.component.scss b/src/browser/note/note-editor/note-editor.component.scss index e910c2ba..3c74607a 100644 --- a/src/browser/note/note-editor/note-editor.component.scss +++ b/src/browser/note/note-editor/note-editor.component.scss @@ -1,5 +1,7 @@ +@import "./note-header/note-header-sizes"; @import "../../ui/style/spacing"; @import "../../ui/style/typography"; +@import "../../ui/form-field/form-field-sizes"; :host { display: flex; @@ -20,6 +22,35 @@ padding: $spacing; } + &__stacks { + min-height: $note-header-size; + padding: 2.5px $spacing; + + label { + margin-right: $spacing-half; + } + + .ChipList { + margin: 2px 0; + } + + input.ChipInput { + min-width: 100px; + background: transparent; + border: none; + outline: 0; + margin: 0; + padding: 0 $spacing-half; + height: $form-control-size; + } + } + + &__stackChip { + gd-stack-item { + margin-right: $spacing-half; + } + } + &__titleTextarea { padding-bottom: $spacing; diff --git a/src/browser/note/note-editor/note-editor.component.spec.ts b/src/browser/note/note-editor/note-editor.component.spec.ts index 57899e12..e2f3ed93 100644 --- a/src/browser/note/note-editor/note-editor.component.spec.ts +++ b/src/browser/note/note-editor/note-editor.component.spec.ts @@ -1,16 +1,37 @@ -import { DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'; +import { COMMA, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'; +import { DebugElement } from '@angular/core'; import { ComponentFixture, discardPeriodicTasks, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { combineReducers, Store, StoreModule } from '@ngrx/store'; -import { of, Subject } from 'rxjs'; -import { dispatchKeyboardEvent, fastTestSetup, typeInElement } from '../../../../test/helpers'; +import { of, ReplaySubject, Subject } from 'rxjs'; +import { + createDummies, + dispatchKeyboardEvent, + expectDom, + fastTestSetup, + sample, + typeInElement, +} from '../../../../test/helpers'; import { MockDialog } from '../../../../test/mocks/browser'; import { Asset, AssetTypes } from '../../../core/asset'; import { NoteSnippetTypes } from '../../../core/note'; -import { MenuEvent, MenuService, NativeDialog, NativeDialogOpenResult, SharedModule } from '../../shared'; +import { + MenuEvent, + MenuService, + NativeDialog, + NativeDialogConfig, + nativeDialogFileFilters, + NativeDialogOpenResult, + NativeDialogProperties, + SharedModule, +} from '../../shared'; +import { Stack, StackModule, StackViewer } from '../../stack'; +import { StackDummy } from '../../stack/dummies'; +import { ChipDirective } from '../../ui/chips'; import { Dialog } from '../../ui/dialog'; import { UiModule } from '../../ui/ui.module'; -import { NoteCollectionService, SelectNoteAction } from '../note-collection'; +import { NoteCollectionService, NoteItem, SelectNoteAction } from '../note-collection'; import { NoteItemDummy } from '../note-collection/dummies'; import { NoteSharedModule } from '../note-shared'; import { noteReducerMap } from '../note.reducer'; @@ -43,23 +64,52 @@ describe('browser.note.noteEditor.NoteEditorComponent', () => { let nativeDialog: NativeDialog; let noteEditor: NoteEditorService; let collection: NoteCollectionService; + let stackViewer: StackViewer; let menuMessages: Subject; + let selectedNoteStream: ReplaySubject; const noteDummy = new NoteItemDummy(); const contentDummy = new NoteContentDummy(); + const stackDummy = new StackDummy(); const getTitleTextareaEl = (): HTMLTextAreaElement => fixture.debugElement.query( By.css('.NoteEditor__titleTextarea > textarea'), ).nativeElement as HTMLTextAreaElement; - function ensureSnippets(snippetCount = 5): void { - const note = noteDummy.create(); - const content = contentDummy.create(snippetCount); + const getStackChipDeList = (): DebugElement[] => + fixture.debugElement.queryAll(By.css('.NoteEditor__stackChip')); + const getNoteStacksInputEl = (): HTMLInputElement => + fixture.debugElement.query(By.css('#note-stacks-input')).nativeElement as HTMLInputElement; + + function ensureSelectNoteAndLoadNoteContent( + note: NoteItem = noteDummy.create(), + content: NoteContent = contentDummy.create(), + ): [NoteItem, NoteContent] { + store.dispatch(new SelectNoteAction({ note })); store.dispatch(new LoadNoteContentCompleteAction({ note, content })); + fixture.detectChanges(); + listManager.addAllSnippetsFromContent(content); + selectedNoteStream.next(note); + fixture.detectChanges(); + + return [note, content]; + } + + function provideStackDummies( + stacks: Stack[] = createDummies(stackDummy, 5), + ): Stack[] { + // Remove all stacks. + while (stackViewer.stacks.length > 0) { + stackViewer.stacks.pop(); + } + + stackViewer.stacks.push(...stacks); + + return stacks; } function activateSnippetAtIndex(index: number): void { @@ -70,6 +120,10 @@ describe('browser.note.noteEditor.NoteEditorComponent', () => { store.dispatch(new BlurSnippetAction()); } + function ignoreStackSearchAutocomplete(): void { + component['subscribeStackAutocompleteSearch'] = jasmine.createSpy('subscribeStackAutocompleteSearch spy'); + } + fastTestSetup(); beforeAll(async () => { @@ -79,16 +133,20 @@ describe('browser.note.noteEditor.NoteEditorComponent', () => { collection = jasmine.createSpyObj('collection', [ 'changeNoteTitle', + 'getSelectedNote', + 'changeNoteStacks', ]); await TestBed .configureTestingModule({ imports: [ + NoopAnimationsModule, UiModule, SharedModule, StoreModule.forRoot({ note: combineReducers(noteReducerMap), }), + StackModule, NoteSharedModule, NoteEditorModule, ...MockDialog.imports(), @@ -108,11 +166,14 @@ describe('browser.note.noteEditor.NoteEditorComponent', () => { mockDialog = TestBed.get(Dialog); nativeDialog = TestBed.get(NativeDialog); noteEditor = TestBed.get(NoteEditorService); + stackViewer = TestBed.get(StackViewer); menuMessages = new Subject(); + selectedNoteStream = new ReplaySubject(1); (menu.onMessage as Spy).and.returnValue(menuMessages.asObservable()); spyOn(listManager, 'handleSnippetRefEvent').and.callThrough(); + (collection.getSelectedNote as Spy).and.callFake(() => selectedNoteStream.asObservable()); fixture = TestBed.createComponent(NoteEditorComponent); component = fixture.componentInstance; @@ -123,6 +184,94 @@ describe('browser.note.noteEditor.NoteEditorComponent', () => { mockDialog.closeAll(); }); + describe('stack input', () => { + beforeEach(() => { + ignoreStackSearchAutocomplete(); + }); + + it('should show chips which from note stacks when ngOnInit.', () => { + const selectedNote = noteDummy.create(); + const stacks = provideStackDummies(); + const noteStack = sample(stacks); + + selectedNote.stackIds.push(noteStack.name); + + ensureSelectNoteAndLoadNoteContent(selectedNote); + + const chipDeList = getStackChipDeList(); + expect(chipDeList.length).toEqual(1); + + // Chip value must be stack name. + const chipInstance = chipDeList[0].injector.get(ChipDirective); + expect(chipInstance.value).toEqual(noteStack.name); + + // Chip name must be shown. + expectDom(chipDeList[0].nativeElement as HTMLElement).toContainText(noteStack.name); + }); + + it('should call \'changeNoteStacks\' from collection service when stack has been ' + + 'added with ENTER keydown. (debounceTime=250ms)', fakeAsync(() => { + const stacks = provideStackDummies(); + const [selectedNote] = ensureSelectNoteAndLoadNoteContent(); + + const stack = sample(stacks); + const stacksInputEl = getNoteStacksInputEl(); + + typeInElement(stack.name, getNoteStacksInputEl()); + dispatchKeyboardEvent(stacksInputEl, 'keydown', ENTER); + fixture.detectChanges(); + tick(250); + + expect(collection.changeNoteStacks).toHaveBeenCalledWith(selectedNote, [stack.name]); + })); + + it('should call \'changeNoteStacks\' from collection service when stack has been ' + + 'added with COMMA keydown. (debounceTime=250ms)', fakeAsync(() => { + ignoreStackSearchAutocomplete(); + + const stacks = provideStackDummies(); + const [selectedNote] = ensureSelectNoteAndLoadNoteContent(); + + const stack = sample(stacks); + const stacksInputEl = getNoteStacksInputEl(); + + typeInElement(stack.name, getNoteStacksInputEl()); + dispatchKeyboardEvent(stacksInputEl, 'keydown', COMMA); + fixture.detectChanges(); + tick(250); + + expect(collection.changeNoteStacks).toHaveBeenCalledWith(selectedNote, [stack.name]); + })); + + it('should call \'changeNoteStacks\' from collection service when stack has been ' + + 'removed. (debounceTime=250ms)', fakeAsync(() => { + ignoreStackSearchAutocomplete(); + + const stacks = provideStackDummies(); + const [selectedNote] = ensureSelectNoteAndLoadNoteContent(); + + const prevStack = sample(stacks); + const stacksInputEl = getNoteStacksInputEl(); + + // Add stack + typeInElement(prevStack.name, getNoteStacksInputEl()); + dispatchKeyboardEvent(stacksInputEl, 'keydown', COMMA); + fixture.detectChanges(); + tick(250); + + expect(collection.changeNoteStacks).toHaveBeenCalledWith(selectedNote, [prevStack.name]); + + // Remove stack + getStackChipDeList()[0].injector + .get(ChipDirective) + .remove(); + fixture.detectChanges(); + tick(250); + + expect(collection.changeNoteStacks).toHaveBeenCalledWith(selectedNote, []); + })); + }); + describe('title textarea', () => { it('should move focus first index of snippet when pressed enter in title textarea.', () => { fixture.detectChanges(); @@ -169,7 +318,7 @@ describe('browser.note.noteEditor.NoteEditorComponent', () => { it('should not handle create new snippet event if active snippet index is \'null\'.', () => { fixture.detectChanges(); - ensureSnippets(); + ensureSelectNoteAndLoadNoteContent(); deactivateSnippet(); fixture.detectChanges(); @@ -181,7 +330,7 @@ describe('browser.note.noteEditor.NoteEditorComponent', () => { it('should handle create new snippet event when active snippet index is exists.', () => { fixture.detectChanges(); - ensureSnippets(); + ensureSelectNoteAndLoadNoteContent(); activateSnippetAtIndex(2); fixture.detectChanges(); @@ -206,7 +355,7 @@ describe('browser.note.noteEditor.NoteEditorComponent', () => { + 'And handle create new snippet when user close dialog with result.', () => { fixture.detectChanges(); - ensureSnippets(); + ensureSelectNoteAndLoadNoteContent(); activateSnippetAtIndex(0); fixture.detectChanges(); @@ -270,7 +419,10 @@ describe('browser.note.noteEditor.NoteEditorComponent', () => { it('should show file open dialog when insert image event received while type of active snippet is ' + '\'TEXT\'. And dispatch NoteSnippetEditorInsertImageEvent when user select file.', fakeAsync(() => { const selectedNote = noteDummy.create(); - store.dispatch(new SelectNoteAction({ note: selectedNote })); + const content = contentDummy.create(); + content.snippets[0] = new NoteSnippetContentDummy().create(NoteSnippetTypes.TEXT); + + ensureSelectNoteAndLoadNoteContent(selectedNote, content); activateSnippetAtIndex(0); fixture.detectChanges(); @@ -290,7 +442,12 @@ describe('browser.note.noteEditor.NoteEditorComponent', () => { flush(); flush(); - expect(nativeDialog.showOpenDialog).toHaveBeenCalled(); + expect(nativeDialog.showOpenDialog).toHaveBeenCalledWith({ + message: 'Choose an image:', + properties: NativeDialogProperties.OPEN_FILE, + fileFilters: [nativeDialogFileFilters.IMAGES], + } as NativeDialogConfig); + expect(noteEditor.copyAssetFile).toHaveBeenCalledWith( AssetTypes.IMAGE, selectedNote.contentFilePath, @@ -315,13 +472,13 @@ describe('browser.note.noteEditor.NoteEditorComponent', () => { describe('Note title', () => { it('should update note title when selected note changes.', () => { const prevSelectedNote = noteDummy.create(); - store.dispatch(new SelectNoteAction({ note: prevSelectedNote })); + ensureSelectNoteAndLoadNoteContent(prevSelectedNote); fixture.detectChanges(); expect(getTitleTextareaEl().value).toContain(prevSelectedNote.title); const nextSelectedNote = noteDummy.create(); - store.dispatch(new SelectNoteAction({ note: nextSelectedNote })); + selectedNoteStream.next(nextSelectedNote); fixture.detectChanges(); expect(getTitleTextareaEl().value).toContain(nextSelectedNote.title); @@ -331,8 +488,7 @@ describe('browser.note.noteEditor.NoteEditorComponent', () => { + 'after 250ms.', fakeAsync(() => { (collection.changeNoteTitle as Spy).and.callFake(() => Promise.resolve(null)); - const selectedNote = noteDummy.create(); - store.dispatch(new SelectNoteAction({ note: selectedNote })); + const [selectedNote] = ensureSelectNoteAndLoadNoteContent(); fixture.detectChanges(); typeInElement('New Title', getTitleTextareaEl()); diff --git a/src/browser/note/note-editor/note-editor.component.ts b/src/browser/note/note-editor/note-editor.component.ts index 5df7ac9d..90df9dea 100644 --- a/src/browser/note/note-editor/note-editor.component.ts +++ b/src/browser/note/note-editor/note-editor.component.ts @@ -1,14 +1,17 @@ -import { DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'; +import { COMMA, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'; import { Component, ElementRef, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; import { FormControl } from '@angular/forms'; import { select, Store } from '@ngrx/store'; -import { from, Observable, of, Subscription } from 'rxjs'; -import { catchError, debounceTime, filter, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { from, Observable, of, Subject, Subscription } from 'rxjs'; +import { catchError, debounceTime, filter, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; import { AssetTypes } from '../../../core/asset'; import { NoteSnippetTypes } from '../../../core/note'; import { toPromise } from '../../../libs/rx'; import { MenuEvent, MenuService, NativeDialog, nativeDialogFileFilters, NativeDialogProperties } from '../../shared'; import { ConfirmDialog } from '../../shared/confirm-dialog'; +import { Stack, StackViewer } from '../../stack'; +import { AutocompleteTriggerDirective } from '../../ui/autocomplete'; +import { ChipInputEvent } from '../../ui/chips'; import { NoteCollectionService } from '../note-collection'; import { NoteContentFileAlreadyExistsError } from '../note-errors'; import { NoteStateWithRoot } from '../note.state'; @@ -28,6 +31,9 @@ import { NoteSnippetListManager } from './note-snippet-list-manager'; selector: 'gd-note-editor', templateUrl: './note-editor.component.html', styleUrls: ['./note-editor.component.scss'], + host: { + 'class': 'NoteEditor', + }, }) export class NoteEditorComponent implements OnInit, OnDestroy { private get filteredMenuMessages(): Observable { @@ -40,16 +46,28 @@ export class NoteEditorComponent implements OnInit, OnDestroy { ); } + searchedStacks: Stack[] = []; + stacks: Stack[] = []; + + readonly stacksChanged = new Subject(); + readonly stackInputControl = new FormControl(''); + readonly stackSearchControl = new FormControl(''); + readonly stackInputSeparatorKeyCodes = [ENTER, COMMA]; + readonly titleInputControl = new FormControl(''); @ViewChild('scrollable') scrollable: ElementRef; @ViewChild('snippetsList') snippetsList: ElementRef; @ViewChild('titleTextarea') titleTextarea: ElementRef; + @ViewChild('stackAutoTrigger') stackAutocompleteTrigger: AutocompleteTriggerDirective; private listTopFocusOutSubscription = Subscription.EMPTY; private menuMessageSubscription = Subscription.EMPTY; - private titleChangeSubscription = Subscription.EMPTY; - private selectedNoteTitleChangedSubscription = Subscription.EMPTY; + private selectedNoteChangedSubscription = Subscription.EMPTY; + + private titleChangesSubscription = Subscription.EMPTY; + private stacksChangesSubscription = Subscription.EMPTY; + private stackAutocompleteSearchSubscription = Subscription.EMPTY; constructor( private snippetListManager: NoteSnippetListManager, @@ -61,80 +79,34 @@ export class NoteEditorComponent implements OnInit, OnDestroy { private editorService: NoteEditorService, private collectionService: NoteCollectionService, private confirmDialog: ConfirmDialog, + private stackViewer: StackViewer, ) { } + get stackNames(): string[] { + return this.stacks.map(stack => stack.name); + } + ngOnInit(): void { this.snippetListManager .setContainerElement(this.snippetsList.nativeElement) .setViewContainerRef(this._viewContainerRef); - this.listTopFocusOutSubscription = this.snippetListManager.topFocusOut() - .subscribe(() => { - if (this.titleTextarea) { - this.titleTextarea.nativeElement.focus(); - } - }); - - this.menuMessageSubscription = this.filteredMenuMessages.pipe( - withLatestFrom(this.store.pipe(select(state => state.note.editor))), - ).subscribe(([event, editorState]) => { - const { activeSnippetIndex } = editorState as NoteEditorState; - - if (activeSnippetIndex === null) { - return; - } - - const ref = this.snippetListManager.getSnippetRefByIndex(activeSnippetIndex); - - // If snippet is not exists, just ignore. - if (!ref) { - return; - } - - switch (event as MenuEvent) { - case MenuEvent.NEW_TEXT_SNIPPET: - this.createNewTextSnippet(ref); - break; - - case MenuEvent.NEW_CODE_SNIPPET: - this.createNewCodeSnippet(ref); - break; - - case MenuEvent.INSERT_IMAGE: - this.insertImageAtSnippet(ref); - break; - } - }); - - this.selectedNoteTitleChangedSubscription = this.store.pipe( - select(state => state.note.collection.selectedNote), - filter(selectedNote => !!selectedNote), - ).subscribe((note) => { - this.titleInputControl.setValue(note.title, { emitEvent: false }); - }); - - this.titleChangeSubscription = this.titleInputControl.valueChanges.pipe( - debounceTime(250), - withLatestFrom(this.store.pipe( - select(state => state.note.collection.selectedNote), - )), - switchMap(([newTitle, note]) => - from(this.collectionService.changeNoteTitle(note, newTitle)).pipe( - catchError((error) => { - this.handleNoteTitleChangeError(error, newTitle); - return of(null); - }), - ), - ), - ).subscribe(); + this.subscribeListTopFocusOut(); + this.subscribeMenuMessage(); + this.subscribeSelectedNoteChanged(); + this.subscribeStackAutocompleteSearch(); + this.subscribeStacksChanges(); + this.subscribeTitleChanges(); } ngOnDestroy(): void { this.listTopFocusOutSubscription.unsubscribe(); this.menuMessageSubscription.unsubscribe(); - this.selectedNoteTitleChangedSubscription.unsubscribe(); - this.titleChangeSubscription.unsubscribe(); + this.selectedNoteChangedSubscription.unsubscribe(); + this.titleChangesSubscription.unsubscribe(); + this.stacksChangesSubscription.unsubscribe(); + this.stackAutocompleteSearchSubscription.unsubscribe(); } moveFocusToSnippetEditor(event: KeyboardEvent): void { @@ -146,6 +118,30 @@ export class NoteEditorComponent implements OnInit, OnDestroy { } } + addStack(event: ChipInputEvent): void { + const name = event.value; + const stack = this.stackViewer.getStackWithSafe(name); + + if (!this.stacks.some(s => s.name === name)) { + this.stacks.push(stack); + this.stacksChanged.next(this.stacks); + } + + this.stackSearchControl.patchValue(''); + } + + removeStack(stack: Stack): void { + const index = this.stacks.findIndex(s => s.name === stack.name); + + if (index !== -1) { + this.stacks.splice(index, 1); + this.stacksChanged.next(this.stacks); + + // Close panel is expected behavior. + this.stackAutocompleteTrigger.closePanel(); + } + } + private createNewTextSnippet(ref: NoteSnippetEditorRef): void { const snippet: NoteSnippetContent = { type: NoteSnippetTypes.TEXT, @@ -187,10 +183,9 @@ export class NoteEditorComponent implements OnInit, OnDestroy { return; } - const currentSelectedNote = await toPromise(this.store.pipe( - select(state => state.note.collection.selectedNote), - take(1), - )); + const currentSelectedNote = await toPromise( + this.collectionService.getSelectedNote().pipe(take(1)), + ); if (!currentSelectedNote) { return; @@ -229,4 +224,91 @@ export class NoteEditorComponent implements OnInit, OnDestroy { body: message, }); } + + private subscribeListTopFocusOut(): void { + this.listTopFocusOutSubscription = this.snippetListManager.topFocusOut() + .subscribe(() => { + if (this.titleTextarea) { + this.titleTextarea.nativeElement.focus(); + } + }); + } + + private subscribeMenuMessage(): void { + this.menuMessageSubscription = this.filteredMenuMessages.pipe( + withLatestFrom(this.store.pipe(select(state => state.note.editor))), + ).subscribe(([event, editorState]) => { + const { activeSnippetIndex } = editorState as NoteEditorState; + + if (activeSnippetIndex === null) { + return; + } + + const ref = this.snippetListManager.getSnippetRefByIndex(activeSnippetIndex); + + // If snippet is not exists, just ignore. + if (!ref) { + return; + } + + switch (event as MenuEvent) { + case MenuEvent.NEW_TEXT_SNIPPET: + this.createNewTextSnippet(ref); + break; + + case MenuEvent.NEW_CODE_SNIPPET: + this.createNewCodeSnippet(ref); + break; + + case MenuEvent.INSERT_IMAGE: + this.insertImageAtSnippet(ref); + break; + } + }); + } + + private subscribeSelectedNoteChanged(): void { + this.selectedNoteChangedSubscription = this.collectionService.getSelectedNote() + .subscribe((note) => { + if (note) { + this.titleInputControl.setValue(note.title, { emitEvent: false }); + this.stacks = note.stackIds.map(name => this.stackViewer.getStackWithSafe(name)); + } + }); + } + + private subscribeTitleChanges(): void { + this.titleChangesSubscription = this.titleInputControl.valueChanges.pipe( + debounceTime(250), + withLatestFrom(this.collectionService.getSelectedNote()), + switchMap(([newTitle, note]) => + from(this.collectionService.changeNoteTitle(note, newTitle)).pipe( + catchError((error) => { + this.handleNoteTitleChangeError(error, newTitle); + return of(null); + }), + ), + ), + ).subscribe(); + } + + private subscribeStacksChanges(): void { + this.stacksChangesSubscription = this.stacksChanged.pipe( + debounceTime(250), + withLatestFrom(this.collectionService.getSelectedNote()), + tap(([stacks, note]) => + this.collectionService.changeNoteStacks(note, stacks.map(stack => stack.name)), + ), + ).subscribe(); + } + + private subscribeStackAutocompleteSearch(): void { + this.stackAutocompleteSearchSubscription = this.stackSearchControl.valueChanges + .pipe(debounceTime(50)) + .subscribe((value) => { + this.searchedStacks = this.stackViewer + .search(value as string) + .filter(stack => !this.stackNames.includes(stack.name)); + }); + } } From 8198c5d70517d2ac62e5584725b2f16fdb263467 Mon Sep 17 00:00:00 2001 From: seokju-na Date: Wed, 19 Dec 2018 17:45:15 +0900 Subject: [PATCH 10/10] Update note snippet editor theme --- .../_note-code-snippet-editor-theme.scss | 9 ++++++++- .../_note-text-snippet-editor-theme.scss | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/browser/note/note-editor/note-code-snippet-editor/_note-code-snippet-editor-theme.scss b/src/browser/note/note-editor/note-code-snippet-editor/_note-code-snippet-editor-theme.scss index 5e4c2d0f..655924ab 100644 --- a/src/browser/note/note-editor/note-code-snippet-editor/_note-code-snippet-editor-theme.scss +++ b/src/browser/note/note-editor/note-code-snippet-editor/_note-code-snippet-editor-theme.scss @@ -2,6 +2,7 @@ @import "../../../ui/style/spacing"; @mixin gd-note-code-snippet-editor-theme($theme) { + $is-dark-theme: map-get($theme, is-dark); $primary: map-get($theme, primary); $background: map-get($theme, background); $foreground: map-get($theme, foreground); @@ -13,6 +14,8 @@ } &__wrapper { + background-color: gd-color($background, background); + border-left: 1px solid gd-color($foreground, divider); border-right: 1px solid gd-color($foreground, divider); border-bottom: 1px solid gd-color($foreground, divider); @@ -55,7 +58,11 @@ } .CodeMirror-activeline-background { - background: gd-color($background, background-highlight, .65); + background: if( + $is-dark-theme, + gd-color($background, background-highlight, .65), + gd-color($background, hover) + ); } } } diff --git a/src/browser/note/note-editor/note-text-snippet-editor/_note-text-snippet-editor-theme.scss b/src/browser/note/note-editor/note-text-snippet-editor/_note-text-snippet-editor-theme.scss index baa89547..3cc859ab 100644 --- a/src/browser/note/note-editor/note-text-snippet-editor/_note-text-snippet-editor-theme.scss +++ b/src/browser/note/note-editor/note-text-snippet-editor/_note-text-snippet-editor-theme.scss @@ -15,7 +15,7 @@ } &__editor { - background-color: gd-color($background, background); + // background-color: gd-color($background, background); } span.cm-variable-2, span.cm-variable-3, span.cm-keyword {