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 @@ +
+ +
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: [