diff --git a/scm-ui/src/repos/containers/Create.js b/scm-ui/src/repos/containers/Create.js index 37197bd5a7..664f7e2d1b 100644 --- a/scm-ui/src/repos/containers/Create.js +++ b/scm-ui/src/repos/containers/Create.js @@ -11,26 +11,50 @@ import { getRepositoryTypes, isFetchRepositoryTypesPending } from "../modules/repository-types"; +import { + createRepo, + createRepoReset, + getCreateRepoFailure, + isCreateRepoPending +} from "../modules/repos"; +import type { Repository } from "../types/Repositories"; +import type { History } from "history"; type Props = { repositoryTypes: RepositoryType[], typesLoading: boolean, + createLoading: boolean, error: Error, // dispatch functions fetchRepositoryTypesIfNeeded: () => void, + createRepo: (Repository, callback: () => void) => void, + resetForm: () => void, // context props - t: string => string + t: string => string, + history: History }; class Create extends React.Component { componentDidMount() { + this.props.resetForm(); this.props.fetchRepositoryTypesIfNeeded(); } + repoCreated = () => { + const { history } = this.props; + history.push("/repos"); + }; + render() { - const { typesLoading, repositoryTypes, error } = this.props; + const { + typesLoading, + createLoading, + repositoryTypes, + createRepo, + error + } = this.props; const { t } = this.props; return ( @@ -39,8 +63,15 @@ class Create extends React.Component { subtitle={t("create.subtitle")} loading={typesLoading} error={error} + showContentOnError={true} > - + { + createRepo(repo, this.repoCreated); + }} + /> ); } @@ -49,10 +80,13 @@ class Create extends React.Component { const mapStateToProps = state => { const repositoryTypes = getRepositoryTypes(state); const typesLoading = isFetchRepositoryTypesPending(state); - const error = getFetchRepositoryTypesFailure(state); + const createLoading = isCreateRepoPending(state); + const error = + getFetchRepositoryTypesFailure(state) || getCreateRepoFailure(state); return { repositoryTypes, typesLoading, + createLoading, error }; }; @@ -61,6 +95,12 @@ const mapDispatchToProps = dispatch => { return { fetchRepositoryTypesIfNeeded: () => { dispatch(fetchRepositoryTypesIfNeeded()); + }, + createRepo: (repository: Repository, callback: () => void) => { + dispatch(createRepo(repository, callback)); + }, + resetForm: () => { + dispatch(createRepoReset()); } }; }; diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index d466ff50d4..54d90a354a 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -10,6 +10,7 @@ import { fetchReposByPage, getFetchReposFailure, getRepositoryCollection, + isAbleToCreateRepos, isFetchReposPending } from "../modules/repos"; import { translate } from "react-i18next"; @@ -25,11 +26,13 @@ type Props = { collection: RepositoryCollection, loading: boolean, error: Error, + showCreateButton: boolean, // dispatched functions fetchRepos: () => void, fetchReposByPage: number => void, fetchReposByLink: string => void, + // context props t: string => string, history: History @@ -69,21 +72,31 @@ class Overview extends React.Component { } renderList() { - const { collection, fetchReposByLink, t } = this.props; + const { collection, fetchReposByLink } = this.props; if (collection) { return (
- + {this.renderCreateButton()}
); } return null; } + + renderCreateButton() { + const { showCreateButton, t } = this.props; + if (showCreateButton) { + return ( + + ); + } + return null; + } } const getPageFromProps = props => { @@ -101,11 +114,13 @@ const mapStateToProps = (state, ownProps) => { const collection = getRepositoryCollection(state); const loading = isFetchReposPending(state); const error = getFetchReposFailure(state); + const showCreateButton = isAbleToCreateRepos(state); return { page, collection, loading, - error + error, + showCreateButton }; }; diff --git a/scm-ui/src/repos/modules/repos.js b/scm-ui/src/repos/modules/repos.js index b23cb07a15..c96719efcb 100644 --- a/scm-ui/src/repos/modules/repos.js +++ b/scm-ui/src/repos/modules/repos.js @@ -2,9 +2,9 @@ import { apiClient } from "../../apiclient"; import * as types from "../../modules/types"; import type { Action } from "../../types/Action"; -import type {Repository, RepositoryCollection} from "../types/Repositories"; -import {isPending} from "../../modules/pending"; -import {getFailure} from "../../modules/failure"; +import type { Repository, RepositoryCollection } from "../types/Repositories"; +import { isPending } from "../../modules/pending"; +import { getFailure } from "../../modules/failure"; export const FETCH_REPOS = "scm/repos/FETCH_REPOS"; export const FETCH_REPOS_PENDING = `${FETCH_REPOS}_${types.PENDING_SUFFIX}`; @@ -16,8 +16,16 @@ export const FETCH_REPO_PENDING = `${FETCH_REPO}_${types.PENDING_SUFFIX}`; export const FETCH_REPO_SUCCESS = `${FETCH_REPO}_${types.SUCCESS_SUFFIX}`; export const FETCH_REPO_FAILURE = `${FETCH_REPO}_${types.FAILURE_SUFFIX}`; +export const CREATE_REPO = "scm/repos/FETCH_REPO"; +export const CREATE_REPO_PENDING = `${CREATE_REPO}_${types.PENDING_SUFFIX}`; +export const CREATE_REPO_SUCCESS = `${CREATE_REPO}_${types.SUCCESS_SUFFIX}`; +export const CREATE_REPO_FAILURE = `${CREATE_REPO}_${types.FAILURE_SUFFIX}`; +export const CREATE_REPO_RESET = `${CREATE_REPO}_${types.RESET_SUFFIX}`; + const REPOS_URL = "repositories"; +const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2"; + // fetch repos const SORT_BY = "sortBy=namespaceAndName"; @@ -84,15 +92,16 @@ export function fetchReposFailure(err: Error): Action { export function fetchRepo(namespace: string, name: string) { return function(dispatch: any) { dispatch(fetchRepoPending(namespace, name)); - return apiClient.get(`${REPOS_URL}/${namespace}/${name}`) + return apiClient + .get(`${REPOS_URL}/${namespace}/${name}`) .then(response => response.json()) - .then( repository => { - dispatch(fetchRepoSuccess(repository)) - } ) + .then(repository => { + dispatch(fetchRepoSuccess(repository)); + }) .catch(err => { - dispatch(fetchRepoFailure(namespace, name, err)) + dispatch(fetchRepoFailure(namespace, name, err)); }); - } + }; } export function fetchRepoPending(namespace: string, name: string): Action { @@ -114,7 +123,11 @@ export function fetchRepoSuccess(repository: Repository): Action { }; } -export function fetchRepoFailure(namespace: string, name: string, error: Error): Action { +export function fetchRepoFailure( + namespace: string, + name: string, + error: Error +): Action { return { type: FETCH_REPO_FAILURE, payload: { @@ -126,6 +139,50 @@ export function fetchRepoFailure(namespace: string, name: string, error: Error): }; } +// create repo + +export function createRepo(repository: Repository, callback?: () => void) { + return function(dispatch: any) { + dispatch(createRepoPending()); + return apiClient + .post(REPOS_URL, repository, CONTENT_TYPE) + .then(() => { + dispatch(createRepoSuccess()); + if (callback) { + callback(); + } + }) + .catch(err => { + dispatch(createRepoFailure(err)); + }); + }; +} + +export function createRepoPending(): Action { + return { + type: CREATE_REPO_PENDING + }; +} + +export function createRepoSuccess(): Action { + return { + type: CREATE_REPO_SUCCESS + }; +} + +export function createRepoFailure(err: Error): Action { + return { + type: CREATE_REPO_FAILURE, + payload: err + }; +} + +export function createRepoReset(): Action { + return { + type: CREATE_REPO_RESET + }; +} + // reducer function createIdentifier(repository: Repository) { @@ -180,7 +237,7 @@ export default function reducer( case FETCH_REPO_SUCCESS: return reducerByNames(state, action.payload); default: - return state; + return state; } } @@ -211,14 +268,42 @@ export function getFetchReposFailure(state: Object) { export function getRepository(state: Object, namespace: string, name: string) { if (state.repos && state.repos.byNames) { - return state.repos.byNames[ namespace + "/" + name]; + return state.repos.byNames[namespace + "/" + name]; } } -export function isFetchRepoPending(state: Object, namespace: string, name: string) { +export function isFetchRepoPending( + state: Object, + namespace: string, + name: string +) { return isPending(state, FETCH_REPO, namespace + "/" + name); } -export function getFetchRepoFailure(state: Object, namespace: string, name: string) { +export function getFetchRepoFailure( + state: Object, + namespace: string, + name: string +) { return getFailure(state, FETCH_REPO, namespace + "/" + name); } + +export function isAbleToCreateRepos(state: Object) { + if ( + state.repos && + state.repos.list && + state.repos.list._links && + state.repos.list._links.create + ) { + return true; + } + return false; +} + +export function isCreateRepoPending(state: Object) { + return isPending(state, CREATE_REPO); +} + +export function getCreateRepoFailure(state: Object) { + return getFailure(state, CREATE_REPO); +} diff --git a/scm-ui/src/repos/modules/repos.test.js b/scm-ui/src/repos/modules/repos.test.js index e214e89520..c35d95c9a4 100644 --- a/scm-ui/src/repos/modules/repos.test.js +++ b/scm-ui/src/repos/modules/repos.test.js @@ -22,7 +22,15 @@ import reducer, { fetchRepoSuccess, getRepository, isFetchRepoPending, - getFetchRepoFailure + getFetchRepoFailure, + CREATE_REPO_PENDING, + CREATE_REPO_SUCCESS, + createRepo, + CREATE_REPO_FAILURE, + isCreateRepoPending, + CREATE_REPO, + getCreateRepoFailure, + isAbleToCreateRepos } from "./repos"; import type { Repository, RepositoryCollection } from "../types/Repositories"; @@ -354,6 +362,56 @@ describe("repos fetch", () => { expect(actions[1].itemId).toBe("slarti/fjords"); }); }); + + it("should successfully create repo slarti/fjords", () => { + fetchMock.postOnce(REPOS_URL, slartiFjords); + + const expectedActions = [ + { + type: CREATE_REPO_PENDING + }, + { + type: CREATE_REPO_SUCCESS + } + ]; + + const store = mockStore({}); + return store.dispatch(createRepo(slartiFjords)).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + it("should successfully create repo slarti/fjords and call the callback", () => { + // unmatched + fetchMock.postOnce(REPOS_URL, { + status: 201 + }); + + let callMe = "not yet"; + + const callback = () => { + callMe = "yeah"; + }; + + const store = mockStore({}); + return store.dispatch(createRepo(slartiFjords, callback)).then(() => { + expect(callMe).toBe("yeah"); + }); + }); + + it("should disapatch failure if server returns status code 500", () => { + fetchMock.postOnce(REPOS_URL, { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(createRepo(slartiFjords)).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(CREATE_REPO_PENDING); + expect(actions[1].type).toEqual(CREATE_REPO_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); }); describe("repos reducer", () => { @@ -471,4 +529,58 @@ describe("repos selectors", () => { }; expect(getFetchRepoFailure(state, "slarti", "fjords")).toEqual(error); }); + + it("should return undefined when fetch repo did not fail", () => { + expect(getFetchRepoFailure({}, "slarti", "fjords")).toBe(undefined); + }); + + // create + + it("should return true, when create repo is pending", () => { + const state = { + pending: { + [CREATE_REPO]: true + } + }; + expect(isCreateRepoPending(state)).toEqual(true); + }); + + it("should return false, when create repo is not pending", () => { + expect(isCreateRepoPending({})).toEqual(false); + }); + + it("should return error when create repo did fail", () => { + const state = { + failure: { + [CREATE_REPO]: error + } + }; + expect(getCreateRepoFailure(state)).toEqual(error); + }); + + it("should return undefined when create repo did not fail", () => { + expect(getCreateRepoFailure({})).toBe(undefined); + }); + + it("should return true if the list contains the create link", () => { + const state = { + repos: { + list: repositoryCollection + } + }; + + expect(isAbleToCreateRepos(state)).toBe(true); + }); + + it("should return false, if create link is unavailable", () => { + const state = { + repos: { + list: { + _links: {} + } + } + }; + + expect(isAbleToCreateRepos(state)).toBe(false); + }); });