Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2018-12-19 15:09:40 +01:00
145 changed files with 3447 additions and 938 deletions

View File

@@ -32,9 +32,8 @@ export function fetchConfig(link: string) {
.then(data => {
dispatch(fetchConfigSuccess(data));
})
.catch(cause => {
const error = new Error(`could not fetch config: ${cause.message}`);
dispatch(fetchConfigFailure(error));
.catch(err => {
dispatch(fetchConfigFailure(err));
});
};
}
@@ -73,13 +72,8 @@ export function modifyConfig(config: Config, callback?: () => void) {
callback();
}
})
.catch(cause => {
dispatch(
modifyConfigFailure(
config,
new Error(`could not modify config: ${cause.message}`)
)
);
.catch(err => {
dispatch(modifyConfigFailure(config, err));
});
};
}

View File

@@ -21,7 +21,8 @@ type State = {
password: string,
loading: boolean,
error?: Error,
passwordChanged: boolean
passwordChanged: boolean,
passwordValid: boolean
};
class ChangeUserPassword extends React.Component<Props, State> {
@@ -35,7 +36,8 @@ class ChangeUserPassword extends React.Component<Props, State> {
passwordConfirmationError: false,
validatePasswordError: false,
validatePassword: "",
passwordChanged: false
passwordChanged: false,
passwordValid: false
};
}
@@ -83,6 +85,10 @@ class ChangeUserPassword extends React.Component<Props, State> {
}
};
isValid = () => {
return this.state.oldPassword && this.state.passwordValid;
};
render() {
const { t } = this.props;
const { loading, passwordChanged, error } = this.state;
@@ -118,7 +124,7 @@ class ChangeUserPassword extends React.Component<Props, State> {
key={this.state.passwordChanged ? "changed" : "unchanged"}
/>
<SubmitButton
disabled={!this.state.password}
disabled={!this.isValid()}
loading={loading}
label={t("password.submit")}
/>
@@ -126,8 +132,8 @@ class ChangeUserPassword extends React.Component<Props, State> {
);
}
passwordChanged = (password: string) => {
this.setState({ ...this.state, password });
passwordChanged = (password: string, passwordValid: boolean) => {
this.setState({ ...this.state, password, passwordValid: (!!password && passwordValid) });
};
onClose = () => {

View File

@@ -1,8 +1,7 @@
// @flow
import React from "react";
import AvatarWrapper from "../repos/components/changesets/AvatarWrapper";
import type { Me } from "@scm-manager/ui-types";
import { MailLink } from "@scm-manager/ui-components";
import { MailLink, AvatarWrapper, AvatarImage } from "@scm-manager/ui-components";
import { compose } from "redux";
import { translate } from "react-i18next";
@@ -18,37 +17,35 @@ class ProfileInfo extends React.Component<Props, State> {
render() {
const { me, t } = this.props;
return (
<>
<div className="media">
<AvatarWrapper>
<div>
<figure className="media-left">
<p className="image is-64x64">
{
// TODO: add avatar
}
</p>
</figure>
</div>
<figure className="media-left">
<p className="image is-64x64">
<AvatarImage person={ me }/>
</p>
</figure>
</AvatarWrapper>
<table className="table">
<tbody>
<tr>
<td className="has-text-weight-semibold">{t("profile.username")}</td>
<td>{me.name}</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("profile.displayName")}</td>
<td>{me.displayName}</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("profile.mail")}</td>
<td>
<MailLink address={me.mail} />
</td>
</tr>
</tbody>
</table>
</>
<div className="media-content">
<table className="table">
<tbody>
<tr>
<td className="has-text-weight-semibold">{t("profile.username")}</td>
<td>{me.name}</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("profile.displayName")}</td>
<td>{me.displayName}</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("profile.mail")}</td>
<td>
<MailLink address={me.mail} />
</td>
</tr>
</tbody>
</table>
</div>
</div>
);
}
}

View File

@@ -2,12 +2,12 @@
import React from "react";
import { translate } from "react-i18next";
import {
AutocompleteAddEntryToTableField,
InputField,
SubmitButton,
Textarea,
AddEntryToTableField
Textarea
} from "@scm-manager/ui-components";
import type { Group } from "@scm-manager/ui-types";
import type { Group, SelectValue } from "@scm-manager/ui-types";
import * as validator from "./groupValidation";
import MemberNameTable from "./MemberNameTable";
@@ -16,7 +16,8 @@ type Props = {
t: string => string,
submitForm: Group => void,
loading?: boolean,
group?: Group
group?: Group,
loadUserSuggestions: string => any
};
type State = {
@@ -70,7 +71,7 @@ class GroupForm extends React.Component<Props, State> {
render() {
const { t, loading } = this.props;
const group = this.state.group;
const { group } = this.state;
let nameField = null;
if (!this.props.group) {
nameField = (
@@ -97,15 +98,20 @@ class GroupForm extends React.Component<Props, State> {
helpText={t("group-form.help.descriptionHelpText")}
/>
<MemberNameTable
members={this.state.group.members}
members={group.members}
memberListChanged={this.memberListChanged}
/>
<AddEntryToTableField
<AutocompleteAddEntryToTableField
addEntry={this.addMember}
disabled={false}
buttonLabel={t("add-member-button.label")}
fieldLabel={t("add-member-textfield.label")}
errorMessage={t("add-member-textfield.error")}
loadSuggestions={this.props.loadUserSuggestions}
placeholder={t("add-member-autocomplete.placeholder")}
loadingMessage={t("add-member-autocomplete.loading")}
noOptionsMessage={t("add-member-autocomplete.no-options")}
/>
<SubmitButton
disabled={!this.isValid()}
@@ -126,8 +132,8 @@ class GroupForm extends React.Component<Props, State> {
});
};
addMember = (membername: string) => {
if (this.isMember(membername)) {
addMember = (value: SelectValue) => {
if (this.isMember(value.value.id)) {
return;
}
@@ -135,7 +141,7 @@ class GroupForm extends React.Component<Props, State> {
...this.state,
group: {
...this.state.group,
members: [...this.state.group.members, membername]
members: [...this.state.group.members, value.value.id]
}
});
};

View File

@@ -13,7 +13,10 @@ import {
} from "../modules/groups";
import type { Group } from "@scm-manager/ui-types";
import type { History } from "history";
import { getGroupsLink } from "../../modules/indexResource";
import {
getGroupsLink,
getUserAutoCompleteLink
} from "../../modules/indexResource";
type Props = {
t: string => string,
@@ -22,7 +25,8 @@ type Props = {
loading?: boolean,
error?: Error,
resetForm: () => void,
createLink: string
createLink: string,
autocompleteLink: string
};
type State = {};
@@ -31,6 +35,7 @@ class AddGroup extends React.Component<Props, State> {
componentDidMount() {
this.props.resetForm();
}
render() {
const { t, loading, error } = this.props;
return (
@@ -43,12 +48,26 @@ class AddGroup extends React.Component<Props, State> {
<GroupForm
submitForm={group => this.createGroup(group)}
loading={loading}
loadUserSuggestions={this.loadUserAutocompletion}
/>
</div>
</Page>
);
}
loadUserAutocompletion = (inputValue: string) => {
const url = this.props.autocompleteLink + "?q=";
return fetch(url + inputValue)
.then(response => response.json())
.then(json => {
return json.map(element => {
return {
value: element,
label: `${element.displayName} (${element.id})`
};
});
});
};
groupCreated = () => {
this.props.history.push("/groups");
};
@@ -71,10 +90,12 @@ const mapStateToProps = state => {
const loading = isCreateGroupPending(state);
const error = getCreateGroupFailure(state);
const createLink = getGroupsLink(state);
const autocompleteLink = getUserAutoCompleteLink(state);
return {
createLink,
loading,
error
error,
autocompleteLink
};
};

View File

@@ -3,21 +3,23 @@ import React from "react";
import { connect } from "react-redux";
import GroupForm from "../components/GroupForm";
import {
modifyGroup,
modifyGroupReset,
getModifyGroupFailure,
isModifyGroupPending,
getModifyGroupFailure
modifyGroup,
modifyGroupReset
} from "../modules/groups";
import type { History } from "history";
import { withRouter } from "react-router-dom";
import type { Group } from "@scm-manager/ui-types";
import { ErrorNotification } from "@scm-manager/ui-components";
import { getUserAutoCompleteLink } from "../../modules/indexResource";
type Props = {
group: Group,
modifyGroup: (group: Group, callback?: () => void) => void,
modifyGroupReset: Group => void,
fetchGroup: (name: string) => void,
autocompleteLink: string,
history: History,
loading?: boolean,
error: Error
@@ -37,6 +39,20 @@ class EditGroup extends React.Component<Props> {
this.props.modifyGroup(group, this.groupModified(group));
};
loadUserAutocompletion = (inputValue: string) => {
const url = this.props.autocompleteLink + "?q=";
return fetch(url + inputValue)
.then(response => response.json())
.then(json => {
return json.map(element => {
return {
value: element,
label: `${element.displayName} (${element.id})`
};
});
});
};
render() {
const { group, loading, error } = this.props;
return (
@@ -48,6 +64,7 @@ class EditGroup extends React.Component<Props> {
this.modifyGroup(group);
}}
loading={loading}
loadUserSuggestions={this.loadUserAutocompletion}
/>
</div>
);
@@ -57,9 +74,11 @@ class EditGroup extends React.Component<Props> {
const mapStateToProps = (state, ownProps) => {
const loading = isModifyGroupPending(state, ownProps.group.name);
const error = getModifyGroupFailure(state, ownProps.group.name);
const autocompleteLink = getUserAutoCompleteLink(state);
return {
loading,
error
error,
autocompleteLink
};
};

View File

@@ -54,9 +54,8 @@ export function fetchGroupsByLink(link: string) {
.then(data => {
dispatch(fetchGroupsSuccess(data));
})
.catch(cause => {
const error = new Error(`could not fetch groups: ${cause.message}`);
dispatch(fetchGroupsFailure(link, error));
.catch(err => {
dispatch(fetchGroupsFailure(link, err));
});
};
}
@@ -105,9 +104,8 @@ function fetchGroup(link: string, name: string) {
.then(data => {
dispatch(fetchGroupSuccess(data));
})
.catch(cause => {
const error = new Error(`could not fetch group: ${cause.message}`);
dispatch(fetchGroupFailure(name, error));
.catch(err => {
dispatch(fetchGroupFailure(name, err));
});
};
}
@@ -151,10 +149,10 @@ export function createGroup(link: string, group: Group, callback?: () => void) {
callback();
}
})
.catch(error => {
.catch(err => {
dispatch(
createGroupFailure(
new Error(`Failed to create group ${group.name}: ${error.message}`)
err
)
);
});
@@ -201,11 +199,11 @@ export function modifyGroup(group: Group, callback?: () => void) {
.then(() => {
dispatch(fetchGroupByLink(group));
})
.catch(cause => {
.catch(err => {
dispatch(
modifyGroupFailure(
group,
new Error(`could not modify group ${group.name}: ${cause.message}`)
err
)
);
});
@@ -259,11 +257,8 @@ export function deleteGroup(group: Group, callback?: () => void) {
callback();
}
})
.catch(cause => {
const error = new Error(
`could not delete group ${group.name}: ${cause.message}`
);
dispatch(deleteGroupFailure(group, error));
.catch(err => {
dispatch(deleteGroupFailure(group, err));
});
};
}

View File

@@ -2,10 +2,7 @@
import type { Me } from "@scm-manager/ui-types";
import * as types from "./types";
import {
apiClient,
UNAUTHORIZED_ERROR_MESSAGE
} from "@scm-manager/ui-components";
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
import { isPending } from "./pending";
import { getFailure } from "./failure";
import {
@@ -190,7 +187,7 @@ export const fetchMe = (link: string) => {
dispatch(fetchMeSuccess(me));
})
.catch((error: Error) => {
if (error.message === UNAUTHORIZED_ERROR_MESSAGE) {
if (error === UNAUTHORIZED_ERROR) {
dispatch(fetchMeUnauthenticated());
} else {
dispatch(fetchMeFailure(error));

View File

@@ -2,7 +2,7 @@
import * as types from "./types";
import { apiClient } from "@scm-manager/ui-components";
import type { Action, IndexResources } from "@scm-manager/ui-types";
import type { Action, IndexResources, Link } from "@scm-manager/ui-types";
import { isPending } from "./pending";
import { getFailure } from "./failure";
@@ -100,6 +100,13 @@ export function getLink(state: Object, name: string) {
}
}
export function getLinkCollection(state: Object, name: string): Link[] {
if (state.indexResources.links && state.indexResources.links[name]) {
return state.indexResources.links[name];
}
return [];
}
export function getUiPluginsLink(state: Object) {
return getLink(state, "uiPlugins");
}
@@ -143,3 +150,23 @@ export function getGitConfigLink(state: Object) {
export function getSvnConfigLink(state: Object) {
return getLink(state, "svnConfig");
}
export function getUserAutoCompleteLink(state: Object): string {
const link = getLinkCollection(state, "autocomplete").find(
i => i.name === "users"
);
if (link) {
return link.href;
}
return "";
}
export function getGroupAutoCompleteLink(state: Object): string {
const link = getLinkCollection(state, "autocomplete").find(
i => i.name === "groups"
);
if (link) {
return link.href;
}
return "";
}

View File

@@ -20,7 +20,11 @@ import reducer, {
getHgConfigLink,
getGitConfigLink,
getSvnConfigLink,
getLinks, getGroupsLink
getLinks,
getGroupsLink,
getLinkCollection,
getUserAutoCompleteLink,
getGroupAutoCompleteLink
} from "./indexResource";
const indexResourcesUnauthenticated = {
@@ -73,354 +77,404 @@ const indexResourcesAuthenticated = {
},
svnConfig: {
href: "http://localhost:8081/scm/api/v2/config/svn"
}
},
autocomplete: [
{
href: "http://localhost:8081/scm/api/v2/autocomplete/users",
name: "users"
},
{
href: "http://localhost:8081/scm/api/v2/autocomplete/groups",
name: "groups"
}
]
}
};
describe("fetch index resource", () => {
const index_url = "/api/v2/";
const mockStore = configureMockStore([thunk]);
describe("index resource", () => {
describe("fetch index resource", () => {
const index_url = "/api/v2/";
const mockStore = configureMockStore([thunk]);
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should successfully fetch index resources when unauthenticated", () => {
fetchMock.getOnce(index_url, indexResourcesUnauthenticated);
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 expectedActions = [
{ type: FETCH_INDEXRESOURCES_PENDING },
{
type: FETCH_INDEXRESOURCES_SUCCESS,
payload: indexResourcesUnauthenticated
}
];
const store = mockStore({});
return store.dispatch(fetchIndexResources()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
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();
});
});
});
it("should successfully fetch index resources when authenticated", () => {
fetchMock.getOnce(index_url, indexResourcesAuthenticated);
describe("index resources reducer", () => {
it("should return empty object, if state and action is undefined", () => {
expect(reducer()).toEqual({});
});
const expectedActions = [
{ type: FETCH_INDEXRESOURCES_PENDING },
{
type: FETCH_INDEXRESOURCES_SUCCESS,
payload: indexResourcesAuthenticated
}
];
it("should return the same state, if the action is undefined", () => {
const state = { x: true };
expect(reducer(state)).toBe(state);
});
const store = mockStore({});
return store.dispatch(fetchIndexResources()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
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);
});
});
it("should dispatch FETCH_INDEX_RESOURCES_FAILURE if request fails", () => {
fetchMock.getOnce(index_url, {
status: 500
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);
});
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();
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);
});
// Autocomplete links
it("should return link collection", () => {
const state = {
indexResources: {
links: indexResourcesAuthenticated._links
}
};
expect(getLinkCollection(state, "autocomplete")).toEqual(
indexResourcesAuthenticated._links.autocomplete
);
});
it("should return user autocomplete link", () => {
const state = {
indexResources: {
links: indexResourcesAuthenticated._links
}
};
expect(getUserAutoCompleteLink(state)).toEqual(
"http://localhost:8081/scm/api/v2/autocomplete/users"
);
});
it("should return group autocomplete link", () => {
const state = {
indexResources: {
links: indexResourcesAuthenticated._links
}
};
expect(getGroupAutoCompleteLink(state)).toEqual(
"http://localhost:8081/scm/api/v2/autocomplete/groups"
);
});
});
});
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

@@ -1,32 +0,0 @@
//@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;

View File

@@ -1,18 +0,0 @@
//@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;

View File

@@ -1,37 +0,0 @@
//@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}>
&lt;
{mail}
&gt;
</a>
);
}
}
}

View File

@@ -3,16 +3,20 @@ import React from "react";
import type { Changeset, Repository } from "@scm-manager/ui-types";
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-manager/ui-components";
import AvatarWrapper from "./AvatarWrapper";
import AvatarImage from "./AvatarImage";
import {
DateFromNow,
ChangesetId,
ChangesetTag,
ChangesetAuthor,
ChangesetDiff,
AvatarWrapper,
AvatarImage,
changesets,
} from "@scm-manager/ui-components";
import classNames from "classnames";
import ChangesetId from "./ChangesetId";
import type { Tag } from "@scm-manager/ui-types";
import ScmDiff from "../../containers/ScmDiff";
const styles = {
spacing: {
@@ -31,12 +35,12 @@ class ChangesetDetails extends React.Component<Props> {
render() {
const { changeset, repository, classes } = this.props;
const description = parseDescription(changeset.description);
const description = changesets.parseDescription(changeset.description);
const id = (
<ChangesetId repository={repository} changeset={changeset} link={false} />
<ChangesetId repository={repository} changeset={changeset} link={false}/>
);
const date = <DateFromNow date={changeset.date} />;
const date = <DateFromNow date={changeset.date}/>;
return (
<div>
@@ -45,12 +49,12 @@ class ChangesetDetails extends React.Component<Props> {
<article className="media">
<AvatarWrapper>
<p className={classNames("image", "is-64x64", classes.spacing)}>
<AvatarImage changeset={changeset} />
<AvatarImage person={changeset.author} />
</p>
</AvatarWrapper>
<div className="media-content">
<p>
<ChangesetAuthor changeset={changeset} />
<ChangesetAuthor changeset={changeset}/>
</p>
<p>
<Interpolate
@@ -67,14 +71,14 @@ class ChangesetDetails extends React.Component<Props> {
return (
<span key={key}>
{item}
<br />
<br/>
</span>
);
})}
</p>
</div>
<div>
<ScmDiff changeset={changeset} sideBySide={false} />
<ChangesetDiff changeset={changeset} />
</div>
</div>
);
@@ -91,7 +95,7 @@ class ChangesetDetails extends React.Component<Props> {
return (
<div className="level-item">
{tags.map((tag: Tag) => {
return <ChangesetTag key={tag.name} tag={tag} />;
return <ChangesetTag key={tag.name} tag={tag}/>;
})}
</div>
);

View File

@@ -1,47 +0,0 @@
//@flow
import {Link} from "react-router-dom";
import React from "react";
import type {Changeset, Repository} 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();
}
}

View File

@@ -1,28 +0,0 @@
// @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;

View File

@@ -1,101 +0,0 @@
//@flow
import React from "react";
import type {Changeset, Repository, Tag} from "@scm-manager/ui-types";
import classNames from "classnames";
import {Interpolate, translate} 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);

View File

@@ -1,32 +0,0 @@
//@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);

View File

@@ -1,25 +0,0 @@
// @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
};
}

View File

@@ -1,22 +0,0 @@
// @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("");
});
});

View File

@@ -12,6 +12,9 @@ const styles = {
zeroflex: {
flexGrow: 0
},
minWidthOfLabel: {
minWidth: "4.5rem"
},
wrapper: {
padding: "1rem 1.5rem 0.25rem 1.5rem",
border: "1px solid #eee",

View File

@@ -12,8 +12,7 @@ import {
} from "../modules/changesets";
import {connect} from "react-redux";
import ChangesetList from "../components/changesets/ChangesetList";
import {ErrorNotification, getPageFromMatch, LinkPaginator, Loading} from "@scm-manager/ui-components";
import {ErrorNotification, getPageFromMatch, LinkPaginator, ChangesetList, Loading} from "@scm-manager/ui-components";
import {compose} from "redux";
type Props = {

View File

@@ -114,7 +114,7 @@ class RepositoryRoot extends React.Component<Props> {
return (
<Page title={repository.namespace + "/" + repository.name}>
<div className="columns">
<div className="column is-three-quarters">
<div className="column is-three-quarters is-clipped">
<Switch>
<Route
path={url}

View File

@@ -1,51 +0,0 @@
// @flow
import React from "react";
import { apiClient } from "@scm-manager/ui-components";
import type { Changeset } from "@scm-manager/ui-types";
import { Diff2Html } from "diff2html";
type Props = {
changeset: Changeset,
sideBySide: boolean
};
type State = {
diff: string,
error?: Error
};
class ScmDiff extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { diff: "" };
}
componentDidMount() {
const { changeset } = this.props;
const url = changeset._links.diff.href+"?format=GIT";
apiClient
.get(url)
.then(response => response.text())
.then(text => this.setState({ ...this.state, diff: text }))
.catch(error => this.setState({ ...this.state, error }));
}
render() {
const options = {
inputFormat: "diff",
outputFormat: this.props.sideBySide ? "side-by-side" : "line-by-line",
showFiles: false,
matching: "lines"
};
const outputHtml = Diff2Html.getPrettyHtml(this.state.diff, options);
return (
// eslint-disable-next-line react/no-danger
<div dangerouslySetInnerHTML={{ __html: outputHtml }} />
);
}
}
export default ScmDiff;

View File

@@ -224,9 +224,8 @@ export function modifyRepo(repository: Repository, callback?: () => void) {
.then(() => {
dispatch(fetchRepoByLink(repository));
})
.catch(cause => {
const error = new Error(`failed to modify repo: ${cause.message}`);
dispatch(modifyRepoFailure(repository, error));
.catch(err => {
dispatch(modifyRepoFailure(repository, err));
});
};
}

View File

@@ -1,23 +1,30 @@
// @flow
import React from "react";
import {translate} from "react-i18next";
import {Checkbox, InputField, SubmitButton} from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import { Autocomplete, SubmitButton } from "@scm-manager/ui-components";
import TypeSelector from "./TypeSelector";
import type {PermissionCollection, PermissionCreateEntry} from "@scm-manager/ui-types";
import type {
PermissionCollection,
PermissionCreateEntry,
SelectValue
} from "@scm-manager/ui-types";
import * as validator from "./permissionValidation";
type Props = {
t: string => string,
createPermission: (permission: PermissionCreateEntry) => void,
loading: boolean,
currentPermissions: PermissionCollection
currentPermissions: PermissionCollection,
groupAutoCompleteLink: string,
userAutoCompleteLink: string
};
type State = {
name: string,
type: string,
groupPermission: boolean,
valid: boolean
valid: boolean,
value?: SelectValue
};
class CreatePermissionForm extends React.Component<Props, State> {
@@ -28,13 +35,95 @@ class CreatePermissionForm extends React.Component<Props, State> {
name: "",
type: "READ",
groupPermission: false,
valid: true
valid: true,
value: undefined
};
}
permissionScopeChanged = event => {
const groupPermission = event.target.value === "GROUP_PERMISSION";
this.setState({
groupPermission: groupPermission,
valid: validator.isPermissionValid(
this.state.name,
groupPermission,
this.props.currentPermissions
)
});
this.setState({ ...this.state, groupPermission });
};
loadUserAutocompletion = (inputValue: string) => {
return this.loadAutocompletion(this.props.userAutoCompleteLink, inputValue);
};
loadGroupAutocompletion = (inputValue: string) => {
return this.loadAutocompletion(
this.props.groupAutoCompleteLink,
inputValue
);
};
loadAutocompletion(url: string, inputValue: string) {
const link = url + "?q=";
return fetch(link + inputValue)
.then(response => response.json())
.then(json => {
return json.map(element => {
const label = element.displayName
? `${element.displayName} (${element.id})`
: element.id;
return {
value: element,
label
};
});
});
}
renderAutocompletionField = () => {
const { t } = this.props;
if (this.state.groupPermission) {
return (
<Autocomplete
loadSuggestions={this.loadGroupAutocompletion}
valueSelected={this.groupOrUserSelected}
value={this.state.value}
label={t("permission.group")}
noOptionsMessage={t("permission.autocomplete.no-group-options")}
loadingMessage={t("permission.autocomplete.loading")}
placeholder={t("permission.autocomplete.group-placeholder")}
/>
);
}
return (
<Autocomplete
loadSuggestions={this.loadUserAutocompletion}
valueSelected={this.groupOrUserSelected}
value={this.state.value}
label={t("permission.user")}
noOptionsMessage={t("permission.autocomplete.no-user-options")}
loadingMessage={t("permission.autocomplete.loading")}
placeholder={t("permission.autocomplete.user-placeholder")}
/>
);
};
groupOrUserSelected = (value: SelectValue) => {
this.setState({
value,
name: value.value.id,
valid: validator.isPermissionValid(
value.value.id,
this.state.groupPermission,
this.props.currentPermissions
)
});
};
render() {
const { t, loading } = this.props;
const { name, type, groupPermission } = this.state;
const { type } = this.state;
return (
<div>
@@ -43,23 +132,32 @@ class CreatePermissionForm extends React.Component<Props, State> {
{t("permission.add-permission.add-permission-heading")}
</h2>
<form onSubmit={this.submit}>
<div className="control">
<label className="radio">
<input
type="radio"
name="permission_scope"
checked={!this.state.groupPermission}
value="USER_PERMISSION"
onChange={this.permissionScopeChanged}
/>
{t("permission.user-permission")}
</label>
<label className="radio">
<input
type="radio"
name="permission_scope"
value="GROUP_PERMISSION"
checked={this.state.groupPermission}
onChange={this.permissionScopeChanged}
/>
{t("permission.group-permission")}
</label>
</div>
<div class="columns">
<div class="column is-three-quarters">
<InputField
label={t("permission.name")}
value={name ? name : ""}
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")}
/>
{this.renderAutocompletionField()}
</div>
<div class="column is-one-quarter">
<TypeSelector
@@ -108,27 +206,6 @@ class CreatePermissionForm extends React.Component<Props, State> {
type: type
});
};
handleNameChange = (name: string) => {
this.setState({
name: name,
valid: validator.isPermissionValid(
name,
this.state.groupPermission,
this.props.currentPermissions
)
});
};
handleGroupPermissionChange = (groupPermission: boolean) => {
this.setState({
groupPermission: groupPermission,
valid: validator.isPermissionValid(
this.state.name,
groupPermission,
this.props.currentPermissions
)
});
};
}
export default translate("repos")(CreatePermissionForm);

View File

@@ -27,6 +27,10 @@ import SinglePermission from "./SinglePermission";
import CreatePermissionForm from "../components/CreatePermissionForm";
import type { History } from "history";
import { getPermissionsLink } from "../../modules/repos";
import {
getGroupAutoCompleteLink,
getUserAutoCompleteLink
} from "../../../modules/indexResource";
type Props = {
namespace: string,
@@ -37,6 +41,8 @@ type Props = {
hasPermissionToCreate: boolean,
loadingCreatePermission: boolean,
permissionsLink: string,
groupAutoCompleteLink: string,
userAutoCompleteLink: string,
//dispatch functions
fetchPermissions: (link: string, namespace: string, repoName: string) => void,
@@ -92,7 +98,9 @@ class Permissions extends React.Component<Props> {
namespace,
repoName,
loadingCreatePermission,
hasPermissionToCreate
hasPermissionToCreate,
userAutoCompleteLink,
groupAutoCompleteLink
} = this.props;
if (error) {
return (
@@ -113,6 +121,8 @@ class Permissions extends React.Component<Props> {
createPermission={permission => this.createPermission(permission)}
loading={loadingCreatePermission}
currentPermissions={permissions}
userAutoCompleteLink={userAutoCompleteLink}
groupAutoCompleteLink={groupAutoCompleteLink}
/>
) : null;
@@ -165,6 +175,8 @@ const mapStateToProps = (state, ownProps) => {
);
const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName);
const permissionsLink = getPermissionsLink(state, namespace, repoName);
const groupAutoCompleteLink = getGroupAutoCompleteLink(state);
const userAutoCompleteLink = getUserAutoCompleteLink(state);
return {
namespace,
repoName,
@@ -173,7 +185,9 @@ const mapStateToProps = (state, ownProps) => {
permissions,
hasPermissionToCreate,
loadingCreatePermission,
permissionsLink
permissionsLink,
groupAutoCompleteLink,
userAutoCompleteLink
};
};
@@ -189,7 +203,9 @@ const mapDispatchToProps = dispatch => {
repoName: string,
callback?: () => void
) => {
dispatch(createPermission(link, permission, namespace, repoName, callback));
dispatch(
createPermission(link, permission, namespace, repoName, callback)
);
},
createPermissionReset: (namespace: string, repoName: string) => {
dispatch(createPermissionReset(namespace, repoName));

View File

@@ -1,12 +1,16 @@
// @flow
import type {Action} from "@scm-manager/ui-components";
import {apiClient} from "@scm-manager/ui-components";
import type { Action } from "@scm-manager/ui-components";
import { apiClient } from "@scm-manager/ui-components";
import * as types from "../../../modules/types";
import type {Permission, PermissionCollection, PermissionCreateEntry} from "@scm-manager/ui-types";
import {isPending} from "../../../modules/pending";
import {getFailure} from "../../../modules/failure";
import {Dispatch} from "redux";
import type {
Permission,
PermissionCollection,
PermissionCreateEntry
} from "@scm-manager/ui-types";
import { isPending } from "../../../modules/pending";
import { getFailure } from "../../../modules/failure";
import { Dispatch } from "redux";
export const FETCH_PERMISSIONS = "scm/permissions/FETCH_PERMISSIONS";
export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
@@ -141,13 +145,8 @@ export function modifyPermission(
callback();
}
})
.catch(cause => {
const error = new Error(
`failed to modify permission: ${cause.message}`
);
dispatch(
modifyPermissionFailure(permission, error, namespace, repoName)
);
.catch(err => {
dispatch(modifyPermissionFailure(permission, err, namespace, repoName));
});
};
}
@@ -241,15 +240,7 @@ export function createPermission(
}
})
.catch(err =>
dispatch(
createPermissionFailure(
new Error(
`failed to add permission ${permission.name}: ${err.message}`
),
namespace,
repoName
)
)
dispatch(createPermissionFailure(err, namespace, repoName))
);
};
}
@@ -318,13 +309,8 @@ export function deletePermission(
callback();
}
})
.catch(cause => {
const error = new Error(
`could not delete permission ${permission.name}: ${cause.message}`
);
dispatch(
deletePermissionFailure(permission, namespace, repoName, error)
);
.catch(err => {
dispatch(deletePermissionFailure(permission, namespace, repoName, err));
});
};
}

View File

@@ -119,7 +119,9 @@ class FileTree extends React.Component<Props> {
<th className="is-hidden-mobile">
{t("sources.file-tree.lastModified")}
</th>
<th>{t("sources.file-tree.description")}</th>
<th className="is-hidden-mobile">
{t("sources.file-tree.description")}
</th>
</tr>
</thead>
<tbody>

View File

@@ -6,10 +6,14 @@ import FileSize from "./FileSize";
import FileIcon from "./FileIcon";
import { Link } from "react-router-dom";
import type { File } from "@scm-manager/ui-types";
import classNames from "classnames";
const styles = {
iconColumn: {
width: "16px"
},
wordBreakMinWidth: {
minWidth: "10em"
}
};
@@ -71,12 +75,14 @@ class FileTreeLeaf extends React.Component<Props> {
return (
<tr>
<td className={classes.iconColumn}>{this.createFileIcon(file)}</td>
<td>{this.createFileName(file)}</td>
<td className={classNames(classes.wordBreakMinWidth, "is-word-break")}>{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>
<td className={classNames(classes.wordBreakMinWidth, "is-word-break", "is-hidden-mobile")}>
{file.description}
</td>
</tr>
);
}

View File

@@ -93,7 +93,7 @@ class Content extends React.Component<Props, State> {
classes.marginInHeader
)}
/>
<span>{file.name}</span>
<span className="is-word-break">{file.name}</span>
</div>
<div className="media-right">{selector}</div>
</article>
@@ -125,11 +125,11 @@ class Content extends React.Component<Props, State> {
<tbody>
<tr>
<td>{t("sources.content.path")}</td>
<td>{file.path}</td>
<td className="is-word-break">{file.path}</td>
</tr>
<tr>
<td>{t("sources.content.branch")}</td>
<td>{revision}</td>
<td className="is-word-break">{revision}</td>
</tr>
<tr>
<td>{t("sources.content.size")}</td>
@@ -141,7 +141,7 @@ class Content extends React.Component<Props, State> {
</tr>
<tr>
<td>{t("sources.content.description")}</td>
<td>{description}</td>
<td className="is-word-break">{description}</td>
</tr>
</tbody>
</table>

View File

@@ -9,10 +9,10 @@ import type {
import {
ErrorNotification,
Loading,
StatePaginator
StatePaginator,
ChangesetList
} from "@scm-manager/ui-components";
import { getHistory } from "./history";
import ChangesetList from "../../components/changesets/ChangesetList";
type Props = {
file: File,

View File

@@ -25,8 +25,7 @@ export function fetchSources(
dispatch(fetchSourcesSuccess(repository, revision, path, sources));
})
.catch(err => {
const error = new Error(`failed to fetch sources: ${err.message}`);
dispatch(fetchSourcesFailure(repository, revision, path, error));
dispatch(fetchSourcesFailure(repository, revision, path, err));
});
};
}

View File

@@ -19,7 +19,8 @@ type State = {
password: string,
loading: boolean,
error?: Error,
passwordChanged: boolean
passwordChanged: boolean,
passwordValid: boolean
};
class SetUserPassword extends React.Component<Props, State> {
@@ -32,7 +33,8 @@ class SetUserPassword extends React.Component<Props, State> {
passwordConfirmationError: false,
validatePasswordError: false,
validatePassword: "",
passwordChanged: false
passwordChanged: false,
passwordValid: false
};
}
@@ -104,7 +106,7 @@ class SetUserPassword extends React.Component<Props, State> {
key={this.state.passwordChanged ? "changed" : "unchanged"}
/>
<SubmitButton
disabled={!this.state.password}
disabled={!this.state.passwordValid}
loading={loading}
label={t("user-form.submit")}
/>
@@ -112,8 +114,8 @@ class SetUserPassword extends React.Component<Props, State> {
);
}
passwordChanged = (password: string) => {
this.setState({ ...this.state, password });
passwordChanged = (password: string, passwordValid: boolean) => {
this.setState({ ...this.state, password, passwordValid: (!!password && passwordValid) });
};
onClose = () => {

View File

@@ -22,7 +22,8 @@ type State = {
user: User,
mailValidationError: boolean,
nameValidationError: boolean,
displayNameValidationError: boolean
displayNameValidationError: boolean,
passwordValid: boolean
};
class UserForm extends React.Component<Props, State> {
@@ -41,7 +42,8 @@ class UserForm extends React.Component<Props, State> {
},
mailValidationError: false,
displayNameValidationError: false,
nameValidationError: false
nameValidationError: false,
passwordValid: false
};
}
@@ -61,7 +63,6 @@ class UserForm extends React.Component<Props, State> {
isValid = () => {
const user = this.state.user;
const passwordValid = this.props.user ? !this.isFalsy(user.password) : true;
return !(
this.state.nameValidationError ||
this.state.mailValidationError ||
@@ -69,7 +70,7 @@ class UserForm extends React.Component<Props, State> {
this.isFalsy(user.name) ||
this.isFalsy(user.displayName) ||
this.isFalsy(user.mail) ||
passwordValid
!this.state.passwordValid
);
};
@@ -180,9 +181,10 @@ class UserForm extends React.Component<Props, State> {
});
};
handlePasswordChange = (password: string) => {
handlePasswordChange = (password: string, passwordValid: boolean) => {
this.setState({
user: { ...this.state.user, password }
user: { ...this.state.user, password },
passwordValid: !this.isFalsy(password) && passwordValid
});
};

View File

@@ -35,8 +35,6 @@ export const DELETE_USER_FAILURE = `${DELETE_USER}_${types.FAILURE_SUFFIX}`;
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
// TODO i18n for error messages
// fetch users
export function fetchUsers(link: string) {
@@ -57,9 +55,8 @@ export function fetchUsersByLink(link: string) {
.then(data => {
dispatch(fetchUsersSuccess(data));
})
.catch(cause => {
const error = new Error(`could not fetch users: ${cause.message}`);
dispatch(fetchUsersFailure(link, error));
.catch(err => {
dispatch(fetchUsersFailure(link, err));
});
};
}
@@ -108,9 +105,8 @@ function fetchUser(link: string, name: string) {
.then(data => {
dispatch(fetchUserSuccess(data));
})
.catch(cause => {
const error = new Error(`could not fetch user: ${cause.message}`);
dispatch(fetchUserFailure(name, error));
.catch(err => {
dispatch(fetchUserFailure(name, err));
});
};
}
@@ -155,13 +151,7 @@ export function createUser(link: string, user: User, callback?: () => void) {
callback();
}
})
.catch(err =>
dispatch(
createUserFailure(
new Error(`failed to add user ${user.name}: ${err.message}`)
)
)
);
.catch(err => dispatch(createUserFailure(err)));
};
}
@@ -260,11 +250,8 @@ export function deleteUser(user: User, callback?: () => void) {
callback();
}
})
.catch(cause => {
const error = new Error(
`could not delete user ${user.name}: ${cause.message}`
);
dispatch(deleteUserFailure(user, error));
.catch(err => {
dispatch(deleteUserFailure(user, err));
});
};
}