From 4e13f910dd60eee059e9932e14ffcc4d2562e002 Mon Sep 17 00:00:00 2001 From: Nejc Zdovc Date: Sun, 25 Sep 2016 19:54:36 +0200 Subject: [PATCH] Data layer - Macro & UX improvments (#117) Closes #75 --- .../macro/header/macro-header.component.ts | 14 +- .../macro/item/macro-item.component.html | 12 +- .../macro/item/macro-item.component.scss | 80 +++++++---- .../macro/item/macro-item.component.ts | 4 +- src/components/macro/macro.component.html | 60 ++++---- src/components/macro/macro.component.scss | 29 +++- src/components/macro/macro.component.ts | 132 +++++++++-------- .../tab/macro/macro-tab.component.html | 2 +- src/store/actions/macro.ts | 78 +++++++++- src/store/effects/macro.ts | 21 +++ src/store/reducers/macro.ts | 134 +++++++++++++++++- 11 files changed, 431 insertions(+), 135 deletions(-) create mode 100644 src/store/effects/macro.ts diff --git a/src/components/macro/header/macro-header.component.ts b/src/components/macro/header/macro-header.component.ts index 27200670..69ff2da9 100644 --- a/src/components/macro/header/macro-header.component.ts +++ b/src/components/macro/header/macro-header.component.ts @@ -1,7 +1,12 @@ import { Component, Input } from '@angular/core'; +import { Store } from '@ngrx/store'; + import { Macro } from '../../../config-serializer/config-items/Macro'; +import { MacroActions } from '../../../store/actions'; +import { AppState } from '../../../store/index'; + @Component({ selector: 'macro-header', template: require('./macro-header.component.html'), @@ -10,18 +15,17 @@ import { Macro } from '../../../config-serializer/config-items/Macro'; export class MacroHeaderComponent { @Input() macro: Macro; - constructor() { } + constructor(private store: Store) { } removeMacro() { - // TODO implement + this.store.dispatch(MacroActions.removeMacro(this.macro.id)); } duplicateMacro() { - // TODO implement + this.store.dispatch(MacroActions.duplicateMacro(this.macro)); } - /* tslint:disable:no-unused-variable */ editMacroName(name: string) { - // TODO implement + this.store.dispatch(MacroActions.editMacroName(this.macro.id, name)); } } diff --git a/src/components/macro/item/macro-item.component.html b/src/components/macro/item/macro-item.component.html index b790ee61..1e6a4e67 100644 --- a/src/components/macro/item/macro-item.component.html +++ b/src/components/macro/item/macro-item.component.html @@ -1,9 +1,11 @@
- - -
{{ title }}
- - + +
+ +
{{ title }}
+ +
+
div { - display: flex; - flex: 1; - } - - &:first-child { - border-radius: 0; - } - - &.is-editing { - background: #f5f5f5; - } + icon { + margin: 0 5px; } - &--movable { - &:hover { - cursor: move; + > div { + display: flex; + flex: 1; + } + + &:first-child { + border-radius: 0; + } + + &.is-editing { + background: #f5f5f5; + } + + &--wrap { + justify-content: space-between; + + &.pointer { + &:hover { + cursor: pointer; + color: $icon-hover; + } } } } - .macro-action-editor__container { - padding-top: 0; - padding-bottom: 0; - border-radius: 0; - border-left: 0; - border-right: 0; + &--title { + display: flex; + flex: 1; + } + + &--movable { + &:hover { + cursor: move; + } } } + +.macro-action-editor__container { + padding-top: 0; + padding-bottom: 0; + border-radius: 0; + border-left: 0; + border-right: 0; +} diff --git a/src/components/macro/item/macro-item.component.ts b/src/components/macro/item/macro-item.component.ts index ef473c90..d1663166 100644 --- a/src/components/macro/item/macro-item.component.ts +++ b/src/components/macro/item/macro-item.component.ts @@ -53,8 +53,6 @@ export class MacroItemComponent implements OnInit, OnChanges { } saveEditedAction(editedAction: MacroAction): void { - // @todo save this to keyboard - console.log('Saved action', editedAction); this.macroAction = editedAction; this.editing = false; this.updateView(); @@ -62,7 +60,7 @@ export class MacroItemComponent implements OnInit, OnChanges { } editAction(): void { - if (!this.editable) { + if (!this.editable || this.editing) { return; } this.editing = true; diff --git a/src/components/macro/macro.component.html b/src/components/macro/macro.component.html index 079b9449..1f0b9858 100644 --- a/src/components/macro/macro.component.html +++ b/src/components/macro/macro.component.html @@ -1,30 +1,42 @@ - -
-
-
- -
-
-
- Add new action item + + +
+ Sorry, there is no macro with this id.
\ No newline at end of file diff --git a/src/components/macro/macro.component.scss b/src/components/macro/macro.component.scss index 8e86418b..bed43ee4 100644 --- a/src/components/macro/macro.component.scss +++ b/src/components/macro/macro.component.scss @@ -104,16 +104,31 @@ h1 { .add-new__action-item { border-radius: 0 0 4px 4px; border-top: 0; + padding: 0; &:hover { cursor: pointer; } - &--link, - &--link:active, - &--link:hover { - text-decoration: none; + &--link { + width: 50%; + float: left; + padding: 10px 5px; + text-align: center; color: $icon-hover; + + &:first-of-type { + border-right: 1px solid #ddd; + } + + &:hover { + text-decoration: none; + background: #e6e6e6; + } + } + + .fa-circle { + color: #c00; } } @@ -137,3 +152,9 @@ h1 { user-select: none; } } + +.not-found { + margin-top: 30px; + font-size: 16px; + text-align: center; +} diff --git a/src/components/macro/macro.component.ts b/src/components/macro/macro.component.ts index 3c7d0b21..ce52b31a 100644 --- a/src/components/macro/macro.component.ts +++ b/src/components/macro/macro.component.ts @@ -1,6 +1,10 @@ -import { Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'; +import { Component, OnDestroy, QueryList, ViewChildren } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import '@ngrx/core/add/operator/select'; +import { Store } from '@ngrx/store'; +import 'rxjs/add/operator/let'; +import 'rxjs/add/operator/switchMap'; import { Subscription } from 'rxjs/Subscription'; import { DragulaService } from 'ng2-dragula/ng2-dragula'; @@ -9,7 +13,9 @@ import { Macro } from '../../config-serializer/config-items/Macro'; import { MacroAction } from '../../config-serializer/config-items/macro-action'; import { MacroItemComponent } from './item/macro-item.component'; -import { UhkConfigurationService } from '../../services/uhk-configuration.service'; +import { AppState } from '../../store'; +import { MacroActions } from '../../store/actions'; +import { getMacro } from '../../store/reducers/macro'; @Component({ selector: 'macro', @@ -17,15 +23,17 @@ import { UhkConfigurationService } from '../../services/uhk-configuration.servic styles: [require('./macro.component.scss')], viewProviders: [DragulaService] }) -export class MacroComponent implements OnInit, OnDestroy { +export class MacroComponent implements OnDestroy { @ViewChildren(MacroItemComponent) macroItems: QueryList; - private macro: Macro; - private routeSubscription: Subscription; - private hasChanges: boolean = false; + private showNew: boolean = false; + private newMacro: Macro = undefined; + private activeEdit: number = undefined; + private dragIndex: number; + private subscription: Subscription; constructor( - private uhkConfigurationService: UhkConfigurationService, + private store: Store, private route: ActivatedRoute, private dragulaService: DragulaService ) { @@ -35,65 +43,75 @@ export class MacroComponent implements OnInit, OnDestroy { return handle.className.includes('action--movable'); } }); - /* tslint:enable:no-unused-variable */ - } - ngOnInit() { - this.routeSubscription = this.route.params.subscribe((params: { id: string }) => { - const id: number = Number(params.id); - this.macro = this.getMacro(id); - this.hasChanges = false; + dragulaService.drag.subscribe((value: any) => { + this.dragIndex = +value[1].getAttribute('data-index'); }); - } - getMacro(id: number): Macro { - const config = this.uhkConfigurationService.getUhkConfiguration(); - const macro: Macro = config.macros.find(item => item.id === id); - if (macro) { - // Clone macro for editing - return new Macro().fromJsObject(macro.toJsObject()); - } - // @todo replace with notification - throw new Error('Macro not found'); - } - - addAction() { - this.hideOtherActionEditors(this.macro.macroActions.length); - this.macro.macroActions.push(undefined); - } - - discardChanges() { - const id: number = this.macro.id; - this.macro = this.getMacro(id); - this.hasChanges = false; - } - - hideOtherActionEditors(index: number) { - this.macroItems.toArray().forEach((macroItem: MacroItemComponent, idx: number) => { - if (idx !== index) { - macroItem.cancelEdit(); + dragulaService.drop.subscribe((value: any) => { + if (value[4]) { + this.store.dispatch(MacroActions.reorderMacroAction( + this.macro.id, + this.dragIndex, + +value[4].getAttribute('data-index') + )); } }); - } - onEditAction(index: number) { - // Hide other editors when clicking edit button of a macro action - this.hideOtherActionEditors(index); - } - - onSaveAction(macroAction: MacroAction, index: number) { - this.hasChanges = true; - this.macro.macroActions[index] = macroAction; - } - - onDeleteAction(index: number) { - // @ todo show confirm action dialog - this.macro.macroActions.splice(index, 1); - this.hasChanges = true; + this.subscription = route + .params + .select('id') + .switchMap((id: string) => store.let(getMacro(+id))) + .subscribe((macro: Macro) => { + this.macro = macro; + }); } ngOnDestroy() { - this.routeSubscription.unsubscribe(); + this.subscription.unsubscribe(); } + showNewAction() { + this.hideActiveEditor(); + + this.newMacro = undefined; + this.showNew = true; + } + + hideNewAction() { + this.showNew = false; + } + + addNewAction(macroAction: MacroAction) { + this.store.dispatch(MacroActions.addMacroAction(this.macro.id, macroAction)); + this.newMacro = undefined; + this.showNew = false; + } + + editAction(index: number) { + // Hide other editors when clicking edit button of a macro action + this.hideActiveEditor(); + this.showNew = false; + this.activeEdit = index; + } + + cancelAction() { + this.activeEdit = undefined; + } + + saveAction(macroAction: MacroAction, index: number) { + this.store.dispatch(MacroActions.saveMacroAction(this.macro.id, index, macroAction)); + this.hideActiveEditor(); + } + + deleteAction(macroAction: MacroAction, index: number) { + this.store.dispatch(MacroActions.deleteMacroAction(this.macro.id, index, macroAction)); + this.hideActiveEditor(); + } + + private hideActiveEditor() { + if (this.activeEdit) { + this.macroItems.toArray()[this.activeEdit].cancelEdit(); + } + } } diff --git a/src/components/popover/tab/macro/macro-tab.component.html b/src/components/popover/tab/macro/macro-tab.component.html index 801069a7..5fcafa4d 100644 --- a/src/components/popover/tab/macro/macro-tab.component.html +++ b/src/components/popover/tab/macro/macro-tab.component.html @@ -6,7 +6,7 @@ diff --git a/src/store/actions/macro.ts b/src/store/actions/macro.ts index b80c8b13..4a9c433a 100644 --- a/src/store/actions/macro.ts +++ b/src/store/actions/macro.ts @@ -1,12 +1,84 @@ import { Action } from '@ngrx/store'; +import { Macro } from '../../config-serializer/config-items/Macro'; +import { MacroAction } from '../../config-serializer/config-items/macro-action'; + export namespace MacroActions { export const PREFIX = '[Macro] '; - export const GET_ALL = MacroActions.PREFIX + 'Get all macros'; - export function getAll(): Action { + export const DUPLICATE = MacroActions.PREFIX + 'Duplicate macro'; + export const EDIT_NAME = MacroActions.PREFIX + 'Edit macro title'; + export const REMOVE = MacroActions.PREFIX + 'Remove macro'; + + export const ADD_ACTION = MacroActions.PREFIX + 'Add macro action'; + export const SAVE_ACTION = MacroActions.PREFIX + 'Save macro action'; + export const DELETE_ACTION = MacroActions.PREFIX + 'Delete macro action'; + export const REORDER_ACTION = MacroActions.PREFIX + 'Reorder macro action'; + + export function removeMacro(id: number): Action { return { - type: MacroActions.GET_ALL + type: MacroActions.REMOVE, + payload: id + }; + } + + export function duplicateMacro(macro: Macro): Action { + return { + type: MacroActions.DUPLICATE, + payload: macro + }; + } + + export function editMacroName(id: number, name: string): Action { + return { + type: MacroActions.EDIT_NAME, + payload: { + id: id, + name: name + } + }; + } + + export function addMacroAction(id: number, action: MacroAction): Action { + return { + type: MacroActions.ADD_ACTION, + payload: { + id: id, + action: action + } + }; + } + + export function saveMacroAction(id: number, index: number, action: MacroAction): Action { + return { + type: MacroActions.SAVE_ACTION, + payload: { + id: id, + index: index, + action: action + } + }; + } + + export function deleteMacroAction(id: number, index: number, action: MacroAction): Action { + return { + type: MacroActions.DELETE_ACTION, + payload: { + id: id, + index: index, + action: action + } + }; + } + + export function reorderMacroAction(id: number, oldIndex: number, newIndex: number): Action { + return { + type: MacroActions.REORDER_ACTION, + payload: { + id: id, + oldIndex: oldIndex, + newIndex: newIndex + } }; } } diff --git a/src/store/effects/macro.ts b/src/store/effects/macro.ts new file mode 100644 index 00000000..7717e4b6 --- /dev/null +++ b/src/store/effects/macro.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; + +import { Actions, Effect } from '@ngrx/effects'; + +import 'rxjs/add/operator/map'; + +import { MacroActions } from '../actions'; + +@Injectable() +export class MacroEffects { + + @Effect()remove$: any = this.actions$ + .ofType(MacroActions.REMOVE) + .map(() => { + // TODO: Waiting for the fix: https://github.com/angular/angular/issues/10770 + // If state is empty router.navigate(['/macro']); + // Else router.navigate(['/macro']); + }); + + constructor(private actions$: Actions) {} +} diff --git a/src/store/reducers/macro.ts b/src/store/reducers/macro.ts index e2ea3eaa..0f188070 100644 --- a/src/store/reducers/macro.ts +++ b/src/store/reducers/macro.ts @@ -1,16 +1,146 @@ +import '@ngrx/core/add/operator/select'; import { Action } from '@ngrx/store'; +import 'rxjs/add/operator/map'; +import { Observable } from 'rxjs/Observable'; + import { Macro } from '../../config-serializer/config-items/Macro'; + import { MacroActions } from '../actions'; +import { AppState } from '../index'; const initialState: Macro[] = []; export default function(state = initialState, action: Action): Macro[] { + let newMacro: Macro; + switch (action.type) { - case MacroActions.GET_ALL: - break; + case MacroActions.DUPLICATE: + + newMacro = new Macro(action.payload); + newMacro.name = generateName(state, newMacro.name); + newMacro.id = generateId(state); + + return [...state, newMacro]; + + case MacroActions.EDIT_NAME: + let name: string = generateName(state, action.payload.name); + + return state.map((macro: Macro) => { + if (macro.id === action.payload.id) { + macro.name = name; + } + + return macro; + }); + + case MacroActions.REMOVE: + return state.filter((macro: Macro) => macro.id !== action.payload); + + case MacroActions.ADD_ACTION: + return state.map((macro: Macro) => { + if (macro.id === action.payload.id) { + newMacro = new Macro(macro); + newMacro.macroActions.push(action.payload.action); + + return newMacro; + } + + return macro; + }); + + case MacroActions.SAVE_ACTION: + return state.map((macro: Macro) => { + if (macro.id === action.payload.id) { + newMacro = new Macro(macro); + newMacro.macroActions[action.payload.index] = action.payload.action; + + return newMacro; + } + + return macro; + }); + + case MacroActions.DELETE_ACTION: + return state.map((macro: Macro) => { + if (macro.id === action.payload.id) { + newMacro = new Macro(macro); + newMacro.macroActions.splice(action.payload.index, 1); + + return newMacro; + } + + return macro; + }); + + case MacroActions.REORDER_ACTION: + return state.map((macro: Macro) => { + if (macro.id === action.payload.id) { + let newIndex: number = action.payload.newIndex; + + // We need to reduce the new index for one when we are moving action down + if (newIndex > action.payload.oldIndex) { + --newIndex; + } + + newMacro = new Macro(macro); + newMacro.macroActions.splice( + newIndex, + 0, + newMacro.macroActions.splice(action.payload.oldIndex, 1)[0] + ); + + return newMacro; + } + + return macro; + }); + default: { return state; } } } + +export function getMacro(id: number) { + if (isNaN(id)) { + return (state$: Observable) => state$ + .select(appState => appState.macros) + .map((macros: Macro[]) => { + if (macros.length > 0) { + return macros[0]; + } else { + return undefined; + } + }); + } else { + return (state$: Observable) => state$ + .select(appState => appState.macros) + .map((macros: Macro[]) => macros.find((macro: Macro) => macro.id === id)); + } +} + +function generateName(macros: Macro[], name: string) { + let suffix = 2; + const oldName: string = name; + + while (macros.some((macro: Macro) => macro.name === name)) { + name = oldName + ` (${suffix})`; + ++suffix; + } + + return name; +} + +function generateId(macros: Macro[]) { + let newId = 0; + + macros.forEach((macro: Macro) => { + if (macro.id > newId) { + newId = macro.id; + } + }); + + return ++newId; + +}