Skip to content

Commit

Permalink
feat(history-commands)
Browse files Browse the repository at this point in the history
Save history command in local storage. Some refactor to make the flow more reactive
  • Loading branch information
gigitux committed Jan 11, 2021
1 parent 5da1257 commit 6823f9e
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 60 deletions.
17 changes: 11 additions & 6 deletions e2e/src/app.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ describe('workspace-project App', () => {

it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('Welcome to redis patterns app !');
expect(page.getTitleText()).toEqual('Redis Patterns Console 1.1.0');
});

afterEach(async () => {
// #important When APIS don't work because the limit has been exceeded, the app prints many errors in console
// In the codebase this case should be handled, in the meantime it is better to disable this check


// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
// const logs = await browser.manage().logs().get(logging.Type.BROWSER);
// expect(logs).not.toContain(jasmine.objectContaining({
// level: logging.Level.SEVERE,
// } as logging.Entry));
// });
})
});
61 changes: 61 additions & 0 deletions e2e/src/command-line/command-line.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { browser, logging } from 'protractor';

import { AppPage } from '../app.po';
import { CommandLineComponent } from './command-line.po';

const command = "set test test"
const command1 = "set test1 test1"
const command2 = "set test2 test2"
const commands = [command, command1, command2]

describe('Command line', () => {
let page: AppPage;
let commandLine: CommandLineComponent

beforeEach(() => {
page = new AppPage();
commandLine = new CommandLineComponent()
});

afterEach(() => {
browser.executeScript('window.localStorage.clear();');
})

it('should check that history is correct [1]', async () => {
await page.navigateTo()


commands.forEach((val) => commandLine.sendCommand(val))
commandLine.upKey()
commandLine.upKey()

const value = await commandLine.getValue()
return expect(value).toEqual(command1)

});

it('should check that history is correct [2]', async () => {
await page.navigateTo()


commands.forEach((val) => commandLine.sendCommand(val))
commandLine.upKey()
commandLine.downKey()

const value = await commandLine.getValue()
return expect(value).toEqual(command2)
});

it('should check that history is correct [3]', async () => {
await page.navigateTo()

const array = new Array(50)

commands.forEach((val) => commandLine.sendCommand(val))
array.forEach(() => commandLine.downKey())


const value = await commandLine.getValue()
return expect(value).toEqual("")
});
})
28 changes: 28 additions & 0 deletions e2e/src/command-line/command-line.po.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { browser, by, element, ElementFinder, Key, WebElement, WebElementPromise } from 'protractor';

export class CommandLineComponent {
element: ElementFinder
constructor() {
this.element = element(by.tagName('tr-command-line'))
}

async sendCommand(command: string) {
const input = this.element.$("input")
await input.sendKeys(command)
await input.sendKeys(Key.ENTER)
}

upKey() {
const input = this.element.$("input")
input.sendKeys(Key.ARROW_UP)
}

downKey() {
const input = this.element.$("input")
input.sendKeys(Key.ARROW_DOWN)
}

getValue() {
return this.element.$("input").getAttribute('value')
}
}
6 changes: 1 addition & 5 deletions e2e/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
"types": ["jasmine", "jasminewd2", "node"]
}
}
8 changes: 8 additions & 0 deletions src/app/core/utilities/local-storage.utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum SESSIONSTORAGEKEYS {
HISTORYCOMMAND = 'HistoryCommand',
}
export const getFromLocalStorage = (key: SESSIONSTORAGEKEYS) =>
sessionStorage.getItem(key);

export const setToLocalStorage = (key: SESSIONSTORAGEKEYS, value: string) =>
sessionStorage.setItem(key, value);
2 changes: 2 additions & 0 deletions src/app/core/utilities/logic.utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const isNil = <T>(value: T): value is null => value === null;
export const isNotNil = <T>(value: T): value is T => value !== null;
30 changes: 13 additions & 17 deletions src/app/features/command/command-line/command-line.component.html
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
<div class="command-line-container">
<input type="text"
class="form-control"
placeholder="Insert command"
[formControl]="commandLine"
(keyup.enter)="executeCommand(commandLine.value)"
(keydown)="getHistory($event)">
<input #input type="text" class="form-control" placeholder="Insert command" [formControl]="commandLine"
(keyup.enter)="executeCommandSubj$.next(commandLine.value)" (keydown.arrowup)="arrowUpClickSubj$.next($event)"
(keydown.arrowdown)="arrowDownClickSubj$.next($event)" />

<div class="font-italic text-muted suggestions pl-2 mb-0 h-auto">
<small *ngIf="!commandLine.dirty || commandLine.value.length < 3">no command enter (min. 3 char)</small>
<small *ngIf="commandLine.valid" class="suggestion"> {{ activeCommand?.suggestion }}</small>
<small
class="text-danger"
*ngIf="!commandLine.valid
&& commandLine.errors
&& commandLine.errors.allowedCommand
&& commandLine.errors.allowedCommand.value
&& commandLine.errors.allowedCommand.value.length >= 3">
command not found
<small *ngIf="
!commandLine.dirty || commandLine.value.length < commandLineMinLetter
">
no command enter (min. 3 char)
</small>
<small *ngIf="commandLine.valid" class="suggestion">
{{ activeCommand?.suggestion }}</small>
<small class="text-danger" *ngIf="isValidCommand()">
command not found
</small>
</div>
</div>
</div>
144 changes: 112 additions & 32 deletions src/app/features/command/command-line/command-line.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
import { Component, EventEmitter, Input, OnInit, Output, ChangeDetectionStrategy } from '@angular/core';
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
ChangeDetectionStrategy,
ViewChild,
ElementRef,
OnDestroy,
} from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
import {
debounceTime,
distinctUntilChanged,
filter,
map,
mapTo,
takeUntil,
tap,
withLatestFrom,
} from 'rxjs/operators';

import { allowedCommandValidator } from './command-line.validator';
import { Command } from '@app/shared/models/command.interface';
import { BehaviorSubject, Observable, ReplaySubject, Subject } from 'rxjs';
import { CommandsHistory } from './command-line.interface';
import {
createHistoryPipe,
getHistoryFromLocalStorage,
setNewCommandsHistoryInLocalStorage,
setValueFn,
} from './command-line.utilities';

@Component({
selector: 'tr-command-line',
Expand All @@ -12,20 +39,36 @@ import { Command } from '@app/shared/models/command.interface';
`
.input-group-text {
background: white;
font-size: .9em;
font-size: 0.9em;
}
.form-control {
font-size: .9em;
font-size: 0.9em;
}
`
`,
],
changeDetection: ChangeDetectionStrategy.OnPush
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CommandLineComponent implements OnInit {
export class CommandLineComponent implements OnInit, OnDestroy {
commandLine: FormControl;
commandsHistory: string[] = [];
commandsHistoryCursor = 0;
commandLineMinLetter = 3;

commandsHistorySubj$ = new BehaviorSubject<CommandsHistory>({
commandsHistory: getHistoryFromLocalStorage(),
commandsHistoryCursor: 0,
});

arrowUpClickSubj$ = new Subject<Event>();
arrowDownClickSubj$ = new Subject<Event>();
inputElementRefSubj$ = new ReplaySubject<ElementRef<HTMLInputElement> | null>(
1
);
executeCommandSubj$ = new ReplaySubject<string>(1);

executeCommandPipe$: Observable<void>;
historyPipe$: Observable<void>;

@Input() allowedCommands: Array<Command> = [];
@Input() activeCommand: Command;
Expand All @@ -41,24 +84,68 @@ export class CommandLineComponent implements OnInit {
}
}

@ViewChild('input') set inputElement(
inputElement: ElementRef<HTMLInputElement> | null
) {
this.inputElementRefSubj$.next(inputElement);
}

@Output() detectCommand: EventEmitter<any> = new EventEmitter();
@Output() execute: EventEmitter<any> = new EventEmitter();

private destroySubj$: Subject<void> = new Subject<void>();

/**
* Init command line input text with its validator
* and start to observe its value changes.
*/
ngOnInit() {
this.commandLine = new FormControl('', [
Validators.required,
allowedCommandValidator(this.allowedCommands)
allowedCommandValidator(this.allowedCommands),
]);
this.commandLine.valueChanges.pipe(
debounceTime(200),

this.historyPipe$ = createHistoryPipe(
this.arrowUpClickSubj$.asObservable(),
this.arrowDownClickSubj$.asObservable(),
this.commandsHistorySubj$,
this.commandsHistorySubj$.asObservable(),
this.inputElementRefSubj$.asObservable(),
this.destroySubj$.asObservable(),
setValueFn(this.commandLine)
);

this.executeCommandPipe$ = this.executeCommandSubj$.pipe(
filter(() => this.commandLine.valid),
map((value) => value.split(' ')[0]),
distinctUntilChanged()
).subscribe((value) => this.detectCommand.emit(value));
tap((value) => this.execute.emit(value.trim())),
withLatestFrom(this.commandsHistorySubj$),
tap(([command, { commandsHistory }]) => {
const newHistory = commandsHistory[0] === command ? commandsHistory : [command, ...commandsHistory]

setNewCommandsHistoryInLocalStorage(newHistory);

this.commandsHistorySubj$.next({
commandsHistory: newHistory,
commandsHistoryCursor: 0,
});
}),
mapTo(void 0),
takeUntil(this.destroySubj$.asObservable())
);

this.historyPipe$.subscribe();
this.executeCommandPipe$.subscribe();

this.commandLine.valueChanges
.pipe(
debounceTime(200),
filter(() => this.commandLine.valid),
map((value) => value.split(' ')[0]),
distinctUntilChanged(),
takeUntil(this.destroySubj$.asObservable())

)
.subscribe((value) => this.detectCommand.emit(value));
}

/**
Expand All @@ -78,25 +165,18 @@ export class CommandLineComponent implements OnInit {
}
}

/**
* Implements command input history on keyboard arrow up/down press event
* @param event a keyboard event
*/
getHistory(event: KeyboardEvent) {
if (event.key === 'ArrowUp') {
event.preventDefault();
if (this.commandsHistory.length > 0 && this.commandsHistory.length > this.commandsHistoryCursor) {
this.commandLine.setValue(this.commandsHistory[this.commandsHistoryCursor++]);
}
}
isValidCommand() {
return (
!this.commandLine.valid &&
this.commandLine.errors &&
this.commandLine.errors.allowedCommand &&
this.commandLine.errors.allowedCommand.value &&
this.commandLine.errors.allowedCommand.value.length >=
this.commandLineMinLetter
);
}

if (event.key === 'ArrowDown') {
if (this.commandsHistory.length > 0 && this.commandsHistoryCursor > 0) {
this.commandLine.setValue(this.commandsHistory[--this.commandsHistoryCursor]);
} else {
this.commandsHistoryCursor = 0;
this.commandLine.setValue('');
}
}
ngOnDestroy() {
this.destroySubj$.next();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface CommandsHistory {
commandsHistory: Array<string>;
commandsHistoryCursor: number;
}
Loading

0 comments on commit 6823f9e

Please sign in to comment.