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

GitHub repository input component #8

Merged
merged 10 commits into from
Nov 30, 2016
34 changes: 2 additions & 32 deletions src/common/actions/repository-input.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,14 @@
import 'isomorphic-fetch';
import parseGitHubUrl from 'parse-github-url';

export const CHANGE_REPOSITORY_INPUT = 'CHANGE_REPOSITORY_INPUT';
export const UPDATE_STATUS_MESSAGE = 'UPDATE_STATUS_MESSAGE';
export const SET_GITHUB_REPOSITORY = 'SET_GITHUB_REPOSITORY';
export const VERIFY_GITHUB_REPOSITORY = 'VERIFY_GITHUB_REPOSITORY';
export const VERIFY_GITHUB_REPOSITORY_SUCCESS = 'VERIFY_GITHUB_REPOSITORY_SUCCESS';
export const VERIFY_GITHUB_REPOSITORY_ERROR = 'VERIFY_GITHUB_REPOSITORY_ERROR';

export function updateInputValue(value) {
return (dispatch) => {
dispatch(changeRepositoryInput(value));
dispatch(validateGitHubRepository(value));
};
}

export function changeRepositoryInput(value) {
return {
type: CHANGE_REPOSITORY_INPUT,
payload: value
};
}

export function setGitHubRepository(repository) {
export function setGitHubRepository(value) {
return {
type: SET_GITHUB_REPOSITORY,
payload: repository
};
}

export function validateGitHubRepository(repository) {
return (dispatch) => {
const gitHubRepo = parseGitHubUrl(repository);
const repo = gitHubRepo ? gitHubRepo.repo : null;

dispatch(setGitHubRepository(repo));

if (!repo) {
dispatch(verifyGitHubRepositoryError(new Error('Invalid repository URL.')));
}
payload: value
};
}

Expand Down
20 changes: 13 additions & 7 deletions src/common/components/repository-input/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';

import {
updateInputValue,
setGitHubRepository,
verifyGitHubRepository
} from '../../actions/repository-input';

Expand All @@ -17,27 +17,29 @@ export class RepositoryInput extends Component {
const input = this.props.repositoryInput;
let message;

if (input.repository && input.isFetching) {
if (input.inputValue.length > 2 && !input.repository) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor ux nit, too early reporting of "invalid" input is annoying, is 2 the smallest this can be?

message = 'Repository URL or name is invalid.';
} else if (input.repository && input.isFetching) {
message = `Verifying ${input.repository} on GitHub...`;
} else if (input.success && input.repositoryUrl) {
message = `Repository ${input.repository} contains snapcraft project and can be built.`;
} else if (input.error) {
if (input.repository) {
message = `Repository ${input.repository} is doesn't exist, is not public or doesn't contain snapcraft.yaml file.`;
} else {
message = 'Repository URL or name is invalid.';
}
}

return message;
}

render() {
const isValid = !!this.props.repositoryInput.repository;

return (
<form onSubmit={this.onSubmit.bind(this)}>
<label>Repository URL:</label>
<input type='text' value={this.props.repositoryInput.inputValue} onChange={this.onInputChange.bind(this)} />
<button type='submit'>Parse</button>
<button type='submit' disabled={!isValid}>Verify</button>
<div>
{this.getStatusMessage()}
</div>
Expand All @@ -46,11 +48,15 @@ export class RepositoryInput extends Component {
}

onInputChange(event) {
this.props.dispatch(updateInputValue(event.target.value));
this.props.dispatch(setGitHubRepository(event.target.value));
}

onSubmit(event) {
this.props.dispatch(verifyGitHubRepository(this.props.repositoryInput.inputValue));
const { repository } = this.props.repositoryInput;

if (repository) {
this.props.dispatch(verifyGitHubRepository(repository));
}
event.preventDefault();
}
}
Expand Down
14 changes: 8 additions & 6 deletions src/common/reducers/repository-input.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import * as ActionTypes from '../actions/repository-input';
import parseGitHubUrl from 'parse-github-url';

function parseRepository(input) {
const gitHubRepo = parseGitHubUrl(input);
return gitHubRepo ? gitHubRepo.repo : null;
}

export function repositoryInput(state = {
isFetching: false,
Expand All @@ -9,18 +15,14 @@ export function repositoryInput(state = {
error: false
}, action) {
switch(action.type) {
case ActionTypes.CHANGE_REPOSITORY_INPUT:
case ActionTypes.SET_GITHUB_REPOSITORY:
return {
...state,
inputValue: action.payload,
repository: parseRepository(action.payload),
success: false,
error: false
};
case ActionTypes.SET_GITHUB_REPOSITORY:
return {
...state,
repository: action.payload
};
case ActionTypes.VERIFY_GITHUB_REPOSITORY:
return {
...state,
Expand Down
85 changes: 1 addition & 84 deletions test/unit/src/common/actions/t_repository-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import nock from 'nock';
import { isFSA } from 'flux-standard-action';

import {
updateInputValue,
changeRepositoryInput,
setGitHubRepository,
validateGitHubRepository,
verifyGitHubRepository,
verifyGitHubRepositorySuccess,
verifyGitHubRepositoryError
Expand All @@ -26,7 +23,7 @@ describe('repository input actions', () => {
repositoryUrl: null,
statusMessage: '',
success: false,
errors: false
error: false
};

let store;
Expand All @@ -36,28 +33,6 @@ describe('repository input actions', () => {
store = mockStore(initialState);
});

context('changeRepositoryInput', () => {
let payload = 'foo input';

beforeEach(() => {
action = changeRepositoryInput(payload);
});

it('should create an action to change repository input value', () => {
const expectedAction = {
type: ActionTypes.CHANGE_REPOSITORY_INPUT,
payload
};

store.dispatch(action);
expect(store.getActions()).toInclude(expectedAction);
});

it('should create a valid flux standard action', () => {
expect(isFSA(action)).toBe(true);
});
});

context('setGitHubRepository', () => {
let payload = 'foo/bar';

Expand All @@ -80,64 +55,6 @@ describe('repository input actions', () => {
});
});

context('validateGitHubRepository', () => {

context('on valid repository input', () => {
let payload = 'foo/bar';

beforeEach(() => {
action = validateGitHubRepository(payload);
});

it('should save an action to change repository input value', () => {
const expectedAction = {
type: ActionTypes.SET_GITHUB_REPOSITORY,
payload
};

store.dispatch(action);
expect(store.getActions()).toInclude(expectedAction);
});
});

context('on invalid repository input', () => {
let payload = 'foo bar';

beforeEach(() => {
action = validateGitHubRepository(payload);
});

it('should dispatch VERIFY_GITHUB_REPOSITORY_ERROR action', () => {
store.dispatch(action);
expect(store.getActions()).toHaveActionOfType(
ActionTypes.VERIFY_GITHUB_REPOSITORY_ERROR
);
});
});
});

context('updateInputValue', () => {
let payload = 'foo/bar';

beforeEach(() => {
action = updateInputValue(payload);
});

it('should dispatch CHANGE_REPOSITORY_INPUT action', () => {
store.dispatch(action);
expect(store.getActions()).toHaveActionOfType(
ActionTypes.CHANGE_REPOSITORY_INPUT
);
});

it('should dispatch SET_GITHUB_REPOSITORY action', () => {
store.dispatch(action);
expect(store.getActions()).toHaveActionOfType(
ActionTypes.SET_GITHUB_REPOSITORY
);
});
});

context('verifyGitHubRepositorySuccess', () => {
let payload = 'http://github.com/foo/bar.git';

Expand Down
57 changes: 37 additions & 20 deletions test/unit/src/common/reducers/t_repository-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,51 +17,68 @@ describe('repositoryInput reducers', () => {
expect(repositoryInput(undefined, {})).toEqual(initialState);
});

context('CHANGE_REPOSITORY_INPUT', () => {
context('SET_GITHUB_REPOSITORY', () => {
let action;

beforeEach(() => {
action = {
type: ActionTypes.CHANGE_REPOSITORY_INPUT,
payload: 'foo'
type: ActionTypes.SET_GITHUB_REPOSITORY
};
});

it('should change repository input value', () => {
expect(repositoryInput(initialState, action)).toEqual({
...initialState,
action.payload = 'foo';

expect(repositoryInput(initialState, action)).toInclude({
inputValue: 'foo'
});
});

it('should reset error status', () => {
it('should save repository name for valid user/repo pair', () => {
action.payload = 'foo/bar';

expect(repositoryInput(initialState, action)).toInclude({
repository: 'foo/bar'
});
});

it('should save repository name for valid repo URL', () => {
action.payload = 'http://github.com/foo/bar';

expect(repositoryInput(initialState, action)).toInclude({
repository: 'foo/bar'
});
});

it('should clear repository name for invalid input', () => {
action.payload = 'foo bar';

const state = {
error: new Error('Test')
...initialState,
repository: 'foo/bar'
};

expect(repositoryInput(state, action).error).toBe(false);
expect(repositoryInput(state, action)).toInclude({
repository: null
});
});

it('should reset error status', () => {
const state = {
success: true
...initialState,
error: new Error('Test')
};

expect(repositoryInput(state, action).success).toBe(false);
expect(repositoryInput(state, action).error).toBe(false);
});
});

context('SET_GITHUB_REPOSITORY', () => {
it('should save validated repository name', () => {
const action = {
type: ActionTypes.SET_GITHUB_REPOSITORY,
payload: 'foo/bar'
it('should reset success status', () => {
const state = {
...initialState,
success: true
};

expect(repositoryInput(initialState, action)).toEqual({
...initialState,
repository: 'foo/bar'
});
expect(repositoryInput(state, action).success).toBe(false);
});
});

Expand Down