diff --git a/electron/src/app.module.ts b/electron/src/app.module.ts
index 784e740c..94ed9b6a 100644
--- a/electron/src/app.module.ts
+++ b/electron/src/app.module.ts
@@ -106,6 +106,7 @@ import { AppUpdateRendererService } from './services/app-update-renderer.service
import { reducer } from './store';
import { AutoUpdateSettings } from './shared/components/auto-update-settings/auto-update-settings';
import { angularNotifierConfig } from './shared/models/angular-notifier-config';
+import { UndoableNotifierComponent } from './shared/components/undoable-notifier';
import { UhkHeader } from './shared/components/uhk-header/uhk-header';
import { AppRendererService } from './services/app-renderer.service';
@@ -166,6 +167,7 @@ import { AppRendererService } from './services/app-renderer.service';
SafeStylePipe,
UpdateAvailableComponent,
AutoUpdateSettings,
+ UndoableNotifierComponent,
UhkHeader
],
imports: [
diff --git a/shared/src/components/macro/edit/macro-edit.component.html b/shared/src/components/macro/edit/macro-edit.component.html
index 46e55d5f..55844cee 100644
--- a/shared/src/components/macro/edit/macro-edit.component.html
+++ b/shared/src/components/macro/edit/macro-edit.component.html
@@ -14,4 +14,4 @@
There is no macro with id {{ route.params.select('id') | async }}.
-
\ No newline at end of file
+
diff --git a/shared/src/components/macro/not-found/macro-not-found.component.html b/shared/src/components/macro/not-found/macro-not-found.component.html
index 6cc47565..95e16826 100644
--- a/shared/src/components/macro/not-found/macro-not-found.component.html
+++ b/shared/src/components/macro/not-found/macro-not-found.component.html
@@ -1,4 +1,8 @@
-
-
- You don't have any macros. Try to add one!
+
+
+
+
+
+ You don't have any macros. Try to add one!
+
diff --git a/shared/src/components/uhk-header/uhk-header.html b/shared/src/components/uhk-header/uhk-header.html
index 93af3044..910b213c 100644
--- a/shared/src/components/uhk-header/uhk-header.html
+++ b/shared/src/components/uhk-header/uhk-header.html
@@ -1,3 +1,12 @@
+
+
diff --git a/shared/src/components/uhk-header/uhk-header.ts b/shared/src/components/uhk-header/uhk-header.ts
index 7e466b1d..49267c52 100644
--- a/shared/src/components/uhk-header/uhk-header.ts
+++ b/shared/src/components/uhk-header/uhk-header.ts
@@ -1,4 +1,10 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { Observable } from 'rxjs/Observable';
+
+import { Notification } from '../../models/notification';
+import { AppState, getUndoableNotification } from '../../store/index';
+import { DismissUndoNotificationAction, UndoLastAction } from '../../store/actions/app.action';
@Component({
selector: 'uhk-header',
@@ -6,4 +12,18 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UhkHeader {
+ undoableNotification$: Observable
;
+
+ constructor(private store: Store) {
+ this.undoableNotification$ = this.store.select(getUndoableNotification);
+ }
+
+ onUndoLastNotification(data: any): void {
+ this.store.dispatch(new UndoLastAction(data));
+ }
+
+ onDismissLastNotification(): void {
+ this.store.dispatch(new DismissUndoNotificationAction());
+ }
+
}
diff --git a/shared/src/components/undoable-notifier/index.ts b/shared/src/components/undoable-notifier/index.ts
new file mode 100644
index 00000000..7fcbbbb0
--- /dev/null
+++ b/shared/src/components/undoable-notifier/index.ts
@@ -0,0 +1 @@
+export * from './undoable-notifier.component';
diff --git a/shared/src/components/undoable-notifier/undoable-notifier.component.html b/shared/src/components/undoable-notifier/undoable-notifier.component.html
new file mode 100644
index 00000000..072c0f78
--- /dev/null
+++ b/shared/src/components/undoable-notifier/undoable-notifier.component.html
@@ -0,0 +1,12 @@
+
+
+
+ ×
+
+ {{text}}
+
Undo
+
+
diff --git a/shared/src/components/undoable-notifier/undoable-notifier.component.scss b/shared/src/components/undoable-notifier/undoable-notifier.component.scss
new file mode 100644
index 00000000..4ba307af
--- /dev/null
+++ b/shared/src/components/undoable-notifier/undoable-notifier.component.scss
@@ -0,0 +1,13 @@
+.alert {
+ padding: 5px 10px 5px 5px;
+
+ margin-bottom: 0.25em;
+ margin-top: -2em;
+
+ .close {
+ right: -5px;
+ }
+ .undo-button {
+ cursor: pointer;
+ }
+}
diff --git a/shared/src/components/undoable-notifier/undoable-notifier.component.ts b/shared/src/components/undoable-notifier/undoable-notifier.component.ts
new file mode 100644
index 00000000..33a19853
--- /dev/null
+++ b/shared/src/components/undoable-notifier/undoable-notifier.component.ts
@@ -0,0 +1,52 @@
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
+import { animate, state, style, transition, trigger } from '@angular/animations';
+
+import { Notification } from '../../models/notification';
+
+@Component({
+ selector: 'undoable-notifier',
+ templateUrl: './undoable-notifier.component.html',
+ styleUrls: ['./undoable-notifier.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ animations: [
+ trigger('slideInOut', [
+ state('in', style({
+ transform: 'translate3d(0, 0, 0)'
+ })),
+ state('out', style({
+ transform: 'translate3d(200%, 0, 0)'
+ })),
+ transition('in => out', animate('400ms ease-in-out')),
+ transition('out => in', animate('400ms ease-in-out'))
+ ])
+ ]
+})
+export class UndoableNotifierComponent implements OnChanges {
+ text: string;
+ undoable: boolean;
+ @Input() notification: Notification;
+ @Output() close = new EventEmitter();
+ @Output() undo = new EventEmitter();
+
+ get slideInOut() {
+ return this.notification ? 'in' : 'out';
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['notification']) {
+ const not: Notification = changes['notification'].currentValue;
+ if (not) {
+ this.text = not.message;
+ this.undoable = !!not.extra;
+ }
+ }
+ }
+
+ clickOnClose(): void {
+ this.close.emit();
+ }
+
+ clickOnUndo(): void {
+ this.undo.emit(this.notification.extra);
+ }
+}
diff --git a/shared/src/models/notification.ts b/shared/src/models/notification.ts
index ad5d540a..d3d17f04 100644
--- a/shared/src/models/notification.ts
+++ b/shared/src/models/notification.ts
@@ -1,14 +1,17 @@
+import { Action } from '@ngrx/store';
+
export enum NotificationType {
- Default,
- Success,
- Error,
- Warning,
- Info
+ Default = 'default',
+ Success = 'success',
+ Error = 'error',
+ Warning = 'warning',
+ Info = 'info',
+ Undoable = 'undoable'
}
export interface Notification {
type: NotificationType;
title?: string;
message: string;
- extra?: any;
+ extra?: Action;
}
diff --git a/shared/src/models/undo-user-config-data.ts b/shared/src/models/undo-user-config-data.ts
new file mode 100644
index 00000000..7a71b236
--- /dev/null
+++ b/shared/src/models/undo-user-config-data.ts
@@ -0,0 +1,4 @@
+export interface UndoUserConfigData {
+ path: string;
+ config: string;
+}
diff --git a/shared/src/store/actions/app.action.ts b/shared/src/store/actions/app.action.ts
index 1ffe045e..0efc47c2 100644
--- a/shared/src/store/actions/app.action.ts
+++ b/shared/src/store/actions/app.action.ts
@@ -12,7 +12,10 @@ export const ActionTypes = {
APP_STARTED: type(PREFIX + 'started'),
APP_SHOW_NOTIFICATION: type(PREFIX + 'show notification'),
APP_TOGGLE_ADDON_MENU: type(PREFIX + 'toggle add-on menu'),
- APP_PROCESS_COMMAND_LINE_ARGS: type(PREFIX + 'process command line args')
+ APP_PROCESS_COMMAND_LINE_ARGS: type(PREFIX + 'process command line args'),
+ UNDO_LAST: type(PREFIX + 'undo last action'),
+ UNDO_LAST_SUCCESS: type(PREFIX + 'undo last action success'),
+ DISMISS_UNDO_NOTIFICATION: type(PREFIX + 'dismiss notification action')
};
export class AppBootsrappedAction implements Action {
@@ -41,9 +44,26 @@ export class ProcessCommandLineArgsAction implements Action {
constructor(public payload: CommandLineArgs) { }
}
+export class UndoLastAction implements Action {
+ type = ActionTypes.UNDO_LAST;
+
+ constructor(public payload: any) {}
+}
+
+export class UndoLastSuccessAction implements Action {
+ type = ActionTypes.UNDO_LAST_SUCCESS;
+}
+
+export class DismissUndoNotificationAction implements Action {
+ type = ActionTypes.DISMISS_UNDO_NOTIFICATION;
+}
+
export type Actions
= AppStartedAction
| AppBootsrappedAction
| ShowNotificationAction
| ToggleAddonMenuAction
- | ProcessCommandLineArgsAction;
+ | ProcessCommandLineArgsAction
+ | UndoLastAction
+ | UndoLastSuccessAction
+ | DismissUndoNotificationAction;
diff --git a/shared/src/store/actions/keymap.ts b/shared/src/store/actions/keymap.ts
index 37674bf0..d1460174 100644
--- a/shared/src/store/actions/keymap.ts
+++ b/shared/src/store/actions/keymap.ts
@@ -16,6 +16,7 @@ export namespace KeymapActions {
export const CHECK_MACRO = KeymapActions.PREFIX + 'Check deleted macro';
export const LOAD_KEYMAPS = KeymapActions.PREFIX + 'Load keymaps';
export const LOAD_KEYMAPS_SUCCESS = KeymapActions.PREFIX + 'Load keymaps success';
+ export const UNDO_LAST_ACTION = KeymapActions.PREFIX + 'Undo last action';
export function loadKeymaps(): Action {
return {
diff --git a/shared/src/store/actions/user-config.ts b/shared/src/store/actions/user-config.ts
index 6e6339a7..b2562e7d 100644
--- a/shared/src/store/actions/user-config.ts
+++ b/shared/src/store/actions/user-config.ts
@@ -19,13 +19,13 @@ export class LoadUserConfigAction implements Action {
export class LoadUserConfigSuccessAction implements Action {
type = ActionTypes.LOAD_USER_CONFIG_SUCCESS;
- constructor(public payload: UserConfiguration) {
-
- }
+ constructor(public payload: UserConfiguration) { }
}
export class SaveUserConfigSuccessAction implements Action {
type = ActionTypes.SAVE_USER_CONFIG_SUCCESS;
+
+ constructor(public payload: UserConfiguration) { }
}
export type Actions
diff --git a/shared/src/store/effects/app.ts b/shared/src/store/effects/app.ts
index 31aabf51..0ff6b5d4 100644
--- a/shared/src/store/effects/app.ts
+++ b/shared/src/store/effects/app.ts
@@ -4,10 +4,13 @@ import { Actions, Effect, toPayload } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
import { NotifierService } from 'angular-notifier';
+import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/mergeMap';
+import 'rxjs/add/operator/catch';
-import { ActionTypes, ToggleAddonMenuAction } from '../actions/app.action';
+import { ActionTypes, DismissUndoNotificationAction, ToggleAddonMenuAction } from '../actions/app.action';
import { Notification, NotificationType } from '../../models/notification';
import { CommandLineArgs } from '../../models/command-line-args';
@@ -19,8 +22,10 @@ export class ApplicationEffects {
.ofType(ActionTypes.APP_SHOW_NOTIFICATION)
.map(toPayload)
.do((notification: Notification) => {
- const type = ApplicationEffects.mapNotificationType(notification.type);
- this.notifierService.notify(type, notification.message);
+ if (notification.type === NotificationType.Undoable) {
+ return;
+ }
+ this.notifierService.notify(notification.type, notification.message);
});
@Effect()
@@ -29,26 +34,10 @@ export class ApplicationEffects {
.map(toPayload)
.map((args: CommandLineArgs) => new ToggleAddonMenuAction(args.addons || false));
- // TODO: Change typescript -> 2.4 and use string enum.
- // Corrently ngrx store is not compatible witn typescript 2.4
- private static mapNotificationType(type: NotificationType): string {
- switch (type) {
- case NotificationType.Success:
- return 'success';
-
- case NotificationType.Error:
- return 'error';
-
- case NotificationType.Info:
- return 'info';
-
- case NotificationType.Warning:
- return 'warning';
-
- default:
- return 'default';
- }
- }
+ @Effect() undoLastNotification$: Observable = this.actions$
+ .ofType(ActionTypes.UNDO_LAST)
+ .map(toPayload)
+ .mergeMap((action: Action) => [action, new DismissUndoNotificationAction()]);
constructor(private actions$: Actions,
private notifierService: NotifierService) { }
diff --git a/shared/src/store/effects/user-config.ts b/shared/src/store/effects/user-config.ts
index 1c729b67..7f3f4336 100644
--- a/shared/src/store/effects/user-config.ts
+++ b/shared/src/store/effects/user-config.ts
@@ -1,5 +1,6 @@
import { Inject, Injectable } from '@angular/core';
-import { Actions, Effect } from '@ngrx/effects';
+import { go } from '@ngrx/router-store';
+import { Actions, Effect, toPayload } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
import { Action, Store } from '@ngrx/store';
@@ -7,6 +8,7 @@ import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/withLatestFrom';
+import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/observable/of';
import {
@@ -19,9 +21,12 @@ import {
import { UserConfiguration } from '../../config-serializer/config-items/user-configuration';
import { DATA_STORAGE_REPOSITORY, DataStorageRepositoryService } from '../../services/datastorage-repository.service';
import { DefaultUserConfigurationService } from '../../services/default-user-configuration.service';
-import { AppState, getUserConfiguration } from '../index';
+import { AppState, getPrevUserConfiguration, getUserConfiguration } from '../index';
import { KeymapActions } from '../actions/keymap';
import { MacroActions } from '../actions/macro';
+import { DismissUndoNotificationAction, ShowNotificationAction } from '../actions/app.action';
+import { NotificationType } from '../../models/notification';
+import { UndoUserConfigData } from '../../models/undo-user-config-data';
@Injectable()
export class UserConfigEffects {
@@ -29,33 +34,49 @@ export class UserConfigEffects {
@Effect() loadUserConfig$: Observable = this.actions$
.ofType(ActionTypes.LOAD_USER_CONFIG)
.startWith(new LoadUserConfigAction())
- .switchMap(() => {
- const configJsonObject = this.dataStorageRepository.getConfig();
- let config: UserConfiguration;
-
- if (configJsonObject) {
- if (configJsonObject.dataModelVersion === this.defaultUserConfigurationService.getDefault().dataModelVersion) {
- config = new UserConfiguration().fromJsonObject(configJsonObject);
- }
- }
-
- if (!config) {
- config = this.defaultUserConfigurationService.getDefault();
- }
-
- return Observable.of(new LoadUserConfigSuccessAction(config));
- });
+ .switchMap(() => Observable.of(new LoadUserConfigSuccessAction(this.getUserConfiguration())));
@Effect() saveUserConfig$: Observable = this.actions$
.ofType(
KeymapActions.ADD, KeymapActions.DUPLICATE, KeymapActions.EDIT_NAME, KeymapActions.EDIT_ABBR,
- KeymapActions.SET_DEFAULT, KeymapActions.REMOVE, KeymapActions.SAVE_KEY, KeymapActions.CHECK_MACRO,
+ KeymapActions.SET_DEFAULT, KeymapActions.REMOVE, KeymapActions.SAVE_KEY,
MacroActions.ADD, MacroActions.DUPLICATE, MacroActions.EDIT_NAME, MacroActions.REMOVE, MacroActions.ADD_ACTION,
MacroActions.SAVE_ACTION, MacroActions.DELETE_ACTION, MacroActions.REORDER_ACTION)
- .withLatestFrom(this.store.select(getUserConfiguration))
- .map(([action, config]) => {
+ .withLatestFrom(this.store.select(getUserConfiguration), this.store.select(getPrevUserConfiguration))
+ .mergeMap(([action, config, prevUserConfiguration]) => {
this.dataStorageRepository.saveConfig(config);
- return new SaveUserConfigSuccessAction();
+
+ if (action.type === KeymapActions.REMOVE || action.type === MacroActions.REMOVE) {
+ const text = action.type === KeymapActions.REMOVE ? 'Keymap' : 'Macro';
+ const pathPrefix = action.type === KeymapActions.REMOVE ? 'keymap' : 'macro';
+ const payload: UndoUserConfigData = {
+ path: `/${pathPrefix}/${action.payload}`,
+ config: prevUserConfiguration.toJsonObject()
+ };
+
+ return [
+ new SaveUserConfigSuccessAction(config),
+ new ShowNotificationAction({
+ type: NotificationType.Undoable,
+ message: `${text} has been deleted`,
+ extra: {
+ payload,
+ type: KeymapActions.UNDO_LAST_ACTION
+ }
+ })
+ ];
+ }
+
+ return [new SaveUserConfigSuccessAction(config), new DismissUndoNotificationAction()];
+ });
+
+ @Effect() undoUserConfig$: Observable = this.actions$
+ .ofType(KeymapActions.UNDO_LAST_ACTION)
+ .map(toPayload)
+ .mergeMap((payload: UndoUserConfigData) => {
+ const config = new UserConfiguration().fromJsonObject(payload.config);
+ this.dataStorageRepository.saveConfig(config);
+ return [new LoadUserConfigSuccessAction(config), go(payload.path)];
});
constructor(private actions$: Actions,
@@ -63,4 +84,22 @@ export class UserConfigEffects {
private store: Store,
private defaultUserConfigurationService: DefaultUserConfigurationService) {
}
+
+ private getUserConfiguration() {
+ const configJsonObject = this.dataStorageRepository.getConfig();
+ let config: UserConfiguration;
+
+ if (configJsonObject) {
+ if (configJsonObject.dataModelVersion === this.defaultUserConfigurationService.getDefault().dataModelVersion) {
+ config = new UserConfiguration().fromJsonObject(configJsonObject);
+ }
+ }
+
+ if (!config) {
+ config = this.defaultUserConfigurationService.getDefault();
+ }
+
+ return config;
+
+ }
}
diff --git a/shared/src/store/index.ts b/shared/src/store/index.ts
index 5577aeb8..8cfad9de 100644
--- a/shared/src/store/index.ts
+++ b/shared/src/store/index.ts
@@ -19,6 +19,8 @@ export const getUserConfiguration = (state: AppState) => state.userConfiguration
export const appState = (state: AppState) => state.app;
export const showAddonMenu = createSelector(appState, fromApp.showAddonMenu);
+export const getUndoableNotification = createSelector(appState, fromApp.getUndoableNotification);
+export const getPrevUserConfiguration = createSelector(appState, fromApp.getPrevUserConfiguration);
export const appUpdateState = (state: AppState) => state.autoUpdateSettings;
diff --git a/shared/src/store/reducers/app.reducer.ts b/shared/src/store/reducers/app.reducer.ts
index 22e976a2..7a991d2e 100644
--- a/shared/src/store/reducers/app.reducer.ts
+++ b/shared/src/store/reducers/app.reducer.ts
@@ -1,25 +1,81 @@
+import { routerActions } from '@ngrx/router-store';
import { Action } from '@ngrx/store';
-import { ActionTypes } from '../actions/app.action';
+import { ActionTypes, ShowNotificationAction } from '../actions/app.action';
+import { ActionTypes as UserConfigActionTypes } from '../actions/user-config';
+import { Notification, NotificationType } from '../../models/notification';
+import { UserConfiguration } from '../../config-serializer/config-items/user-configuration';
export interface State {
started: boolean;
showAddonMenu: boolean;
+ undoableNotification?: Notification;
+ navigationCountAfterNotification: number;
+ prevUserConfig?: UserConfiguration;
}
const initialState: State = {
started: false,
- showAddonMenu: false
+ showAddonMenu: false,
+ navigationCountAfterNotification: 0
};
export function reducer(state = initialState, action: Action) {
switch (action.type) {
case ActionTypes.APP_STARTED: {
- return Object.assign({ ...state }, { started: true });
+ return {
+ ...state,
+ started: true
+ };
}
case ActionTypes.APP_TOGGLE_ADDON_MENU: {
- return Object.assign({ ...state }, { showAddonMenu: action.payload });
+ return {
+ ...state,
+ showAddonMenu: action.payload
+ };
+ }
+
+ case ActionTypes.APP_SHOW_NOTIFICATION: {
+ const currentAction = action;
+ if (currentAction.payload.type !== NotificationType.Undoable) {
+ return state;
+ }
+ return {
+ ...state,
+ undoableNotification: currentAction.payload,
+ navigationCountAfterNotification: 0
+ };
+ }
+
+ // Required to dismiss the undoNotification dialog, when user navigate in the app.
+ // When deleted a keymap or macro the app automaticaly navigate to other keymap, or macro, so
+ // so we have to count the navigations and when reach the 2nd then remove the dialog.
+ case routerActions.UPDATE_LOCATION: {
+ const newState = { ...state };
+ newState.navigationCountAfterNotification++;
+
+ if (newState.navigationCountAfterNotification > 1) {
+ newState.undoableNotification = null;
+ }
+
+ return newState;
+ }
+
+ case ActionTypes.UNDO_LAST_SUCCESS:
+ case ActionTypes.DISMISS_UNDO_NOTIFICATION: {
+ return {
+ ...state,
+ undoableNotification: null
+ };
+ }
+
+ case UserConfigActionTypes.LOAD_USER_CONFIG_SUCCESS:
+ case UserConfigActionTypes.SAVE_USER_CONFIG_SUCCESS: {
+ return {
+ ...state,
+ prevUserConfig: action.payload
+ };
}
default:
@@ -28,3 +84,5 @@ export function reducer(state = initialState, action: Action) {
}
export const showAddonMenu = (state: State) => state.showAddonMenu;
+export const getUndoableNotification = (state: State) => state.undoableNotification;
+export const getPrevUserConfiguration = (state: State) => state.prevUserConfig;
diff --git a/web/src/app.module.ts b/web/src/app.module.ts
index 8ff4a14c..ffbbd039 100644
--- a/web/src/app.module.ts
+++ b/web/src/app.module.ts
@@ -87,6 +87,7 @@ import { reducer } from './shared/store/reducers/index';
import { LogService } from './shared/services/logger.service';
import { AutoUpdateSettings } from './shared/components/auto-update-settings/auto-update-settings';
import { angularNotifierConfig } from './shared/models/angular-notifier-config';
+import { UndoableNotifierComponent } from './shared/components/undoable-notifier';
import { UhkHeader } from './shared/components/uhk-header/uhk-header';
@NgModule({
@@ -141,6 +142,7 @@ import { UhkHeader } from './shared/components/uhk-header/uhk-header';
TooltipDirective,
SafeStylePipe,
AutoUpdateSettings,
+ UndoableNotifierComponent,
UhkHeader
],
imports: [