Merged in feature/ui-reconstructionOfUrlsToIndexResource (pull request #91)

Feature/ui reconstructionOfUrlsToIndexResource
This commit is contained in:
Sebastian Sdorra
2018-10-24 10:06:54 +00:00
38 changed files with 1223 additions and 276 deletions

View File

@@ -4,38 +4,58 @@ import { translate } from "react-i18next";
import PrimaryNavigationLink from "./PrimaryNavigationLink";
type Props = {
t: string => string
t: string => string,
repositoriesLink: string,
usersLink: string,
groupsLink: string,
configLink: string,
logoutLink: string
};
class PrimaryNavigation extends React.Component<Props> {
render() {
const { t } = this.props;
const { t, repositoriesLink, usersLink, groupsLink, configLink, logoutLink } = this.props;
const links = [
repositoriesLink ? (
<PrimaryNavigationLink
to="/repos"
match="/(repo|repos)"
label={t("primary-navigation.repositories")}
key={"repositoriesLink"}
/>): null,
usersLink ? (
<PrimaryNavigationLink
to="/users"
match="/(user|users)"
label={t("primary-navigation.users")}
key={"usersLink"}
/>) : null,
groupsLink ? (
<PrimaryNavigationLink
to="/groups"
match="/(group|groups)"
label={t("primary-navigation.groups")}
key={"groupsLink"}
/>) : null,
configLink ? (
<PrimaryNavigationLink
to="/config"
label={t("primary-navigation.config")}
key={"configLink"}
/>) : null,
logoutLink ? (
<PrimaryNavigationLink
to="/logout"
label={t("primary-navigation.logout")}
key={"logoutLink"}
/>) : null
];
return (
<nav className="tabs is-boxed">
<ul>
<PrimaryNavigationLink
to="/repos"
match="/(repo|repos)"
label={t("primary-navigation.repositories")}
/>
<PrimaryNavigationLink
to="/users"
match="/(user|users)"
label={t("primary-navigation.users")}
/>
<PrimaryNavigationLink
to="/groups"
match="/(group|groups)"
label={t("primary-navigation.groups")}
/>
<PrimaryNavigationLink
to="/config"
label={t("primary-navigation.config")}
/>
<PrimaryNavigationLink
to="/logout"
label={t("primary-navigation.logout")}
/>
{links}
</ul>
</nav>
);

View File

@@ -0,0 +1,7 @@
//@flow
import type { Links } from "./hal";
export type IndexResources = {
version: string,
_links: Links
};

View File

@@ -17,6 +17,8 @@ export type { Tag } from "./Tags";
export type { Config } from "./Config";
export type { IndexResources } from "./IndexResources";
export type { Permission, PermissionCreateEntry, PermissionCollection } from "./RepositoryPermissions";
export type { SubRepository, File } from "./Sources";

View File

@@ -73,29 +73,34 @@
"label": "Branches"
},
"permission": {
"error-title": "Error",
"error-subtitle": "Unknown permissions error",
"name": "User or Group",
"type": "Type",
"group-permission": "Group Permission",
"edit-permission": {
"delete-button": "Delete",
"save-button": "Save Changes"
},
"delete-permission-button": {
"label": "Delete",
"confirm-alert": {
"title": "Delete permission",
"message": "Do you really want to delete the permission?",
"submit": "Yes",
"cancel": "No"
"error-title": "Error",
"error-subtitle": "Unknown permissions error",
"name": "User or Group",
"type": "Type",
"group-permission": "Group Permission",
"edit-permission": {
"delete-button": "Delete",
"save-button": "Save Changes"
},
"delete-permission-button": {
"label": "Delete",
"confirm-alert": {
"title": "Delete permission",
"message": "Do you really want to delete the permission?",
"submit": "Yes",
"cancel": "No"
}
},
"add-permission": {
"add-permission-heading": "Add new Permission",
"submit-button": "Submit",
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
},
"help": {
"groupPermissionHelpText": "States if a permission is a group permission.",
"nameHelpText": "Manage permissions for a specific user or group",
"typeHelpText": "READ = read; WRITE = read and write; OWNER = read, write and also the ability to manage the properties and permissions"
}
},
"add-permission": {
"add-permission-heading": "Add new Permission",
"submit-button": "Submit",
"name-input-invalid": "Permission is not allowed to be empty! If it is not empty, your input name is invalid or it already exists!"
}
},
"help": {
"nameHelpText": "The name of the repository. This name will be part of the repository url.",

View File

@@ -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
};
};

View File

@@ -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();
})

View File

@@ -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);

View File

@@ -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
};
};

View 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))
);

View File

@@ -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))
};
};

View File

@@ -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))
};
};

View File

@@ -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);

View File

@@ -15,6 +15,7 @@ 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";
@@ -27,6 +28,7 @@ function createReduxStore(history: BrowserHistory) {
router: routerReducer,
pending,
failure,
indexResources,
users,
repos,
repositoryTypes,

View File

@@ -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
};

View File

@@ -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));

View File

@@ -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));

View File

@@ -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) {

View File

@@ -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: {

View File

@@ -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>,

View File

@@ -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));
});

View File

@@ -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);

View 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");
}

View 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);
});
});

View File

@@ -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());

View File

@@ -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));

View File

@@ -34,6 +34,7 @@ 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,
@@ -41,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
@@ -54,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) => {
@@ -212,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));

View File

@@ -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;
}

View File

@@ -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,7 +397,7 @@ 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");
});
});
@@ -411,7 +408,7 @@ describe("repos fetch", () => {
});
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: {

View File

@@ -51,14 +51,17 @@ 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"}
/>

View File

@@ -7,13 +7,15 @@ 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 types = ["READ", "WRITE", "OWNER"];
const { type, handleTypeChange, loading, label, helpText } = this.props;
const types = ["READ", "OWNER", "WRITE"];
return (
<Select
@@ -21,6 +23,8 @@ class TypeSelector extends React.Component<Props> {
value={type ? type : "READ"}
options={this.createSelectOptions(types)}
loading={loading}
label={label}
helpText={helpText}
/>
);
}
@@ -35,4 +39,4 @@ class TypeSelector extends React.Component<Props> {
}
}
export default translate("permissions")(TypeSelector);
export default translate("repos")(TypeSelector);

View File

@@ -26,6 +26,7 @@ import type {
import SinglePermission from "./SinglePermission";
import CreatePermissionForm from "../components/CreatePermissionForm";
import type { History } from "history";
import { getPermissionsLink } from "../../modules/repos";
type Props = {
namespace: string,
@@ -35,10 +36,12 @@ 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: (
link: string,
permission: PermissionCreateEntry,
namespace: string,
repoName: string,
@@ -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: (
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));

View File

@@ -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,6 +221,7 @@ export function modifyPermissionReset(namespace: string, repoName: string) {
// create permission
export function createPermission(
link: string,
permission: PermissionCreateEntry,
namespace: string,
repoName: string,
@@ -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);

View File

@@ -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",

View File

@@ -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
};

View File

@@ -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));

View File

@@ -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));

View File

@@ -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) {

View File

@@ -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");
});
});