Skip to content
This repository has been archived by the owner on Jul 21, 2020. It is now read-only.

Commit

Permalink
feat(upload): prompt to rename project on conflict (fixes #99)
Browse files Browse the repository at this point in the history
  • Loading branch information
connor4312 committed May 23, 2018
1 parent 3d21e36 commit 20376b9
Show file tree
Hide file tree
Showing 15 changed files with 217 additions and 10 deletions.
13 changes: 12 additions & 1 deletion src/app/editor/account/account.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IUser } from '../../../server/profile';
export const enum AccountActionTypes {
START_LOGOUT = '[Account] Start Logout',
FINISH_LOGOUT = '[Account] Finish Logout',
SWITCH_ACCOUNTS = '[Account] Log out and in again',
OPEN_LOGIN_FLOW = '[Account] Open Login Flow',
SET_LOGGED_IN_ACCOUNT = '[Account] Set logged in account',
LINK_SET_CODE = '[Account] Set linking code',
Expand Down Expand Up @@ -66,9 +67,19 @@ export class RequireAuth implements Action {
constructor(public readonly successAction: Action, public readonly failedAction?: Action) {}
}

/**
* Changes the account the user is logged into.
*/
export class SwitchAccounts implements Action {
public readonly type = AccountActionTypes.SWITCH_ACCOUNTS;

constructor(public readonly successAction?: Action, public readonly failedAction?: Action) {}
}

export type AccountActions =
| StartLogout
| FinishLogout
| SetLoggedInAccount
| SetLinkCode
| CancelLinking;
| CancelLinking
| SwitchAccounts;
15 changes: 15 additions & 0 deletions src/app/editor/account/account.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ export class AccountEffects {
}),
);

/**
* Runs an action that requires auth, see the RequireAuth action for details.
*/
@Effect()
public readonly switchAccounts = this.actions
.ofType<RequireAuth>(AccountActionTypes.SWITCH_ACCOUNTS)
.pipe(
switchMap(action => {
return this.dialog
.open(LoginDialogComponent)
.afterClosed()
.pipe(map(u => (u ? action.successAction : action.failedAction)), filter(a => !!a));
}),
);

constructor(
private readonly actions: Actions,
private readonly electron: ElectronService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, OnDestroy, Output } from '@angular/
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';

import { filter, map } from 'rxjs/operators';
import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators';

import { IUser } from '../../../../server/profile';
import * as fromRoot from '../../bedrock.reducers';
Expand All @@ -23,12 +23,15 @@ import * as fromAccount from '../account.reducer';
})
export class LoginWalkthroughComponent implements OnDestroy {
/**
* Emits once the user logs in.
* Emits once the user logs in. We skip the first emission because we want
* to wait for the account to truthy and _changed_, e.g. if someone is
* switching accounts they'll already be logged in but we want to
* close when they change accoutns.
*/
@Output()
public loggedIn: Observable<IUser> = this.store
.select(fromAccount.currentUser)
.pipe(filter<IUser>(user => !!user));
.pipe(distinctUntilChanged(), skip(1), filter<IUser>(user => !!user));

/**
* code is the shortcode to display to the user that they need to enter
Expand Down
14 changes: 13 additions & 1 deletion src/app/editor/project/project.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,14 @@ export const enum ProjectActionTypes {
LOAD_OWNED_GAMES = '[Project] Load games the user owns',
SET_OWNED_GAMES = '[Project] Set games the user owns',
REQUIRE_LINK = '[Project] Require project link',
RENAME_PROJECT = '[Project] Rename project',
}

export const enum ProjectMethods {
OpenDirectory = '[Project] Open directory',
LinkGameToControls = '[Project] Link game to controls',
GetOwnedGames = '[Project] Get owned games',
RenameProject = '[Project] Rename',
}

/**
Expand Down Expand Up @@ -176,6 +178,15 @@ export class RequireLink implements Action {
) {}
}

/**
* Renames the currently open project.
*/
export class RenameProject implements Action {
public readonly type = ProjectActionTypes.RENAME_PROJECT;

constructor(public readonly newName: string) {}
}

export type ProjectActions =
| CloseProject
| StartOpenProject
Expand All @@ -185,4 +196,5 @@ export type ProjectActions =
| OpenDirectory
| LoadOwnedGames
| SetOwnedGames
| RequireLink;
| RequireLink
| RenameProject;
20 changes: 19 additions & 1 deletion src/app/editor/project/project.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
LoadOwnedGames,
ProjectActionTypes,
ProjectMethods,
RenameProject,
RequireLink,
SetInteractiveGame,
SetOpenProject,
Expand Down Expand Up @@ -60,7 +61,7 @@ export class ProjectEffects {
directory: action.directory,
})
.then(results => new SetOpenProject(results))
.catch(RpcError, (err: RpcError) => new forToast.OpenToast(ErrorToastComponent, err)),
.catch(RpcError, err => new forToast.OpenToast(ErrorToastComponent, err)),
),
);

Expand Down Expand Up @@ -95,6 +96,23 @@ export class ProjectEffects {
),
);

/**
* Updates the name of the project package.json.
*/
@Effect()
public readonly renameProejct = this.actions
.ofType<RenameProject>(ProjectActionTypes.RENAME_PROJECT)
.pipe(
withLatestDirectory(this.store),
switchMap(([{ newName }, directory]) =>
this.electron
.call(ProjectMethods.RenameProject, { directory, name: newName })
.return()
.catch(RpcError, err => new forToast.OpenToast(ErrorToastComponent, err)),
),
filter(action => !!action),
);

/**
* Opens the dialog to change what project we're linked to.
*/
Expand Down
8 changes: 8 additions & 0 deletions src/app/editor/project/project.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ export function projectReducer(state: IProjectState = {}, action: ProjectActions
return { ...state, project: { ...state.project, interactiveGame: action.game } };
case ProjectActionTypes.SET_CONFIRM_SCHEMA:
return { ...state, project: { ...state.project, confirmSchemaUpload: action.confirm } };
case ProjectActionTypes.RENAME_PROJECT:
return {
...state,
project: {
...state.project,
packageJson: { ...state.project.packageJson, name: action.newName },
},
};
default:
return state;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
<uploader-uploading-schema *ngSwitchCase="Screens.UploadingSchema"></uploader-uploading-schema>
<uploader-uploading-controls *ngSwitchCase="Screens.UploadingControls"></uploader-uploading-controls>
<uploader-linking-game *ngSwitchCase="Screens.LinkingGame"></uploader-linking-game>
<uploader-rename *ngSwitchCase="Screens.Rename"></uploader-rename>
<uploader-completed *ngSwitchCase="Screens.Completed"></uploader-completed>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<h1 mat-dialog-title>Rename Bundle</h1>
<div mat-dialog-content>
<p>Darn, someone already took that bundle name! Try picking a different name or, if this bundle is yours, <a (click)="switchAccounts()" class="body-link">switch accounts</a>.</p>

<form (ngSubmit)="submit()" [formGroup]="form">
<mat-form-field style="display:block">
<input matInput #message maxlength="60" placeholder="Project Name*" formControlName="projectName">
<mat-hint align="start">Should be globally unique.</mat-hint>
<mat-error *ngIf="getInput('projectName').hasError('length')">
Your project name should be between 2 and 60 characters.
</mat-error>
<mat-error *ngIf="getInput('projectName').hasError('projectName')">
Your project name can only contain alphanumeric characters with dashes.
</mat-error>
<mat-error *ngIf="getInput('projectName').hasError('projectNameTaken')">
Shucks! That project name has already been taken.
</mat-error>
<mat-error *ngIf="getInput('projectName').hasError('required')">
The project name is required.
</mat-error>
</mat-form-field>
</form>
</div>
<div mat-dialog-actions>
<button mat-button (click)="dialogRef.close()">Cancel</button>
<span class="flex-fill"></span>
<button mat-button (click)="submit()" cdkFocusInitial>Rename</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:host {
width: 500px;
display: block;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AsyncValidatorFn, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef } from '@angular/material';
import { Store } from '@ngrx/store';
import { timer } from 'rxjs/observable/timer';
import { map, switchMap, take } from 'rxjs/operators';

import * as forAccount from '../../account/account.actions';
import { CommonMethods } from '../../bedrock.actions';
import * as fromRoot from '../../bedrock.reducers';
import { ElectronService } from '../../electron.service';
import * as forProject from '../../project/project.actions';
import * as fromProject from '../../project/project.reducer';
import { projectNameValidator } from '../../shared/validators';
import { OpenUploader, StartUploading } from '../uploader.actions';

/**
* Presents a brief into to uploading and allows users to select what
* things they want to upload.
*/
@Component({
selector: 'uploader-rename',
templateUrl: './uploader-rename.component.html',
styleUrls: ['./uploader-rename.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UploaderRenameComponent {
/**
* Form for input validation.
*/
public form: FormGroup;

constructor(
public readonly dialogRef: MatDialogRef<any>,
private readonly store: Store<fromRoot.IState>,
private readonly formBuilder: FormBuilder,
private readonly electron: ElectronService,
) {
this.store
.select(fromProject.projectState)
.pipe(map(p => p.project && p.project.packageJson.name), take(1))
.subscribe(previousName => {
this.form = this.formBuilder.group({
projectName: [
previousName || '',
[Validators.required, projectNameValidator],
this.checkProjectNameAvailable,
],
});
});
}

public getInput(name: string) {
return this.form.get(name)!;
}

public submit() {
this.store.dispatch(new forProject.RenameProject(this.form.value.projectName));
this.store.dispatch(new StartUploading());
}

public switchAccounts() {
this.dialogRef.close();
this.store.dispatch(new forAccount.SwitchAccounts(new OpenUploader()));
}

private checkProjectNameAvailable: AsyncValidatorFn = control => {
return timer(750).pipe(
switchMap(() =>
this.electron.call(CommonMethods.CheckBundleNameTaken, { name: control.value }),
),
map(taken => (taken ? { projectNameTaken: { value: control.value } } : null)),
);
};
}
1 change: 1 addition & 0 deletions src/app/editor/uploader/uploader.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const enum UploaderMethods {

export enum UploaderScreen {
Confirming,
Rename,
UploadingSchema,
UploadingControls,
LinkingGame,
Expand Down
14 changes: 12 additions & 2 deletions src/app/editor/uploader/uploader.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import {
ErrorStateMatcher,
MatButtonModule,
MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatProgressSpinnerModule,
} from '@angular/material';
import { EffectsModule } from '@ngrx/effects';
Expand All @@ -18,11 +21,14 @@ import { UploaderConfirmingComponent } from './uploader-confirming/uploader-conf
import { UploaderConsoleService } from './uploader-console.service';
import { UploaderDialogComponent } from './uploader-dialog/uploader-dialog.component';
import { UploaderLinkingGameComponent } from './uploader-linking-game/uploader-linking-game.component';
import { UploaderRenameComponent } from './uploader-rename/uploader-rename.component';
import { UploaderUploadingControls } from './uploader-uploading-controls/uploader-uploading-controls.component';
import { UploaderUploadingSchema } from './uploader-uploading-schema/uploader-uploading-schema.component';
import { UploaderEffects } from './uploader.effects';
import { uploaderReducer } from './uploader.reducer';

const errorStateMatcher = { isErrorState: (ctrl: FormControl) => ctrl.invalid };

/**
* Module containing the flow to upload a control bundle to Mixer.
*/
Expand All @@ -36,17 +42,21 @@ import { uploaderReducer } from './uploader.reducer';
MatButtonModule,
MatCheckboxModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatProgressSpinnerModule,
ReactiveFormsModule,
SharedModule,
StoreModule.forFeature('uploader', uploaderReducer),
],
entryComponents: [UploaderDialogComponent],
providers: [UploaderConsoleService],
providers: [UploaderConsoleService, { provide: ErrorStateMatcher, useValue: errorStateMatcher }],
declarations: [
UploaderCompletedComponent,
UploaderConfirmingComponent,
UploaderDialogComponent,
UploaderLinkingGameComponent,
UploaderRenameComponent,
UploaderUploadingControls,
UploaderUploadingControls,
UploaderUploadingSchema,
Expand Down
5 changes: 4 additions & 1 deletion src/app/editor/uploader/uploader.reducer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createFeatureSelector, createSelector, MemoizedSelector } from '@ngrx/store';
import { BundleNameTakenError } from '../../../server/errors';

import * as fromRoot from '../bedrock.reducers';
import { WebpackState } from '../controls/controls.actions';
Expand Down Expand Up @@ -38,7 +39,9 @@ export function uploaderReducer(
case UploaderActionTypes.SET_SCREEN:
return { ...state, screen: action.screen };
case UploaderActionTypes.SET_ERROR:
return { ...state, error: action.error };
return action.error.originalName === BundleNameTakenError.name
? { ...state, screen: UploaderScreen.Rename }
: { ...state, error: action.error };
case UploaderActionTypes.UPLOADER_CLOSED:
return { ...state, screen: UploaderScreen.Confirming, error: undefined };
default:
Expand Down
11 changes: 11 additions & 0 deletions src/server/electron-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,17 @@ const methods: { [methodName: string]: (data: any, server: ElectronServer) => Pr
return new ProjectLinker(new Project(options.directory)).linkGame(options.game);
},

/**
* Links the Interactive game to the set of controls.
*/
[forProject.ProjectMethods.RenameProject]: async (options: {
name: string;
directory: string;
}) => {
const project = new Project(options.directory);
await project.putPackageJson({ ...(await project.packageJson()), name: options.name });
},

/**
* Boots a webpack server, which asynchronously sends updates down to the
* renderer. It'll run until it crashes or StopWebpack is called.
Expand Down
Loading

0 comments on commit 20376b9

Please sign in to comment.