mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-16 10:16:16 +01:00
merge 2.0.0-m3
This commit is contained in:
@@ -14,18 +14,20 @@ import {
|
||||
modifyConfigReset
|
||||
} from "../modules/config";
|
||||
import { connect } from "react-redux";
|
||||
import type { Config } from "@scm-manager/ui-types";
|
||||
import type { Config, Link } from "@scm-manager/ui-types";
|
||||
import ConfigForm from "../components/form/ConfigForm";
|
||||
import { getConfigLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
config: Config,
|
||||
configUpdatePermission: boolean,
|
||||
configLink: string,
|
||||
|
||||
// dispatch functions
|
||||
modifyConfig: (config: Config, callback?: () => void) => void,
|
||||
fetchConfig: void => void,
|
||||
fetchConfig: (link: string) => void,
|
||||
configReset: void => void,
|
||||
|
||||
// context objects
|
||||
@@ -35,7 +37,7 @@ type Props = {
|
||||
class GlobalConfig extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.configReset();
|
||||
this.props.fetchConfig();
|
||||
this.props.fetchConfig(this.props.configLink);
|
||||
}
|
||||
|
||||
modifyConfig = (config: Config) => {
|
||||
@@ -75,8 +77,8 @@ class GlobalConfig extends React.Component<Props> {
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchConfig: () => {
|
||||
dispatch(fetchConfig());
|
||||
fetchConfig: (link: string) => {
|
||||
dispatch(fetchConfig(link));
|
||||
},
|
||||
modifyConfig: (config: Config, callback?: () => void) => {
|
||||
dispatch(modifyConfig(config, callback));
|
||||
@@ -92,12 +94,14 @@ const mapStateToProps = state => {
|
||||
const error = getFetchConfigFailure(state) || getModifyConfigFailure(state);
|
||||
const config = getConfig(state);
|
||||
const configUpdatePermission = getConfigUpdatePermission(state);
|
||||
const configLink = getConfigLink(state);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
config,
|
||||
configUpdatePermission
|
||||
configUpdatePermission,
|
||||
configLink
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -18,15 +18,14 @@ export const MODIFY_CONFIG_SUCCESS = `${MODIFY_CONFIG}_${types.SUCCESS_SUFFIX}`;
|
||||
export const MODIFY_CONFIG_FAILURE = `${MODIFY_CONFIG}_${types.FAILURE_SUFFIX}`;
|
||||
export const MODIFY_CONFIG_RESET = `${MODIFY_CONFIG}_${types.RESET_SUFFIX}`;
|
||||
|
||||
const CONFIG_URL = "config";
|
||||
const CONTENT_TYPE_CONFIG = "application/vnd.scmm-config+json;v=2";
|
||||
|
||||
//fetch config
|
||||
export function fetchConfig() {
|
||||
export function fetchConfig(link: string) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchConfigPending());
|
||||
return apiClient
|
||||
.get(CONFIG_URL)
|
||||
.get(link)
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
|
||||
@@ -22,8 +22,10 @@ import reducer, {
|
||||
getConfig,
|
||||
getConfigUpdatePermission
|
||||
} from "./config";
|
||||
import { getConfigLink } from "../../modules/indexResource";
|
||||
|
||||
const CONFIG_URL = "/api/v2/config";
|
||||
const CONFIG_URL = "/config";
|
||||
const URL = "/api/v2" + CONFIG_URL;
|
||||
|
||||
const error = new Error("You have an error!");
|
||||
|
||||
@@ -103,7 +105,7 @@ describe("config fetch()", () => {
|
||||
});
|
||||
|
||||
it("should successfully fetch config", () => {
|
||||
fetchMock.getOnce(CONFIG_URL, response);
|
||||
fetchMock.getOnce(URL, response);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_CONFIG_PENDING },
|
||||
@@ -113,20 +115,36 @@ describe("config fetch()", () => {
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
const store = mockStore({
|
||||
indexResources: {
|
||||
links: {
|
||||
config: {
|
||||
href: CONFIG_URL
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return store.dispatch(fetchConfig()).then(() => {
|
||||
return store.dispatch(fetchConfig(CONFIG_URL)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail getting config on HTTP 500", () => {
|
||||
fetchMock.getOnce(CONFIG_URL, {
|
||||
fetchMock.getOnce(URL, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchConfig()).then(() => {
|
||||
const store = mockStore({
|
||||
indexResources: {
|
||||
links: {
|
||||
config: {
|
||||
href: CONFIG_URL
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return store.dispatch(fetchConfig(CONFIG_URL)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_CONFIG_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_CONFIG_FAILURE);
|
||||
|
||||
@@ -19,16 +19,33 @@ import {
|
||||
Footer,
|
||||
Header
|
||||
} from "@scm-manager/ui-components";
|
||||
import type { Me } from "@scm-manager/ui-types";
|
||||
import type { Me, Link } from "@scm-manager/ui-types";
|
||||
import {
|
||||
fetchIndexResources,
|
||||
getConfigLink,
|
||||
getFetchIndexResourcesFailure,
|
||||
getGroupsLink,
|
||||
getLogoutLink,
|
||||
getMeLink,
|
||||
getRepositoriesLink,
|
||||
getUsersLink,
|
||||
isFetchIndexResourcesPending
|
||||
} from "../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
me: Me,
|
||||
authenticated: boolean,
|
||||
error: Error,
|
||||
loading: boolean,
|
||||
repositoriesLink: string,
|
||||
usersLink: string,
|
||||
groupsLink: string,
|
||||
configLink: string,
|
||||
logoutLink: string,
|
||||
meLink: string,
|
||||
|
||||
// dispatcher functions
|
||||
fetchMe: () => void,
|
||||
fetchMe: (link: string) => void,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
@@ -36,14 +53,37 @@ type Props = {
|
||||
|
||||
class App extends Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchMe();
|
||||
if (this.props.meLink) {
|
||||
this.props.fetchMe(this.props.meLink);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { me, loading, error, authenticated, t } = this.props;
|
||||
const {
|
||||
me,
|
||||
loading,
|
||||
error,
|
||||
authenticated,
|
||||
t,
|
||||
repositoriesLink,
|
||||
usersLink,
|
||||
groupsLink,
|
||||
configLink,
|
||||
logoutLink
|
||||
} = this.props;
|
||||
|
||||
let content;
|
||||
const navigation = authenticated ? <PrimaryNavigation /> : "";
|
||||
const navigation = authenticated ? (
|
||||
<PrimaryNavigation
|
||||
repositoriesLink={repositoriesLink}
|
||||
usersLink={usersLink}
|
||||
groupsLink={groupsLink}
|
||||
configLink={configLink}
|
||||
logoutLink={logoutLink}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
content = <Loading />;
|
||||
@@ -70,20 +110,34 @@ class App extends Component<Props> {
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
fetchMe: () => dispatch(fetchMe())
|
||||
fetchMe: (link: string) => dispatch(fetchMe(link))
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const authenticated = isAuthenticated(state);
|
||||
const me = getMe(state);
|
||||
const loading = isFetchMePending(state);
|
||||
const error = getFetchMeFailure(state);
|
||||
const loading =
|
||||
isFetchMePending(state) || isFetchIndexResourcesPending(state);
|
||||
const error =
|
||||
getFetchMeFailure(state) || getFetchIndexResourcesFailure(state);
|
||||
const repositoriesLink = getRepositoriesLink(state);
|
||||
const usersLink = getUsersLink(state);
|
||||
const groupsLink = getGroupsLink(state);
|
||||
const configLink = getConfigLink(state);
|
||||
const logoutLink = getLogoutLink(state);
|
||||
const meLink = getMeLink(state);
|
||||
return {
|
||||
authenticated,
|
||||
me,
|
||||
loading,
|
||||
error
|
||||
error,
|
||||
repositoriesLink,
|
||||
usersLink,
|
||||
groupsLink,
|
||||
configLink,
|
||||
logoutLink,
|
||||
meLink
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
80
scm-ui/src/containers/Index.js
Normal file
80
scm-ui/src/containers/Index.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// @flow
|
||||
import React, { Component } from "react";
|
||||
import App from "./App";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||
import {
|
||||
fetchIndexResources,
|
||||
getFetchIndexResourcesFailure,
|
||||
getLinks,
|
||||
isFetchIndexResourcesPending
|
||||
} from "../modules/indexResource";
|
||||
import PluginLoader from "./PluginLoader";
|
||||
import type { IndexResources } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
error: Error,
|
||||
loading: boolean,
|
||||
indexResources: IndexResources,
|
||||
|
||||
// dispatcher functions
|
||||
fetchIndexResources: () => void,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class Index extends Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchIndexResources();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { indexResources, loading, error, t } = this.props;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPage
|
||||
title={t("app.error.title")}
|
||||
subtitle={t("app.error.subtitle")}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
} else if (loading || !indexResources) {
|
||||
return <Loading />;
|
||||
} else {
|
||||
return (
|
||||
<PluginLoader>
|
||||
<App />
|
||||
</PluginLoader>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => {
|
||||
return {
|
||||
fetchIndexResources: () => dispatch(fetchIndexResources())
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const loading = isFetchIndexResourcesPending(state);
|
||||
const error = getFetchIndexResourcesFailure(state);
|
||||
const indexResources = getLinks(state);
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
indexResources
|
||||
};
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("commons")(Index))
|
||||
);
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Image
|
||||
} from "@scm-manager/ui-components";
|
||||
import classNames from "classnames";
|
||||
import { getLoginLink } from "../modules/indexResource";
|
||||
|
||||
const styles = {
|
||||
avatar: {
|
||||
@@ -41,9 +42,10 @@ type Props = {
|
||||
authenticated: boolean,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
link: string,
|
||||
|
||||
// dispatcher props
|
||||
login: (username: string, password: string) => void,
|
||||
login: (link: string, username: string, password: string) => void,
|
||||
|
||||
// context props
|
||||
t: string => string,
|
||||
@@ -74,7 +76,11 @@ class Login extends React.Component<Props, State> {
|
||||
handleSubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
if (this.isValid()) {
|
||||
this.props.login(this.state.username, this.state.password);
|
||||
this.props.login(
|
||||
this.props.link,
|
||||
this.state.username,
|
||||
this.state.password
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -145,17 +151,19 @@ const mapStateToProps = state => {
|
||||
const authenticated = isAuthenticated(state);
|
||||
const loading = isLoginPending(state);
|
||||
const error = getLoginFailure(state);
|
||||
const link = getLoginLink(state);
|
||||
return {
|
||||
authenticated,
|
||||
loading,
|
||||
error
|
||||
error,
|
||||
link
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
login: (username: string, password: string) =>
|
||||
dispatch(login(username, password))
|
||||
login: (loginLink: string, username: string, password: string) =>
|
||||
dispatch(login(loginLink, username, password))
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -11,14 +11,16 @@ import {
|
||||
getLogoutFailure
|
||||
} from "../modules/auth";
|
||||
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||
import { fetchIndexResources, getLogoutLink } from "../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
authenticated: boolean,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
logoutLink: string,
|
||||
|
||||
// dispatcher functions
|
||||
logout: () => void,
|
||||
logout: (link: string) => void,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
@@ -26,7 +28,7 @@ type Props = {
|
||||
|
||||
class Logout extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.logout();
|
||||
this.props.logout(this.props.logoutLink);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -51,16 +53,18 @@ const mapStateToProps = state => {
|
||||
const authenticated = isAuthenticated(state);
|
||||
const loading = isLogoutPending(state);
|
||||
const error = getLogoutFailure(state);
|
||||
const logoutLink = getLogoutLink(state);
|
||||
return {
|
||||
authenticated,
|
||||
loading,
|
||||
error
|
||||
error,
|
||||
logoutLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
logout: () => dispatch(logout())
|
||||
logout: (link: string) => dispatch(logout(link))
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
|
||||
import { Route, Redirect, withRouter } from "react-router";
|
||||
import { Redirect, Route, Switch, withRouter } from "react-router-dom";
|
||||
|
||||
import Overview from "../repos/containers/Overview";
|
||||
import Users from "../users/containers/Users";
|
||||
import Login from "../containers/Login";
|
||||
import Logout from "../containers/Logout";
|
||||
|
||||
import { Switch } from "react-router-dom";
|
||||
import { ProtectedRoute } from "@scm-manager/ui-components";
|
||||
import AddUser from "../users/containers/AddUser";
|
||||
import SingleUser from "../users/containers/SingleUser";
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { apiClient, Loading } from "@scm-manager/ui-components";
|
||||
import { getUiPluginsLink } from "../modules/indexResource";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
type Props = {
|
||||
children: React.Node
|
||||
children: React.Node,
|
||||
link: string
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -29,8 +32,13 @@ class PluginLoader extends React.Component<Props, State> {
|
||||
this.setState({
|
||||
message: "loading plugin information"
|
||||
});
|
||||
apiClient
|
||||
.get("ui/plugins")
|
||||
|
||||
this.getPlugins(this.props.link);
|
||||
}
|
||||
|
||||
getPlugins = (link: string): Promise<any> => {
|
||||
return apiClient
|
||||
.get(link)
|
||||
.then(response => response.text())
|
||||
.then(JSON.parse)
|
||||
.then(pluginCollection => pluginCollection._embedded.plugins)
|
||||
@@ -40,7 +48,7 @@ class PluginLoader extends React.Component<Props, State> {
|
||||
finished: true
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadPlugins = (plugins: Plugin[]) => {
|
||||
this.setState({
|
||||
@@ -87,4 +95,11 @@ class PluginLoader extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginLoader;
|
||||
const mapStateToProps = state => {
|
||||
const link = getUiPluginsLink(state);
|
||||
return {
|
||||
link
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(PluginLoader);
|
||||
|
||||
@@ -7,14 +7,18 @@ import { routerReducer, routerMiddleware } from "react-router-redux";
|
||||
import users from "./users/modules/users";
|
||||
import repos from "./repos/modules/repos";
|
||||
import repositoryTypes from "./repos/modules/repositoryTypes";
|
||||
import changesets from "./repos/modules/changesets";
|
||||
import sources from "./repos/sources/modules/sources";
|
||||
import groups from "./groups/modules/groups";
|
||||
import auth from "./modules/auth";
|
||||
import pending from "./modules/pending";
|
||||
import failure from "./modules/failure";
|
||||
import permissions from "./repos/permissions/modules/permissions";
|
||||
import config from "./config/modules/config";
|
||||
import indexResources from "./modules/indexResource";
|
||||
|
||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||
import branches from "./repos/modules/branches";
|
||||
|
||||
function createReduxStore(history: BrowserHistory) {
|
||||
const composeEnhancers =
|
||||
@@ -24,13 +28,17 @@ function createReduxStore(history: BrowserHistory) {
|
||||
router: routerReducer,
|
||||
pending,
|
||||
failure,
|
||||
indexResources,
|
||||
users,
|
||||
repos,
|
||||
repositoryTypes,
|
||||
changesets,
|
||||
branches,
|
||||
permissions,
|
||||
groups,
|
||||
auth,
|
||||
config
|
||||
config,
|
||||
sources
|
||||
});
|
||||
|
||||
return createStore(
|
||||
|
||||
@@ -9,18 +9,21 @@ import {
|
||||
createGroup,
|
||||
isCreateGroupPending,
|
||||
getCreateGroupFailure,
|
||||
createGroupReset
|
||||
createGroupReset,
|
||||
getCreateGroupLink
|
||||
} from "../modules/groups";
|
||||
import type { Group } from "@scm-manager/ui-types";
|
||||
import type { History } from "history";
|
||||
import { getGroupsLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
createGroup: (group: Group, callback?: () => void) => void,
|
||||
createGroup: (link: string, group: Group, callback?: () => void) => void,
|
||||
history: History,
|
||||
loading?: boolean,
|
||||
error?: Error,
|
||||
resetForm: () => void
|
||||
resetForm: () => void,
|
||||
createLink: string
|
||||
};
|
||||
|
||||
type State = {};
|
||||
@@ -51,14 +54,14 @@ class AddGroup extends React.Component<Props, State> {
|
||||
this.props.history.push("/groups");
|
||||
};
|
||||
createGroup = (group: Group) => {
|
||||
this.props.createGroup(group, this.groupCreated);
|
||||
this.props.createGroup(this.props.createLink, group, this.groupCreated);
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
createGroup: (group: Group, callback?: () => void) =>
|
||||
dispatch(createGroup(group, callback)),
|
||||
createGroup: (link: string, group: Group, callback?: () => void) =>
|
||||
dispatch(createGroup(link, group, callback)),
|
||||
resetForm: () => {
|
||||
dispatch(createGroupReset());
|
||||
}
|
||||
@@ -68,7 +71,9 @@ const mapDispatchToProps = dispatch => {
|
||||
const mapStateToProps = state => {
|
||||
const loading = isCreateGroupPending(state);
|
||||
const error = getCreateGroupFailure(state);
|
||||
const createLink = getGroupsLink(state);
|
||||
return {
|
||||
createLink,
|
||||
loading,
|
||||
error
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
isPermittedToCreateGroups,
|
||||
selectListAsCollection
|
||||
} from "../modules/groups";
|
||||
import { getGroupsLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
groups: Group[],
|
||||
@@ -26,19 +27,20 @@ type Props = {
|
||||
canAddGroups: boolean,
|
||||
list: PagedCollection,
|
||||
page: number,
|
||||
groupLink: string,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
history: History,
|
||||
|
||||
// dispatch functions
|
||||
fetchGroupsByPage: (page: number) => void,
|
||||
fetchGroupsByPage: (link: string, page: number) => void,
|
||||
fetchGroupsByLink: (link: string) => void
|
||||
};
|
||||
|
||||
class Groups extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchGroupsByPage(this.props.page);
|
||||
this.props.fetchGroupsByPage(this.props.groupLink, this.props.page);
|
||||
}
|
||||
|
||||
onPageChange = (link: string) => {
|
||||
@@ -111,20 +113,23 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const canAddGroups = isPermittedToCreateGroups(state);
|
||||
const list = selectListAsCollection(state);
|
||||
|
||||
const groupLink = getGroupsLink(state);
|
||||
|
||||
return {
|
||||
groups,
|
||||
loading,
|
||||
error,
|
||||
canAddGroups,
|
||||
list,
|
||||
page
|
||||
page,
|
||||
groupLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchGroupsByPage: (page: number) => {
|
||||
dispatch(fetchGroupsByPage(page));
|
||||
fetchGroupsByPage: (link: string, page: number) => {
|
||||
dispatch(fetchGroupsByPage(link, page));
|
||||
},
|
||||
fetchGroupsByLink: (link: string) => {
|
||||
dispatch(fetchGroupsByLink(link));
|
||||
|
||||
@@ -26,16 +26,18 @@ import {
|
||||
|
||||
import { translate } from "react-i18next";
|
||||
import EditGroup from "./EditGroup";
|
||||
import { getGroupsLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
group: Group,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
groupLink: string,
|
||||
|
||||
// dispatcher functions
|
||||
deleteGroup: (group: Group, callback?: () => void) => void,
|
||||
fetchGroup: string => void,
|
||||
fetchGroup: (string, string) => void,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
@@ -45,7 +47,7 @@ type Props = {
|
||||
|
||||
class SingleGroup extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchGroup(this.props.name);
|
||||
this.props.fetchGroup(this.props.groupLink, this.props.name);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
@@ -132,19 +134,21 @@ const mapStateToProps = (state, ownProps) => {
|
||||
isFetchGroupPending(state, name) || isDeleteGroupPending(state, name);
|
||||
const error =
|
||||
getFetchGroupFailure(state, name) || getDeleteGroupFailure(state, name);
|
||||
const groupLink = getGroupsLink(state);
|
||||
|
||||
return {
|
||||
name,
|
||||
group,
|
||||
loading,
|
||||
error
|
||||
error,
|
||||
groupLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchGroup: (name: string) => {
|
||||
dispatch(fetchGroup(name));
|
||||
fetchGroup: (link: string, name: string) => {
|
||||
dispatch(fetchGroup(link, name));
|
||||
},
|
||||
deleteGroup: (group: Group, callback?: () => void) => {
|
||||
dispatch(deleteGroup(group, callback));
|
||||
|
||||
@@ -32,17 +32,16 @@ export const DELETE_GROUP_PENDING = `${DELETE_GROUP}_${types.PENDING_SUFFIX}`;
|
||||
export const DELETE_GROUP_SUCCESS = `${DELETE_GROUP}_${types.SUCCESS_SUFFIX}`;
|
||||
export const DELETE_GROUP_FAILURE = `${DELETE_GROUP}_${types.FAILURE_SUFFIX}`;
|
||||
|
||||
const GROUPS_URL = "groups";
|
||||
const CONTENT_TYPE_GROUP = "application/vnd.scmm-group+json;v=2";
|
||||
|
||||
// fetch groups
|
||||
export function fetchGroups() {
|
||||
return fetchGroupsByLink(GROUPS_URL);
|
||||
export function fetchGroups(link: string) {
|
||||
return fetchGroupsByLink(link);
|
||||
}
|
||||
|
||||
export function fetchGroupsByPage(page: number) {
|
||||
export function fetchGroupsByPage(link: string, page: number) {
|
||||
// backend start counting by 0
|
||||
return fetchGroupsByLink(GROUPS_URL + "?page=" + (page - 1));
|
||||
return fetchGroupsByLink(link + "?page=" + (page - 1));
|
||||
}
|
||||
|
||||
export function fetchGroupsByLink(link: string) {
|
||||
@@ -56,7 +55,7 @@ export function fetchGroupsByLink(link: string) {
|
||||
})
|
||||
.catch(cause => {
|
||||
const error = new Error(`could not fetch groups: ${cause.message}`);
|
||||
dispatch(fetchGroupsFailure(GROUPS_URL, error));
|
||||
dispatch(fetchGroupsFailure(link, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -85,8 +84,8 @@ export function fetchGroupsFailure(url: string, error: Error): Action {
|
||||
}
|
||||
|
||||
//fetch group
|
||||
export function fetchGroup(name: string) {
|
||||
const groupUrl = GROUPS_URL + "/" + name;
|
||||
export function fetchGroup(link: string, name: string) {
|
||||
const groupUrl = link.endsWith("/") ? link + name : link + "/" + name;
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchGroupPending(name));
|
||||
return apiClient
|
||||
@@ -132,11 +131,11 @@ export function fetchGroupFailure(name: string, error: Error): Action {
|
||||
}
|
||||
|
||||
//create group
|
||||
export function createGroup(group: Group, callback?: () => void) {
|
||||
export function createGroup(link: string, group: Group, callback?: () => void) {
|
||||
return function(dispatch: Dispatch) {
|
||||
dispatch(createGroupPending());
|
||||
return apiClient
|
||||
.post(GROUPS_URL, group, CONTENT_TYPE_GROUP)
|
||||
.post(link, group, CONTENT_TYPE_GROUP)
|
||||
.then(() => {
|
||||
dispatch(createGroupSuccess());
|
||||
if (callback) {
|
||||
@@ -410,6 +409,12 @@ export const isPermittedToCreateGroups = (state: Object): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
export function getCreateGroupLink(state: Object) {
|
||||
if (state.groups.list.entry && state.groups.list.entry._links)
|
||||
return state.groups.list.entry._links.create.href;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getGroupsFromState(state: Object) {
|
||||
const groupNames = selectList(state).entries;
|
||||
if (!groupNames) {
|
||||
|
||||
@@ -42,9 +42,11 @@ import reducer, {
|
||||
modifyGroup,
|
||||
MODIFY_GROUP_PENDING,
|
||||
MODIFY_GROUP_SUCCESS,
|
||||
MODIFY_GROUP_FAILURE
|
||||
MODIFY_GROUP_FAILURE,
|
||||
getCreateGroupLink
|
||||
} from "./groups";
|
||||
const GROUPS_URL = "/api/v2/groups";
|
||||
const URL = "/groups";
|
||||
|
||||
const error = new Error("You have an error!");
|
||||
|
||||
@@ -63,7 +65,7 @@ const humanGroup = {
|
||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||
},
|
||||
update: {
|
||||
href:"http://localhost:8081/api/v2/groups/humanGroup"
|
||||
href: "http://localhost:8081/api/v2/groups/humanGroup"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -95,7 +97,7 @@ const emptyGroup = {
|
||||
href: "http://localhost:8081/api/v2/groups/emptyGroup"
|
||||
},
|
||||
update: {
|
||||
href:"http://localhost:8081/api/v2/groups/emptyGroup"
|
||||
href: "http://localhost:8081/api/v2/groups/emptyGroup"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
@@ -150,7 +152,7 @@ describe("groups fetch()", () => {
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(fetchGroups()).then(() => {
|
||||
return store.dispatch(fetchGroups(URL)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -161,7 +163,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchGroups()).then(() => {
|
||||
return store.dispatch(fetchGroups(URL)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_GROUPS_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_GROUPS_FAILURE);
|
||||
@@ -173,7 +175,7 @@ describe("groups fetch()", () => {
|
||||
fetchMock.getOnce(GROUPS_URL + "/humanGroup", humanGroup);
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchGroup("humanGroup")).then(() => {
|
||||
return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_GROUP_SUCCESS);
|
||||
@@ -187,7 +189,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchGroup("humanGroup")).then(() => {
|
||||
return store.dispatch(fetchGroup(URL, "humanGroup")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_GROUP_FAILURE);
|
||||
@@ -195,14 +197,13 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should successfully create group", () => {
|
||||
fetchMock.postOnce(GROUPS_URL, {
|
||||
status: 201
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createGroup(humanGroup)).then(() => {
|
||||
return store.dispatch(createGroup(URL, humanGroup)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS);
|
||||
@@ -219,14 +220,13 @@ describe("groups fetch()", () => {
|
||||
called = true;
|
||||
};
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createGroup(humanGroup, callMe)).then(() => {
|
||||
return store.dispatch(createGroup(URL, humanGroup, callMe)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_GROUP_SUCCESS);
|
||||
expect(called).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should fail creating group on HTTP 500", () => {
|
||||
fetchMock.postOnce(GROUPS_URL, {
|
||||
@@ -234,7 +234,7 @@ describe("groups fetch()", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createGroup(humanGroup)).then(() => {
|
||||
return store.dispatch(createGroup(URL, humanGroup)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_GROUP_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_GROUP_FAILURE);
|
||||
@@ -248,7 +248,7 @@ describe("groups fetch()", () => {
|
||||
status: 204
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(modifyGroup(humanGroup)).then(() => {
|
||||
const actions = store.getActions();
|
||||
@@ -267,7 +267,7 @@ describe("groups fetch()", () => {
|
||||
const callback = () => {
|
||||
called = true;
|
||||
};
|
||||
const store = mockStore({});
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(modifyGroup(humanGroup, callback)).then(() => {
|
||||
const actions = store.getActions();
|
||||
@@ -282,7 +282,7 @@ describe("groups fetch()", () => {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(modifyGroup(humanGroup)).then(() => {
|
||||
const actions = store.getActions();
|
||||
@@ -337,13 +337,10 @@ describe("groups fetch()", () => {
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("groups reducer", () => {
|
||||
|
||||
it("should update state correctly according to FETCH_GROUPS_SUCCESS action", () => {
|
||||
|
||||
const newState = reducer({}, fetchGroupsSuccess(responseBody));
|
||||
|
||||
expect(newState.list).toEqual({
|
||||
@@ -391,7 +388,6 @@ describe("groups reducer", () => {
|
||||
expect(newState.byNames["humanGroup"]).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it("should update state according to FETCH_GROUP_SUCCESS action", () => {
|
||||
const newState = reducer({}, fetchGroupSuccess(emptyGroup));
|
||||
expect(newState.byNames["emptyGroup"]).toBe(emptyGroup);
|
||||
@@ -426,7 +422,6 @@ describe("groups reducer", () => {
|
||||
expect(newState.byNames["emptyGroup"]).toBeFalsy();
|
||||
expect(newState.list.entries).toEqual(["humanGroup"]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("selector tests", () => {
|
||||
@@ -476,6 +471,23 @@ describe("selector tests", () => {
|
||||
expect(isPermittedToCreateGroups(state)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return create Group link", () => {
|
||||
const state = {
|
||||
groups: {
|
||||
list: {
|
||||
entry: {
|
||||
_links: {
|
||||
create: {
|
||||
href: "/create"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
expect(getCreateGroupLink(state)).toBe("/create");
|
||||
});
|
||||
|
||||
it("should get groups from state", () => {
|
||||
const state = {
|
||||
groups: {
|
||||
@@ -488,7 +500,7 @@ describe("selector tests", () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
expect(getGroupsFromState(state)).toEqual([{ name: "a" }, { name: "b" }]);
|
||||
});
|
||||
|
||||
@@ -560,9 +572,13 @@ describe("selector tests", () => {
|
||||
});
|
||||
|
||||
it("should return true if create group is pending", () => {
|
||||
expect(isCreateGroupPending({pending: {
|
||||
[CREATE_GROUP]: true
|
||||
}})).toBeTruthy();
|
||||
expect(
|
||||
isCreateGroupPending({
|
||||
pending: {
|
||||
[CREATE_GROUP]: true
|
||||
}
|
||||
})
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false if create group is not pending", () => {
|
||||
@@ -570,18 +586,19 @@ describe("selector tests", () => {
|
||||
});
|
||||
|
||||
it("should return error if creating group failed", () => {
|
||||
expect(getCreateGroupFailure({
|
||||
failure: {
|
||||
[CREATE_GROUP]: error
|
||||
}
|
||||
})).toEqual(error);
|
||||
expect(
|
||||
getCreateGroupFailure({
|
||||
failure: {
|
||||
[CREATE_GROUP]: error
|
||||
}
|
||||
})
|
||||
).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined if creating group did not fail", () => {
|
||||
expect(getCreateGroupFailure({})).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
it("should return true, when delete group humanGroup is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./containers/App";
|
||||
import Index from "./containers/Index";
|
||||
import registerServiceWorker from "./registerServiceWorker";
|
||||
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
@@ -37,9 +37,7 @@ ReactDOM.render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
{/* ConnectedRouter will use the store from Provider automatically */}
|
||||
<ConnectedRouter history={history}>
|
||||
<PluginLoader>
|
||||
<App />
|
||||
</PluginLoader>
|
||||
<Index />
|
||||
</ConnectedRouter>
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
|
||||
@@ -5,6 +5,12 @@ import * as types from "./types";
|
||||
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
|
||||
import { isPending } from "./pending";
|
||||
import { getFailure } from "./failure";
|
||||
import {
|
||||
callFetchIndexResources,
|
||||
FETCH_INDEXRESOURCES_SUCCESS,
|
||||
fetchIndexResources, fetchIndexResourcesPending,
|
||||
fetchIndexResourcesSuccess
|
||||
} from "./indexResource";
|
||||
|
||||
// Action
|
||||
|
||||
@@ -121,16 +127,11 @@ export const fetchMeFailure = (error: Error) => {
|
||||
};
|
||||
};
|
||||
|
||||
// urls
|
||||
|
||||
const ME_URL = "/me";
|
||||
const LOGIN_URL = "/auth/access_token";
|
||||
|
||||
// side effects
|
||||
|
||||
const callFetchMe = (): Promise<Me> => {
|
||||
const callFetchMe = (link: string): Promise<Me> => {
|
||||
return apiClient
|
||||
.get(ME_URL)
|
||||
.get(link)
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
@@ -139,7 +140,11 @@ const callFetchMe = (): Promise<Me> => {
|
||||
});
|
||||
};
|
||||
|
||||
export const login = (username: string, password: string) => {
|
||||
export const login = (
|
||||
loginLink: string,
|
||||
username: string,
|
||||
password: string
|
||||
) => {
|
||||
const login_data = {
|
||||
cookie: true,
|
||||
grant_type: "password",
|
||||
@@ -149,9 +154,15 @@ export const login = (username: string, password: string) => {
|
||||
return function(dispatch: any) {
|
||||
dispatch(loginPending());
|
||||
return apiClient
|
||||
.post(LOGIN_URL, login_data)
|
||||
.post(loginLink, login_data)
|
||||
.then(response => {
|
||||
return callFetchMe();
|
||||
dispatch(fetchIndexResourcesPending())
|
||||
return callFetchIndexResources();
|
||||
})
|
||||
.then(response => {
|
||||
dispatch(fetchIndexResourcesSuccess(response));
|
||||
const meLink = response._links.me.href;
|
||||
return callFetchMe(meLink);
|
||||
})
|
||||
.then(me => {
|
||||
dispatch(loginSuccess(me));
|
||||
@@ -162,10 +173,10 @@ export const login = (username: string, password: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchMe = () => {
|
||||
export const fetchMe = (link: string) => {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchMePending());
|
||||
return callFetchMe()
|
||||
return callFetchMe(link)
|
||||
.then(me => {
|
||||
dispatch(fetchMeSuccess(me));
|
||||
})
|
||||
@@ -179,14 +190,17 @@ export const fetchMe = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const logout = () => {
|
||||
export const logout = (link: string) => {
|
||||
return function(dispatch: any) {
|
||||
dispatch(logoutPending());
|
||||
return apiClient
|
||||
.delete(LOGIN_URL)
|
||||
.delete(link)
|
||||
.then(() => {
|
||||
dispatch(logoutSuccess());
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(fetchIndexResources());
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(logoutFailure(error));
|
||||
});
|
||||
|
||||
@@ -32,6 +32,10 @@ import reducer, {
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import {
|
||||
FETCH_INDEXRESOURCES_PENDING,
|
||||
FETCH_INDEXRESOURCES_SUCCESS
|
||||
} from "./indexResource";
|
||||
|
||||
const me = { name: "tricia", displayName: "Tricia McMillian" };
|
||||
|
||||
@@ -93,16 +97,30 @@ describe("auth actions", () => {
|
||||
headers: { "content-type": "application/json" }
|
||||
});
|
||||
|
||||
const meLink = {
|
||||
me: {
|
||||
href: "/me"
|
||||
}
|
||||
};
|
||||
|
||||
fetchMock.getOnce("/api/v2/", {
|
||||
_links: meLink
|
||||
});
|
||||
|
||||
const expectedActions = [
|
||||
{ type: LOGIN_PENDING },
|
||||
{ type: FETCH_INDEXRESOURCES_PENDING },
|
||||
{ type: FETCH_INDEXRESOURCES_SUCCESS, payload: { _links: meLink } },
|
||||
{ type: LOGIN_SUCCESS, payload: me }
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(login("tricia", "secret123")).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
return store
|
||||
.dispatch(login("/auth/access_token", "tricia", "secret123"))
|
||||
.then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch login failure", () => {
|
||||
@@ -111,12 +129,14 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(login("tricia", "secret123")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(LOGIN_PENDING);
|
||||
expect(actions[1].type).toEqual(LOGIN_FAILURE);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
return store
|
||||
.dispatch(login("/auth/access_token", "tricia", "secret123"))
|
||||
.then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(LOGIN_PENDING);
|
||||
expect(actions[1].type).toEqual(LOGIN_FAILURE);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch fetch me success", () => {
|
||||
@@ -135,7 +155,7 @@ describe("auth actions", () => {
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(fetchMe()).then(() => {
|
||||
return store.dispatch(fetchMe("me")).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -146,7 +166,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchMe()).then(() => {
|
||||
return store.dispatch(fetchMe("me")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_ME_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_ME_FAILURE);
|
||||
@@ -166,7 +186,7 @@ describe("auth actions", () => {
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(fetchMe()).then(() => {
|
||||
return store.dispatch(fetchMe("me")).then(() => {
|
||||
// return of async actions
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
@@ -181,14 +201,23 @@ describe("auth actions", () => {
|
||||
status: 401
|
||||
});
|
||||
|
||||
fetchMock.getOnce("/api/v2/", {
|
||||
_links: {
|
||||
login: {
|
||||
login: "/login"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const expectedActions = [
|
||||
{ type: LOGOUT_PENDING },
|
||||
{ type: LOGOUT_SUCCESS }
|
||||
{ type: LOGOUT_SUCCESS },
|
||||
{ type: FETCH_INDEXRESOURCES_PENDING }
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(logout()).then(() => {
|
||||
return store.dispatch(logout("/auth/access_token")).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -199,7 +228,7 @@ describe("auth actions", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(logout()).then(() => {
|
||||
return store.dispatch(logout("/auth/access_token")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(LOGOUT_PENDING);
|
||||
expect(actions[1].type).toEqual(LOGOUT_FAILURE);
|
||||
|
||||
145
scm-ui/src/modules/indexResource.js
Normal file
145
scm-ui/src/modules/indexResource.js
Normal file
@@ -0,0 +1,145 @@
|
||||
// @flow
|
||||
import * as types from "./types";
|
||||
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import type { Action, IndexResources } from "@scm-manager/ui-types";
|
||||
import { isPending } from "./pending";
|
||||
import { getFailure } from "./failure";
|
||||
|
||||
// Action
|
||||
|
||||
export const FETCH_INDEXRESOURCES = "scm/INDEXRESOURCES";
|
||||
export const FETCH_INDEXRESOURCES_PENDING = `${FETCH_INDEXRESOURCES}_${
|
||||
types.PENDING_SUFFIX
|
||||
}`;
|
||||
export const FETCH_INDEXRESOURCES_SUCCESS = `${FETCH_INDEXRESOURCES}_${
|
||||
types.SUCCESS_SUFFIX
|
||||
}`;
|
||||
export const FETCH_INDEXRESOURCES_FAILURE = `${FETCH_INDEXRESOURCES}_${
|
||||
types.FAILURE_SUFFIX
|
||||
}`;
|
||||
|
||||
const INDEX_RESOURCES_LINK = "/";
|
||||
|
||||
export const callFetchIndexResources = (): Promise<IndexResources> => {
|
||||
return apiClient.get(INDEX_RESOURCES_LINK).then(response => {
|
||||
return response.json();
|
||||
});
|
||||
};
|
||||
|
||||
export function fetchIndexResources() {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchIndexResourcesPending());
|
||||
return callFetchIndexResources()
|
||||
.then(resources => {
|
||||
dispatch(fetchIndexResourcesSuccess(resources));
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(fetchIndexResourcesFailure(err));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchIndexResourcesPending(): Action {
|
||||
return {
|
||||
type: FETCH_INDEXRESOURCES_PENDING
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchIndexResourcesSuccess(resources: IndexResources): Action {
|
||||
return {
|
||||
type: FETCH_INDEXRESOURCES_SUCCESS,
|
||||
payload: resources
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchIndexResourcesFailure(err: Error): Action {
|
||||
return {
|
||||
type: FETCH_INDEXRESOURCES_FAILURE,
|
||||
payload: err
|
||||
};
|
||||
}
|
||||
|
||||
// reducer
|
||||
export default function reducer(
|
||||
state: Object = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): Object {
|
||||
if (!action.payload) {
|
||||
return state;
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case FETCH_INDEXRESOURCES_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
links: action.payload._links
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// selectors
|
||||
|
||||
export function isFetchIndexResourcesPending(state: Object) {
|
||||
return isPending(state, FETCH_INDEXRESOURCES);
|
||||
}
|
||||
|
||||
export function getFetchIndexResourcesFailure(state: Object) {
|
||||
return getFailure(state, FETCH_INDEXRESOURCES);
|
||||
}
|
||||
|
||||
export function getLinks(state: Object) {
|
||||
return state.indexResources.links;
|
||||
}
|
||||
|
||||
export function getLink(state: Object, name: string) {
|
||||
if (state.indexResources.links && state.indexResources.links[name]) {
|
||||
return state.indexResources.links[name].href;
|
||||
}
|
||||
}
|
||||
|
||||
export function getUiPluginsLink(state: Object) {
|
||||
return getLink(state, "uiPlugins");
|
||||
}
|
||||
|
||||
export function getMeLink(state: Object) {
|
||||
return getLink(state, "me");
|
||||
}
|
||||
|
||||
export function getLogoutLink(state: Object) {
|
||||
return getLink(state, "logout");
|
||||
}
|
||||
|
||||
export function getLoginLink(state: Object) {
|
||||
return getLink(state, "login");
|
||||
}
|
||||
|
||||
export function getUsersLink(state: Object) {
|
||||
return getLink(state, "users");
|
||||
}
|
||||
|
||||
export function getGroupsLink(state: Object) {
|
||||
return getLink(state, "groups");
|
||||
}
|
||||
|
||||
export function getConfigLink(state: Object) {
|
||||
return getLink(state, "config");
|
||||
}
|
||||
|
||||
export function getRepositoriesLink(state: Object) {
|
||||
return getLink(state, "repositories");
|
||||
}
|
||||
|
||||
export function getHgConfigLink(state: Object) {
|
||||
return getLink(state, "hgConfig");
|
||||
}
|
||||
|
||||
export function getGitConfigLink(state: Object) {
|
||||
return getLink(state, "gitConfig");
|
||||
}
|
||||
|
||||
export function getSvnConfigLink(state: Object) {
|
||||
return getLink(state, "svnConfig");
|
||||
}
|
||||
426
scm-ui/src/modules/indexResource.test.js
Normal file
426
scm-ui/src/modules/indexResource.test.js
Normal file
@@ -0,0 +1,426 @@
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import reducer, {
|
||||
FETCH_INDEXRESOURCES_PENDING,
|
||||
FETCH_INDEXRESOURCES_SUCCESS,
|
||||
FETCH_INDEXRESOURCES_FAILURE,
|
||||
fetchIndexResources,
|
||||
fetchIndexResourcesSuccess,
|
||||
FETCH_INDEXRESOURCES,
|
||||
isFetchIndexResourcesPending,
|
||||
getFetchIndexResourcesFailure,
|
||||
getUiPluginsLink,
|
||||
getMeLink,
|
||||
getLogoutLink,
|
||||
getLoginLink,
|
||||
getUsersLink,
|
||||
getConfigLink,
|
||||
getRepositoriesLink,
|
||||
getHgConfigLink,
|
||||
getGitConfigLink,
|
||||
getSvnConfigLink,
|
||||
getLinks, getGroupsLink
|
||||
} from "./indexResource";
|
||||
|
||||
const indexResourcesUnauthenticated = {
|
||||
version: "2.0.0-SNAPSHOT",
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/scm/api/v2/"
|
||||
},
|
||||
uiPlugins: {
|
||||
href: "http://localhost:8081/scm/api/v2/ui/plugins"
|
||||
},
|
||||
login: {
|
||||
href: "http://localhost:8081/scm/api/v2/auth/access_token"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const indexResourcesAuthenticated = {
|
||||
version: "2.0.0-SNAPSHOT",
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://localhost:8081/scm/api/v2/"
|
||||
},
|
||||
uiPlugins: {
|
||||
href: "http://localhost:8081/scm/api/v2/ui/plugins"
|
||||
},
|
||||
me: {
|
||||
href: "http://localhost:8081/scm/api/v2/me/"
|
||||
},
|
||||
logout: {
|
||||
href: "http://localhost:8081/scm/api/v2/auth/access_token"
|
||||
},
|
||||
users: {
|
||||
href: "http://localhost:8081/scm/api/v2/users/"
|
||||
},
|
||||
groups: {
|
||||
href: "http://localhost:8081/scm/api/v2/groups/"
|
||||
},
|
||||
config: {
|
||||
href: "http://localhost:8081/scm/api/v2/config"
|
||||
},
|
||||
repositories: {
|
||||
href: "http://localhost:8081/scm/api/v2/repositories/"
|
||||
},
|
||||
hgConfig: {
|
||||
href: "http://localhost:8081/scm/api/v2/config/hg"
|
||||
},
|
||||
gitConfig: {
|
||||
href: "http://localhost:8081/scm/api/v2/config/git"
|
||||
},
|
||||
svnConfig: {
|
||||
href: "http://localhost:8081/scm/api/v2/config/svn"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe("fetch index resource", () => {
|
||||
const index_url = "/api/v2/";
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should successfully fetch index resources when unauthenticated", () => {
|
||||
fetchMock.getOnce(index_url, indexResourcesUnauthenticated);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_INDEXRESOURCES_PENDING },
|
||||
{
|
||||
type: FETCH_INDEXRESOURCES_SUCCESS,
|
||||
payload: indexResourcesUnauthenticated
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchIndexResources()).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully fetch index resources when authenticated", () => {
|
||||
fetchMock.getOnce(index_url, indexResourcesAuthenticated);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_INDEXRESOURCES_PENDING },
|
||||
{
|
||||
type: FETCH_INDEXRESOURCES_SUCCESS,
|
||||
payload: indexResourcesAuthenticated
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchIndexResources()).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch FETCH_INDEX_RESOURCES_FAILURE if request fails", () => {
|
||||
fetchMock.getOnce(index_url, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchIndexResources()).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_INDEXRESOURCES_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_INDEXRESOURCES_FAILURE);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("index resources reducer", () => {
|
||||
it("should return empty object, if state and action is undefined", () => {
|
||||
expect(reducer()).toEqual({});
|
||||
});
|
||||
|
||||
it("should return the same state, if the action is undefined", () => {
|
||||
const state = { x: true };
|
||||
expect(reducer(state)).toBe(state);
|
||||
});
|
||||
|
||||
it("should return the same state, if the action is unknown to the reducer", () => {
|
||||
const state = { x: true };
|
||||
expect(reducer(state, { type: "EL_SPECIALE" })).toBe(state);
|
||||
});
|
||||
|
||||
it("should store the index resources on FETCH_INDEXRESOURCES_SUCCESS", () => {
|
||||
const newState = reducer(
|
||||
{},
|
||||
fetchIndexResourcesSuccess(indexResourcesAuthenticated)
|
||||
);
|
||||
expect(newState.links).toBe(indexResourcesAuthenticated._links);
|
||||
});
|
||||
});
|
||||
|
||||
describe("index resources selectors", () => {
|
||||
const error = new Error("something goes wrong");
|
||||
|
||||
it("should return true, when fetch index resources is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_INDEXRESOURCES]: true
|
||||
}
|
||||
};
|
||||
expect(isFetchIndexResourcesPending(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false, when fetch index resources is not pending", () => {
|
||||
expect(isFetchIndexResourcesPending({})).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error when fetch index resources did fail", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_INDEXRESOURCES]: error
|
||||
}
|
||||
};
|
||||
expect(getFetchIndexResourcesFailure(state)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined when fetch index resources did not fail", () => {
|
||||
expect(getFetchIndexResourcesFailure({})).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should return all links", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesAuthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getLinks(state)).toBe(indexResourcesAuthenticated._links);
|
||||
});
|
||||
|
||||
// ui plugins link
|
||||
it("should return ui plugins link when authenticated and has permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesAuthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getUiPluginsLink(state)).toBe(
|
||||
"http://localhost:8081/scm/api/v2/ui/plugins"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return ui plugins links when unauthenticated and has permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesUnauthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getUiPluginsLink(state)).toBe(
|
||||
"http://localhost:8081/scm/api/v2/ui/plugins"
|
||||
);
|
||||
});
|
||||
|
||||
// me link
|
||||
it("should return me link when authenticated and has permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesAuthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getMeLink(state)).toBe("http://localhost:8081/scm/api/v2/me/");
|
||||
});
|
||||
|
||||
it("should return undefined for me link when unauthenticated or has not permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesUnauthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getMeLink(state)).toBe(undefined);
|
||||
});
|
||||
|
||||
// logout link
|
||||
it("should return logout link when authenticated and has permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesAuthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getLogoutLink(state)).toBe(
|
||||
"http://localhost:8081/scm/api/v2/auth/access_token"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return undefined for logout link when unauthenticated or has not permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesUnauthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getLogoutLink(state)).toBe(undefined);
|
||||
});
|
||||
|
||||
// login link
|
||||
it("should return login link when unauthenticated and has permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesUnauthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getLoginLink(state)).toBe(
|
||||
"http://localhost:8081/scm/api/v2/auth/access_token"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return undefined for login link when authenticated or has not permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesAuthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getLoginLink(state)).toBe(undefined);
|
||||
});
|
||||
|
||||
// users link
|
||||
it("should return users link when authenticated and has permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesAuthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getUsersLink(state)).toBe("http://localhost:8081/scm/api/v2/users/");
|
||||
});
|
||||
|
||||
it("should return undefined for users link when unauthenticated or has not permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesUnauthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getUsersLink(state)).toBe(undefined);
|
||||
});
|
||||
|
||||
// groups link
|
||||
it("should return groups link when authenticated and has permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesAuthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getGroupsLink(state)).toBe("http://localhost:8081/scm/api/v2/groups/");
|
||||
});
|
||||
|
||||
it("should return undefined for groups link when unauthenticated or has not permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesUnauthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getGroupsLink(state)).toBe(undefined);
|
||||
});
|
||||
|
||||
// config link
|
||||
it("should return config link when authenticated and has permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesAuthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getConfigLink(state)).toBe(
|
||||
"http://localhost:8081/scm/api/v2/config"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return undefined for config link when unauthenticated or has not permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesUnauthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getConfigLink(state)).toBe(undefined);
|
||||
});
|
||||
|
||||
// repositories link
|
||||
it("should return repositories link when authenticated and has permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesAuthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getRepositoriesLink(state)).toBe(
|
||||
"http://localhost:8081/scm/api/v2/repositories/"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return config for repositories link when unauthenticated or has not permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesUnauthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getRepositoriesLink(state)).toBe(undefined);
|
||||
});
|
||||
|
||||
// hgConfig link
|
||||
it("should return hgConfig link when authenticated and has permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesAuthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getHgConfigLink(state)).toBe(
|
||||
"http://localhost:8081/scm/api/v2/config/hg"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return config for hgConfig link when unauthenticated or has not permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesUnauthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getHgConfigLink(state)).toBe(undefined);
|
||||
});
|
||||
|
||||
// gitConfig link
|
||||
it("should return gitConfig link when authenticated and has permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesAuthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getGitConfigLink(state)).toBe(
|
||||
"http://localhost:8081/scm/api/v2/config/git"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return config for gitConfig link when unauthenticated or has not permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesUnauthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getGitConfigLink(state)).toBe(undefined);
|
||||
});
|
||||
|
||||
// svnConfig link
|
||||
it("should return svnConfig link when authenticated and has permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesAuthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getSvnConfigLink(state)).toBe(
|
||||
"http://localhost:8081/scm/api/v2/config/svn"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return config for svnConfig link when unauthenticated or has not permission to see it", () => {
|
||||
const state = {
|
||||
indexResources: {
|
||||
links: indexResourcesUnauthenticated._links
|
||||
}
|
||||
};
|
||||
expect(getSvnConfigLink(state)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
40
scm-ui/src/repos/components/DropDown.js
Normal file
40
scm-ui/src/repos/components/DropDown.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
options: string[],
|
||||
optionSelected: string => void,
|
||||
preselectedOption?: string,
|
||||
className: any
|
||||
};
|
||||
|
||||
class DropDown extends React.Component<Props> {
|
||||
render() {
|
||||
const { options, preselectedOption, className } = this.props;
|
||||
return (
|
||||
<div className={classNames(className, "select")}>
|
||||
<select
|
||||
value={preselectedOption ? preselectedOption : ""}
|
||||
onChange={this.change}
|
||||
>
|
||||
<option key="" />
|
||||
{options.map(option => {
|
||||
return (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
change = (event: SyntheticInputEvent<HTMLSelectElement>) => {
|
||||
this.props.optionSelected(event.target.value);
|
||||
};
|
||||
}
|
||||
|
||||
export default DropDown;
|
||||
@@ -4,7 +4,6 @@ import "../../tests/enzyme";
|
||||
import "../../tests/i18n";
|
||||
import ReactRouterEnzymeContext from "react-router-enzyme-context";
|
||||
import PermissionsNavLink from "./PermissionsNavLink";
|
||||
import EditNavLink from "./EditNavLink";
|
||||
|
||||
describe("PermissionsNavLink", () => {
|
||||
const options = new ReactRouterEnzymeContext();
|
||||
|
||||
30
scm-ui/src/repos/components/RepositoryNavLink.js
Normal file
30
scm-ui/src/repos/components/RepositoryNavLink.js
Normal file
@@ -0,0 +1,30 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import { NavLink } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
to: string,
|
||||
label: string,
|
||||
linkName: string,
|
||||
activeWhenMatch?: (route: any) => boolean,
|
||||
activeOnlyWhenExact: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
* Component renders only if the repository contains the link with the given name.
|
||||
*/
|
||||
class RepositoryNavLink extends React.Component<Props> {
|
||||
render() {
|
||||
const { repository, linkName } = this.props;
|
||||
|
||||
if (!repository._links[linkName]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <NavLink {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default RepositoryNavLink;
|
||||
49
scm-ui/src/repos/components/RepositoryNavLink.test.js
Normal file
49
scm-ui/src/repos/components/RepositoryNavLink.test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import "../../tests/enzyme";
|
||||
import "../../tests/i18n";
|
||||
import ReactRouterEnzymeContext from "react-router-enzyme-context";
|
||||
import RepositoryNavLink from "./RepositoryNavLink";
|
||||
|
||||
describe("RepositoryNavLink", () => {
|
||||
const options = new ReactRouterEnzymeContext();
|
||||
|
||||
it("should render nothing, if the sources link is missing", () => {
|
||||
const repository = {
|
||||
_links: {}
|
||||
};
|
||||
|
||||
const navLink = shallow(
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="sources"
|
||||
to="/sources"
|
||||
label="Sources"
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
expect(navLink.text()).toBe("");
|
||||
});
|
||||
|
||||
it("should render the navLink", () => {
|
||||
const repository = {
|
||||
_links: {
|
||||
sources: {
|
||||
href: "/sources"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const navLink = mount(
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="sources"
|
||||
to="/sources"
|
||||
label="Sources"
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
expect(navLink.text()).toBe("Sources");
|
||||
});
|
||||
});
|
||||
32
scm-ui/src/repos/components/changesets/AvatarImage.js
Normal file
32
scm-ui/src/repos/components/changesets/AvatarImage.js
Normal file
@@ -0,0 +1,32 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { binder } from "@scm-manager/ui-extensions";
|
||||
import type { Changeset } from "@scm-manager/ui-types";
|
||||
import { Image } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
changeset: Changeset
|
||||
};
|
||||
|
||||
class AvatarImage extends React.Component<Props> {
|
||||
render() {
|
||||
const { changeset } = this.props;
|
||||
|
||||
const avatarFactory = binder.getExtension("changeset.avatar-factory");
|
||||
if (avatarFactory) {
|
||||
const avatar = avatarFactory(changeset);
|
||||
|
||||
return (
|
||||
<Image
|
||||
className="has-rounded-border"
|
||||
src={avatar}
|
||||
alt={changeset.author.name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default AvatarImage;
|
||||
18
scm-ui/src/repos/components/changesets/AvatarWrapper.js
Normal file
18
scm-ui/src/repos/components/changesets/AvatarWrapper.js
Normal file
@@ -0,0 +1,18 @@
|
||||
//@flow
|
||||
import * as React from "react";
|
||||
import { binder } from "@scm-manager/ui-extensions";
|
||||
|
||||
type Props = {
|
||||
children: React.Node
|
||||
};
|
||||
|
||||
class AvatarWrapper extends React.Component<Props> {
|
||||
render() {
|
||||
if (binder.hasExtension("changeset.avatar-factory")) {
|
||||
return <>{this.props.children}</>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default AvatarWrapper;
|
||||
37
scm-ui/src/repos/components/changesets/ChangesetAuthor.js
Normal file
37
scm-ui/src/repos/components/changesets/ChangesetAuthor.js
Normal file
@@ -0,0 +1,37 @@
|
||||
//@flow
|
||||
|
||||
import React from "react";
|
||||
import type { Changeset } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
changeset: Changeset
|
||||
};
|
||||
|
||||
export default class ChangesetAuthor extends React.Component<Props> {
|
||||
render() {
|
||||
const { changeset } = this.props;
|
||||
if (!changeset.author) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name } = changeset.author;
|
||||
return (
|
||||
<>
|
||||
{name} {this.renderMail()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderMail() {
|
||||
const { mail } = this.props.changeset.author;
|
||||
if (mail) {
|
||||
return (
|
||||
<a className="is-hidden-mobile" href={"mailto:" + mail}>
|
||||
<
|
||||
{mail}
|
||||
>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
100
scm-ui/src/repos/components/changesets/ChangesetDetails.js
Normal file
100
scm-ui/src/repos/components/changesets/ChangesetDetails.js
Normal file
@@ -0,0 +1,100 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type {
|
||||
Changeset,
|
||||
Repository
|
||||
} from "../../../../../scm-ui-components/packages/ui-types/src/index";
|
||||
import { Interpolate, translate } from "react-i18next";
|
||||
import injectSheet from "react-jss";
|
||||
import ChangesetTag from "./ChangesetTag";
|
||||
import ChangesetAuthor from "./ChangesetAuthor";
|
||||
import { parseDescription } from "./changesets";
|
||||
import { DateFromNow } from "../../../../../scm-ui-components/packages/ui-components/src/index";
|
||||
import AvatarWrapper from "./AvatarWrapper";
|
||||
import AvatarImage from "./AvatarImage";
|
||||
import classNames from "classnames";
|
||||
import ChangesetId from "./ChangesetId";
|
||||
import type { Tag } from "@scm-manager/ui-types";
|
||||
|
||||
const styles = {
|
||||
spacing: {
|
||||
marginRight: "1em"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
changeset: Changeset,
|
||||
repository: Repository,
|
||||
t: string => string,
|
||||
classes: any
|
||||
};
|
||||
|
||||
class ChangesetDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { changeset, repository, classes } = this.props;
|
||||
|
||||
const description = parseDescription(changeset.description);
|
||||
|
||||
const id = (
|
||||
<ChangesetId repository={repository} changeset={changeset} link={false} />
|
||||
);
|
||||
const date = <DateFromNow date={changeset.date} />;
|
||||
|
||||
return (
|
||||
<div className="content">
|
||||
<h4>{description.title}</h4>
|
||||
<article className="media">
|
||||
<AvatarWrapper>
|
||||
<p className={classNames("image", "is-64x64", classes.spacing)}>
|
||||
<AvatarImage changeset={changeset} />
|
||||
</p>
|
||||
</AvatarWrapper>
|
||||
<div className="media-content">
|
||||
<p>
|
||||
<ChangesetAuthor changeset={changeset} />
|
||||
</p>
|
||||
<p>
|
||||
<Interpolate
|
||||
i18nKey="changesets.changeset.summary"
|
||||
id={id}
|
||||
time={date}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div className="media-right">{this.renderTags()}</div>
|
||||
</article>
|
||||
<p>
|
||||
{description.message.split("\n").map((item, key) => {
|
||||
return (
|
||||
<span key={key}>
|
||||
{item}
|
||||
<br />
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getTags = () => {
|
||||
const { changeset } = this.props;
|
||||
return changeset._embedded.tags || [];
|
||||
};
|
||||
|
||||
renderTags = () => {
|
||||
const tags = this.getTags();
|
||||
if (tags.length > 0) {
|
||||
return (
|
||||
<div className="level-item">
|
||||
{tags.map((tag: Tag) => {
|
||||
return <ChangesetTag key={tag.name} tag={tag} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(translate("repos")(ChangesetDetails));
|
||||
47
scm-ui/src/repos/components/changesets/ChangesetId.js
Normal file
47
scm-ui/src/repos/components/changesets/ChangesetId.js
Normal file
@@ -0,0 +1,47 @@
|
||||
//@flow
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import React from "react";
|
||||
import type { Repository, Changeset } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
changeset: Changeset,
|
||||
link: boolean
|
||||
};
|
||||
|
||||
export default class ChangesetId extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
link: true
|
||||
};
|
||||
|
||||
shortId = (changeset: Changeset) => {
|
||||
return changeset.id.substr(0, 7);
|
||||
};
|
||||
|
||||
renderLink = () => {
|
||||
const { changeset, repository } = this.props;
|
||||
return (
|
||||
<Link
|
||||
to={`/repo/${repository.namespace}/${repository.name}/changeset/${
|
||||
changeset.id
|
||||
}`}
|
||||
>
|
||||
{this.shortId(changeset)}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
renderText = () => {
|
||||
const { changeset } = this.props;
|
||||
return this.shortId(changeset);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { link } = this.props;
|
||||
if (link) {
|
||||
return this.renderLink();
|
||||
}
|
||||
return this.renderText();
|
||||
}
|
||||
}
|
||||
28
scm-ui/src/repos/components/changesets/ChangesetList.js
Normal file
28
scm-ui/src/repos/components/changesets/ChangesetList.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// @flow
|
||||
import ChangesetRow from "./ChangesetRow";
|
||||
import React from "react";
|
||||
import type { Changeset, Repository } from "@scm-manager/ui-types";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
changesets: Changeset[]
|
||||
};
|
||||
|
||||
class ChangesetList extends React.Component<Props> {
|
||||
render() {
|
||||
const { repository, changesets } = this.props;
|
||||
const content = changesets.map(changeset => {
|
||||
return (
|
||||
<ChangesetRow
|
||||
key={changeset.id}
|
||||
repository={repository}
|
||||
changeset={changeset}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return <div className={classNames("box")}>{content}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default ChangesetList;
|
||||
101
scm-ui/src/repos/components/changesets/ChangesetRow.js
Normal file
101
scm-ui/src/repos/components/changesets/ChangesetRow.js
Normal file
@@ -0,0 +1,101 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Changeset, Repository, Tag } from "@scm-manager/ui-types";
|
||||
import classNames from "classnames";
|
||||
import { translate, Interpolate } from "react-i18next";
|
||||
import ChangesetId from "./ChangesetId";
|
||||
import injectSheet from "react-jss";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import ChangesetAuthor from "./ChangesetAuthor";
|
||||
import ChangesetTag from "./ChangesetTag";
|
||||
import { compose } from "redux";
|
||||
import { parseDescription } from "./changesets";
|
||||
import AvatarWrapper from "./AvatarWrapper";
|
||||
import AvatarImage from "./AvatarImage";
|
||||
|
||||
const styles = {
|
||||
pointer: {
|
||||
cursor: "pointer"
|
||||
},
|
||||
changesetGroup: {
|
||||
marginBottom: "1em"
|
||||
},
|
||||
withOverflow: {
|
||||
overflow: "auto"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
changeset: Changeset,
|
||||
t: any,
|
||||
classes: any
|
||||
};
|
||||
|
||||
class ChangesetRow extends React.Component<Props> {
|
||||
createLink = (changeset: Changeset) => {
|
||||
const { repository } = this.props;
|
||||
return <ChangesetId changeset={changeset} repository={repository} />;
|
||||
};
|
||||
|
||||
getTags = () => {
|
||||
const { changeset } = this.props;
|
||||
return changeset._embedded.tags || [];
|
||||
};
|
||||
|
||||
render() {
|
||||
const { changeset, classes } = this.props;
|
||||
const changesetLink = this.createLink(changeset);
|
||||
const dateFromNow = <DateFromNow date={changeset.date} />;
|
||||
const authorLine = <ChangesetAuthor changeset={changeset} />;
|
||||
const description = parseDescription(changeset.description);
|
||||
|
||||
return (
|
||||
<article className={classNames("media", classes.inner)}>
|
||||
<AvatarWrapper>
|
||||
<div>
|
||||
<figure className="media-left">
|
||||
<p className="image is-64x64">
|
||||
<AvatarImage changeset={changeset} />
|
||||
</p>
|
||||
</figure>
|
||||
</div>
|
||||
</AvatarWrapper>
|
||||
<div className={classNames("media-content", classes.withOverflow)}>
|
||||
<div className="content">
|
||||
<p className="is-ellipsis-overflow">
|
||||
<strong>{description.title}</strong>
|
||||
<br />
|
||||
<Interpolate
|
||||
i18nKey="changesets.changeset.summary"
|
||||
id={changesetLink}
|
||||
time={dateFromNow}
|
||||
/>
|
||||
</p>{" "}
|
||||
<div className="is-size-7">{authorLine}</div>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderTags()}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
renderTags = () => {
|
||||
const tags = this.getTags();
|
||||
if (tags.length > 0) {
|
||||
return (
|
||||
<div className="media-right">
|
||||
{tags.map((tag: Tag) => {
|
||||
return <ChangesetTag key={tag.name} tag={tag} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export default compose(
|
||||
injectSheet(styles),
|
||||
translate("repos")
|
||||
)(ChangesetRow);
|
||||
32
scm-ui/src/repos/components/changesets/ChangesetTag.js
Normal file
32
scm-ui/src/repos/components/changesets/ChangesetTag.js
Normal file
@@ -0,0 +1,32 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Tag } from "@scm-manager/ui-types";
|
||||
import injectSheet from "react-jss";
|
||||
import classNames from "classnames";
|
||||
|
||||
const styles = {
|
||||
spacing: {
|
||||
marginRight: "4px"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
tag: Tag,
|
||||
|
||||
// context props
|
||||
classes: Object
|
||||
};
|
||||
|
||||
class ChangesetTag extends React.Component<Props> {
|
||||
render() {
|
||||
const { tag, classes } = this.props;
|
||||
return (
|
||||
<span className="tag is-info">
|
||||
<span className={classNames("fa", "fa-tag", classes.spacing)} />{" "}
|
||||
{tag.name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(ChangesetTag);
|
||||
25
scm-ui/src/repos/components/changesets/changesets.js
Normal file
25
scm-ui/src/repos/components/changesets/changesets.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// @flow
|
||||
export type Description = {
|
||||
title: string,
|
||||
message: string
|
||||
};
|
||||
|
||||
export function parseDescription(description?: string): Description {
|
||||
const desc = description ? description : "";
|
||||
const lineBreak = desc.indexOf("\n");
|
||||
|
||||
let title;
|
||||
let message = "";
|
||||
|
||||
if (lineBreak > 0) {
|
||||
title = desc.substring(0, lineBreak);
|
||||
message = desc.substring(lineBreak + 1);
|
||||
} else {
|
||||
title = desc;
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
message
|
||||
};
|
||||
}
|
||||
22
scm-ui/src/repos/components/changesets/changesets.test.js
Normal file
22
scm-ui/src/repos/components/changesets/changesets.test.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// @flow
|
||||
|
||||
import { parseDescription } from "./changesets";
|
||||
|
||||
describe("parseDescription tests", () => {
|
||||
it("should return a description with title and message", () => {
|
||||
const desc = parseDescription("Hello\nTrillian");
|
||||
expect(desc.title).toBe("Hello");
|
||||
expect(desc.message).toBe("Trillian");
|
||||
});
|
||||
|
||||
it("should return a description with title and without message", () => {
|
||||
const desc = parseDescription("Hello Trillian");
|
||||
expect(desc.title).toBe("Hello Trillian");
|
||||
});
|
||||
|
||||
it("should return an empty description for undefined", () => {
|
||||
const desc = parseDescription();
|
||||
expect(desc.title).toBe("");
|
||||
expect(desc.message).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {Link} from "react-router-dom";
|
||||
import injectSheet from "react-jss";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import type {Repository} from "@scm-manager/ui-types";
|
||||
import {DateFromNow} from "@scm-manager/ui-components";
|
||||
import RepositoryEntryLink from "./RepositoryEntryLink";
|
||||
import classNames from "classnames";
|
||||
import RepositoryAvatar from "./RepositoryAvatar";
|
||||
@@ -45,7 +45,7 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
iconClass="fa-code-branch"
|
||||
to={repositoryLink + "/changesets"}
|
||||
to={repositoryLink + "/history"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -67,10 +67,7 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
renderModifyLink = (repository: Repository, repositoryLink: string) => {
|
||||
if (repository._links["update"]) {
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
iconClass="fa-cog"
|
||||
to={repositoryLink + "/modify"}
|
||||
/>
|
||||
<RepositoryEntryLink iconClass="fa-cog" to={repositoryLink + "/edit"} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
87
scm-ui/src/repos/containers/BranchSelector.js
Normal file
87
scm-ui/src/repos/containers/BranchSelector.js
Normal file
@@ -0,0 +1,87 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import type { Branch } from "@scm-manager/ui-types";
|
||||
import DropDown from "../components/DropDown";
|
||||
import { translate } from "react-i18next";
|
||||
import injectSheet from "react-jss";
|
||||
import { compose } from "redux";
|
||||
import classNames from "classnames";
|
||||
|
||||
const styles = {
|
||||
zeroflex: {
|
||||
flexGrow: 0
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
branches: Branch[], // TODO: Use generics?
|
||||
selected: (branch?: Branch) => void,
|
||||
selectedBranch: string,
|
||||
|
||||
// context props
|
||||
classes: Object,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = { selectedBranch?: Branch };
|
||||
|
||||
class BranchSelector extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.branches
|
||||
.filter(branch => branch.name === this.props.selectedBranch)
|
||||
.forEach(branch => this.setState({ selectedBranch: branch }));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { branches, classes, t } = this.props;
|
||||
|
||||
if (branches) {
|
||||
return (
|
||||
<div className="box field is-horizontal">
|
||||
<div
|
||||
className={classNames("field-label", "is-normal", classes.zeroflex)}
|
||||
>
|
||||
<label className="label">{t("branch-selector.label")}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field is-narrow">
|
||||
<div className="control">
|
||||
<DropDown
|
||||
className="is-fullwidth"
|
||||
options={branches.map(b => b.name)}
|
||||
optionSelected={this.branchSelected}
|
||||
preselectedOption={
|
||||
this.state.selectedBranch
|
||||
? this.state.selectedBranch.name
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
branchSelected = (branchName: string) => {
|
||||
const { branches, selected } = this.props;
|
||||
const branch = branches.find(b => b.name === branchName);
|
||||
|
||||
selected(branch);
|
||||
this.setState({ selectedBranch: branch });
|
||||
};
|
||||
}
|
||||
|
||||
export default compose(
|
||||
injectSheet(styles),
|
||||
translate("repos")
|
||||
)(BranchSelector);
|
||||
75
scm-ui/src/repos/containers/ChangesetView.js
Normal file
75
scm-ui/src/repos/containers/ChangesetView.js
Normal file
@@ -0,0 +1,75 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import type { Changeset, Repository } from "@scm-manager/ui-types";
|
||||
import {
|
||||
fetchChangesetIfNeeded,
|
||||
getChangeset,
|
||||
getFetchChangesetFailure,
|
||||
isFetchChangesetPending
|
||||
} from "../modules/changesets";
|
||||
import ChangesetDetails from "../components/changesets/ChangesetDetails";
|
||||
import { translate } from "react-i18next";
|
||||
import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
id: string,
|
||||
changeset: Changeset,
|
||||
repository: Repository,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
fetchChangesetIfNeeded: (repository: Repository, id: string) => void,
|
||||
match: any,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class ChangesetView extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchChangesetIfNeeded, repository } = this.props;
|
||||
const id = this.props.match.params.id;
|
||||
fetchChangesetIfNeeded(repository, id);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { changeset, loading, error, t, repository } = this.props;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPage
|
||||
title={t("changeset-error.title")}
|
||||
subtitle={t("changeset-error.subtitle")}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!changeset || loading) return <Loading />;
|
||||
|
||||
return <ChangesetDetails changeset={changeset} repository={repository} />;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps: Props) => {
|
||||
const repository = ownProps.repository;
|
||||
const id = ownProps.match.params.id;
|
||||
const changeset = getChangeset(state, repository, id);
|
||||
const loading = isFetchChangesetPending(state, repository, id);
|
||||
const error = getFetchChangesetFailure(state, repository, id);
|
||||
return { changeset, error, loading };
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchChangesetIfNeeded: (repository: Repository, id: string) => {
|
||||
dispatch(fetchChangesetIfNeeded(repository, id));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("changesets")(ChangesetView))
|
||||
);
|
||||
115
scm-ui/src/repos/containers/Changesets.js
Normal file
115
scm-ui/src/repos/containers/Changesets.js
Normal file
@@ -0,0 +1,115 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import type {
|
||||
Branch,
|
||||
Changeset,
|
||||
PagedCollection,
|
||||
Repository
|
||||
} from "@scm-manager/ui-types";
|
||||
import {
|
||||
fetchChangesets,
|
||||
getChangesets,
|
||||
getFetchChangesetsFailure,
|
||||
isFetchChangesetsPending,
|
||||
selectListAsCollection
|
||||
} from "../modules/changesets";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import ChangesetList from "../components/changesets/ChangesetList";
|
||||
import {
|
||||
ErrorNotification,
|
||||
LinkPaginator,
|
||||
Loading,
|
||||
getPageFromMatch
|
||||
} from "@scm-manager/ui-components";
|
||||
import { compose } from "redux";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
branch: Branch,
|
||||
page: number,
|
||||
|
||||
// State props
|
||||
changesets: Changeset[],
|
||||
list: PagedCollection,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
|
||||
// Dispatch props
|
||||
fetchChangesets: (Repository, Branch, number) => void,
|
||||
|
||||
// context props
|
||||
match: any
|
||||
};
|
||||
|
||||
class Changesets extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchChangesets, repository, branch, page } = this.props;
|
||||
|
||||
fetchChangesets(repository, branch, page);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { changesets, loading, error } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!changesets || changesets.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{this.renderList()}
|
||||
{this.renderPaginator()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderList = () => {
|
||||
const { repository, changesets } = this.props;
|
||||
return <ChangesetList repository={repository} changesets={changesets} />;
|
||||
};
|
||||
|
||||
renderPaginator = () => {
|
||||
const { page, list } = this.props;
|
||||
if (list) {
|
||||
return <LinkPaginator page={page} collection={list} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchChangesets: (repo: Repository, branch: Branch, page: number) => {
|
||||
dispatch(fetchChangesets(repo, branch, page));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
const { repository, branch, match } = ownProps;
|
||||
const changesets = getChangesets(state, repository, branch);
|
||||
const loading = isFetchChangesetsPending(state, repository, branch);
|
||||
const error = getFetchChangesetsFailure(state, repository, branch);
|
||||
const list = selectListAsCollection(state, repository, branch);
|
||||
const page = getPageFromMatch(match);
|
||||
|
||||
return { changesets, list, page, loading, error };
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(Changesets);
|
||||
140
scm-ui/src/repos/containers/ChangesetsRoot.js
Normal file
140
scm-ui/src/repos/containers/ChangesetsRoot.js
Normal file
@@ -0,0 +1,140 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import type { Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { Route, withRouter } from "react-router-dom";
|
||||
import Changesets from "./Changesets";
|
||||
import BranchSelector from "./BranchSelector";
|
||||
import { connect } from "react-redux";
|
||||
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import {
|
||||
fetchBranches,
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} from "../modules/branches";
|
||||
import { compose } from "redux";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
baseUrl: string,
|
||||
selected: string,
|
||||
baseUrlWithBranch: string,
|
||||
baseUrlWithoutBranch: string,
|
||||
|
||||
// State props
|
||||
branches: Branch[],
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
|
||||
// Dispatch props
|
||||
fetchBranches: Repository => void,
|
||||
|
||||
// Context props
|
||||
history: any, // TODO flow type
|
||||
match: any
|
||||
};
|
||||
|
||||
class BranchRoot extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchBranches(this.props.repository);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
branchSelected = (branch?: Branch) => {
|
||||
let url;
|
||||
if (branch) {
|
||||
url = `${this.props.baseUrlWithBranch}/${encodeURIComponent(
|
||||
branch.name
|
||||
)}/changesets/`;
|
||||
} else {
|
||||
url = `${this.props.baseUrlWithoutBranch}/`;
|
||||
}
|
||||
this.props.history.push(url);
|
||||
};
|
||||
|
||||
findSelectedBranch = () => {
|
||||
const { selected, branches } = this.props;
|
||||
return branches.find((branch: Branch) => branch.name === selected);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { repository, error, loading, match, branches } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!repository || !branches) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = this.stripEndingSlash(match.url);
|
||||
const branch = this.findSelectedBranch();
|
||||
const changesets = <Changesets repository={repository} branch={branch} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderBranchSelector()}
|
||||
<Route path={`${url}/:page?`} component={() => changesets} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderBranchSelector = () => {
|
||||
const { repository, branches, selected } = this.props;
|
||||
if (repository._links.branches) {
|
||||
return (
|
||||
<BranchSelector
|
||||
branches={branches}
|
||||
selectedBranch={selected}
|
||||
selected={(b: Branch) => {
|
||||
this.branchSelected(b);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchBranches: (repo: Repository) => {
|
||||
dispatch(fetchBranches(repo));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
const { repository, match } = ownProps;
|
||||
const loading = isFetchBranchesPending(state, repository);
|
||||
const error = getFetchBranchesFailure(state, repository);
|
||||
const branches = getBranches(state, repository);
|
||||
const selected = decodeURIComponent(match.params.branch);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
branches,
|
||||
selected
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(BranchRoot);
|
||||
@@ -18,16 +18,18 @@ import {
|
||||
isCreateRepoPending
|
||||
} from "../modules/repos";
|
||||
import type { History } from "history";
|
||||
import { getRepositoriesLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
repositoryTypes: RepositoryType[],
|
||||
typesLoading: boolean,
|
||||
createLoading: boolean,
|
||||
error: Error,
|
||||
repoLink: string,
|
||||
|
||||
// dispatch functions
|
||||
fetchRepositoryTypesIfNeeded: () => void,
|
||||
createRepo: (Repository, callback: () => void) => void,
|
||||
createRepo: (link: string, Repository, callback: () => void) => void,
|
||||
resetForm: () => void,
|
||||
|
||||
// context props
|
||||
@@ -55,7 +57,7 @@ class Create extends React.Component<Props> {
|
||||
error
|
||||
} = this.props;
|
||||
|
||||
const { t } = this.props;
|
||||
const { t, repoLink } = this.props;
|
||||
return (
|
||||
<Page
|
||||
title={t("create.title")}
|
||||
@@ -68,7 +70,7 @@ class Create extends React.Component<Props> {
|
||||
repositoryTypes={repositoryTypes}
|
||||
loading={createLoading}
|
||||
submitForm={repo => {
|
||||
createRepo(repo, this.repoCreated);
|
||||
createRepo(repoLink, repo, this.repoCreated);
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
@@ -82,11 +84,13 @@ const mapStateToProps = state => {
|
||||
const createLoading = isCreateRepoPending(state);
|
||||
const error =
|
||||
getFetchRepositoryTypesFailure(state) || getCreateRepoFailure(state);
|
||||
const repoLink = getRepositoriesLink(state);
|
||||
return {
|
||||
repositoryTypes,
|
||||
typesLoading,
|
||||
createLoading,
|
||||
error
|
||||
error,
|
||||
repoLink
|
||||
};
|
||||
};
|
||||
|
||||
@@ -95,8 +99,12 @@ const mapDispatchToProps = dispatch => {
|
||||
fetchRepositoryTypesIfNeeded: () => {
|
||||
dispatch(fetchRepositoryTypesIfNeeded());
|
||||
},
|
||||
createRepo: (repository: Repository, callback: () => void) => {
|
||||
dispatch(createRepo(repository, callback));
|
||||
createRepo: (
|
||||
link: string,
|
||||
repository: Repository,
|
||||
callback: () => void
|
||||
) => {
|
||||
dispatch(createRepo(link, repository, callback));
|
||||
},
|
||||
resetForm: () => {
|
||||
dispatch(createRepoReset());
|
||||
|
||||
@@ -18,6 +18,7 @@ import { CreateButton, Page, Paginator } from "@scm-manager/ui-components";
|
||||
import RepositoryList from "../components/list";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import type { History } from "history";
|
||||
import { getRepositoriesLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
page: number,
|
||||
@@ -25,10 +26,11 @@ type Props = {
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
showCreateButton: boolean,
|
||||
reposLink: string,
|
||||
|
||||
// dispatched functions
|
||||
fetchRepos: () => void,
|
||||
fetchReposByPage: number => void,
|
||||
fetchRepos: string => void,
|
||||
fetchReposByPage: (string, number) => void,
|
||||
fetchReposByLink: string => void,
|
||||
|
||||
// context props
|
||||
@@ -38,7 +40,7 @@ type Props = {
|
||||
|
||||
class Overview extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchReposByPage(this.props.page);
|
||||
this.props.fetchReposByPage(this.props.reposLink, this.props.page);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,7 +115,9 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const loading = isFetchReposPending(state);
|
||||
const error = getFetchReposFailure(state);
|
||||
const showCreateButton = isAbleToCreateRepos(state);
|
||||
const reposLink = getRepositoriesLink(state);
|
||||
return {
|
||||
reposLink,
|
||||
page,
|
||||
collection,
|
||||
loading,
|
||||
@@ -124,11 +128,11 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchRepos: () => {
|
||||
dispatch(fetchRepos());
|
||||
fetchRepos: (link: string) => {
|
||||
dispatch(fetchRepos(link));
|
||||
},
|
||||
fetchReposByPage: (page: number) => {
|
||||
dispatch(fetchReposByPage(page));
|
||||
fetchReposByPage: (link: string, page: number) => {
|
||||
dispatch(fetchReposByPage(link, page));
|
||||
},
|
||||
fetchReposByLink: (link: string) => {
|
||||
dispatch(fetchReposByLink(link));
|
||||
|
||||
@@ -7,15 +7,17 @@ import {
|
||||
getRepository,
|
||||
isFetchRepoPending
|
||||
} from "../modules/repos";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { Route } from "react-router-dom";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
import {
|
||||
Page,
|
||||
Loading,
|
||||
ErrorPage,
|
||||
Loading,
|
||||
Navigation,
|
||||
NavLink,
|
||||
Page,
|
||||
Section
|
||||
} from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
@@ -26,7 +28,13 @@ import Permissions from "../permissions/containers/Permissions";
|
||||
|
||||
import type { History } from "history";
|
||||
import EditNavLink from "../components/EditNavLink";
|
||||
|
||||
import BranchRoot from "./ChangesetsRoot";
|
||||
import ChangesetView from "./ChangesetView";
|
||||
import PermissionsNavLink from "../components/PermissionsNavLink";
|
||||
import Sources from "../sources/containers/Sources";
|
||||
import RepositoryNavLink from "../components/RepositoryNavLink";
|
||||
import { getRepositoriesLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
namespace: string,
|
||||
@@ -34,9 +42,10 @@ type Props = {
|
||||
repository: Repository,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
repoLink: string,
|
||||
|
||||
// dispatch functions
|
||||
fetchRepo: (namespace: string, name: string) => void,
|
||||
fetchRepo: (link: string, namespace: string, name: string) => void,
|
||||
deleteRepo: (repository: Repository, () => void) => void,
|
||||
|
||||
// context props
|
||||
@@ -47,9 +56,9 @@ type Props = {
|
||||
|
||||
class RepositoryRoot extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchRepo, namespace, name } = this.props;
|
||||
const { fetchRepo, namespace, name, repoLink } = this.props;
|
||||
|
||||
fetchRepo(namespace, name);
|
||||
fetchRepo(repoLink, namespace, name);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
@@ -71,6 +80,17 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
this.props.deleteRepo(repository, this.deleted);
|
||||
};
|
||||
|
||||
matchChangeset = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
return route.location.pathname.match(`${url}/changeset/`);
|
||||
};
|
||||
|
||||
matches = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const regex = new RegExp(`${url}(/branches)?/?[^/]*/changesets?.*`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, error, repository, t } = this.props;
|
||||
|
||||
@@ -89,39 +109,93 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
|
||||
return (
|
||||
<Page title={repository.namespace + "/" + repository.name}>
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters">
|
||||
<Route
|
||||
path={url}
|
||||
exact
|
||||
component={() => <RepositoryDetails repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/edit`}
|
||||
component={() => <Edit repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/permissions`}
|
||||
render={props => (
|
||||
<Permissions
|
||||
namespace={this.props.repository.namespace}
|
||||
repoName={this.props.repository.name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Switch>
|
||||
<Route
|
||||
path={url}
|
||||
exact
|
||||
component={() => <RepositoryDetails repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/edit`}
|
||||
component={() => <Edit repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/permissions`}
|
||||
render={props => (
|
||||
<Permissions
|
||||
namespace={this.props.repository.namespace}
|
||||
repoName={this.props.repository.name}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${url}/changeset/:id`}
|
||||
render={() => <ChangesetView repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/sources`}
|
||||
exact={true}
|
||||
render={() => (
|
||||
<Sources repository={repository} baseUrl={`${url}/sources`} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/sources/:revision/:path*`}
|
||||
render={() => (
|
||||
<Sources repository={repository} baseUrl={`${url}/sources`} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/changesets`}
|
||||
render={() => (
|
||||
<BranchRoot
|
||||
repository={repository}
|
||||
baseUrlWithBranch={`${url}/branches`}
|
||||
baseUrlWithoutBranch={`${url}/changesets`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/branches/:branch/changesets`}
|
||||
render={() => (
|
||||
<BranchRoot
|
||||
repository={repository}
|
||||
baseUrlWithBranch={`${url}/branches`}
|
||||
baseUrlWithoutBranch={`${url}/changesets`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="column">
|
||||
<Navigation>
|
||||
<Section label={t("repository-root.navigation-label")}>
|
||||
<NavLink to={url} label={t("repository-root.information")} />
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="changesets"
|
||||
to={`${url}/changesets/`}
|
||||
label={t("repository-root.history")}
|
||||
activeWhenMatch={this.matches}
|
||||
activeOnlyWhenExact={false}
|
||||
/>
|
||||
<RepositoryNavLink
|
||||
repository={repository}
|
||||
linkName="sources"
|
||||
to={`${url}/sources`}
|
||||
label={t("repository-root.sources")}
|
||||
activeOnlyWhenExact={false}
|
||||
/>
|
||||
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
||||
<PermissionsNavLink
|
||||
permissionUrl={`${url}/permissions`}
|
||||
repository={repository}
|
||||
/>
|
||||
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
||||
</Section>
|
||||
<Section label={t("repository-root.actions-label")}>
|
||||
<DeleteNavAction repository={repository} delete={this.delete} />
|
||||
@@ -140,19 +214,21 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const repository = getRepository(state, namespace, name);
|
||||
const loading = isFetchRepoPending(state, namespace, name);
|
||||
const error = getFetchRepoFailure(state, namespace, name);
|
||||
const repoLink = getRepositoriesLink(state);
|
||||
return {
|
||||
namespace,
|
||||
name,
|
||||
repository,
|
||||
loading,
|
||||
error
|
||||
error,
|
||||
repoLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchRepo: (namespace: string, name: string) => {
|
||||
dispatch(fetchRepo(namespace, name));
|
||||
fetchRepo: (link: string, namespace: string, name: string) => {
|
||||
dispatch(fetchRepo(link, namespace, name));
|
||||
},
|
||||
deleteRepo: (repository: Repository, callback: () => void) => {
|
||||
dispatch(deleteRepo(repository, callback));
|
||||
|
||||
134
scm-ui/src/repos/modules/branches.js
Normal file
134
scm-ui/src/repos/modules/branches.js
Normal file
@@ -0,0 +1,134 @@
|
||||
// @flow
|
||||
import {
|
||||
FAILURE_SUFFIX,
|
||||
PENDING_SUFFIX,
|
||||
SUCCESS_SUFFIX
|
||||
} from "../../modules/types";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import type { Action, Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { isPending } from "../../modules/pending";
|
||||
import { getFailure } from "../../modules/failure";
|
||||
|
||||
export const FETCH_BRANCHES = "scm/repos/FETCH_BRANCHES";
|
||||
export const FETCH_BRANCHES_PENDING = `${FETCH_BRANCHES}_${PENDING_SUFFIX}`;
|
||||
export const FETCH_BRANCHES_SUCCESS = `${FETCH_BRANCHES}_${SUCCESS_SUFFIX}`;
|
||||
export const FETCH_BRANCHES_FAILURE = `${FETCH_BRANCHES}_${FAILURE_SUFFIX}`;
|
||||
|
||||
// Fetching branches
|
||||
|
||||
export function fetchBranches(repository: Repository) {
|
||||
if (!repository._links.branches) {
|
||||
return {
|
||||
type: FETCH_BRANCHES_SUCCESS,
|
||||
payload: { repository, data: {} },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchBranchesPending(repository));
|
||||
return apiClient
|
||||
.get(repository._links.branches.href)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
dispatch(fetchBranchesSuccess(data, repository));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(fetchBranchesFailure(repository, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Action creators
|
||||
export function fetchBranchesPending(repository: Repository) {
|
||||
return {
|
||||
type: FETCH_BRANCHES_PENDING,
|
||||
payload: { repository },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchBranchesSuccess(data: string, repository: Repository) {
|
||||
return {
|
||||
type: FETCH_BRANCHES_SUCCESS,
|
||||
payload: { data, repository },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchBranchesFailure(repository: Repository, error: Error) {
|
||||
return {
|
||||
type: FETCH_BRANCHES_FAILURE,
|
||||
payload: { error, repository },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
// Reducers
|
||||
|
||||
type State = { [string]: Branch[] };
|
||||
|
||||
export default function reducer(
|
||||
state: State = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): State {
|
||||
if (!action.payload) {
|
||||
return state;
|
||||
}
|
||||
const payload = action.payload;
|
||||
switch (action.type) {
|
||||
case FETCH_BRANCHES_SUCCESS:
|
||||
const key = createKey(payload.repository);
|
||||
return {
|
||||
...state,
|
||||
[key]: extractBranchesFromPayload(payload.data)
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function extractBranchesFromPayload(payload: any) {
|
||||
if (payload._embedded && payload._embedded.branches) {
|
||||
return payload._embedded.branches;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Selectors
|
||||
|
||||
export function getBranches(state: Object, repository: Repository) {
|
||||
const key = createKey(repository);
|
||||
if (state.branches[key]) {
|
||||
return state.branches[key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getBranch(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
name: string
|
||||
): ?Branch {
|
||||
const key = createKey(repository);
|
||||
if (state.branches[key]) {
|
||||
return state.branches[key].find((b: Branch) => b.name === name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isFetchBranchesPending(
|
||||
state: Object,
|
||||
repository: Repository
|
||||
): boolean {
|
||||
return isPending(state, FETCH_BRANCHES, createKey(repository));
|
||||
}
|
||||
|
||||
export function getFetchBranchesFailure(state: Object, repository: Repository) {
|
||||
return getFailure(state, FETCH_BRANCHES, createKey(repository));
|
||||
}
|
||||
|
||||
function createKey(repository: Repository): string {
|
||||
const { namespace, name } = repository;
|
||||
return `${namespace}/${name}`;
|
||||
}
|
||||
195
scm-ui/src/repos/modules/branches.test.js
Normal file
195
scm-ui/src/repos/modules/branches.test.js
Normal file
@@ -0,0 +1,195 @@
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import reducer, {
|
||||
FETCH_BRANCHES,
|
||||
FETCH_BRANCHES_FAILURE,
|
||||
FETCH_BRANCHES_PENDING,
|
||||
FETCH_BRANCHES_SUCCESS,
|
||||
fetchBranches,
|
||||
getBranch,
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} from "./branches";
|
||||
|
||||
const namespace = "foo";
|
||||
const name = "bar";
|
||||
const key = namespace + "/" + name;
|
||||
const repository = {
|
||||
namespace: "foo",
|
||||
name: "bar",
|
||||
_links: {
|
||||
branches: {
|
||||
href: "http://scm/api/rest/v2/repositories/foo/bar/branches"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const branch1 = { name: "branch1", revision: "revision1" };
|
||||
const branch2 = { name: "branch2", revision: "revision2" };
|
||||
const branch3 = { name: "branch3", revision: "revision3" };
|
||||
|
||||
describe("branches", () => {
|
||||
describe("fetch branches", () => {
|
||||
const URL = "http://scm/api/rest/v2/repositories/foo/bar/branches";
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should fetch branches", () => {
|
||||
const collection = {};
|
||||
|
||||
fetchMock.getOnce(URL, "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_BRANCHES_PENDING,
|
||||
payload: { repository },
|
||||
itemId: key
|
||||
},
|
||||
{
|
||||
type: FETCH_BRANCHES_SUCCESS,
|
||||
payload: { data: collection, repository },
|
||||
itemId: key
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchBranches(repository)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail fetching branches on HTTP 500", () => {
|
||||
const collection = {};
|
||||
|
||||
fetchMock.getOnce(URL, 500);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_BRANCHES_PENDING,
|
||||
payload: { repository },
|
||||
itemId: key
|
||||
},
|
||||
{
|
||||
type: FETCH_BRANCHES_FAILURE,
|
||||
payload: { error: collection, repository },
|
||||
itemId: key
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchBranches(repository)).then(() => {
|
||||
expect(store.getActions()[0]).toEqual(expectedActions[0]);
|
||||
expect(store.getActions()[1].type).toEqual(FETCH_BRANCHES_FAILURE);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("branches reducer", () => {
|
||||
const branches = {
|
||||
_embedded: {
|
||||
branches: [branch1, branch2]
|
||||
}
|
||||
};
|
||||
const action = {
|
||||
type: FETCH_BRANCHES_SUCCESS,
|
||||
payload: {
|
||||
repository,
|
||||
data: branches
|
||||
}
|
||||
};
|
||||
|
||||
it("should update state according to successful fetch", () => {
|
||||
const newState = reducer({}, action);
|
||||
expect(newState).toBeDefined();
|
||||
expect(newState[key]).toBeDefined();
|
||||
expect(newState[key]).toContain(branch1);
|
||||
expect(newState[key]).toContain(branch2);
|
||||
});
|
||||
|
||||
it("should not delete existing branches from state", () => {
|
||||
const oldState = {
|
||||
"hitchhiker/heartOfGold": [branch3]
|
||||
};
|
||||
|
||||
const newState = reducer(oldState, action);
|
||||
expect(newState[key]).toContain(branch1);
|
||||
expect(newState[key]).toContain(branch2);
|
||||
|
||||
expect(newState["hitchhiker/heartOfGold"]).toContain(branch3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("branch selectors", () => {
|
||||
const error = new Error("Something went wrong");
|
||||
|
||||
const state = {
|
||||
branches: {
|
||||
[key]: [branch1, branch2]
|
||||
}
|
||||
};
|
||||
|
||||
it("should return true, when fetching branches is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_BRANCHES + "/foo/bar"]: true
|
||||
}
|
||||
};
|
||||
|
||||
expect(isFetchBranchesPending(state, repository)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return branches", () => {
|
||||
const branches = getBranches(state, repository);
|
||||
expect(branches.length).toEqual(2);
|
||||
expect(branches).toContain(branch1);
|
||||
expect(branches).toContain(branch2);
|
||||
});
|
||||
|
||||
it("should return always the same reference for branches", () => {
|
||||
const one = getBranches(state, repository);
|
||||
const two = getBranches(state, repository);
|
||||
expect(one).toBe(two);
|
||||
});
|
||||
|
||||
it("should return null, if no branches for the repository available", () => {
|
||||
const branches = getBranches({ branches: {} }, repository);
|
||||
expect(branches).toBeNull();
|
||||
});
|
||||
|
||||
it("should return single branch by name", () => {
|
||||
const branch = getBranch(state, repository, "branch1");
|
||||
expect(branch).toEqual(branch1);
|
||||
});
|
||||
|
||||
it("should return same reference for single branch by name", () => {
|
||||
const one = getBranch(state, repository, "branch1");
|
||||
const two = getBranch(state, repository, "branch1");
|
||||
expect(one).toBe(two);
|
||||
});
|
||||
|
||||
it("should return undefined if branch does not exist", () => {
|
||||
const branch = getBranch(state, repository, "branch42");
|
||||
expect(branch).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return error if fetching branches failed", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_BRANCHES + "/foo/bar"]: error
|
||||
}
|
||||
};
|
||||
|
||||
expect(getFetchBranchesFailure(state, repository)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return false if fetching branches did not fail", () => {
|
||||
expect(getFetchBranchesFailure({}, repository)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
400
scm-ui/src/repos/modules/changesets.js
Normal file
400
scm-ui/src/repos/modules/changesets.js
Normal file
@@ -0,0 +1,400 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
FAILURE_SUFFIX,
|
||||
PENDING_SUFFIX,
|
||||
SUCCESS_SUFFIX
|
||||
} from "../../modules/types";
|
||||
import { apiClient, urls } from "@scm-manager/ui-components";
|
||||
import { isPending } from "../../modules/pending";
|
||||
import { getFailure } from "../../modules/failure";
|
||||
import type {
|
||||
Action,
|
||||
Branch,
|
||||
PagedCollection,
|
||||
Repository
|
||||
} from "@scm-manager/ui-types";
|
||||
|
||||
export const FETCH_CHANGESETS = "scm/repos/FETCH_CHANGESETS";
|
||||
export const FETCH_CHANGESETS_PENDING = `${FETCH_CHANGESETS}_${PENDING_SUFFIX}`;
|
||||
export const FETCH_CHANGESETS_SUCCESS = `${FETCH_CHANGESETS}_${SUCCESS_SUFFIX}`;
|
||||
export const FETCH_CHANGESETS_FAILURE = `${FETCH_CHANGESETS}_${FAILURE_SUFFIX}`;
|
||||
|
||||
export const FETCH_CHANGESET = "scm/repos/FETCH_CHANGESET";
|
||||
export const FETCH_CHANGESET_PENDING = `${FETCH_CHANGESET}_${PENDING_SUFFIX}`;
|
||||
export const FETCH_CHANGESET_SUCCESS = `${FETCH_CHANGESET}_${SUCCESS_SUFFIX}`;
|
||||
export const FETCH_CHANGESET_FAILURE = `${FETCH_CHANGESET}_${FAILURE_SUFFIX}`;
|
||||
|
||||
// actions
|
||||
//TODO: Content type
|
||||
|
||||
export function fetchChangesetIfNeeded(repository: Repository, id: string) {
|
||||
return (dispatch: any, getState: any) => {
|
||||
if (shouldFetchChangeset(getState(), repository, id)) {
|
||||
return dispatch(fetchChangeset(repository, id));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchChangeset(repository: Repository, id: string) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchChangesetPending(repository, id));
|
||||
return apiClient
|
||||
.get(createChangesetUrl(repository, id))
|
||||
.then(response => response.json())
|
||||
.then(data => dispatch(fetchChangesetSuccess(data, repository, id)))
|
||||
.catch(err => {
|
||||
dispatch(fetchChangesetFailure(repository, id, err));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createChangesetUrl(repository: Repository, id: string) {
|
||||
return urls.concat(repository._links.changesets.href, id);
|
||||
}
|
||||
|
||||
export function fetchChangesetPending(
|
||||
repository: Repository,
|
||||
id: string
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_CHANGESET_PENDING,
|
||||
itemId: createChangesetItemId(repository, id)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchChangesetSuccess(
|
||||
changeset: any,
|
||||
repository: Repository,
|
||||
id: string
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_CHANGESET_SUCCESS,
|
||||
payload: { changeset, repository, id },
|
||||
itemId: createChangesetItemId(repository, id)
|
||||
};
|
||||
}
|
||||
|
||||
function fetchChangesetFailure(
|
||||
repository: Repository,
|
||||
id: string,
|
||||
error: Error
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_CHANGESET_FAILURE,
|
||||
payload: {
|
||||
repository,
|
||||
id,
|
||||
error
|
||||
},
|
||||
itemId: createChangesetItemId(repository, id)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchChangesets(
|
||||
repository: Repository,
|
||||
branch?: Branch,
|
||||
page?: number
|
||||
) {
|
||||
const link = createChangesetsLink(repository, branch, page);
|
||||
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchChangesetsPending(repository, branch));
|
||||
return apiClient
|
||||
.get(link)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
dispatch(fetchChangesetsSuccess(repository, branch, data));
|
||||
})
|
||||
.catch(cause => {
|
||||
dispatch(fetchChangesetsFailure(repository, branch, cause));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createChangesetsLink(
|
||||
repository: Repository,
|
||||
branch?: Branch,
|
||||
page?: number
|
||||
) {
|
||||
let link = repository._links.changesets.href;
|
||||
|
||||
if (branch) {
|
||||
link = branch._links.history.href;
|
||||
}
|
||||
|
||||
if (page) {
|
||||
link = link + `?page=${page - 1}`;
|
||||
}
|
||||
return link;
|
||||
}
|
||||
|
||||
export function fetchChangesetsPending(
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
): Action {
|
||||
const itemId = createItemId(repository, branch);
|
||||
|
||||
return {
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchChangesetsSuccess(
|
||||
repository: Repository,
|
||||
branch?: Branch,
|
||||
changesets: any
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: {
|
||||
repository,
|
||||
branch,
|
||||
changesets
|
||||
},
|
||||
itemId: createItemId(repository, branch)
|
||||
};
|
||||
}
|
||||
|
||||
function fetchChangesetsFailure(
|
||||
repository: Repository,
|
||||
branch?: Branch,
|
||||
error: Error
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_CHANGESETS_FAILURE,
|
||||
payload: {
|
||||
repository,
|
||||
error,
|
||||
branch
|
||||
},
|
||||
itemId: createItemId(repository, branch)
|
||||
};
|
||||
}
|
||||
|
||||
function createChangesetItemId(repository: Repository, id: string) {
|
||||
const { namespace, name } = repository;
|
||||
return namespace + "/" + name + "/" + id;
|
||||
}
|
||||
|
||||
function createItemId(repository: Repository, branch?: Branch): string {
|
||||
const { namespace, name } = repository;
|
||||
let itemId = namespace + "/" + name;
|
||||
if (branch) {
|
||||
itemId = itemId + "/" + branch.name;
|
||||
}
|
||||
return itemId;
|
||||
}
|
||||
|
||||
// reducer
|
||||
export default function reducer(
|
||||
state: any = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): Object {
|
||||
if (!action.payload) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const payload = action.payload;
|
||||
switch (action.type) {
|
||||
case FETCH_CHANGESET_SUCCESS:
|
||||
const _key = createItemId(payload.repository);
|
||||
|
||||
let _oldByIds = {};
|
||||
if (state[_key] && state[_key].byId) {
|
||||
_oldByIds = state[_key].byId;
|
||||
}
|
||||
|
||||
const changeset = payload.changeset;
|
||||
|
||||
return {
|
||||
...state,
|
||||
[_key]: {
|
||||
...state[_key],
|
||||
byId: {
|
||||
..._oldByIds,
|
||||
[changeset.id]: changeset
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
case FETCH_CHANGESETS_SUCCESS:
|
||||
const changesets = payload.changesets._embedded.changesets;
|
||||
const changesetIds = changesets.map(c => c.id);
|
||||
const key = action.itemId;
|
||||
|
||||
if (!key) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const repoId = createItemId(payload.repository);
|
||||
|
||||
let oldState = {};
|
||||
if (state[repoId]) {
|
||||
oldState = state[repoId];
|
||||
}
|
||||
|
||||
const branchName = payload.branch ? payload.branch.name : "";
|
||||
const byIds = extractChangesetsByIds(changesets);
|
||||
|
||||
return {
|
||||
...state,
|
||||
[repoId]: {
|
||||
byId: {
|
||||
...oldState.byId,
|
||||
...byIds
|
||||
},
|
||||
byBranch: {
|
||||
...oldState.byBranch,
|
||||
[branchName]: {
|
||||
entries: changesetIds,
|
||||
entry: {
|
||||
page: payload.changesets.page,
|
||||
pageTotal: payload.changesets.pageTotal,
|
||||
_links: payload.changesets._links
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function extractChangesetsByIds(changesets: any) {
|
||||
const changesetsByIds = {};
|
||||
|
||||
for (let changeset of changesets) {
|
||||
changesetsByIds[changeset.id] = changeset;
|
||||
}
|
||||
|
||||
return changesetsByIds;
|
||||
}
|
||||
|
||||
//selectors
|
||||
export function getChangesets(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
) {
|
||||
const repoKey = createItemId(repository);
|
||||
|
||||
const stateRoot = state.changesets[repoKey];
|
||||
if (!stateRoot || !stateRoot.byBranch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const branchName = branch ? branch.name : "";
|
||||
|
||||
const changesets = stateRoot.byBranch[branchName];
|
||||
if (!changesets) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return changesets.entries.map((id: string) => {
|
||||
return stateRoot.byId[id];
|
||||
});
|
||||
}
|
||||
|
||||
export function getChangeset(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
id: string
|
||||
) {
|
||||
const key = createItemId(repository);
|
||||
const changesets =
|
||||
state.changesets && state.changesets[key]
|
||||
? state.changesets[key].byId
|
||||
: null;
|
||||
if (changesets != null && changesets[id]) {
|
||||
return changesets[id];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function shouldFetchChangeset(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
id: string
|
||||
) {
|
||||
if (getChangeset(state, repository, id)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isFetchChangesetPending(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
id: string
|
||||
) {
|
||||
return isPending(
|
||||
state,
|
||||
FETCH_CHANGESET,
|
||||
createChangesetItemId(repository, id)
|
||||
);
|
||||
}
|
||||
|
||||
export function getFetchChangesetFailure(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
id: string
|
||||
) {
|
||||
return getFailure(
|
||||
state,
|
||||
FETCH_CHANGESET,
|
||||
createChangesetItemId(repository, id)
|
||||
);
|
||||
}
|
||||
|
||||
export function isFetchChangesetsPending(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
) {
|
||||
return isPending(state, FETCH_CHANGESETS, createItemId(repository, branch));
|
||||
}
|
||||
|
||||
export function getFetchChangesetsFailure(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
) {
|
||||
return getFailure(state, FETCH_CHANGESETS, createItemId(repository, branch));
|
||||
}
|
||||
|
||||
const selectList = (state: Object, repository: Repository, branch?: Branch) => {
|
||||
const repoId = createItemId(repository);
|
||||
|
||||
const branchName = branch ? branch.name : "";
|
||||
if (state.changesets[repoId]) {
|
||||
const repoState = state.changesets[repoId];
|
||||
|
||||
if (repoState.byBranch && repoState.byBranch[branchName]) {
|
||||
return repoState.byBranch[branchName];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const selectListEntry = (
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
): Object => {
|
||||
const list = selectList(state, repository, branch);
|
||||
if (list.entry) {
|
||||
return list.entry;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const selectListAsCollection = (
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
): PagedCollection => {
|
||||
return selectListEntry(state, repository, branch);
|
||||
};
|
||||
620
scm-ui/src/repos/modules/changesets.test.js
Normal file
620
scm-ui/src/repos/modules/changesets.test.js
Normal file
@@ -0,0 +1,620 @@
|
||||
// @flow
|
||||
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import reducer, {
|
||||
FETCH_CHANGESETS,
|
||||
FETCH_CHANGESETS_FAILURE,
|
||||
FETCH_CHANGESETS_PENDING,
|
||||
FETCH_CHANGESETS_SUCCESS,
|
||||
FETCH_CHANGESET,
|
||||
FETCH_CHANGESET_FAILURE,
|
||||
FETCH_CHANGESET_PENDING,
|
||||
FETCH_CHANGESET_SUCCESS,
|
||||
fetchChangesets,
|
||||
fetchChangesetsSuccess,
|
||||
getChangesets,
|
||||
getFetchChangesetsFailure,
|
||||
isFetchChangesetsPending,
|
||||
fetchChangeset,
|
||||
getChangeset,
|
||||
fetchChangesetIfNeeded,
|
||||
shouldFetchChangeset,
|
||||
isFetchChangesetPending,
|
||||
getFetchChangesetFailure,
|
||||
fetchChangesetSuccess,
|
||||
selectListAsCollection
|
||||
} from "./changesets";
|
||||
|
||||
const branch = {
|
||||
name: "specific",
|
||||
revision: "123",
|
||||
_links: {
|
||||
history: {
|
||||
href:
|
||||
"http://scm.hitchhicker.com/api/v2/repositories/foo/bar/branches/specific/changesets"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const repository = {
|
||||
namespace: "foo",
|
||||
name: "bar",
|
||||
type: "GIT",
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://scm.hitchhicker.com/api/v2/repositories/foo/bar"
|
||||
},
|
||||
changesets: {
|
||||
href: "http://scm.hitchhicker.com/api/v2/repositories/foo/bar/changesets"
|
||||
},
|
||||
branches: {
|
||||
href:
|
||||
"http://scm.hitchhicker.com/api/v2/repositories/foo/bar/branches/specific/branches"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const changesets = {};
|
||||
|
||||
describe("changesets", () => {
|
||||
describe("fetching of changesets", () => {
|
||||
const DEFAULT_BRANCH_URL =
|
||||
"http://scm.hitchhicker.com/api/v2/repositories/foo/bar/changesets";
|
||||
const SPECIFIC_BRANCH_URL =
|
||||
"http://scm.hitchhicker.com/api/v2/repositories/foo/bar/branches/specific/changesets";
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
const changesetId = "aba876c0625d90a6aff1494f3d161aaa7008b958";
|
||||
|
||||
it("should fetch changeset", () => {
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL + "/" + changesetId, "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESET_PENDING,
|
||||
itemId: "foo/bar/" + changesetId
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESET_SUCCESS,
|
||||
payload: {
|
||||
changeset: {},
|
||||
id: changesetId,
|
||||
repository: repository
|
||||
},
|
||||
itemId: "foo/bar/" + changesetId
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(fetchChangeset(repository, changesetId))
|
||||
.then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail fetching changeset on error", () => {
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL + "/" + changesetId, 500);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESET_PENDING,
|
||||
itemId: "foo/bar/" + changesetId
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(fetchChangeset(repository, changesetId))
|
||||
.then(() => {
|
||||
expect(store.getActions()[0]).toEqual(expectedActions[0]);
|
||||
expect(store.getActions()[1].type).toEqual(FETCH_CHANGESET_FAILURE);
|
||||
expect(store.getActions()[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch changeset if needed", () => {
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL + "/id3", "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESET_PENDING,
|
||||
itemId: "foo/bar/id3"
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESET_SUCCESS,
|
||||
payload: {
|
||||
changeset: {},
|
||||
id: "id3",
|
||||
repository: repository
|
||||
},
|
||||
itemId: "foo/bar/id3"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(fetchChangesetIfNeeded(repository, "id3"))
|
||||
.then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not fetch changeset if not needed", () => {
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL + "/id1", 500);
|
||||
|
||||
const state = {
|
||||
changesets: {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id1: { id: "id1" },
|
||||
id2: { id: "id2" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const store = mockStore(state);
|
||||
return expect(
|
||||
store.dispatch(fetchChangesetIfNeeded(repository, "id1"))
|
||||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("should fetch changesets for default branch", () => {
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL, "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId: "foo/bar"
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: {
|
||||
repository,
|
||||
undefined,
|
||||
changesets
|
||||
},
|
||||
itemId: "foo/bar"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets(repository)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch changesets for specific branch", () => {
|
||||
const itemId = "foo/bar/specific";
|
||||
fetchMock.getOnce(SPECIFIC_BRANCH_URL, "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: {
|
||||
repository,
|
||||
branch,
|
||||
changesets
|
||||
},
|
||||
itemId
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets(repository, branch)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail fetching changesets on error", () => {
|
||||
const itemId = "foo/bar";
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL, 500);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets(repository)).then(() => {
|
||||
expect(store.getActions()[0]).toEqual(expectedActions[0]);
|
||||
expect(store.getActions()[1].type).toEqual(FETCH_CHANGESETS_FAILURE);
|
||||
expect(store.getActions()[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail fetching changesets for specific branch on error", () => {
|
||||
const itemId = "foo/bar/specific";
|
||||
fetchMock.getOnce(SPECIFIC_BRANCH_URL, 500);
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets(repository, branch)).then(() => {
|
||||
expect(store.getActions()[0]).toEqual(expectedActions[0]);
|
||||
expect(store.getActions()[1].type).toEqual(FETCH_CHANGESETS_FAILURE);
|
||||
expect(store.getActions()[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch changesets by page", () => {
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL + "?page=4", "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId: "foo/bar"
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: {
|
||||
repository,
|
||||
undefined,
|
||||
changesets
|
||||
},
|
||||
itemId: "foo/bar"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(fetchChangesets(repository, undefined, 5))
|
||||
.then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch changesets by branch and page", () => {
|
||||
fetchMock.getOnce(SPECIFIC_BRANCH_URL + "?page=4", "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId: "foo/bar/specific"
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: {
|
||||
repository,
|
||||
branch,
|
||||
changesets
|
||||
},
|
||||
itemId: "foo/bar/specific"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets(repository, branch, 5)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("changesets reducer", () => {
|
||||
const responseBody = {
|
||||
page: 1,
|
||||
pageTotal: 10,
|
||||
_links: {},
|
||||
_embedded: {
|
||||
changesets: [
|
||||
{ id: "changeset1", author: { mail: "z@phod.com", name: "zaphod" } },
|
||||
{ id: "changeset2", description: "foo" },
|
||||
{ id: "changeset3", description: "bar" }
|
||||
],
|
||||
_embedded: {
|
||||
tags: [],
|
||||
branches: [],
|
||||
parents: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it("should set state to received changesets", () => {
|
||||
const newState = reducer(
|
||||
{},
|
||||
fetchChangesetsSuccess(repository, undefined, responseBody)
|
||||
);
|
||||
expect(newState).toBeDefined();
|
||||
expect(newState["foo/bar"].byId["changeset1"].author.mail).toEqual(
|
||||
"z@phod.com"
|
||||
);
|
||||
expect(newState["foo/bar"].byId["changeset2"].description).toEqual("foo");
|
||||
expect(newState["foo/bar"].byId["changeset3"].description).toEqual("bar");
|
||||
expect(newState["foo/bar"].byBranch[""]).toEqual({
|
||||
entry: {
|
||||
page: 1,
|
||||
pageTotal: 10,
|
||||
_links: {}
|
||||
},
|
||||
entries: ["changeset1", "changeset2", "changeset3"]
|
||||
});
|
||||
});
|
||||
|
||||
it("should store the changeset list to branch", () => {
|
||||
const newState = reducer(
|
||||
{},
|
||||
fetchChangesetsSuccess(repository, branch, responseBody)
|
||||
);
|
||||
|
||||
expect(newState["foo/bar"].byId["changeset1"]).toBeDefined();
|
||||
expect(newState["foo/bar"].byBranch["specific"].entries).toEqual([
|
||||
"changeset1",
|
||||
"changeset2",
|
||||
"changeset3"
|
||||
]);
|
||||
});
|
||||
|
||||
it("should not remove existing changesets", () => {
|
||||
const state = {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id2: { id: "id2" },
|
||||
id1: { id: "id1" }
|
||||
},
|
||||
byBranch: {
|
||||
"": {
|
||||
entries: ["id1", "id2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const newState = reducer(
|
||||
state,
|
||||
fetchChangesetsSuccess(repository, undefined, responseBody)
|
||||
);
|
||||
|
||||
const fooBar = newState["foo/bar"];
|
||||
|
||||
expect(fooBar.byBranch[""].entries).toEqual([
|
||||
"changeset1",
|
||||
"changeset2",
|
||||
"changeset3"
|
||||
]);
|
||||
expect(fooBar.byId["id2"]).toEqual({ id: "id2" });
|
||||
expect(fooBar.byId["id1"]).toEqual({ id: "id1" });
|
||||
});
|
||||
|
||||
const responseBodySingleChangeset = {
|
||||
id: "id3",
|
||||
author: {
|
||||
mail: "z@phod.com",
|
||||
name: "zaphod"
|
||||
},
|
||||
date: "2018-09-13T08:46:22Z",
|
||||
description: "added testChangeset",
|
||||
_links: {},
|
||||
_embedded: {
|
||||
tags: [],
|
||||
branches: []
|
||||
}
|
||||
};
|
||||
|
||||
it("should add changeset to state", () => {
|
||||
const newState = reducer(
|
||||
{
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
"id2": {
|
||||
id: "id2",
|
||||
author: { mail: "mail@author.com", name: "author" }
|
||||
}
|
||||
},
|
||||
list: {
|
||||
entry: {
|
||||
page: 1,
|
||||
pageTotal: 10,
|
||||
_links: {}
|
||||
},
|
||||
entries: ["id2"]
|
||||
}
|
||||
}
|
||||
},
|
||||
fetchChangesetSuccess(responseBodySingleChangeset, repository, "id3")
|
||||
);
|
||||
|
||||
expect(newState).toBeDefined();
|
||||
expect(newState["foo/bar"].byId["id3"].description).toEqual(
|
||||
"added testChangeset"
|
||||
);
|
||||
expect(newState["foo/bar"].byId["id3"].author.mail).toEqual("z@phod.com");
|
||||
expect(newState["foo/bar"].byId["id2"]).toBeDefined();
|
||||
expect(newState["foo/bar"].byId["id3"]).toBeDefined();
|
||||
expect(newState["foo/bar"].list).toEqual({
|
||||
entry: {
|
||||
page: 1,
|
||||
pageTotal: 10,
|
||||
_links: {}
|
||||
},
|
||||
entries: ["id2"]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("changeset selectors", () => {
|
||||
const error = new Error("Something went wrong");
|
||||
|
||||
it("should return changeset", () => {
|
||||
const state = {
|
||||
changesets: {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id1: { id: "id1" },
|
||||
id2: { id: "id2" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = getChangeset(state, repository, "id1");
|
||||
expect(result).toEqual({ id: "id1" });
|
||||
});
|
||||
|
||||
it("should return null if changeset does not exist", () => {
|
||||
const state = {
|
||||
changesets: {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id1: { id: "id1" },
|
||||
id2: { id: "id2" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = getChangeset(state, repository, "id3");
|
||||
expect(result).toEqual(null);
|
||||
});
|
||||
|
||||
it("should return true if changeset does not exist", () => {
|
||||
const state = {
|
||||
changesets: {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id1: { id: "id1" },
|
||||
id2: { id: "id2" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = shouldFetchChangeset(state, repository, "id3");
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false if changeset exists", () => {
|
||||
const state = {
|
||||
changesets: {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id1: { id: "id1" },
|
||||
id2: { id: "id2" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = shouldFetchChangeset(state, repository, "id2");
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true, when fetching changeset is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_CHANGESET + "/foo/bar/id1"]: true
|
||||
}
|
||||
};
|
||||
|
||||
expect(isFetchChangesetPending(state, repository, "id1")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false, when fetching changeset is not pending", () => {
|
||||
expect(isFetchChangesetPending({}, repository, "id1")).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error if fetching changeset failed", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_CHANGESET + "/foo/bar/id1"]: error
|
||||
}
|
||||
};
|
||||
|
||||
expect(getFetchChangesetFailure(state, repository, "id1")).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return false if fetching changeset did not fail", () => {
|
||||
expect(getFetchChangesetFailure({}, repository, "id1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should get all changesets for a given repository", () => {
|
||||
const state = {
|
||||
changesets: {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id2: { id: "id2" },
|
||||
id1: { id: "id1" }
|
||||
},
|
||||
byBranch: {
|
||||
"": {
|
||||
entries: ["id1", "id2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = getChangesets(state, repository);
|
||||
expect(result).toEqual([{ id: "id1" }, { id: "id2" }]);
|
||||
});
|
||||
|
||||
it("should return true, when fetching changesets is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_CHANGESETS + "/foo/bar"]: true
|
||||
}
|
||||
};
|
||||
|
||||
expect(isFetchChangesetsPending(state, repository)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false, when fetching changesets is not pending", () => {
|
||||
expect(isFetchChangesetsPending({}, repository)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error if fetching changesets failed", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_CHANGESETS + "/foo/bar"]: error
|
||||
}
|
||||
};
|
||||
|
||||
expect(getFetchChangesetsFailure(state, repository)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return false if fetching changesets did not fail", () => {
|
||||
expect(getFetchChangesetsFailure({}, repository)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return list as collection for the default branch", () => {
|
||||
const state = {
|
||||
changesets: {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id2: { id: "id2" },
|
||||
id1: { id: "id1" }
|
||||
},
|
||||
byBranch: {
|
||||
"": {
|
||||
entry: {
|
||||
page: 1,
|
||||
pageTotal: 10,
|
||||
_links: {}
|
||||
},
|
||||
entries: ["id1", "id2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const collection = selectListAsCollection(state, repository);
|
||||
expect(collection.page).toBe(1);
|
||||
expect(collection.pageTotal).toBe(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -35,20 +35,18 @@ export const DELETE_REPO_PENDING = `${DELETE_REPO}_${types.PENDING_SUFFIX}`;
|
||||
export const DELETE_REPO_SUCCESS = `${DELETE_REPO}_${types.SUCCESS_SUFFIX}`;
|
||||
export const DELETE_REPO_FAILURE = `${DELETE_REPO}_${types.FAILURE_SUFFIX}`;
|
||||
|
||||
const REPOS_URL = "repositories";
|
||||
|
||||
const CONTENT_TYPE = "application/vnd.scmm-repository+json;v=2";
|
||||
|
||||
// fetch repos
|
||||
|
||||
const SORT_BY = "sortBy=namespaceAndName";
|
||||
|
||||
export function fetchRepos() {
|
||||
return fetchReposByLink(REPOS_URL);
|
||||
export function fetchRepos(link: string) {
|
||||
return fetchReposByLink(link);
|
||||
}
|
||||
|
||||
export function fetchReposByPage(page: number) {
|
||||
return fetchReposByLink(`${REPOS_URL}?page=${page - 1}`);
|
||||
export function fetchReposByPage(link: string, page: number) {
|
||||
return fetchReposByLink(`${link}?page=${page - 1}`);
|
||||
}
|
||||
|
||||
function appendSortByLink(url: string) {
|
||||
@@ -102,11 +100,12 @@ export function fetchReposFailure(err: Error): Action {
|
||||
|
||||
// fetch repo
|
||||
|
||||
export function fetchRepo(namespace: string, name: string) {
|
||||
export function fetchRepo(link: string, namespace: string, name: string) {
|
||||
const repoUrl = link.endsWith("/") ? link : link + "/";
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchRepoPending(namespace, name));
|
||||
return apiClient
|
||||
.get(`${REPOS_URL}/${namespace}/${name}`)
|
||||
.get(`${repoUrl}${namespace}/${name}`)
|
||||
.then(response => response.json())
|
||||
.then(repository => {
|
||||
dispatch(fetchRepoSuccess(repository));
|
||||
@@ -154,11 +153,15 @@ export function fetchRepoFailure(
|
||||
|
||||
// create repo
|
||||
|
||||
export function createRepo(repository: Repository, callback?: () => void) {
|
||||
export function createRepo(
|
||||
link: string,
|
||||
repository: Repository,
|
||||
callback?: () => void
|
||||
) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(createRepoPending());
|
||||
return apiClient
|
||||
.post(REPOS_URL, repository, CONTENT_TYPE)
|
||||
.post(link, repository, CONTENT_TYPE)
|
||||
.then(() => {
|
||||
dispatch(createRepoSuccess());
|
||||
if (callback) {
|
||||
@@ -448,3 +451,12 @@ export function getDeleteRepoFailure(
|
||||
) {
|
||||
return getFailure(state, DELETE_REPO, namespace + "/" + name);
|
||||
}
|
||||
|
||||
export function getPermissionsLink(
|
||||
state: Object,
|
||||
namespace: string,
|
||||
name: string
|
||||
) {
|
||||
const repo = getRepository(state, namespace, name);
|
||||
return repo && repo._links ? repo._links.permissions.href : undefined;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ import reducer, {
|
||||
MODIFY_REPO,
|
||||
isModifyRepoPending,
|
||||
getModifyRepoFailure,
|
||||
modifyRepoSuccess
|
||||
modifyRepoSuccess,
|
||||
getPermissionsLink
|
||||
} from "./repos";
|
||||
import type { Repository, RepositoryCollection } from "@scm-manager/ui-types";
|
||||
|
||||
@@ -99,16 +100,13 @@ const hitchhikerRestatend: Repository = {
|
||||
type: "git",
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
|
||||
href: "http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
|
||||
},
|
||||
delete: {
|
||||
href:
|
||||
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
|
||||
href: "http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
|
||||
},
|
||||
update: {
|
||||
href:
|
||||
"http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
|
||||
href: "http://localhost:8081/api/v2/repositories/hitchhiker/restatend"
|
||||
},
|
||||
permissions: {
|
||||
href:
|
||||
@@ -158,16 +156,14 @@ const slartiFjords: Repository = {
|
||||
href: "http://localhost:8081/api/v2/repositories/slarti/fjords/tags/"
|
||||
},
|
||||
branches: {
|
||||
href:
|
||||
"http://localhost:8081/api/v2/repositories/slarti/fjords/branches/"
|
||||
href: "http://localhost:8081/api/v2/repositories/slarti/fjords/branches/"
|
||||
},
|
||||
changesets: {
|
||||
href:
|
||||
"http://localhost:8081/api/v2/repositories/slarti/fjords/changesets/"
|
||||
},
|
||||
sources: {
|
||||
href:
|
||||
"http://localhost:8081/api/v2/repositories/slarti/fjords/sources/"
|
||||
href: "http://localhost:8081/api/v2/repositories/slarti/fjords/sources/"
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -221,6 +217,7 @@ const repositoryCollectionWithNames: RepositoryCollection = {
|
||||
};
|
||||
|
||||
describe("repos fetch", () => {
|
||||
const URL = "repositories";
|
||||
const REPOS_URL = "/api/v2/repositories";
|
||||
const SORT = "sortBy=namespaceAndName";
|
||||
const REPOS_URL_WITH_SORT = REPOS_URL + "?" + SORT;
|
||||
@@ -243,7 +240,7 @@ describe("repos fetch", () => {
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchRepos()).then(() => {
|
||||
return store.dispatch(fetchRepos(URL)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -262,7 +259,7 @@ describe("repos fetch", () => {
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(fetchReposByPage(43)).then(() => {
|
||||
return store.dispatch(fetchReposByPage(URL, 43)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -318,7 +315,7 @@ describe("repos fetch", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchRepos()).then(() => {
|
||||
return store.dispatch(fetchRepos(URL)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_REPOS_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_REPOS_FAILURE);
|
||||
@@ -346,7 +343,7 @@ describe("repos fetch", () => {
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchRepo("slarti", "fjords")).then(() => {
|
||||
return store.dispatch(fetchRepo(URL, "slarti", "fjords")).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -357,7 +354,7 @@ describe("repos fetch", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchRepo("slarti", "fjords")).then(() => {
|
||||
return store.dispatch(fetchRepo(URL, "slarti", "fjords")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_REPO_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_REPO_FAILURE);
|
||||
@@ -383,7 +380,7 @@ describe("repos fetch", () => {
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createRepo(slartiFjords)).then(() => {
|
||||
return store.dispatch(createRepo(URL, slartiFjords)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -400,18 +397,18 @@ describe("repos fetch", () => {
|
||||
};
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createRepo(slartiFjords, callback)).then(() => {
|
||||
return store.dispatch(createRepo(URL, slartiFjords, callback)).then(() => {
|
||||
expect(callMe).toBe("yeah");
|
||||
});
|
||||
});
|
||||
|
||||
it("should disapatch failure if server returns status code 500", () => {
|
||||
it("should dispatch failure if server returns status code 500", () => {
|
||||
fetchMock.postOnce(REPOS_URL, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createRepo(slartiFjords)).then(() => {
|
||||
return store.dispatch(createRepo(URL, slartiFjords)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_REPO_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_REPO_FAILURE);
|
||||
@@ -649,6 +646,21 @@ describe("repos selectors", () => {
|
||||
expect(repository).toEqual(slartiFjords);
|
||||
});
|
||||
|
||||
it("should return permissions link", () => {
|
||||
const state = {
|
||||
repos: {
|
||||
byNames: {
|
||||
"slarti/fjords": slartiFjords
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const link = getPermissionsLink(state, "slarti", "fjords");
|
||||
expect(link).toEqual(
|
||||
"http://localhost:8081/api/v2/repositories/slarti/fjords/permissions/"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return true, when fetch repo is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
|
||||
@@ -5,13 +5,13 @@ import { Checkbox, InputField, SubmitButton } from "@scm-manager/ui-components";
|
||||
import TypeSelector from "./TypeSelector";
|
||||
import type {
|
||||
PermissionCollection,
|
||||
PermissionEntry
|
||||
PermissionCreateEntry
|
||||
} from "@scm-manager/ui-types";
|
||||
import * as validator from "./permissionValidation";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
createPermission: (permission: PermissionEntry) => void,
|
||||
createPermission: (permission: PermissionCreateEntry) => void,
|
||||
loading: boolean,
|
||||
currentPermissions: PermissionCollection
|
||||
};
|
||||
@@ -51,21 +51,24 @@ class CreatePermissionForm extends React.Component<Props, State> {
|
||||
onChange={this.handleNameChange}
|
||||
validationError={!this.state.valid}
|
||||
errorMessage={t("permission.add-permission.name-input-invalid")}
|
||||
helpText={t("permission.help.nameHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t("permission.group-permission")}
|
||||
checked={groupPermission ? groupPermission : false}
|
||||
onChange={this.handleGroupPermissionChange}
|
||||
helpText={t("permission.help.groupPermissionHelpText")}
|
||||
/>
|
||||
<TypeSelector
|
||||
label={t("permission.type")}
|
||||
helpText={t("permission.help.typeHelpText")}
|
||||
handleTypeChange={this.handleTypeChange}
|
||||
type={type ? type : "READ"}
|
||||
/>
|
||||
<SubmitButton
|
||||
label={t("permission.add-permission.submit-button")}
|
||||
loading={loading}
|
||||
disabled={!this.state.valid || this.state.name == ""}
|
||||
disabled={!this.state.valid || this.state.name === ""}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import {
|
||||
Select
|
||||
} from "@scm-manager/ui-components";
|
||||
import { Select } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
handleTypeChange: string => void,
|
||||
type: string,
|
||||
label?: string,
|
||||
helpText?: string,
|
||||
loading?: boolean
|
||||
};
|
||||
|
||||
class TypeSelector extends React.Component<Props> {
|
||||
render() {
|
||||
const { type, handleTypeChange, loading } = this.props;
|
||||
const { type, handleTypeChange, loading, label, helpText } = this.props;
|
||||
const types = ["READ", "OWNER", "WRITE"];
|
||||
|
||||
return (
|
||||
@@ -23,6 +23,8 @@ class TypeSelector extends React.Component<Props> {
|
||||
value={type ? type : "READ"}
|
||||
options={this.createSelectOptions(types)}
|
||||
loading={loading}
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -37,4 +39,4 @@ class TypeSelector extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("permissions")(TypeSelector);
|
||||
export default translate("repos")(TypeSelector);
|
||||
|
||||
@@ -5,12 +5,15 @@ import "../../../../tests/i18n";
|
||||
import DeletePermissionButton from "./DeletePermissionButton";
|
||||
|
||||
import { confirmAlert } from "@scm-manager/ui-components";
|
||||
import ReactRouterEnzymeContext from "react-router-enzyme-context";
|
||||
jest.mock("@scm-manager/ui-components", () => ({
|
||||
confirmAlert: jest.fn(),
|
||||
DeleteButton: require.requireActual("@scm-manager/ui-components").DeleteButton
|
||||
}));
|
||||
|
||||
describe("DeletePermissionButton", () => {
|
||||
const options = new ReactRouterEnzymeContext();
|
||||
|
||||
it("should render nothing, if the delete link is missing", () => {
|
||||
const permission = {
|
||||
_links: {}
|
||||
@@ -20,7 +23,8 @@ describe("DeletePermissionButton", () => {
|
||||
<DeletePermissionButton
|
||||
permission={permission}
|
||||
deletePermission={() => {}}
|
||||
/>
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
expect(navLink.text()).toBe("");
|
||||
});
|
||||
@@ -38,7 +42,8 @@ describe("DeletePermissionButton", () => {
|
||||
<DeletePermissionButton
|
||||
permission={permission}
|
||||
deletePermission={() => {}}
|
||||
/>
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
expect(navLink.text()).not.toBe("");
|
||||
});
|
||||
@@ -56,7 +61,8 @@ describe("DeletePermissionButton", () => {
|
||||
<DeletePermissionButton
|
||||
permission={permission}
|
||||
deletePermission={() => {}}
|
||||
/>
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
button.find("button").simulate("click");
|
||||
|
||||
@@ -82,7 +88,8 @@ describe("DeletePermissionButton", () => {
|
||||
permission={permission}
|
||||
confirmDialog={false}
|
||||
deletePermission={capture}
|
||||
/>
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
button.find("button").simulate("click");
|
||||
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
// @flow
|
||||
import { validation } from "@scm-manager/ui-components";
|
||||
import type {
|
||||
PermissionCollection,
|
||||
} from "@scm-manager/ui-types";
|
||||
import type { PermissionCollection } from "@scm-manager/ui-types";
|
||||
const isNameValid = validation.isNameValid;
|
||||
|
||||
export { isNameValid };
|
||||
|
||||
export const isPermissionValid = (name: string, groupPermission: boolean, permissions: PermissionCollection) => {
|
||||
return isNameValid(name) && !currentPermissionIncludeName(name, groupPermission, permissions);
|
||||
export const isPermissionValid = (
|
||||
name: string,
|
||||
groupPermission: boolean,
|
||||
permissions: PermissionCollection
|
||||
) => {
|
||||
return (
|
||||
isNameValid(name) &&
|
||||
!currentPermissionIncludeName(name, groupPermission, permissions)
|
||||
);
|
||||
};
|
||||
|
||||
const currentPermissionIncludeName = (name: string, groupPermission: boolean, permissions: PermissionCollection) => {
|
||||
const currentPermissionIncludeName = (
|
||||
name: string,
|
||||
groupPermission: boolean,
|
||||
permissions: PermissionCollection
|
||||
) => {
|
||||
for (let i = 0; i < permissions.length; i++) {
|
||||
if (
|
||||
permissions[i].name === name &&
|
||||
permissions[i].groupPermission == groupPermission
|
||||
permissions[i].groupPermission === groupPermission
|
||||
)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ describe("permission validation", () => {
|
||||
{
|
||||
name: "PermissionName",
|
||||
groupPermission: true,
|
||||
type: "READ"
|
||||
type: "READ",
|
||||
_links: {}
|
||||
}
|
||||
];
|
||||
const name = "PermissionName";
|
||||
@@ -33,7 +34,8 @@ describe("permission validation", () => {
|
||||
{
|
||||
name: "PermissionName",
|
||||
groupPermission: false,
|
||||
type: "READ"
|
||||
type: "READ",
|
||||
_links: {}
|
||||
}
|
||||
];
|
||||
const name = "PermissionName";
|
||||
|
||||
@@ -21,11 +21,12 @@ import { Loading, ErrorPage } from "@scm-manager/ui-components";
|
||||
import type {
|
||||
Permission,
|
||||
PermissionCollection,
|
||||
PermissionEntry
|
||||
PermissionCreateEntry
|
||||
} from "@scm-manager/ui-types";
|
||||
import SinglePermission from "./SinglePermission";
|
||||
import CreatePermissionForm from "../components/CreatePermissionForm";
|
||||
import type { History } from "history";
|
||||
import { getPermissionsLink } from "../../modules/repos";
|
||||
|
||||
type Props = {
|
||||
namespace: string,
|
||||
@@ -35,11 +36,13 @@ type Props = {
|
||||
permissions: PermissionCollection,
|
||||
hasPermissionToCreate: boolean,
|
||||
loadingCreatePermission: boolean,
|
||||
permissionsLink: string,
|
||||
|
||||
//dispatch functions
|
||||
fetchPermissions: (namespace: string, repoName: string) => void,
|
||||
fetchPermissions: (link: string, namespace: string, repoName: string) => void,
|
||||
createPermission: (
|
||||
permission: PermissionEntry,
|
||||
link: string,
|
||||
permission: PermissionCreateEntry,
|
||||
namespace: string,
|
||||
repoName: string,
|
||||
callback?: () => void
|
||||
@@ -61,17 +64,19 @@ class Permissions extends React.Component<Props> {
|
||||
repoName,
|
||||
modifyPermissionReset,
|
||||
createPermissionReset,
|
||||
deletePermissionReset
|
||||
deletePermissionReset,
|
||||
permissionsLink
|
||||
} = this.props;
|
||||
|
||||
createPermissionReset(namespace, repoName);
|
||||
modifyPermissionReset(namespace, repoName);
|
||||
deletePermissionReset(namespace, repoName);
|
||||
fetchPermissions(namespace, repoName);
|
||||
fetchPermissions(permissionsLink, namespace, repoName);
|
||||
}
|
||||
|
||||
createPermission = (permission: Permission) => {
|
||||
this.props.createPermission(
|
||||
this.props.permissionsLink,
|
||||
permission,
|
||||
this.props.namespace,
|
||||
this.props.repoName
|
||||
@@ -159,6 +164,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
repoName
|
||||
);
|
||||
const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName);
|
||||
const permissionsLink = getPermissionsLink(state, namespace, repoName);
|
||||
return {
|
||||
namespace,
|
||||
repoName,
|
||||
@@ -166,22 +172,24 @@ const mapStateToProps = (state, ownProps) => {
|
||||
loading,
|
||||
permissions,
|
||||
hasPermissionToCreate,
|
||||
loadingCreatePermission
|
||||
loadingCreatePermission,
|
||||
permissionsLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchPermissions: (namespace: string, repoName: string) => {
|
||||
dispatch(fetchPermissions(namespace, repoName));
|
||||
fetchPermissions: (link: string, namespace: string, repoName: string) => {
|
||||
dispatch(fetchPermissions(link, namespace, repoName));
|
||||
},
|
||||
createPermission: (
|
||||
permission: PermissionEntry,
|
||||
link: string,
|
||||
permission: PermissionCreateEntry,
|
||||
namespace: string,
|
||||
repoName: string,
|
||||
callback?: () => void
|
||||
) => {
|
||||
dispatch(createPermission(permission, namespace, repoName, callback));
|
||||
dispatch(createPermission(link, permission, namespace, repoName, callback));
|
||||
},
|
||||
createPermissionReset: (namespace: string, repoName: string) => {
|
||||
dispatch(createPermissionReset(namespace, repoName));
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { Action } from "@scm-manager/ui-components";
|
||||
import type {
|
||||
PermissionCollection,
|
||||
Permission,
|
||||
PermissionEntry
|
||||
PermissionCreateEntry
|
||||
} from "@scm-manager/ui-types";
|
||||
import { isPending } from "../../../modules/pending";
|
||||
import { getFailure } from "../../../modules/failure";
|
||||
@@ -62,17 +62,19 @@ export const DELETE_PERMISSION_RESET = `${DELETE_PERMISSION}_${
|
||||
types.RESET_SUFFIX
|
||||
}`;
|
||||
|
||||
const REPOS_URL = "repositories";
|
||||
const PERMISSIONS_URL = "permissions";
|
||||
const CONTENT_TYPE = "application/vnd.scmm-permission+json";
|
||||
|
||||
// fetch permissions
|
||||
|
||||
export function fetchPermissions(namespace: string, repoName: string) {
|
||||
export function fetchPermissions(
|
||||
link: string,
|
||||
namespace: string,
|
||||
repoName: string
|
||||
) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchPermissionsPending(namespace, repoName));
|
||||
return apiClient
|
||||
.get(`${REPOS_URL}/${namespace}/${repoName}/${PERMISSIONS_URL}`)
|
||||
.get(link)
|
||||
.then(response => response.json())
|
||||
.then(permissions => {
|
||||
dispatch(fetchPermissionsSuccess(permissions, namespace, repoName));
|
||||
@@ -219,7 +221,8 @@ export function modifyPermissionReset(namespace: string, repoName: string) {
|
||||
|
||||
// create permission
|
||||
export function createPermission(
|
||||
permission: PermissionEntry,
|
||||
link: string,
|
||||
permission: PermissionCreateEntry,
|
||||
namespace: string,
|
||||
repoName: string,
|
||||
callback?: () => void
|
||||
@@ -227,11 +230,7 @@ export function createPermission(
|
||||
return function(dispatch: Dispatch) {
|
||||
dispatch(createPermissionPending(permission, namespace, repoName));
|
||||
return apiClient
|
||||
.post(
|
||||
`${REPOS_URL}/${namespace}/${repoName}/${PERMISSIONS_URL}`,
|
||||
permission,
|
||||
CONTENT_TYPE
|
||||
)
|
||||
.post(link, permission, CONTENT_TYPE)
|
||||
.then(response => {
|
||||
const location = response.headers.get("Location");
|
||||
return apiClient.get(location);
|
||||
@@ -260,7 +259,7 @@ export function createPermission(
|
||||
}
|
||||
|
||||
export function createPermissionPending(
|
||||
permission: PermissionEntry,
|
||||
permission: PermissionCreateEntry,
|
||||
namespace: string,
|
||||
repoName: string
|
||||
): Action {
|
||||
@@ -272,7 +271,7 @@ export function createPermissionPending(
|
||||
}
|
||||
|
||||
export function createPermissionSuccess(
|
||||
permission: PermissionEntry,
|
||||
permission: PermissionCreateEntry,
|
||||
namespace: string,
|
||||
repoName: string
|
||||
): Action {
|
||||
|
||||
@@ -101,6 +101,7 @@ const hitchhiker_puzzle42RepoPermissions = {
|
||||
|
||||
describe("permission fetch", () => {
|
||||
const REPOS_URL = "/api/v2/repositories";
|
||||
const URL = "repositories";
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
@@ -132,7 +133,13 @@ describe("permission fetch", () => {
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(fetchPermissions("hitchhiker", "puzzle42"))
|
||||
.dispatch(
|
||||
fetchPermissions(
|
||||
URL + "/hitchhiker/puzzle42/permissions",
|
||||
"hitchhiker",
|
||||
"puzzle42"
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
@@ -145,7 +152,13 @@ describe("permission fetch", () => {
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(fetchPermissions("hitchhiker", "puzzle42"))
|
||||
.dispatch(
|
||||
fetchPermissions(
|
||||
URL + "/hitchhiker/puzzle42/permissions",
|
||||
"hitchhiker",
|
||||
"puzzle42"
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_PERMISSIONS_PENDING);
|
||||
@@ -247,6 +260,7 @@ describe("permission fetch", () => {
|
||||
return store
|
||||
.dispatch(
|
||||
createPermission(
|
||||
URL + "/hitchhiker/puzzle42/permissions",
|
||||
hitchhiker_puzzle42Permission_user_eins,
|
||||
"hitchhiker",
|
||||
"puzzle42"
|
||||
@@ -268,6 +282,7 @@ describe("permission fetch", () => {
|
||||
return store
|
||||
.dispatch(
|
||||
createPermission(
|
||||
URL + "/hitchhiker/puzzle42/permissions",
|
||||
hitchhiker_puzzle42Permission_user_eins,
|
||||
"hitchhiker",
|
||||
"puzzle42"
|
||||
@@ -304,6 +319,7 @@ describe("permission fetch", () => {
|
||||
return store
|
||||
.dispatch(
|
||||
createPermission(
|
||||
URL + "/hitchhiker/puzzle42/permissions",
|
||||
hitchhiker_puzzle42Permission_user_eins,
|
||||
"hitchhiker",
|
||||
"puzzle42",
|
||||
@@ -640,7 +656,7 @@ describe("permissions selectors", () => {
|
||||
it("should return true, when createPermission is true", () => {
|
||||
const state = {
|
||||
permissions: {
|
||||
["hitchhiker/puzzle42"]: {
|
||||
"hitchhiker/puzzle42": {
|
||||
createPermission: true
|
||||
}
|
||||
}
|
||||
@@ -651,7 +667,7 @@ describe("permissions selectors", () => {
|
||||
it("should return false, when createPermission is false", () => {
|
||||
const state = {
|
||||
permissions: {
|
||||
["hitchhiker/puzzle42"]: {
|
||||
"hitchhiker/puzzle42": {
|
||||
createPermission: false
|
||||
}
|
||||
}
|
||||
|
||||
22
scm-ui/src/repos/sources/components/FileIcon.js
Normal file
22
scm-ui/src/repos/sources/components/FileIcon.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import type { File } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
file: File
|
||||
};
|
||||
|
||||
class FileIcon extends React.Component<Props> {
|
||||
render() {
|
||||
const { file } = this.props;
|
||||
let icon = "file";
|
||||
if (file.subRepository) {
|
||||
icon = "folder-plus";
|
||||
} else if (file.directory) {
|
||||
icon = "folder";
|
||||
}
|
||||
return <i className={`fa fa-${icon}`} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default FileIcon;
|
||||
27
scm-ui/src/repos/sources/components/FileSize.js
Normal file
27
scm-ui/src/repos/sources/components/FileSize.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
bytes: number
|
||||
};
|
||||
|
||||
class FileSize extends React.Component<Props> {
|
||||
static format(bytes: number) {
|
||||
if (!bytes) {
|
||||
return "0 B";
|
||||
}
|
||||
|
||||
const units = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
const size = i === 0 ? bytes : (bytes / 1024 ** i).toFixed(2);
|
||||
return `${size} ${units[i]}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const formattedBytes = FileSize.format(this.props.bytes);
|
||||
return <span>{formattedBytes}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
export default FileSize;
|
||||
10
scm-ui/src/repos/sources/components/FileSize.test.js
Normal file
10
scm-ui/src/repos/sources/components/FileSize.test.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import FileSize from "./FileSize";
|
||||
|
||||
it("should format bytes", () => {
|
||||
expect(FileSize.format(0)).toBe("0 B");
|
||||
expect(FileSize.format(160)).toBe("160 B");
|
||||
expect(FileSize.format(6304)).toBe("6.16 K");
|
||||
expect(FileSize.format(28792588)).toBe("27.46 M");
|
||||
expect(FileSize.format(1369510189)).toBe("1.28 G");
|
||||
expect(FileSize.format(42949672960)).toBe("40.00 G");
|
||||
});
|
||||
184
scm-ui/src/repos/sources/components/FileTree.js
Normal file
184
scm-ui/src/repos/sources/components/FileTree.js
Normal file
@@ -0,0 +1,184 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import injectSheet from "react-jss";
|
||||
import FileTreeLeaf from "./FileTreeLeaf";
|
||||
import type { Repository, File } from "@scm-manager/ui-types";
|
||||
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import {
|
||||
fetchSources,
|
||||
getFetchSourcesFailure,
|
||||
isFetchSourcesPending,
|
||||
getSources
|
||||
} from "../modules/sources";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import { compose } from "redux";
|
||||
|
||||
const styles = {
|
||||
iconColumn: {
|
||||
width: "16px"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
tree: File,
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string,
|
||||
baseUrl: string,
|
||||
fetchSources: (Repository, string, string) => void,
|
||||
// context props
|
||||
classes: any,
|
||||
t: string => string,
|
||||
match: any
|
||||
};
|
||||
|
||||
export function findParent(path: string) {
|
||||
if (path.endsWith("/")) {
|
||||
path = path.substring(0, path.length - 1);
|
||||
}
|
||||
|
||||
const index = path.lastIndexOf("/");
|
||||
if (index > 0) {
|
||||
return path.substring(0, index);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
class FileTree extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchSources, repository, revision, path } = this.props;
|
||||
|
||||
fetchSources(repository, revision, path);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { fetchSources, repository, revision, path } = this.props;
|
||||
if (prevProps.revision !== revision || prevProps.path !== path) {
|
||||
fetchSources(repository, revision, path);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
error,
|
||||
loading,
|
||||
tree,
|
||||
revision,
|
||||
path,
|
||||
baseUrl,
|
||||
classes,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const compareFiles = function(f1: File, f2: File): number {
|
||||
if (f1.directory) {
|
||||
if (f2.directory) {
|
||||
return f1.name.localeCompare(f2.name);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
if (f2.directory) {
|
||||
return 1;
|
||||
} else {
|
||||
return f1.name.localeCompare(f2.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (!tree) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const files = [];
|
||||
|
||||
if (path) {
|
||||
files.push({
|
||||
name: "..",
|
||||
path: findParent(path),
|
||||
directory: true
|
||||
});
|
||||
}
|
||||
|
||||
if (tree._embedded) {
|
||||
files.push(...tree._embedded.children.sort(compareFiles));
|
||||
}
|
||||
|
||||
let baseUrlWithRevision = baseUrl;
|
||||
if (revision) {
|
||||
baseUrlWithRevision += "/" + encodeURIComponent(revision);
|
||||
} else {
|
||||
baseUrlWithRevision += "/" + encodeURIComponent(tree.revision);
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="table table-hover table-sm is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={classes.iconColumn} />
|
||||
<th>{t("sources.file-tree.name")}</th>
|
||||
<th className="is-hidden-mobile">
|
||||
{t("sources.file-tree.length")}
|
||||
</th>
|
||||
<th className="is-hidden-mobile">
|
||||
{t("sources.file-tree.lastModified")}
|
||||
</th>
|
||||
<th>{t("sources.file-tree.description")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map(file => (
|
||||
<FileTreeLeaf
|
||||
key={file.name}
|
||||
file={file}
|
||||
baseUrl={baseUrlWithRevision}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
const { repository, revision, path } = ownProps;
|
||||
|
||||
const loading = isFetchSourcesPending(state, repository, revision, path);
|
||||
const error = getFetchSourcesFailure(state, repository, revision, path);
|
||||
const tree = getSources(state, repository, revision, path);
|
||||
|
||||
return {
|
||||
revision,
|
||||
path,
|
||||
loading,
|
||||
error,
|
||||
tree
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchSources: (repository: Repository, revision: string, path: string) => {
|
||||
dispatch(fetchSources(repository, revision, path));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(injectSheet(styles)(translate("repos")(FileTree)));
|
||||
12
scm-ui/src/repos/sources/components/FileTree.test.js
Normal file
12
scm-ui/src/repos/sources/components/FileTree.test.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import { findParent } from "./FileTree";
|
||||
|
||||
describe("find parent tests", () => {
|
||||
it("should return the parent path", () => {
|
||||
expect(findParent("src/main/js/")).toBe("src/main");
|
||||
expect(findParent("src/main/js")).toBe("src/main");
|
||||
expect(findParent("src/main")).toBe("src");
|
||||
expect(findParent("src")).toBe("");
|
||||
});
|
||||
});
|
||||
81
scm-ui/src/repos/sources/components/FileTreeLeaf.js
Normal file
81
scm-ui/src/repos/sources/components/FileTreeLeaf.js
Normal file
@@ -0,0 +1,81 @@
|
||||
//@flow
|
||||
import * as React from "react";
|
||||
import injectSheet from "react-jss";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import FileSize from "./FileSize";
|
||||
import FileIcon from "./FileIcon";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { File } from "@scm-manager/ui-types";
|
||||
|
||||
const styles = {
|
||||
iconColumn: {
|
||||
width: "16px"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
file: File,
|
||||
baseUrl: string,
|
||||
|
||||
// context props
|
||||
classes: any
|
||||
};
|
||||
|
||||
export function createLink(base: string, file: File) {
|
||||
let link = base;
|
||||
if (file.path) {
|
||||
let path = file.path;
|
||||
if (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
link += "/" + path;
|
||||
}
|
||||
if (!link.endsWith("/")) {
|
||||
link += "/";
|
||||
}
|
||||
return link;
|
||||
}
|
||||
|
||||
class FileTreeLeaf extends React.Component<Props> {
|
||||
createLink = (file: File) => {
|
||||
return createLink(this.props.baseUrl, file);
|
||||
};
|
||||
|
||||
createFileIcon = (file: File) => {
|
||||
if (file.directory) {
|
||||
return (
|
||||
<Link to={this.createLink(file)}>
|
||||
<FileIcon file={file} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return <FileIcon file={file} />;
|
||||
};
|
||||
|
||||
createFileName = (file: File) => {
|
||||
if (file.directory) {
|
||||
return <Link to={this.createLink(file)}>{file.name}</Link>;
|
||||
}
|
||||
return file.name;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { file, classes } = this.props;
|
||||
|
||||
const fileSize = file.directory ? "" : <FileSize bytes={file.length} />;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className={classes.iconColumn}>{this.createFileIcon(file)}</td>
|
||||
<td>{this.createFileName(file)}</td>
|
||||
<td className="is-hidden-mobile">{fileSize}</td>
|
||||
<td className="is-hidden-mobile">
|
||||
<DateFromNow date={file.lastModified} />
|
||||
</td>
|
||||
<td>{file.description}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(FileTreeLeaf);
|
||||
24
scm-ui/src/repos/sources/components/FileTreeLeaf.test.js
Normal file
24
scm-ui/src/repos/sources/components/FileTreeLeaf.test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// @flow
|
||||
|
||||
import { createLink } from "./FileTreeLeaf";
|
||||
import type { File } from "@scm-manager/ui-types";
|
||||
|
||||
describe("create link tests", () => {
|
||||
function dir(path: string): File {
|
||||
return {
|
||||
name: "dir",
|
||||
path: path,
|
||||
directory: true
|
||||
};
|
||||
}
|
||||
|
||||
it("should create link", () => {
|
||||
expect(createLink("src", dir("main"))).toBe("src/main/");
|
||||
expect(createLink("src", dir("/main"))).toBe("src/main/");
|
||||
expect(createLink("src", dir("/main/"))).toBe("src/main/");
|
||||
});
|
||||
|
||||
it("should return base url if the directory path is empty", () => {
|
||||
expect(createLink("src", dir(""))).toBe("src/");
|
||||
});
|
||||
});
|
||||
131
scm-ui/src/repos/sources/containers/Sources.js
Normal file
131
scm-ui/src/repos/sources/containers/Sources.js
Normal file
@@ -0,0 +1,131 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import type { Repository, Branch } from "@scm-manager/ui-types";
|
||||
import FileTree from "../components/FileTree";
|
||||
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import BranchSelector from "../../containers/BranchSelector";
|
||||
import {
|
||||
fetchBranches,
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} from "../../modules/branches";
|
||||
import { compose } from "redux";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
baseUrl: string,
|
||||
branches: Branch[],
|
||||
revision: string,
|
||||
path: string,
|
||||
|
||||
// dispatch props
|
||||
fetchBranches: Repository => void,
|
||||
|
||||
// Context props
|
||||
history: any,
|
||||
match: any
|
||||
};
|
||||
|
||||
class Sources extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { fetchBranches, repository } = this.props;
|
||||
|
||||
fetchBranches(repository);
|
||||
}
|
||||
|
||||
branchSelected = (branch?: Branch) => {
|
||||
const { baseUrl, history, path } = this.props;
|
||||
|
||||
let url;
|
||||
if (branch) {
|
||||
if (path) {
|
||||
url = `${baseUrl}/${encodeURIComponent(branch.name)}/${path}`;
|
||||
} else {
|
||||
url = `${baseUrl}/${encodeURIComponent(branch.name)}/`;
|
||||
}
|
||||
} else {
|
||||
url = `${baseUrl}/`;
|
||||
}
|
||||
history.push(url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { repository, baseUrl, loading, error, revision, path } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderBranchSelector()}
|
||||
<FileTree
|
||||
repository={repository}
|
||||
revision={revision}
|
||||
path={path}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderBranchSelector = () => {
|
||||
const { repository, branches, revision } = this.props;
|
||||
if (repository._links.branches) {
|
||||
return (
|
||||
<BranchSelector
|
||||
branches={branches}
|
||||
selectedBranch={revision}
|
||||
selected={(b: Branch) => {
|
||||
this.branchSelected(b);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { repository, match } = ownProps;
|
||||
const { revision, path } = match.params;
|
||||
const decodedRevision = revision ? decodeURIComponent(revision) : undefined;
|
||||
|
||||
const loading = isFetchBranchesPending(state, repository);
|
||||
const error = getFetchBranchesFailure(state, repository);
|
||||
const branches = getBranches(state, repository);
|
||||
|
||||
return {
|
||||
repository,
|
||||
revision: decodedRevision,
|
||||
path,
|
||||
loading,
|
||||
error,
|
||||
branches
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchBranches: (repository: Repository) => {
|
||||
dispatch(fetchBranches(repository));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(Sources);
|
||||
141
scm-ui/src/repos/sources/modules/sources.js
Normal file
141
scm-ui/src/repos/sources/modules/sources.js
Normal file
@@ -0,0 +1,141 @@
|
||||
// @flow
|
||||
|
||||
import * as types from "../../../modules/types";
|
||||
import type { Repository, File, Action } from "@scm-manager/ui-types";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import { isPending } from "../../../modules/pending";
|
||||
import { getFailure } from "../../../modules/failure";
|
||||
|
||||
export const FETCH_SOURCES = "scm/repos/FETCH_SOURCES";
|
||||
export const FETCH_SOURCES_PENDING = `${FETCH_SOURCES}_${types.PENDING_SUFFIX}`;
|
||||
export const FETCH_SOURCES_SUCCESS = `${FETCH_SOURCES}_${types.SUCCESS_SUFFIX}`;
|
||||
export const FETCH_SOURCES_FAILURE = `${FETCH_SOURCES}_${types.FAILURE_SUFFIX}`;
|
||||
|
||||
export function fetchSources(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchSourcesPending(repository, revision, path));
|
||||
return apiClient
|
||||
.get(createUrl(repository, revision, path))
|
||||
.then(response => response.json())
|
||||
.then(sources => {
|
||||
dispatch(fetchSourcesSuccess(repository, revision, path, sources));
|
||||
})
|
||||
.catch(err => {
|
||||
const error = new Error(`failed to fetch sources: ${err.message}`);
|
||||
dispatch(fetchSourcesFailure(repository, revision, path, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createUrl(repository: Repository, revision: string, path: string) {
|
||||
const base = repository._links.sources.href;
|
||||
if (!revision && !path) {
|
||||
return base;
|
||||
}
|
||||
|
||||
// TODO handle trailing slash
|
||||
const pathDefined = path ? path : "";
|
||||
return `${base}${encodeURIComponent(revision)}/${pathDefined}`;
|
||||
}
|
||||
|
||||
export function fetchSourcesPending(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_SOURCES_PENDING,
|
||||
itemId: createItemId(repository, revision, path)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSourcesSuccess(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string,
|
||||
sources: File
|
||||
) {
|
||||
return {
|
||||
type: FETCH_SOURCES_SUCCESS,
|
||||
payload: sources,
|
||||
itemId: createItemId(repository, revision, path)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSourcesFailure(
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string,
|
||||
error: Error
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_SOURCES_FAILURE,
|
||||
payload: error,
|
||||
itemId: createItemId(repository, revision, path)
|
||||
};
|
||||
}
|
||||
|
||||
function createItemId(repository: Repository, revision: string, path: string) {
|
||||
const revPart = revision ? revision : "_";
|
||||
const pathPart = path ? path : "";
|
||||
return `${repository.namespace}/${repository.name}/${revPart}/${pathPart}`;
|
||||
}
|
||||
|
||||
// reducer
|
||||
|
||||
export default function reducer(
|
||||
state: any = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): any {
|
||||
if (action.type === FETCH_SOURCES_SUCCESS) {
|
||||
return {
|
||||
[action.itemId]: action.payload,
|
||||
...state
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// selectors
|
||||
|
||||
export function getSources(
|
||||
state: any,
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
): ?File {
|
||||
if (state.sources) {
|
||||
return state.sources[createItemId(repository, revision, path)];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isFetchSourcesPending(
|
||||
state: any,
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
): boolean {
|
||||
return isPending(
|
||||
state,
|
||||
FETCH_SOURCES,
|
||||
createItemId(repository, revision, path)
|
||||
);
|
||||
}
|
||||
|
||||
export function getFetchSourcesFailure(
|
||||
state: any,
|
||||
repository: Repository,
|
||||
revision: string,
|
||||
path: string
|
||||
): ?Error {
|
||||
return getFailure(
|
||||
state,
|
||||
FETCH_SOURCES,
|
||||
createItemId(repository, revision, path)
|
||||
);
|
||||
}
|
||||
220
scm-ui/src/repos/sources/modules/sources.test.js
Normal file
220
scm-ui/src/repos/sources/modules/sources.test.js
Normal file
@@ -0,0 +1,220 @@
|
||||
// @flow
|
||||
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import {
|
||||
FETCH_SOURCES,
|
||||
FETCH_SOURCES_FAILURE,
|
||||
FETCH_SOURCES_PENDING,
|
||||
FETCH_SOURCES_SUCCESS,
|
||||
fetchSources,
|
||||
getFetchSourcesFailure,
|
||||
isFetchSourcesPending,
|
||||
default as reducer,
|
||||
getSources,
|
||||
fetchSourcesSuccess
|
||||
} from "./sources";
|
||||
|
||||
const sourcesUrl =
|
||||
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/";
|
||||
|
||||
const repository: Repository = {
|
||||
name: "core",
|
||||
namespace: "scm",
|
||||
type: "git",
|
||||
_links: {
|
||||
sources: {
|
||||
href: sourcesUrl
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const collection = {
|
||||
revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4",
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/"
|
||||
}
|
||||
},
|
||||
_embedded: {
|
||||
files: [
|
||||
{
|
||||
name: "src",
|
||||
path: "src",
|
||||
directory: true,
|
||||
description: null,
|
||||
length: 176,
|
||||
lastModified: null,
|
||||
subRepository: null,
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/src"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "package.json",
|
||||
path: "package.json",
|
||||
directory: false,
|
||||
description: "bump version",
|
||||
length: 780,
|
||||
lastModified: "2017-07-31T11:17:19Z",
|
||||
subRepository: null,
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/content/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/package.json"
|
||||
},
|
||||
history: {
|
||||
href:
|
||||
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/history/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/package.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
describe("sources fetch", () => {
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should fetch the sources of the repository", () => {
|
||||
fetchMock.getOnce(sourcesUrl, collection);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_SOURCES_PENDING, itemId: "scm/core/_/" },
|
||||
{
|
||||
type: FETCH_SOURCES_SUCCESS,
|
||||
itemId: "scm/core/_/",
|
||||
payload: collection
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchSources(repository)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch the sources of the repository with the given revision and path", () => {
|
||||
fetchMock.getOnce(sourcesUrl + "abc/src", collection);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_SOURCES_PENDING, itemId: "scm/core/abc/src" },
|
||||
{
|
||||
type: FETCH_SOURCES_SUCCESS,
|
||||
itemId: "scm/core/abc/src",
|
||||
payload: collection
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchSources(repository, "abc", "src")).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch FETCH_SOURCES_FAILURE on server error", () => {
|
||||
fetchMock.getOnce(sourcesUrl, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchSources(repository)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toBe(FETCH_SOURCES_PENDING);
|
||||
expect(actions[1].type).toBe(FETCH_SOURCES_FAILURE);
|
||||
expect(actions[1].itemId).toBe("scm/core/_/");
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("reducer tests", () => {
|
||||
it("should return unmodified state on unknown action", () => {
|
||||
const state = {};
|
||||
expect(reducer(state)).toBe(state);
|
||||
});
|
||||
|
||||
it("should store the collection, without revision and path", () => {
|
||||
const expectedState = {
|
||||
"scm/core/_/": collection
|
||||
};
|
||||
expect(
|
||||
reducer({}, fetchSourcesSuccess(repository, null, null, collection))
|
||||
).toEqual(expectedState);
|
||||
});
|
||||
|
||||
it("should store the collection, with revision and path", () => {
|
||||
const expectedState = {
|
||||
"scm/core/abc/src/main": collection
|
||||
};
|
||||
expect(
|
||||
reducer(
|
||||
{},
|
||||
fetchSourcesSuccess(repository, "abc", "src/main", collection)
|
||||
)
|
||||
).toEqual(expectedState);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selector tests", () => {
|
||||
it("should return null", () => {
|
||||
expect(getSources({}, repository)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return the source collection without revision and path", () => {
|
||||
const state = {
|
||||
sources: {
|
||||
"scm/core/_/": collection
|
||||
}
|
||||
};
|
||||
expect(getSources(state, repository)).toBe(collection);
|
||||
});
|
||||
|
||||
it("should return the source collection without revision and path", () => {
|
||||
const state = {
|
||||
sources: {
|
||||
"scm/core/abc/src/main": collection
|
||||
}
|
||||
};
|
||||
expect(getSources(state, repository, "abc", "src/main")).toBe(collection);
|
||||
});
|
||||
|
||||
it("should return true, when fetch sources is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_SOURCES + "/scm/core/_/"]: true
|
||||
}
|
||||
};
|
||||
expect(isFetchSourcesPending(state, repository)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false, when fetch sources is not pending", () => {
|
||||
expect(isFetchSourcesPending({}, repository)).toEqual(false);
|
||||
});
|
||||
|
||||
const error = new Error("incredible error from hell");
|
||||
|
||||
it("should return error when fetch sources did fail", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_SOURCES + "/scm/core/_/"]: error
|
||||
}
|
||||
};
|
||||
expect(getFetchSourcesFailure(state, repository)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined when fetch sources did not fail", () => {
|
||||
expect(getFetchSourcesFailure({}, repository)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -12,13 +12,15 @@ import {
|
||||
} from "../modules/users";
|
||||
import { Page } from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
import {getUsersLink} from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
loading?: boolean,
|
||||
error?: Error,
|
||||
usersLink: string,
|
||||
|
||||
// dispatcher functions
|
||||
addUser: (user: User, callback?: () => void) => void,
|
||||
addUser: (link: string, user: User, callback?: () => void) => void,
|
||||
resetForm: () => void,
|
||||
|
||||
// context objects
|
||||
@@ -37,7 +39,7 @@ class AddUser extends React.Component<Props> {
|
||||
};
|
||||
|
||||
createUser = (user: User) => {
|
||||
this.props.addUser(user, this.userCreated);
|
||||
this.props.addUser(this.props.usersLink, user, this.userCreated);
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -61,8 +63,8 @@ class AddUser extends React.Component<Props> {
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
addUser: (user: User, callback?: () => void) => {
|
||||
dispatch(createUser(user, callback));
|
||||
addUser: (link: string, user: User, callback?: () => void) => {
|
||||
dispatch(createUser(link, user, callback));
|
||||
},
|
||||
resetForm: () => {
|
||||
dispatch(createUserReset());
|
||||
@@ -73,7 +75,9 @@ const mapDispatchToProps = dispatch => {
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const loading = isCreateUserPending(state);
|
||||
const error = getCreateUserFailure(state);
|
||||
const usersLink = getUsersLink(state);
|
||||
return {
|
||||
usersLink,
|
||||
loading,
|
||||
error
|
||||
};
|
||||
|
||||
@@ -26,16 +26,18 @@ import {
|
||||
|
||||
import { DeleteUserNavLink, EditUserNavLink } from "./../components/navLinks";
|
||||
import { translate } from "react-i18next";
|
||||
import { getUsersLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
user: User,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
usersLink: string,
|
||||
|
||||
// dispatcher functions
|
||||
deleteUser: (user: User, callback?: () => void) => void,
|
||||
fetchUser: string => void,
|
||||
fetchUser: (string, string) => void,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
@@ -45,7 +47,7 @@ type Props = {
|
||||
|
||||
class SingleUser extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchUser(this.props.name);
|
||||
this.props.fetchUser(this.props.usersLink, this.props.name);
|
||||
}
|
||||
|
||||
userDeleted = () => {
|
||||
@@ -124,8 +126,9 @@ const mapStateToProps = (state, ownProps) => {
|
||||
isFetchUserPending(state, name) || isDeleteUserPending(state, name);
|
||||
const error =
|
||||
getFetchUserFailure(state, name) || getDeleteUserFailure(state, name);
|
||||
|
||||
const usersLink = getUsersLink(state);
|
||||
return {
|
||||
usersLink,
|
||||
name,
|
||||
user,
|
||||
loading,
|
||||
@@ -135,8 +138,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchUser: (name: string) => {
|
||||
dispatch(fetchUser(name));
|
||||
fetchUser: (link: string, name: string) => {
|
||||
dispatch(fetchUser(link, name));
|
||||
},
|
||||
deleteUser: (user: User, callback?: () => void) => {
|
||||
dispatch(deleteUser(user, callback));
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Page, Paginator } from "@scm-manager/ui-components";
|
||||
import { UserTable } from "./../components/table";
|
||||
import type { User, PagedCollection } from "@scm-manager/ui-types";
|
||||
import CreateUserButton from "../components/buttons/CreateUserButton";
|
||||
import { getUsersLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
users: User[],
|
||||
@@ -26,19 +27,20 @@ type Props = {
|
||||
canAddUsers: boolean,
|
||||
list: PagedCollection,
|
||||
page: number,
|
||||
usersLink: string,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
history: History,
|
||||
|
||||
// dispatch functions
|
||||
fetchUsersByPage: (page: number) => void,
|
||||
fetchUsersByPage: (link: string, page: number) => void,
|
||||
fetchUsersByLink: (link: string) => void
|
||||
};
|
||||
|
||||
class Users extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchUsersByPage(this.props.page);
|
||||
this.props.fetchUsersByPage(this.props.usersLink, this.props.page);
|
||||
}
|
||||
|
||||
onPageChange = (link: string) => {
|
||||
@@ -107,6 +109,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const loading = isFetchUsersPending(state);
|
||||
const error = getFetchUsersFailure(state);
|
||||
|
||||
const usersLink = getUsersLink(state);
|
||||
|
||||
const page = getPageFromProps(ownProps);
|
||||
const canAddUsers = isPermittedToCreateUsers(state);
|
||||
const list = selectListAsCollection(state);
|
||||
@@ -117,14 +121,15 @@ const mapStateToProps = (state, ownProps) => {
|
||||
error,
|
||||
canAddUsers,
|
||||
list,
|
||||
page
|
||||
page,
|
||||
usersLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchUsersByPage: (page: number) => {
|
||||
dispatch(fetchUsersByPage(page));
|
||||
fetchUsersByPage: (link: string, page: number) => {
|
||||
dispatch(fetchUsersByPage(link, page));
|
||||
},
|
||||
fetchUsersByLink: (link: string) => {
|
||||
dispatch(fetchUsersByLink(link));
|
||||
|
||||
@@ -32,21 +32,19 @@ export const DELETE_USER_PENDING = `${DELETE_USER}_${types.PENDING_SUFFIX}`;
|
||||
export const DELETE_USER_SUCCESS = `${DELETE_USER}_${types.SUCCESS_SUFFIX}`;
|
||||
export const DELETE_USER_FAILURE = `${DELETE_USER}_${types.FAILURE_SUFFIX}`;
|
||||
|
||||
const USERS_URL = "users";
|
||||
|
||||
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
|
||||
|
||||
// TODO i18n for error messages
|
||||
|
||||
// fetch users
|
||||
|
||||
export function fetchUsers() {
|
||||
return fetchUsersByLink(USERS_URL);
|
||||
export function fetchUsers(link: string) {
|
||||
return fetchUsersByLink(link);
|
||||
}
|
||||
|
||||
export function fetchUsersByPage(page: number) {
|
||||
export function fetchUsersByPage(link: string, page: number) {
|
||||
// backend start counting by 0
|
||||
return fetchUsersByLink(USERS_URL + "?page=" + (page - 1));
|
||||
return fetchUsersByLink(link + "?page=" + (page - 1));
|
||||
}
|
||||
|
||||
export function fetchUsersByLink(link: string) {
|
||||
@@ -60,7 +58,7 @@ export function fetchUsersByLink(link: string) {
|
||||
})
|
||||
.catch(cause => {
|
||||
const error = new Error(`could not fetch users: ${cause.message}`);
|
||||
dispatch(fetchUsersFailure(USERS_URL, error));
|
||||
dispatch(fetchUsersFailure(link, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -89,8 +87,8 @@ export function fetchUsersFailure(url: string, error: Error): Action {
|
||||
}
|
||||
|
||||
//fetch user
|
||||
export function fetchUser(name: string) {
|
||||
const userUrl = USERS_URL + "/" + name;
|
||||
export function fetchUser(link: string, name: string) {
|
||||
const userUrl = link.endsWith("/") ? link + name : link + "/" + name;
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchUserPending(name));
|
||||
return apiClient
|
||||
@@ -137,11 +135,11 @@ export function fetchUserFailure(name: string, error: Error): Action {
|
||||
|
||||
//create user
|
||||
|
||||
export function createUser(user: User, callback?: () => void) {
|
||||
export function createUser(link: string, user: User, callback?: () => void) {
|
||||
return function(dispatch: Dispatch) {
|
||||
dispatch(createUserPending(user));
|
||||
return apiClient
|
||||
.post(USERS_URL, user, CONTENT_TYPE_USER)
|
||||
.post(link, user, CONTENT_TYPE_USER)
|
||||
.then(() => {
|
||||
dispatch(createUserSuccess());
|
||||
if (callback) {
|
||||
|
||||
@@ -122,6 +122,7 @@ const response = {
|
||||
responseBody
|
||||
};
|
||||
|
||||
const URL = "users";
|
||||
const USERS_URL = "/api/v2/users";
|
||||
|
||||
const error = new Error("KAPUTT");
|
||||
@@ -146,7 +147,7 @@ describe("users fetch()", () => {
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(fetchUsers()).then(() => {
|
||||
return store.dispatch(fetchUsers(URL)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -157,7 +158,7 @@ describe("users fetch()", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchUsers()).then(() => {
|
||||
return store.dispatch(fetchUsers(URL)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_USERS_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_USERS_FAILURE);
|
||||
@@ -169,7 +170,7 @@ describe("users fetch()", () => {
|
||||
fetchMock.getOnce(USERS_URL + "/zaphod", userZaphod);
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchUser("zaphod")).then(() => {
|
||||
return store.dispatch(fetchUser(URL, "zaphod")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_USER_SUCCESS);
|
||||
@@ -183,7 +184,7 @@ describe("users fetch()", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchUser("zaphod")).then(() => {
|
||||
return store.dispatch(fetchUser(URL, "zaphod")).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_USER_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_USER_FAILURE);
|
||||
@@ -201,7 +202,7 @@ describe("users fetch()", () => {
|
||||
fetchMock.getOnce(USERS_URL, response);
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createUser(userZaphod)).then(() => {
|
||||
return store.dispatch(createUser(URL, userZaphod)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_USER_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_USER_SUCCESS);
|
||||
@@ -214,7 +215,7 @@ describe("users fetch()", () => {
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createUser(userZaphod)).then(() => {
|
||||
return store.dispatch(createUser(URL, userZaphod)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(CREATE_USER_PENDING);
|
||||
expect(actions[1].type).toEqual(CREATE_USER_FAILURE);
|
||||
@@ -235,7 +236,7 @@ describe("users fetch()", () => {
|
||||
};
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(createUser(userZaphod, callback)).then(() => {
|
||||
return store.dispatch(createUser(URL, userZaphod, callback)).then(() => {
|
||||
expect(callMe).toBe("yeah");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user