From 1cd8f3e89716a18ec1202bc4fbb1875b5eb24083 Mon Sep 17 00:00:00 2001 From: Isaac Mann Date: Tue, 22 Jan 2019 16:04:04 -0500 Subject: [PATCH] feat: Show all actions from project view --- .../src/integration/projects.spec.ts | 45 +++- .../lib/graphql/update-recent-actions.graphql | 16 ++ .../lib/graphql/workspace-schematics.graphql | 12 + .../src/lib/graphql/workspace.graphql | 4 + .../src/lib/projects/projects.component.html | 31 ++- .../src/lib/projects/projects.component.ts | 226 ++++++++++++++---- libs/schema/src/index.ts | 6 + libs/server/src/assets/schema.graphql | 12 + libs/server/src/lib/api/read-projects.ts | 11 +- .../server/src/lib/api/read-recent-actions.ts | 34 +++ libs/server/src/lib/api/read-settings.ts | 3 +- .../src/lib/resolvers/mutation.resolver.ts | 16 ++ .../src/lib/resolvers/query.resolver.ts | 2 +- 13 files changed, 360 insertions(+), 58 deletions(-) create mode 100644 libs/feature-workspaces/src/lib/graphql/update-recent-actions.graphql create mode 100644 libs/feature-workspaces/src/lib/graphql/workspace-schematics.graphql create mode 100644 libs/server/src/lib/api/read-recent-actions.ts diff --git a/apps/angular-console-e2e/src/integration/projects.spec.ts b/apps/angular-console-e2e/src/integration/projects.spec.ts index aaac9b23fc..4f61381a51 100644 --- a/apps/angular-console-e2e/src/integration/projects.spec.ts +++ b/apps/angular-console-e2e/src/integration/projects.spec.ts @@ -25,15 +25,18 @@ describe('Projects', () => { }); it('checks that hot actions work', () => { - cy.contains('button', 'Generate Component').click(); + cy.contains('angular-console-projects button', 'Component').should( + 'not.exist' + ); + + cy.contains('mat-icon', 'more_horiz') + .first() + .click(); + cy.contains('.cdk-overlay-pane button', 'Component').click(); cy.contains('div.context-title', '@schematics/angular - component'); cy.get('input[name="project"]').should(($p: any) => { expect($p[0].value).to.equal('proj'); }); - }); - - it('provides navigation to and from command runners', () => { - cy.contains('Generate Component').click(); cy.get('.exit-action').click(); projectNames($p => { @@ -41,5 +44,37 @@ describe('Projects', () => { expect(texts($p)[0]).to.contain('proj'); expect(texts($p)[1]).to.contain('proj-e2e'); }); + cy.contains('angular-console-projects button', 'Component'); + + cy.contains('angular-console-projects button', 'Build') + .first() + .click(); + cy.contains('div.context-title', 'ng build proj'); + cy.get('.exit-action').click(); + cy.contains('angular-console-projects button', 'Serve') + .first() + .click(); + cy.contains('div.context-title', 'ng serve proj'); + cy.get('.exit-action').click(); + cy.contains('angular-console-projects button', 'Extract-i18n') + .first() + .click(); + cy.contains('div.context-title', 'ng extract-i18n proj'); + cy.get('.exit-action').click(); + cy.contains('angular-console-projects button', 'Test') + .first() + .click(); + cy.contains('div.context-title', 'ng test proj'); + cy.get('.exit-action').click(); + cy.contains('mat-icon', 'more_horiz') + .first() + .click(); + cy.contains('.cdk-overlay-pane button', 'Lint').click(); + cy.contains('div.context-title', 'ng lint proj'); + cy.get('.exit-action').click(); + + cy.contains('angular-console-projects button', 'Component').should( + 'not.exist' + ); }); }); diff --git a/libs/feature-workspaces/src/lib/graphql/update-recent-actions.graphql b/libs/feature-workspaces/src/lib/graphql/update-recent-actions.graphql new file mode 100644 index 0000000000..6ca0b8b1ae --- /dev/null +++ b/libs/feature-workspaces/src/lib/graphql/update-recent-actions.graphql @@ -0,0 +1,16 @@ +mutation SaveRecentAction( + $workspacePath: String! + $projectName: String! + $actionName: String! + $schematicName: String +) { + saveRecentAction( + workspacePath: $workspacePath + projectName: $projectName + actionName: $actionName + schematicName: $schematicName + ) { + actionName + schematicName + } +} diff --git a/libs/feature-workspaces/src/lib/graphql/workspace-schematics.graphql b/libs/feature-workspaces/src/lib/graphql/workspace-schematics.graphql new file mode 100644 index 0000000000..6ceaf1b528 --- /dev/null +++ b/libs/feature-workspaces/src/lib/graphql/workspace-schematics.graphql @@ -0,0 +1,12 @@ +query WorkspaceSchematics($path: String!) { + workspace(path: $path) { + schematicCollections { + name + schematics { + name + description + collection + } + } + } +} diff --git a/libs/feature-workspaces/src/lib/graphql/workspace.graphql b/libs/feature-workspaces/src/lib/graphql/workspace.graphql index 37a6e9aa53..a0c784dd94 100644 --- a/libs/feature-workspaces/src/lib/graphql/workspace.graphql +++ b/libs/feature-workspaces/src/lib/graphql/workspace.graphql @@ -13,6 +13,10 @@ query Workspace($path: String!) { architect { name } + recentActions { + actionName + schematicName + } } } } diff --git a/libs/feature-workspaces/src/lib/projects/projects.component.html b/libs/feature-workspaces/src/lib/projects/projects.component.html index 7ecb887b88..38c19d2446 100644 --- a/libs/feature-workspaces/src/lib/projects/projects.component.html +++ b/libs/feature-workspaces/src/lib/projects/projects.component.html @@ -1,4 +1,4 @@ -
+
- + + + + + + + + {{ a.actionDescription }} + + + + + +
diff --git a/libs/feature-workspaces/src/lib/projects/projects.component.ts b/libs/feature-workspaces/src/lib/projects/projects.component.ts index 555acbbdf1..2289ffa0d3 100644 --- a/libs/feature-workspaces/src/lib/projects/projects.component.ts +++ b/libs/feature-workspaces/src/lib/projects/projects.component.ts @@ -1,16 +1,39 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { Project } from '@angular-console/schema'; -import { combineLatest, Observable, of } from 'rxjs'; -import { map, startWith, switchMap, shareReplay } from 'rxjs/operators'; +import { ActivatedRoute, Router, NavigationEnd } from '@angular/router'; +import { Project, SchematicCollection } from '@angular-console/schema'; +import { combineLatest, Observable, of, Subject } from 'rxjs'; +import { + map, + startWith, + switchMap, + shareReplay, + filter, + catchError +} from 'rxjs/operators'; import { PROJECTS_POLLING, Settings, CommandRunner } from '@angular-console/utils'; -import { WorkspaceDocsGQL, WorkspaceGQL } from '../generated/graphql'; +import { + WorkspaceDocsGQL, + WorkspaceGQL, + SaveRecentActionGQL, + WorkspaceSchematicsGQL, + WorkspaceSchematics +} from '../generated/graphql'; import { FormControl } from '@angular/forms'; +export interface ProjectAction { + name: string; + actionDescription: string; + schematicName?: string; + link?: (string | { project: string })[]; +} +export interface ProjectActionMap { + [projectName: string]: ProjectAction[]; +} + @Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'angular-console-projects', @@ -18,8 +41,9 @@ import { FormControl } from '@angular/forms'; styleUrls: ['./projects.component.scss'] }) export class ProjectsComponent implements OnInit { - projects$: Observable; - filteredProjects$: Observable; + workspacePath$: Observable; + projects$: Observable; + filteredProjects$: Observable; docs$ = this.settings.showDocs ? this.route.params.pipe( switchMap(p => this.workspaceDocsGQL.fetch({ path: p.path })), @@ -37,31 +61,64 @@ export class ProjectsComponent implements OnInit { shareReplay() ); + recentActions$: Observable; + constructor( private readonly route: ActivatedRoute, private readonly workspaceGQL: WorkspaceGQL, private readonly settings: Settings, private readonly workspaceDocsGQL: WorkspaceDocsGQL, - private readonly commandRunner: CommandRunner + private readonly saveRecentActionGQL: SaveRecentActionGQL, + private readonly commandRunner: CommandRunner, + private readonly workspaceSchematicsGQL: WorkspaceSchematicsGQL ) {} ngOnInit() { - this.projects$ = this.route.params.pipe( + const workspace$ = this.route.params.pipe( map(m => m.path), switchMap(path => { - return this.workspaceGQL.watch( - { - path - }, - { - pollInterval: PROJECTS_POLLING - } - ).valueChanges; + return combineLatest( + this.workspaceGQL.watch( + { + path + }, + { + pollInterval: PROJECTS_POLLING + } + ).valueChanges, + this.workspaceSchematicsGQL + .watch({ path }, { pollInterval: PROJECTS_POLLING }) + .valueChanges.pipe( + catchError(() => + of({ + data: { + workspace: { + schematicCollections: [] as WorkspaceSchematics.SchematicCollections[] + } + } + }) + ) + ) + ); }), - map((r: any) => { - const w = r.data.workspace; - const projects = w.projects.map((p: any) => { - return { ...p, actions: this.createActions(p) }; + filter(([r1, r2]) => !!r1 && !!r2), + map(([r1, r2]) => { + return { + workspace: r1.data.workspace, + schematicCollections: r2.data.workspace.schematicCollections + }; + }) + ); + this.workspacePath$ = workspace$.pipe( + map(({ workspace }) => workspace.path) + ); + this.projects$ = workspace$.pipe( + map(({ workspace, schematicCollections }) => { + const projects = workspace.projects.map((p: any) => { + return { + ...p, + actions: this.createActions(p, schematicCollections) + }; }); return projects; }) @@ -80,16 +137,67 @@ export class ProjectsComponent implements OnInit { ) ) ); + + const MAX_RECENT_ACTIONS = 5; + this.recentActions$ = this.projects$.pipe( + map(projects => { + return projects.reduce( + (projectActions, nextProject) => { + const recentActions = nextProject.recentActions + .map(recentAction => + (nextProject as any).actions.find( + (action: ProjectAction) => + action.name === recentAction.actionName && + action.schematicName === recentAction.schematicName + ) + ) + .filter(action => action !== undefined); + projectActions[nextProject.name] = [ + ...recentActions, + ...(nextProject as any).actions + .filter( + (action: ProjectAction) => + action.link !== undefined && !recentActions.includes(action) + ) + .slice(0, MAX_RECENT_ACTIONS - recentActions.length) + ]; + return projectActions; + }, + {} + ); + }) + ); + } + + onActionTriggered( + workspacePath: string, + project: Project, + action: ProjectAction + ) { + this.saveRecentActionGQL + .mutate({ + workspacePath: workspacePath, + projectName: project.name, + schematicName: action.schematicName, + actionName: action.name + }) + .subscribe(); } - private createActions(p: any) { + private createActions( + p: Project, + schematicCollections: WorkspaceSchematics.SchematicCollections[] + ) { return [ - ...createLinkForTask(p, 'serve', 'Serve'), - ...createLinkForTask(p, 'test', 'Test'), - ...createLinkForTask(p, 'build', 'Build'), - ...createLinkForTask(p, 'e2e', 'E2E'), - ...createLinkForCoreSchematic(p, 'component', 'Generate Component') - ] as any[]; + { actionDescription: 'Tasks' }, + ...p.architect + .map(task => createLinkForTask(p, task.name, task.name)) + .filter(isDefinedProjectAction), + ...schematicCollections.reduce((links, collection) => { + links.push(...createLinksForCollection(p, collection)); + return links; + }, []) + ]; } trackByName(p: any) { @@ -103,34 +211,64 @@ function createLinkForTask( actionDescription: string ) { if (project.architect.find(a => a.name === name)) { - return [{ actionDescription, link: ['../tasks', name, project.name] }]; + return { actionDescription, name, link: ['../tasks', name, project.name] }; } else { - return []; + return undefined; } } -function createLinkForCoreSchematic( +function createLinksForCollection( project: Project, + collection: WorkspaceSchematics.SchematicCollections +): ProjectAction[] { + const newLinks = (collection.schematics || []) + .map(schematic => + createLinkForSchematic( + project, + '@schematics/angular', + schematic ? schematic.name : '', + schematic ? schematic.name : '' + ) + ) + .filter(isDefinedProjectAction); + if (newLinks.length > 0) { + newLinks.unshift({ + name: collection.name, + actionDescription: collection.name + }); + } + return newLinks; +} + +function createLinkForSchematic( + project: Project, + schematicName: string, name: string, actionDescription: string -) { +): ProjectAction | undefined { if ( (project.projectType === 'application' || project.projectType === 'library') && !project.architect.find(a => a.name === 'e2e') ) { - return [ - { - actionDescription, - link: [ - '../generate', - decodeURIComponent('@schematics/angular'), - name, - { project: project.name } - ] - } - ]; + return { + name, + schematicName, + actionDescription, + link: [ + '../generate', + decodeURIComponent(schematicName), + name, + { project: project.name } + ] + }; } else { - return []; + return undefined; } } + +function isDefinedProjectAction( + action: ProjectAction | undefined +): action is ProjectAction { + return action !== undefined; +} diff --git a/libs/schema/src/index.ts b/libs/schema/src/index.ts index ff8d4eb095..eed4690235 100644 --- a/libs/schema/src/index.ts +++ b/libs/schema/src/index.ts @@ -76,6 +76,12 @@ export interface Project { projectType: string; root: string; architect: Builder[]; + recentActions: RecentAction[]; +} + +export interface RecentAction { + actionName: string; + schematicName?: string; } export interface NpmScript { diff --git a/libs/server/src/assets/schema.graphql b/libs/server/src/assets/schema.graphql index 6c9896aeab..963dce1d71 100644 --- a/libs/server/src/assets/schema.graphql +++ b/libs/server/src/assets/schema.graphql @@ -151,6 +151,12 @@ type Mutation { restartCommand(id: String!): RemoveResult openInEditor(editor: String!, path: String!): OpenInEditor updateSettings(data: String!): Settings! + saveRecentAction( + workspacePath: String! + projectName: String! + actionName: String! + schematicName: String + ): [RecentAction!]! installNodeJs: InstallNodeJsStatus openInBrowser(url: String!): OpenInBrowserResult selectDirectory( @@ -182,6 +188,12 @@ type Project { root: String! projectType: String! architect(name: String): [Architect] + recentActions: [RecentAction!]! +} + +type RecentAction { + actionName: String! + schematicName: String } type Schematic { diff --git a/libs/server/src/lib/api/read-projects.ts b/libs/server/src/lib/api/read-projects.ts index c984056bbb..df1f031c6b 100644 --- a/libs/server/src/lib/api/read-projects.ts +++ b/libs/server/src/lib/api/read-projects.ts @@ -1,15 +1,22 @@ import { normalizeSchema, readJsonFile } from '../utils/utils'; import { Project, Architect } from '../generated/graphql-types'; import * as path from 'path'; +import { Store } from '@nrwl/angular-console-enterprise-electron'; +import { readRecentActions } from './read-recent-actions'; -export function readProjects(basedir: string, json: any): Project[] { +export function readProjects( + basedir: string, + json: any, + store: Store +): Project[] { return Object.entries(json) .map(([key, value]: [string, any]) => { return { name: key, root: value.root, projectType: value.projectType, - architect: readArchitect(key, basedir, value.architect) + architect: readArchitect(key, basedir, value.architect), + recentActions: readRecentActions(store, `${basedir}/${key}`) }; }) .sort(compareProjects); diff --git a/libs/server/src/lib/api/read-recent-actions.ts b/libs/server/src/lib/api/read-recent-actions.ts new file mode 100644 index 0000000000..7d5331d8b6 --- /dev/null +++ b/libs/server/src/lib/api/read-recent-actions.ts @@ -0,0 +1,34 @@ +import { Store } from '@nrwl/angular-console-enterprise-electron'; +import { RecentAction } from '../generated/graphql-types'; + +function getRecentActionsKey(projectPath: string): string { + return `recentActions:${projectPath}`; +} + +export function readRecentActions( + store: Store, + projectPath: string +): RecentAction[] { + const actions: any[] = store.get(getRecentActionsKey(projectPath)); + return (actions || []).filter(action => action && !!action.actionName); +} + +export function storeTriggeredAction( + store: Store, + projectPath: string, + actionName: string, + schematicName?: string +) { + const MAX_RECENT_ACTIONS = 5; + const existingActions = readRecentActions(store, projectPath); + store.set(getRecentActionsKey(projectPath), [ + { actionName, schematicName }, + ...existingActions + .filter( + action => + action.actionName !== actionName || + action.schematicName !== schematicName + ) + .slice(0, MAX_RECENT_ACTIONS - 1) + ]); +} diff --git a/libs/server/src/lib/api/read-settings.ts b/libs/server/src/lib/api/read-settings.ts index db19050cda..b6ffd8f763 100644 --- a/libs/server/src/lib/api/read-settings.ts +++ b/libs/server/src/lib/api/read-settings.ts @@ -1,10 +1,9 @@ import { Store } from '@nrwl/angular-console-enterprise-electron'; import { Settings } from '../generated/graphql-types'; -/* tslint:disable */ export function readSettings(store: Store): Settings { const settings: Settings = store.get('settings') || {}; - if (settings.canCollectData === undefined) { + if ((settings as any).canCollectData === undefined) { settings.canCollectData = store.get('canCollectData', false); } if (settings.recent === undefined) { diff --git a/libs/server/src/lib/resolvers/mutation.resolver.ts b/libs/server/src/lib/resolvers/mutation.resolver.ts index cefb832190..98234e6844 100644 --- a/libs/server/src/lib/resolvers/mutation.resolver.ts +++ b/libs/server/src/lib/resolvers/mutation.resolver.ts @@ -9,6 +9,10 @@ import { Args, Mutation, Resolver } from '@nestjs/graphql'; import * as path from 'path'; import * as semver from 'semver'; import { SelectDirectory } from '../types'; +import { + storeTriggeredAction, + readRecentActions +} from '../api/read-recent-actions'; function disableInteractivePrompts(p: string) { try { @@ -283,6 +287,18 @@ export class MutationResolver { return readSettings(this.store); } + @Mutation() + saveRecentAction( + @Args('workspacePath') workspacePath: string, + @Args('projectName') projectName: string, + @Args('actionName') actionName: string, + @Args('schematicName') schematicName: string + ) { + const key = `${workspacePath}/${projectName}`; + storeTriggeredAction(this.store, key, actionName, schematicName); + return readRecentActions(this.store, key); + } + @Mutation() async openDoc(@Args('id') id: string) { const result = await docs.openDoc(id).toPromise(); diff --git a/libs/server/src/lib/resolvers/query.resolver.ts b/libs/server/src/lib/resolvers/query.resolver.ts index c9c1638a57..c8f6c63af3 100644 --- a/libs/server/src/lib/resolvers/query.resolver.ts +++ b/libs/server/src/lib/resolvers/query.resolver.ts @@ -66,7 +66,7 @@ export class QueryResolver { path: p, dependencies: readDependencies(packageJson), extensions: readExtensions(packageJson), - projects: readProjects(p, angularJson.projects), + projects: readProjects(p, angularJson.projects, this.store), npmScripts: readNpmScripts(p, packageJson), docs: {} as any, schematicCollections: [] as any