Data layer - Macro & UX improvments (#117)

Closes #75
This commit is contained in:
Nejc Zdovc
2016-09-25 19:54:36 +02:00
committed by József Farkas
parent ee1d8ec59e
commit 4e13f910dd
11 changed files with 431 additions and 135 deletions

View File

@@ -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<AppState>) { }
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));
}
}

View File

@@ -1,9 +1,11 @@
<div class="list-group-item action--item" [class.is-editing]="editing">
<span *ngIf="moveable" class="glyphicon glyphicon-option-vertical action--movable" aria-hidden="true"></span>
<icon [name]="iconName"></icon>
<div class="action--title" (click)="editAction()">{{ title }}</div>
<icon *ngIf="deletable" name="trash" (click)="deleteAction()"></icon>
<icon *ngIf="editable" name="pencil" (click)="editAction()"></icon>
<span *ngIf="moveable" class="glyphicon glyphicon-option-vertical action--movable" aria-hidden="true"></span>
<div class="action--item--wrap" [class.pointer]="!editing && editable" (click)="editAction()">
<icon [name]="iconName"></icon>
<div class="action--title">{{ title }}</div>
<icon *ngIf="editable && macroAction && !editing" name="pencil"></icon>
</div>
<icon *ngIf="deletable" name="trash" (click)="deleteAction()"></icon>
</div>
<div class="list-group-item macro-action-editor__container" *ngIf="editable && editing">
<macro-action-editor

View File

@@ -1,3 +1,5 @@
@import '../../../main-app/global-styles';
:host {
&.macro-item:first-of-type {
.list-group-item {
@@ -18,44 +20,60 @@
background: #f5f5f5;
}
}
}
.action {
&--item {
display: flex;
flex-shrink: 0;
border: 0;
border-bottom: 1px solid #ddd;
.action {
&--item {
display: flex;
flex-shrink: 0;
border: 0;
border-bottom: 1px solid #ddd;
icon {
margin: 0 5px;
}
> 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;
}

View File

@@ -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;

View File

@@ -1,30 +1,42 @@
<macro-header [macro]="macro"></macro-header>
<div class="row list-container">
<div class="col-xs-10 col-xs-offset-1 list-group">
<div class="macro-actions-container" [dragula]="'macroActions'" [dragulaModel]="macro.macroActions">
<macro-item *ngFor="let macroAction of macro.macroActions; let macroActionIndex = index"
[macroAction]="macroAction"
[editable]="true"
[deletable]="true"
[moveable]="true"
(save)="onSaveAction($event, index)"
(edit)="onEditAction(macroActionIndex)"
(delete)="onDeleteAction(macroActionIndex)"></macro-item>
</div>
<div class="list-group add-new__action-container">
<div class="list-group-item action--item add-new__action-item no-reorder">
<a class="add-new__action-item--link" (click)="addAction()"><i class="fa fa-plus"></i> Add new action item</a>
<template [ngIf]="macro">
<macro-header [macro]="macro"></macro-header>
<div class="row list-container">
<div class="col-xs-10 col-xs-offset-1 list-group">
<div class="macro-actions-container" [dragula]="'macroActions'" [dragulaModel]="(macro)?.macroActions">
<macro-item *ngFor="let macroAction of (macro)?.macroActions; let macroActionIndex = index"
[macroAction]="macroAction"
[editable]="true"
[deletable]="true"
[moveable]="true"
(save)="saveAction($event, macroActionIndex)"
(edit)="editAction(macroActionIndex)"
(cancel)="cancelAction()"
(delete)="deleteAction(macroAction, macroActionIndex)"
[attr.data-index]="macroActionIndex"
></macro-item>
<macro-item *ngIf="showNew" [macroAction]="newMacro"
[editable]="true"
[deletable]="false"
[moveable]="false"
(save)="addNewAction($event)"
(cancel)="hideNewAction()"
></macro-item>
</div>
<div class="list-group-item new-macro-settings" *ngIf="hasChanges">
<div class="helper"></div>
<p>Remember to save your changes!</p>
<div class="row">
<div class="col-sm-12 flex-button-wrapper">
<button class="btn btn-primary btn-sm pull-right flex-button settings-save" (click)="saveMacro()">Save macro</button>
<button class="btn btn-default btn-sm pull-right flex-button settings-cancel" style="margin-right: .5em;" (click)="discardChanges()">Discard changes</button>
</div>
<div class="list-group add-new__action-container" *ngIf="!showNew">
<div class="list-group-item action--item add-new__action-item no-reorder clearfix">
<a class="add-new__action-item--link" (click)="showNewAction()">
<i class="fa fa-plus"></i> Add new macro action
</a>
<a class="add-new__action-item--link">
<i class="fa fa fa-circle"></i> Add new capture keystroke
</a>
</div>
</div>
</div>
</div>
</template>
<div *ngIf="!macro" class="not-found">
Sorry, there is no macro with this id.
</div>

View File

@@ -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;
}

View File

@@ -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<MacroItemComponent>;
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<AppState>,
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<string>('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();
}
}
}

View File

@@ -6,7 +6,7 @@
<template [ngIf]="selectedMacroIndex >= 0">
<div class="list-group">
<macro-item *ngFor="let macroAction of macros[selectedMacroIndex].macroActions"
[macroAction]="macroAction">
[macroAction]="macroAction" [editable]="false">
</macro-item>
</div>
</template>

View File

@@ -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
}
};
}
}

View File

@@ -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) {}
}

View File

@@ -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<AppState>) => state$
.select(appState => appState.macros)
.map((macros: Macro[]) => {
if (macros.length > 0) {
return macros[0];
} else {
return undefined;
}
});
} else {
return (state$: Observable<AppState>) => 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;
}