From e6ed3c15c0b4b1487a8e04db54d26f91b737f26e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 25 May 2018 13:44:54 -0700 Subject: [PATCH] fix(layout): remove and prettify recent projects that have been moved (fixes #109) --- src/app/editor/layout/layout.actions.ts | 14 ++++- src/app/editor/layout/layout.effects.ts | 13 +++++ src/app/editor/layout/layout.reducer.ts | 5 ++ src/app/editor/project/project.effects.ts | 14 ++++- src/server/electron-server.ts | 13 +++-- src/server/errors.ts | 10 ++++ src/server/recent-projects.ts | 62 +++++++++++------------ src/server/webpack-task.ts | 2 +- 8 files changed, 95 insertions(+), 38 deletions(-) diff --git a/src/app/editor/layout/layout.actions.ts b/src/app/editor/layout/layout.actions.ts index dc05dd4..9a322ad 100644 --- a/src/app/editor/layout/layout.actions.ts +++ b/src/app/editor/layout/layout.actions.ts @@ -31,6 +31,7 @@ export const enum LayoutActionTypes { SET_GOLDEN_LAYOUT = '[Layout] Set the golden layout instance', CLEAR_GOLDEN_LAYOUT = '[Layout] Unsets the golden layout component', GET_RECENT_PROJECTS = '[Layout] Gets the recent projects', + REMOVE_RECENT_PROJECT = '[Layout] Removes a recent project', } export const enum LayoutMethod { @@ -38,6 +39,7 @@ export const enum LayoutMethod { LoadPanels = '[Layout] Load panels', GetRecentProjects = '[Layout] Get recent projects', UpdateRecentProjects = '[Layout] Update recent projects', + RemoveRecentProject = '[Layout] Remove recent project', } /** @@ -195,6 +197,15 @@ export class GetRecentProjects implements Action { constructor(public readonly projects?: IRecentProject[]) {} } +/** + * Fired to remove a recent project from the list. + */ +export class RemoveRecentProject implements Action { + public readonly type = LayoutActionTypes.REMOVE_RECENT_PROJECT; + + constructor(public readonly directory: string) {} +} + export type LayoutActions = | OpenScreen | SavePanels @@ -202,4 +213,5 @@ export type LayoutActions = | ClosePanel | SetGoldenLayout | ClearGoldenLayout - | GetRecentProjects; + | GetRecentProjects + | RemoveRecentProject; diff --git a/src/app/editor/layout/layout.effects.ts b/src/app/editor/layout/layout.effects.ts index 0582f0f..fcbdc04 100644 --- a/src/app/editor/layout/layout.effects.ts +++ b/src/app/editor/layout/layout.effects.ts @@ -26,6 +26,7 @@ import { OpenPanel, OpenScreen, panelTitles, + RemoveRecentProject, SavePanels, SetGoldenLayout, } from './layout.actions'; @@ -66,6 +67,18 @@ export class LayoutEffects { .ofType(ProjectActionTypes.CLOSE_PROJECT) .pipe(mapTo(new OpenScreen(LayoutScreen.Welcome))); + /** + * Goes back to the welcome screen when we close a project. + */ + @Effect({ dispatch: false }) + public readonly removeRecentProject = this.actions + .ofType(LayoutActionTypes.REMOVE_RECENT_PROJECT) + .pipe( + switchMap(({ directory }) => + this.electron.call(LayoutMethod.RemoveRecentProject, { directory }), + ), + ); + /** * Persists panel configuration to the server when it chamges. */ diff --git a/src/app/editor/layout/layout.reducer.ts b/src/app/editor/layout/layout.reducer.ts index 6eae223..8bcd6d9 100644 --- a/src/app/editor/layout/layout.reducer.ts +++ b/src/app/editor/layout/layout.reducer.ts @@ -62,6 +62,11 @@ export function layoutReducer( return { ...state, goldenLayout: null }; case LayoutActionTypes.GET_RECENT_PROJECTS: return { ...state, recent: action.projects || null }; + case LayoutActionTypes.REMOVE_RECENT_PROJECT: + return { + ...state, + recent: state.recent ? state.recent.filter(s => s.url !== action.directory) : null, + }; default: return state; } diff --git a/src/app/editor/project/project.effects.ts b/src/app/editor/project/project.effects.ts index 2abf096..6318781 100644 --- a/src/app/editor/project/project.effects.ts +++ b/src/app/editor/project/project.effects.ts @@ -1,13 +1,15 @@ import { Injectable } from '@angular/core'; -import { MatDialog } from '@angular/material'; +import { MatDialog, MatSnackBar } from '@angular/material'; import { Actions, Effect } from '@ngrx/effects'; import { Action, Store } from '@ngrx/store'; import { of } from 'rxjs/observable/of'; import { filter, map, startWith, switchMap, tap, withLatestFrom } from 'rxjs/operators'; +import { ProjectNotFoundError } from '../../../server/errors'; import { CommonMethods } from '../bedrock.actions'; import * as fromRoot from '../bedrock.reducers'; import { ElectronService, RpcError } from '../electron.service'; +import * as forLayout from '../layout/layout.actions'; import { DirectoryOpener } from '../shared/directory-opener'; import { ErrorToastComponent } from '../toasts/error-toast/error-toast.component'; import * as forToast from '../toasts/toasts.actions'; @@ -61,7 +63,14 @@ export class ProjectEffects { directory: action.directory, }) .then(results => new SetOpenProject(results)) - .catch(RpcError, err => new forToast.OpenToast(ErrorToastComponent, err)), + .catch(RpcError, err => { + if (err.originalName === ProjectNotFoundError.name) { + this.snack.open(err.message, undefined, { duration: 5000 }); + return new forLayout.RemoveRecentProject(action.directory); + } else { + return new forToast.OpenToast(ErrorToastComponent, err); + } + }), ), ); @@ -172,5 +181,6 @@ export class ProjectEffects { private readonly electron: ElectronService, private readonly store: Store, private readonly dialog: MatDialog, + private readonly snack: MatSnackBar, ) {} } diff --git a/src/server/electron-server.ts b/src/server/electron-server.ts index c6f87ab..f8ac983 100644 --- a/src/server/electron-server.ts +++ b/src/server/electron-server.ts @@ -17,7 +17,7 @@ import * as forUploader from '../app/editor/uploader/uploader.actions'; import { spawn } from 'child_process'; import { IRemoteError } from '../app/editor/electron.service'; import { FileDataStore } from './datastore'; -import { hasMetadata, NoAuthenticationError } from './errors'; +import { hasMetadata, NoAuthenticationError, ProjectNotFoundError } from './errors'; import { OpenBuilder } from './file-selector'; import { IssueTracker } from './issue-tracker'; import { NodeChecker } from './node-checker'; @@ -29,7 +29,7 @@ import { Quickstarter } from './quickstart'; import { RecentProjects } from './recent-projects'; import { SnapshotStore } from './snapshot-store'; import { TaskList } from './tasks/task'; -import { Fetcher } from './util'; +import { exists, Fetcher } from './util'; import { WebpackBundleTask } from './webpack-bundler-task'; import { WebpackDevServer } from './webpack-dev-server-task'; @@ -155,6 +155,10 @@ const methods: { [methodName: string]: (data: any, server: ElectronServer) => Pr return await new RecentProjects().updateProjects(options.project); }, + [forLayout.LayoutMethod.RemoveRecentProject]: async (options: { directory: string }) => { + return await new RecentProjects().removeProject(options.directory); + }, + [forLayout.LayoutMethod.GetRecentProjects]: async () => { return await new RecentProjects().loadProjects(); }, @@ -192,8 +196,11 @@ const methods: { [methodName: string]: (data: any, server: ElectronServer) => Pr */ [forProject.ProjectMethods.OpenDirectory]: async (options: { directory: string }) => { const store = new FileDataStore(); - const project = new Project(options.directory); + if (!(await exists(options.directory))) { + throw new ProjectNotFoundError(); + } + const project = new Project(options.directory); const json = await project.packageJson(); // validate the package is there, throws if not return { diff --git a/src/server/errors.ts b/src/server/errors.ts index 5846d1a..8df121b 100644 --- a/src/server/errors.ts +++ b/src/server/errors.ts @@ -365,3 +365,13 @@ export class MissingWebpackConfig extends Error { super('Webpack config not found.'); } } + +/** + * ProjectNotFoundError is thrown when trying to operate on a Project which + * doesn't exist on the filesystem. + */ +export class ProjectNotFoundError extends Error { + constructor() { + super('Project not found. It may have been moved or deleted.'); + } +} diff --git a/src/server/recent-projects.ts b/src/server/recent-projects.ts index bb195ad..cc6135c 100644 --- a/src/server/recent-projects.ts +++ b/src/server/recent-projects.ts @@ -10,40 +10,40 @@ export interface IRecentProject { * Loads and updates the recent projects used in CDK. */ export class RecentProjects { + /** + * Maximum number of recent projects to store. + */ + public static readonly maxRecentProject = 8; + constructor(private readonly store: IDataStore = new FileDataStore()) {} - public async loadProjects(): Promise { - return await this.store.loadGlobal('recent'); + public async loadProjects(): Promise { + return (await this.store.loadGlobal('recent')) || []; + } + + /** + * Removes a project from the list of recent projects. + */ + public async removeProject(directory: string) { + const projects = (await this.loadProjects()) || []; + await this.store.saveGlobal('recent', projects.filter(p => p.url !== directory)); } - public async updateProjects(url: string): Promise { - let projects = await this.loadProjects(); - if (!projects) { - projects = []; - } - const project = new Project(url); - const pkg = await project.packageJson(); - const name = pkg.name; - - const index = projects.findIndex(e => e.url === url); - - if (index >= 0) { - projects.splice(index, 1); - } - - const newProjects: IRecentProject[] = [ - { - name, - url, - }, - ]; - - for (let i = 0; i < 4; i++) { - if (projects[i]) { - newProjects.push(projects[i]); - } - } - - return await this.store.saveGlobal('recent', newProjects); + /** + * Adds a project to the list of recent projects. + */ + public async updateProjects(directory: string): Promise { + const projects = (await this.loadProjects()).filter(p => p.url !== directory); + const pkg = await new Project(directory).packageJson(); + + projects.unshift({ + name: pkg.name, + url: directory, + }); + + return await this.store.saveGlobal( + 'recent', + projects.slice(0, RecentProjects.maxRecentProject), + ); } } diff --git a/src/server/webpack-task.ts b/src/server/webpack-task.ts index 09ecfac..9d41114 100644 --- a/src/server/webpack-task.ts +++ b/src/server/webpack-task.ts @@ -11,8 +11,8 @@ import { WebpackState } from '../app/editor/controls/controls.actions'; import { MissingWebpackConfig } from './errors'; import { Project } from './project'; import { ConsoleTask } from './tasks/console-task'; -import { exists } from './util'; import { NpmInstallTask } from './tasks/npm-install-task'; +import { exists } from './util'; /** * Just making a note of some investigations here. *Ideally* I wanted to embed