scm-ui: new repository layout

This commit is contained in:
Sebastian Sdorra
2019-10-07 10:57:09 +02:00
parent 09c7def874
commit c05798e254
417 changed files with 3620 additions and 52971 deletions

View File

@@ -0,0 +1,31 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Checkbox } from "@scm-manager/ui-components";
type Props = {
t: string => string,
disabled: boolean,
name: string,
checked: boolean,
onChange?: (value: boolean, name?: string) => void
};
class PermissionCheckbox extends React.Component<Props> {
render() {
const { t } = this.props;
return (
<Checkbox
key={this.props.name}
name={this.props.name}
helpText={t("verbs.repository." + this.props.name + ".description")}
label={t("verbs.repository." + this.props.name + ".displayName")}
checked={this.props.checked}
onChange={this.props.onChange}
disabled={this.props.disabled}
/>
);
}
}
export default translate("plugins")(PermissionCheckbox);

View File

@@ -0,0 +1,55 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Select } from "@scm-manager/ui-components";
type Props = {
t: string => string,
availableRoles: string[],
handleRoleChange: string => void,
role: string,
label?: string,
helpText?: string,
loading?: boolean
};
class RoleSelector extends React.Component<Props> {
render() {
const {
availableRoles,
role,
handleRoleChange,
loading,
label,
helpText
} = this.props;
if (!availableRoles) return null;
const options = role
? this.createSelectOptions(availableRoles)
: ["", ...this.createSelectOptions(availableRoles)];
return (
<Select
onChange={handleRoleChange}
value={role ? role : ""}
options={options}
loading={loading}
label={label}
helpText={helpText}
/>
);
}
createSelectOptions(roles: string[]) {
return roles.map(role => {
return {
label: role,
value: role
};
});
}
}
export default translate("repos")(RoleSelector);

View File

@@ -0,0 +1,73 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import type { Permission } from "@scm-manager/ui-types";
import { confirmAlert } from "@scm-manager/ui-components";
type Props = {
permission: Permission,
namespace: string,
repoName: string,
confirmDialog?: boolean,
t: string => string,
deletePermission: (
permission: Permission,
namespace: string,
repoName: string
) => void,
loading: boolean
};
class DeletePermissionButton extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
};
deletePermission = () => {
this.props.deletePermission(
this.props.permission,
this.props.namespace,
this.props.repoName
);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("permission.delete-permission-button.confirm-alert.title"),
message: t("permission.delete-permission-button.confirm-alert.message"),
buttons: [
{
label: t("permission.delete-permission-button.confirm-alert.submit"),
onClick: () => this.deletePermission()
},
{
label: t("permission.delete-permission-button.confirm-alert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.permission._links.delete;
};
render() {
const { confirmDialog } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deletePermission;
if (!this.isDeletable()) {
return null;
}
return (
<a className="level-item" onClick={action}>
<span className="icon is-small">
<i className="fas fa-trash" />
</span>
</a>
);
}
}
export default translate("repos")(DeletePermissionButton);

View File

@@ -0,0 +1,98 @@
import React from "react";
import { mount, shallow } from "enzyme";
import "../../../../tests/enzyme";
import "../../../../tests/i18n";
import DeletePermissionButton from "./DeletePermissionButton";
import { confirmAlert } from "@scm-manager/ui-components";
import ReactRouterEnzymeContext from "react-router-enzyme-context";
jest.mock("@scm-manager/ui-components", () => ({
confirmAlert: jest.fn(),
DeleteButton: require.requireActual("@scm-manager/ui-components").DeleteButton
}));
describe("DeletePermissionButton", () => {
const options = new ReactRouterEnzymeContext();
it("should render nothing, if the delete link is missing", () => {
const permission = {
_links: {}
};
const navLink = shallow(
<DeletePermissionButton
permission={permission}
deletePermission={() => {}}
/>,
options.get()
);
expect(navLink.text()).toBe("");
});
it("should render the delete icon", () => {
const permission = {
_links: {
delete: {
href: "/permission"
}
}
};
const deleteIcon = mount(
<DeletePermissionButton
permission={permission}
deletePermission={() => {}}
/>,
options.get()
);
expect(deleteIcon.html()).not.toBe("");
});
it("should open the confirm dialog on button click", () => {
const permission = {
_links: {
delete: {
href: "/permission"
}
}
};
const button = mount(
<DeletePermissionButton
permission={permission}
deletePermission={() => {}}
/>,
options.get()
);
button.find(".fa-trash").simulate("click");
expect(confirmAlert.mock.calls.length).toBe(1);
});
it("should call the delete permission function with delete url", () => {
const permission = {
_links: {
delete: {
href: "/permission"
}
}
};
let calledUrl = null;
function capture(permission) {
calledUrl = permission._links.delete.href;
}
const button = mount(
<DeletePermissionButton
permission={permission}
confirmDialog={false}
deletePermission={capture}
/>,
options.get()
);
button.find(".fa-trash").simulate("click");
expect(calledUrl).toBe("/permission");
});
});

View File

@@ -0,0 +1,33 @@
// @flow
import {validation} from "@scm-manager/ui-components";
import type {PermissionCollection} from "@scm-manager/ui-types";
const isNameValid = validation.isNameValid;
export { isNameValid };
export const isPermissionValid = (
name: string,
groupPermission: boolean,
permissions: PermissionCollection
) => {
return (
isNameValid(name) &&
!currentPermissionIncludeName(name, groupPermission, permissions)
);
};
const currentPermissionIncludeName = (
name: string,
groupPermission: boolean,
permissions: PermissionCollection
) => {
for (let i = 0; i < permissions.length; i++) {
if (
permissions[i].name === name &&
permissions[i].groupPermission === groupPermission
)
return true;
}
return false;
};

View File

@@ -0,0 +1,70 @@
//@flow
import * as validator from "./permissionValidation";
describe("permission validation", () => {
it("should return true if permission is valid and does not exist", () => {
const permissions = [];
const name = "PermissionName";
const groupPermission = false;
expect(
validator.isPermissionValid(name, groupPermission, permissions)
).toBe(true);
});
it("should return true if permission is valid and does not exists with same group permission", () => {
const permissions = [
{
name: "PermissionName",
groupPermission: true,
type: "READ",
_links: {},
verbs: []
}
];
const name = "PermissionName";
const groupPermission = false;
expect(
validator.isPermissionValid(name, groupPermission, permissions)
).toBe(true);
});
it("should return false if permission is valid but exists", () => {
const permissions = [
{
name: "PermissionName",
groupPermission: false,
type: "READ",
_links: {},
verbs: []
}
];
const name = "PermissionName";
const groupPermission = false;
expect(
validator.isPermissionValid(name, groupPermission, permissions)
).toBe(false);
});
it("should return false if permission does not exist but is invalid", () => {
const permissions = [];
const name = "@PermissionName";
const groupPermission = false;
expect(
validator.isPermissionValid(name, groupPermission, permissions)
).toBe(false);
});
it("should return false if permission is not valid and does not exist", () => {
const permissions = [];
const name = "@PermissionName";
const groupPermission = false;
expect(
validator.isPermissionValid(name, groupPermission, permissions)
).toBe(false);
});
});

View File

@@ -0,0 +1,99 @@
// @flow
import React from "react";
import {
ButtonGroup,
Button,
SubmitButton,
Modal
} from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import PermissionCheckbox from "../components/PermissionCheckbox";
type Props = {
readOnly: boolean,
availableVerbs: string[],
selectedVerbs: string[],
onSubmit: (string[]) => void,
onClose: () => void,
// context props
t: string => string
};
type State = {
verbs: any
};
class AdvancedPermissionsDialog extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
const verbs = {};
props.availableVerbs.forEach(
verb =>
(verbs[verb] = props.selectedVerbs
? props.selectedVerbs.includes(verb)
: false)
);
this.state = { verbs };
}
render() {
const { t, onClose, readOnly } = this.props;
const { verbs } = this.state;
const verbSelectBoxes = Object.entries(verbs).map(e => (
<PermissionCheckbox
key={e[0]}
disabled={readOnly}
name={e[0]}
checked={e[1]}
onChange={this.handleChange}
/>
));
const submitButton = !readOnly ? (
<SubmitButton label={t("permission.advanced.dialog.submit")} />
) : null;
const body = <>{verbSelectBoxes}</>;
const footer = (
<form onSubmit={this.onSubmit}>
<ButtonGroup>
{submitButton}
<Button
label={t("permission.advanced.dialog.abort")}
action={onClose}
/>
</ButtonGroup>
</form>
);
return (
<Modal
title={t("permission.advanced.dialog.title")}
closeFunction={() => onClose()}
body={body}
footer={footer}
active={true}
/>
);
}
handleChange = (value: boolean, name: string) => {
const { verbs } = this.state;
const newVerbs = { ...verbs, [name]: value };
this.setState({ verbs: newVerbs });
};
onSubmit = () => {
this.props.onSubmit(
Object.entries(this.state.verbs)
.filter(e => e[1])
.map(e => e[0])
);
};
}
export default translate("repos")(AdvancedPermissionsDialog);

View File

@@ -0,0 +1,235 @@
// @flow
import React from "react";
import {translate} from "react-i18next";
import type {PermissionCollection, PermissionCreateEntry, RepositoryRole, SelectValue} from "@scm-manager/ui-types";
import {
Button,
GroupAutocomplete,
LabelWithHelpIcon,
Radio,
SubmitButton,
Subtitle,
UserAutocomplete
} from "@scm-manager/ui-components";
import * as validator from "../components/permissionValidation";
import RoleSelector from "../components/RoleSelector";
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
import {findVerbsForRole} from "../modules/permissions";
type Props = {
availableRoles: RepositoryRole[],
availableVerbs: string[],
createPermission: (permission: PermissionCreateEntry) => void,
loading: boolean,
currentPermissions: PermissionCollection,
groupAutocompleteLink: string,
userAutocompleteLink: string,
// Context props
t: string => string
};
type State = {
name: string,
role?: string,
verbs?: string[],
groupPermission: boolean,
valid: boolean,
value?: SelectValue,
showAdvancedDialog: boolean
};
class CreatePermissionForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
name: "",
role: props.availableRoles[0].name,
verbs: undefined,
groupPermission: false,
valid: true,
value: undefined,
showAdvancedDialog: false
};
}
permissionScopeChanged = event => {
const groupPermission = event.target.value === "GROUP_PERMISSION";
this.setState({
value: undefined,
name: "",
groupPermission: groupPermission,
valid: false
});
};
renderAutocompletionField = () => {
const group = this.state.groupPermission;
if (group) {
return (
<GroupAutocomplete
autocompleteLink={this.props.groupAutocompleteLink}
valueSelected={this.selectName}
value={this.state.value ? this.state.value : ""}
/>
);
}
return (
<UserAutocomplete
autocompleteLink={this.props.userAutocompleteLink}
valueSelected={this.selectName}
value={this.state.value ? this.state.value : ""}
/>
);
};
selectName = (value: SelectValue) => {
this.setState({
value,
name: value.value.id,
valid: validator.isPermissionValid(
value.value.id,
this.state.groupPermission,
this.props.currentPermissions
)
});
};
render() {
const { t, availableRoles, availableVerbs, loading } = this.props;
const { role, verbs, showAdvancedDialog } = this.state;
const availableRoleNames = availableRoles.map(r => r.name);
const selectedVerbs = role ? findVerbsForRole(availableRoles, role) : verbs;
const advancedDialog = showAdvancedDialog ? (
<AdvancedPermissionsDialog
availableVerbs={availableVerbs}
selectedVerbs={selectedVerbs}
onClose={this.toggleAdvancedPermissionsDialog}
onSubmit={this.submitAdvancedPermissionsDialog}
/>
) : null;
return (
<>
<hr />
<Subtitle
subtitle={t("permission.add-permission.add-permission-heading")}
/>
{advancedDialog}
<form onSubmit={this.submit}>
<div className="field is-grouped">
<div className="control">
<Radio
name="permission_scope"
value="USER_PERMISSION"
checked={!this.state.groupPermission}
label={t("permission.user-permission")}
onChange={this.permissionScopeChanged}
/>
<Radio
name="permission_scope"
value="GROUP_PERMISSION"
checked={this.state.groupPermission}
label={t("permission.group-permission")}
onChange={this.permissionScopeChanged}
/>
</div>
</div>
<div className="columns">
<div className="column is-three-fifths">
{this.renderAutocompletionField()}
</div>
<div className="column is-two-fifths">
<div className="columns">
<div className="column is-narrow">
<RoleSelector
availableRoles={availableRoleNames}
label={t("permission.role")}
helpText={t("permission.help.roleHelpText")}
handleRoleChange={this.handleRoleChange}
role={role}
/>
</div>
<div className="column">
<LabelWithHelpIcon
label={t("permission.permissions")}
helpText={t("permission.help.permissionsHelpText")}
/>
<Button
label={t("permission.advanced-button.label")}
action={this.toggleAdvancedPermissionsDialog}
/>
</div>
</div>
</div>
</div>
<div className="columns">
<div className="column">
<SubmitButton
label={t("permission.add-permission.submit-button")}
loading={loading}
disabled={!this.state.valid || this.state.name === ""}
/>
</div>
</div>
</form>
</>
);
}
toggleAdvancedPermissionsDialog = () => {
this.setState(prevState => ({
showAdvancedDialog: !prevState.showAdvancedDialog
}));
};
submitAdvancedPermissionsDialog = (newVerbs: string[]) => {
this.setState({
showAdvancedDialog: false,
role: undefined,
verbs: newVerbs
});
};
submit = e => {
this.props.createPermission({
name: this.state.name,
role: this.state.role,
verbs: this.state.verbs,
groupPermission: this.state.groupPermission
});
this.removeState();
e.preventDefault();
};
removeState = () => {
this.setState({
name: "",
role: this.props.availableRoles[0].name,
verbs: undefined,
valid: true,
value: undefined
});
};
handleRoleChange = (role: string) => {
const selectedRole = this.findAvailableRole(role);
if (!selectedRole) {
return;
}
this.setState({
role: selectedRole.name,
verbs: []
});
};
findAvailableRole = (roleName: string) => {
return this.props.availableRoles.find(role => role.name === roleName);
};
}
export default translate("repos")(CreatePermissionForm);

View File

@@ -0,0 +1,288 @@
//@flow
import React from "react";
import {connect} from "react-redux";
import {translate} from "react-i18next";
import {
createPermission,
createPermissionReset,
deletePermissionReset,
fetchAvailablePermissionsIfNeeded,
fetchPermissions,
getAvailablePermissions,
getAvailableRepositoryRoles,
getAvailableRepositoryVerbs,
getCreatePermissionFailure,
getDeletePermissionsFailure,
getFetchAvailablePermissionsFailure,
getFetchPermissionsFailure,
getModifyPermissionsFailure,
getPermissionsOfRepo,
hasCreatePermission,
isCreatePermissionPending,
isFetchAvailablePermissionsPending,
isFetchPermissionsPending,
modifyPermissionReset
} from "../modules/permissions";
import {ErrorPage, LabelWithHelpIcon, Loading, Subtitle} from "@scm-manager/ui-components";
import type {Permission, PermissionCollection, PermissionCreateEntry, RepositoryRole} from "@scm-manager/ui-types";
import SinglePermission from "./SinglePermission";
import CreatePermissionForm from "./CreatePermissionForm";
import type {History} from "history";
import {getPermissionsLink} from "../../modules/repos";
import {
getGroupAutoCompleteLink,
getRepositoryRolesLink,
getRepositoryVerbsLink,
getUserAutoCompleteLink
} from "../../../modules/indexResource";
type Props = {
availablePermissions: boolean,
availableRepositoryRoles: RepositoryRole[],
availableVerbs: string[],
namespace: string,
repoName: string,
loading: boolean,
error: Error,
permissions: PermissionCollection,
hasPermissionToCreate: boolean,
loadingCreatePermission: boolean,
repositoryRolesLink: string,
repositoryVerbsLink: string,
permissionsLink: string,
groupAutocompleteLink: string,
userAutocompleteLink: string,
//dispatch functions
fetchAvailablePermissionsIfNeeded: (
repositoryRolesLink: string,
repositoryVerbsLink: string
) => void,
fetchPermissions: (link: string, namespace: string, repoName: string) => void,
createPermission: (
link: string,
permission: PermissionCreateEntry,
namespace: string,
repoName: string,
callback?: () => void
) => void,
createPermissionReset: (string, string) => void,
modifyPermissionReset: (string, string) => void,
deletePermissionReset: (string, string) => void,
// context props
t: string => string,
match: any,
history: History
};
class Permissions extends React.Component<Props> {
componentDidMount() {
const {
fetchAvailablePermissionsIfNeeded,
fetchPermissions,
namespace,
repoName,
modifyPermissionReset,
createPermissionReset,
deletePermissionReset,
permissionsLink,
repositoryRolesLink,
repositoryVerbsLink
} = this.props;
createPermissionReset(namespace, repoName);
modifyPermissionReset(namespace, repoName);
deletePermissionReset(namespace, repoName);
fetchAvailablePermissionsIfNeeded(repositoryRolesLink, repositoryVerbsLink);
fetchPermissions(permissionsLink, namespace, repoName);
}
createPermission = (permission: Permission) => {
this.props.createPermission(
this.props.permissionsLink,
permission,
this.props.namespace,
this.props.repoName
);
};
render() {
const {
availablePermissions,
availableRepositoryRoles,
availableVerbs,
loading,
error,
permissions,
t,
namespace,
repoName,
loadingCreatePermission,
hasPermissionToCreate,
userAutocompleteLink,
groupAutocompleteLink
} = this.props;
if (error) {
return (
<ErrorPage
title={t("permission.error-title")}
subtitle={t("permission.error-subtitle")}
error={error}
/>
);
}
if (loading || !permissions || !availablePermissions) {
return <Loading />;
}
const createPermissionForm = hasPermissionToCreate ? (
<CreatePermissionForm
availableRoles={availableRepositoryRoles}
availableVerbs={availableVerbs}
createPermission={permission => this.createPermission(permission)}
loading={loadingCreatePermission}
currentPermissions={permissions}
userAutocompleteLink={userAutocompleteLink}
groupAutocompleteLink={groupAutocompleteLink}
/>
) : null;
return (
<div>
<Subtitle subtitle={t("permission.title")} />
<table className="card-table table is-hoverable is-fullwidth">
<thead>
<tr>
<th>
<LabelWithHelpIcon
label={t("permission.name")}
helpText={t("permission.help.nameHelpText")}
/>
</th>
<th>
<LabelWithHelpIcon
label={t("permission.role")}
helpText={t("permission.help.roleHelpText")}
/>
</th>
<th>
<LabelWithHelpIcon
label={t("permission.permissions")}
helpText={t("permission.help.permissionsHelpText")}
/>
</th>
<th />
</tr>
</thead>
<tbody>
{permissions.map(permission => {
return (
<SinglePermission
availableRepositoryRoles={availableRepositoryRoles}
availableRepositoryVerbs={availableVerbs}
key={permission.name + permission.groupPermission.toString()}
namespace={namespace}
repoName={repoName}
permission={permission}
/>
);
})}
</tbody>
</table>
{createPermissionForm}
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
const namespace = ownProps.namespace;
const repoName = ownProps.repoName;
const error =
getFetchPermissionsFailure(state, namespace, repoName) ||
getCreatePermissionFailure(state, namespace, repoName) ||
getDeletePermissionsFailure(state, namespace, repoName) ||
getModifyPermissionsFailure(state, namespace, repoName) ||
getFetchAvailablePermissionsFailure(state);
const loading =
isFetchPermissionsPending(state, namespace, repoName) ||
isFetchAvailablePermissionsPending(state);
const permissions = getPermissionsOfRepo(state, namespace, repoName);
const loadingCreatePermission = isCreatePermissionPending(
state,
namespace,
repoName
);
const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName);
const repositoryRolesLink = getRepositoryRolesLink(state);
const repositoryVerbsLink = getRepositoryVerbsLink(state);
const permissionsLink = getPermissionsLink(state, namespace, repoName);
const groupAutocompleteLink = getGroupAutoCompleteLink(state);
const userAutocompleteLink = getUserAutoCompleteLink(state);
const availablePermissions = getAvailablePermissions(state);
const availableRepositoryRoles = getAvailableRepositoryRoles(state);
const availableVerbs = getAvailableRepositoryVerbs(state);
return {
availablePermissions,
availableRepositoryRoles,
availableVerbs,
namespace,
repoName,
repositoryRolesLink,
repositoryVerbsLink,
error,
loading,
permissions,
hasPermissionToCreate,
loadingCreatePermission,
permissionsLink,
groupAutocompleteLink,
userAutocompleteLink
};
};
const mapDispatchToProps = dispatch => {
return {
fetchPermissions: (link: string, namespace: string, repoName: string) => {
dispatch(fetchPermissions(link, namespace, repoName));
},
fetchAvailablePermissionsIfNeeded: (
repositoryRolesLink: string,
repositoryVerbsLink: string
) => {
dispatch(
fetchAvailablePermissionsIfNeeded(
repositoryRolesLink,
repositoryVerbsLink
)
);
},
createPermission: (
link: string,
permission: PermissionCreateEntry,
namespace: string,
repoName: string,
callback?: () => void
) => {
dispatch(
createPermission(link, permission, namespace, repoName, callback)
);
},
createPermissionReset: (namespace: string, repoName: string) => {
dispatch(createPermissionReset(namespace, repoName));
},
modifyPermissionReset: (namespace: string, repoName: string) => {
dispatch(modifyPermissionReset(namespace, repoName));
},
deletePermissionReset: (namespace: string, repoName: string) => {
dispatch(deletePermissionReset(namespace, repoName));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("repos")(Permissions));

View File

@@ -0,0 +1,277 @@
// @flow
import React from "react";
import type { RepositoryRole, Permission } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import {
modifyPermission,
isModifyPermissionPending,
deletePermission,
isDeletePermissionPending,
findVerbsForRole
} from "../modules/permissions";
import { connect } from "react-redux";
import type { History } from "history";
import { Button, Icon } from "@scm-manager/ui-components";
import DeletePermissionButton from "../components/buttons/DeletePermissionButton";
import RoleSelector from "../components/RoleSelector";
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
import classNames from "classnames";
import injectSheet from "react-jss";
type Props = {
availableRepositoryRoles: RepositoryRole[],
availableRepositoryVerbs: string[],
submitForm: Permission => void,
modifyPermission: (
permission: Permission,
namespace: string,
name: string
) => void,
permission: Permission,
t: string => string,
namespace: string,
repoName: string,
match: any,
history: History,
loading: boolean,
deletePermission: (
permission: Permission,
namespace: string,
name: string
) => void,
deleteLoading: boolean,
classes: any
};
type State = {
permission: Permission,
showAdvancedDialog: boolean
};
const styles = {
centerMiddle: {
display: "table-cell",
verticalAlign: "middle !important"
},
columnWidth: {
width: "100%"
}
};
class SinglePermission extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
const defaultPermission = props.availableRepositoryRoles
? props.availableRepositoryRoles[0]
: {};
this.state = {
permission: {
name: "",
role: undefined,
verbs: defaultPermission.verbs,
groupPermission: false,
_links: {}
},
showAdvancedDialog: false
};
}
componentDidMount() {
const { permission } = this.props;
if (permission) {
this.setState({
permission: {
name: permission.name,
role: permission.role,
verbs: permission.verbs,
groupPermission: permission.groupPermission,
_links: permission._links
}
});
}
}
deletePermission = () => {
this.props.deletePermission(
this.props.permission,
this.props.namespace,
this.props.repoName
);
};
render() {
const { permission, showAdvancedDialog } = this.state;
const {
t,
availableRepositoryRoles,
availableRepositoryVerbs,
loading,
namespace,
repoName,
classes
} = this.props;
const availableRoleNames =
!!availableRepositoryRoles && availableRepositoryRoles.map(r => r.name);
const readOnly = !this.mayChangePermissions();
const roleSelector = readOnly ? (
<td>{permission.role ? permission.role : t("permission.custom")}</td>
) : (
<td>
<RoleSelector
handleRoleChange={this.handleRoleChange}
availableRoles={availableRoleNames}
role={permission.role}
loading={loading}
/>
</td>
);
const selectedVerbs = permission.role
? findVerbsForRole(availableRepositoryRoles, permission.role)
: permission.verbs;
const advancedDialog = showAdvancedDialog ? (
<AdvancedPermissionsDialog
readOnly={readOnly}
availableVerbs={availableRepositoryVerbs}
selectedVerbs={selectedVerbs}
onClose={this.closeAdvancedPermissionsDialog}
onSubmit={this.submitAdvancedPermissionsDialog}
/>
) : null;
const iconType =
permission && permission.groupPermission ? (
<Icon title={t("permission.group")} name="user-friends" />
) : (
<Icon title={t("permission.user")} name="user" />
);
return (
<tr className={classes.columnWidth}>
<td className={classes.centerMiddle}>
{iconType} {permission.name}
</td>
{roleSelector}
<td className={classes.centerMiddle}>
<Button
label={t("permission.advanced-button.label")}
action={this.handleDetailedPermissionsPressed}
/>
</td>
<td className={classNames("is-darker", classes.centerMiddle)}>
<DeletePermissionButton
permission={permission}
namespace={namespace}
repoName={repoName}
deletePermission={this.deletePermission}
loading={this.props.deleteLoading}
/>
{advancedDialog}
</td>
</tr>
);
}
mayChangePermissions = () => {
return this.props.permission._links && this.props.permission._links.update;
};
handleDetailedPermissionsPressed = () => {
this.setState({ showAdvancedDialog: true });
};
closeAdvancedPermissionsDialog = () => {
this.setState({ showAdvancedDialog: false });
};
submitAdvancedPermissionsDialog = (newVerbs: string[]) => {
const { permission } = this.state;
this.setState(
{
showAdvancedDialog: false,
permission: { ...permission, role: undefined, verbs: newVerbs }
},
() => this.modifyPermissionVerbs(newVerbs)
);
};
handleRoleChange = (role: string) => {
const { permission } = this.state;
this.setState(
{
permission: { ...permission, role: role, verbs: undefined }
},
() => this.modifyPermissionRole(role)
);
};
findAvailableRole = (roleName: string) => {
const { availableRepositoryRoles } = this.props;
return availableRepositoryRoles.find(role => role.name === roleName);
};
modifyPermissionRole = (role: string) => {
let permission = this.state.permission;
permission.role = role;
this.props.modifyPermission(
permission,
this.props.namespace,
this.props.repoName
);
};
modifyPermissionVerbs = (verbs: string[]) => {
let permission = this.state.permission;
permission.verbs = verbs;
this.props.modifyPermission(
permission,
this.props.namespace,
this.props.repoName
);
};
}
const mapStateToProps = (state, ownProps) => {
const permission = ownProps.permission;
const loading = isModifyPermissionPending(
state,
ownProps.namespace,
ownProps.repoName,
permission
);
const deleteLoading = isDeletePermissionPending(
state,
ownProps.namespace,
ownProps.repoName,
permission
);
return { loading, deleteLoading };
};
const mapDispatchToProps = dispatch => {
return {
modifyPermission: (
permission: Permission,
namespace: string,
repoName: string
) => {
dispatch(modifyPermission(permission, namespace, repoName));
},
deletePermission: (
permission: Permission,
namespace: string,
repoName: string
) => {
dispatch(deletePermission(permission, namespace, repoName));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("repos")(injectSheet(styles)(SinglePermission)));

View File

@@ -0,0 +1,755 @@
// @flow
import type { Action } from "@scm-manager/ui-components";
import { apiClient } from "@scm-manager/ui-components";
import * as types from "../../../modules/types";
import type {
RepositoryRole,
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_AVAILABLE = "scm/permissions/FETCH_AVAILABLE";
export const FETCH_AVAILABLE_PENDING = `${FETCH_AVAILABLE}_${
types.PENDING_SUFFIX
}`;
export const FETCH_AVAILABLE_SUCCESS = `${FETCH_AVAILABLE}_${
types.SUCCESS_SUFFIX
}`;
export const FETCH_AVAILABLE_FAILURE = `${FETCH_AVAILABLE}_${
types.FAILURE_SUFFIX
}`;
export const FETCH_PERMISSIONS = "scm/permissions/FETCH_PERMISSIONS";
export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
types.PENDING_SUFFIX
}`;
export const FETCH_PERMISSIONS_SUCCESS = `${FETCH_PERMISSIONS}_${
types.SUCCESS_SUFFIX
}`;
export const FETCH_PERMISSIONS_FAILURE = `${FETCH_PERMISSIONS}_${
types.FAILURE_SUFFIX
}`;
export const MODIFY_PERMISSION = "scm/permissions/MODFIY_PERMISSION";
export const MODIFY_PERMISSION_PENDING = `${MODIFY_PERMISSION}_${
types.PENDING_SUFFIX
}`;
export const MODIFY_PERMISSION_SUCCESS = `${MODIFY_PERMISSION}_${
types.SUCCESS_SUFFIX
}`;
export const MODIFY_PERMISSION_FAILURE = `${MODIFY_PERMISSION}_${
types.FAILURE_SUFFIX
}`;
export const MODIFY_PERMISSION_RESET = `${MODIFY_PERMISSION}_${
types.RESET_SUFFIX
}`;
export const CREATE_PERMISSION = "scm/permissions/CREATE_PERMISSION";
export const CREATE_PERMISSION_PENDING = `${CREATE_PERMISSION}_${
types.PENDING_SUFFIX
}`;
export const CREATE_PERMISSION_SUCCESS = `${CREATE_PERMISSION}_${
types.SUCCESS_SUFFIX
}`;
export const CREATE_PERMISSION_FAILURE = `${CREATE_PERMISSION}_${
types.FAILURE_SUFFIX
}`;
export const CREATE_PERMISSION_RESET = `${CREATE_PERMISSION}_${
types.RESET_SUFFIX
}`;
export const DELETE_PERMISSION = "scm/permissions/DELETE_PERMISSION";
export const DELETE_PERMISSION_PENDING = `${DELETE_PERMISSION}_${
types.PENDING_SUFFIX
}`;
export const DELETE_PERMISSION_SUCCESS = `${DELETE_PERMISSION}_${
types.SUCCESS_SUFFIX
}`;
export const DELETE_PERMISSION_FAILURE = `${DELETE_PERMISSION}_${
types.FAILURE_SUFFIX
}`;
export const DELETE_PERMISSION_RESET = `${DELETE_PERMISSION}_${
types.RESET_SUFFIX
}`;
const CONTENT_TYPE = "application/vnd.scmm-repositoryPermission+json";
// fetch available permissions
export function fetchAvailablePermissionsIfNeeded(
repositoryRolesLink: string,
repositoryVerbsLink: string
) {
return function(dispatch: any, getState: () => Object) {
if (shouldFetchAvailablePermissions(getState())) {
return fetchAvailablePermissions(
dispatch,
getState,
repositoryRolesLink,
repositoryVerbsLink
);
}
};
}
export function fetchAvailablePermissions(
dispatch: any,
getState: () => Object,
repositoryRolesLink: string,
repositoryVerbsLink: string
) {
dispatch(fetchAvailablePending());
return apiClient
.get(repositoryRolesLink)
.then(repositoryRoles => repositoryRoles.json())
.then(repositoryRoles => repositoryRoles._embedded.repositoryRoles)
.then(repositoryRoles => {
return apiClient
.get(repositoryVerbsLink)
.then(repositoryVerbs => repositoryVerbs.json())
.then(repositoryVerbs => repositoryVerbs.verbs)
.then(repositoryVerbs => {
return {
repositoryVerbs,
repositoryRoles
};
});
})
.then(available => {
dispatch(fetchAvailableSuccess(available));
})
.catch(err => {
dispatch(fetchAvailableFailure(err));
});
}
export function shouldFetchAvailablePermissions(state: Object) {
if (
isFetchAvailablePermissionsPending(state) ||
getFetchAvailablePermissionsFailure(state)
) {
return false;
}
return !state.available;
}
export function fetchAvailablePending(): Action {
return {
type: FETCH_AVAILABLE_PENDING,
payload: {},
itemId: "available"
};
}
export function fetchAvailableSuccess(
available: [RepositoryRole[], string[]]
): Action {
return {
type: FETCH_AVAILABLE_SUCCESS,
payload: available,
itemId: "available"
};
}
export function fetchAvailableFailure(error: Error): Action {
return {
type: FETCH_AVAILABLE_FAILURE,
payload: {
error
},
itemId: "available"
};
}
// fetch permissions
export function fetchPermissions(
link: string,
namespace: string,
repoName: string
) {
return function(dispatch: any) {
dispatch(fetchPermissionsPending(namespace, repoName));
return apiClient
.get(link)
.then(response => response.json())
.then(permissions => {
dispatch(fetchPermissionsSuccess(permissions, namespace, repoName));
})
.catch(err => {
dispatch(fetchPermissionsFailure(namespace, repoName, err));
});
};
}
export function fetchPermissionsPending(
namespace: string,
repoName: string
): Action {
return {
type: FETCH_PERMISSIONS_PENDING,
payload: {
namespace,
repoName
},
itemId: namespace + "/" + repoName
};
}
export function fetchPermissionsSuccess(
permissions: any,
namespace: string,
repoName: string
): Action {
return {
type: FETCH_PERMISSIONS_SUCCESS,
payload: permissions,
itemId: namespace + "/" + repoName
};
}
export function fetchPermissionsFailure(
namespace: string,
repoName: string,
error: Error
): Action {
return {
type: FETCH_PERMISSIONS_FAILURE,
payload: {
namespace,
repoName,
error
},
itemId: namespace + "/" + repoName
};
}
// modify permission
export function modifyPermission(
permission: Permission,
namespace: string,
repoName: string,
callback?: () => void
) {
return function(dispatch: any) {
dispatch(modifyPermissionPending(permission, namespace, repoName));
return apiClient
.put(permission._links.update.href, permission, CONTENT_TYPE)
.then(() => {
dispatch(modifyPermissionSuccess(permission, namespace, repoName));
if (callback) {
callback();
}
})
.catch(err => {
dispatch(modifyPermissionFailure(permission, err, namespace, repoName));
});
};
}
export function modifyPermissionPending(
permission: Permission,
namespace: string,
repoName: string
): Action {
return {
type: MODIFY_PERMISSION_PENDING,
payload: permission,
itemId: createItemId(permission, namespace, repoName)
};
}
export function modifyPermissionSuccess(
permission: Permission,
namespace: string,
repoName: string
): Action {
return {
type: MODIFY_PERMISSION_SUCCESS,
payload: {
permission,
position: namespace + "/" + repoName
},
itemId: createItemId(permission, namespace, repoName)
};
}
export function modifyPermissionFailure(
permission: Permission,
error: Error,
namespace: string,
repoName: string
): Action {
return {
type: MODIFY_PERMISSION_FAILURE,
payload: { error, permission },
itemId: createItemId(permission, namespace, repoName)
};
}
function newPermissions(
oldPermissions: PermissionCollection,
newPermission: Permission
) {
for (let i = 0; i < oldPermissions.length; i++) {
if (oldPermissions[i].name === newPermission.name) {
oldPermissions.splice(i, 1, newPermission);
return oldPermissions;
}
}
}
export function modifyPermissionReset(namespace: string, repoName: string) {
return {
type: MODIFY_PERMISSION_RESET,
payload: {
namespace,
repoName
},
itemId: namespace + "/" + repoName
};
}
// create permission
export function createPermission(
link: string,
permission: PermissionCreateEntry,
namespace: string,
repoName: string,
callback?: () => void
) {
return function(dispatch: Dispatch) {
dispatch(createPermissionPending(permission, namespace, repoName));
return apiClient
.post(link, permission, CONTENT_TYPE)
.then(response => {
const location = response.headers.get("Location");
return apiClient.get(location);
})
.then(response => response.json())
.then(createdPermission => {
dispatch(
createPermissionSuccess(createdPermission, namespace, repoName)
);
if (callback) {
callback();
}
})
.catch(err =>
dispatch(createPermissionFailure(err, namespace, repoName))
);
};
}
export function createPermissionPending(
permission: PermissionCreateEntry,
namespace: string,
repoName: string
): Action {
return {
type: CREATE_PERMISSION_PENDING,
payload: permission,
itemId: namespace + "/" + repoName
};
}
export function createPermissionSuccess(
permission: PermissionCreateEntry,
namespace: string,
repoName: string
): Action {
return {
type: CREATE_PERMISSION_SUCCESS,
payload: {
permission,
position: namespace + "/" + repoName
},
itemId: namespace + "/" + repoName
};
}
export function createPermissionFailure(
error: Error,
namespace: string,
repoName: string
): Action {
return {
type: CREATE_PERMISSION_FAILURE,
payload: error,
itemId: namespace + "/" + repoName
};
}
export function createPermissionReset(namespace: string, repoName: string) {
return {
type: CREATE_PERMISSION_RESET,
itemId: namespace + "/" + repoName
};
}
// delete permission
export function deletePermission(
permission: Permission,
namespace: string,
repoName: string,
callback?: () => void
) {
return function(dispatch: any) {
dispatch(deletePermissionPending(permission, namespace, repoName));
return apiClient
.delete(permission._links.delete.href)
.then(() => {
dispatch(deletePermissionSuccess(permission, namespace, repoName));
if (callback) {
callback();
}
})
.catch(err => {
dispatch(deletePermissionFailure(permission, namespace, repoName, err));
});
};
}
export function deletePermissionPending(
permission: Permission,
namespace: string,
repoName: string
): Action {
return {
type: DELETE_PERMISSION_PENDING,
payload: permission,
itemId: createItemId(permission, namespace, repoName)
};
}
export function deletePermissionSuccess(
permission: Permission,
namespace: string,
repoName: string
): Action {
return {
type: DELETE_PERMISSION_SUCCESS,
payload: {
permission,
position: namespace + "/" + repoName
},
itemId: createItemId(permission, namespace, repoName)
};
}
export function deletePermissionFailure(
permission: Permission,
namespace: string,
repoName: string,
error: Error
): Action {
return {
type: DELETE_PERMISSION_FAILURE,
payload: {
error,
permission
},
itemId: createItemId(permission, namespace, repoName)
};
}
export function deletePermissionReset(namespace: string, repoName: string) {
return {
type: DELETE_PERMISSION_RESET,
payload: {
namespace,
repoName
},
itemId: namespace + "/" + repoName
};
}
function deletePermissionFromState(
oldPermissions: PermissionCollection,
permission: Permission
) {
let newPermission = [];
for (let i = 0; i < oldPermissions.length; i++) {
if (
oldPermissions[i].name !== permission.name ||
oldPermissions[i].groupPermission !== permission.groupPermission
) {
newPermission.push(oldPermissions[i]);
}
}
return newPermission;
}
function createItemId(
permission: Permission,
namespace: string,
repoName: string
) {
let groupPermission = permission.groupPermission ? "@" : "";
return namespace + "/" + repoName + "/" + groupPermission + permission.name;
}
// reducer
export default function reducer(
state: Object = {},
action: Action = { type: "UNKNOWN" }
): Object {
if (!action.payload) {
return state;
}
switch (action.type) {
case FETCH_AVAILABLE_SUCCESS:
return {
...state,
available: action.payload
};
case FETCH_PERMISSIONS_SUCCESS:
return {
...state,
[action.itemId]: {
entries: action.payload._embedded.permissions,
createPermission: !!action.payload._links.create
}
};
case MODIFY_PERMISSION_SUCCESS:
const positionOfPermission = action.payload.position;
const newPermission = newPermissions(
state[action.payload.position].entries,
action.payload.permission
);
return {
...state,
[positionOfPermission]: {
...state[positionOfPermission],
entries: newPermission
}
};
case CREATE_PERMISSION_SUCCESS:
// return state;
const position = action.payload.position;
const permissions = state[action.payload.position].entries;
permissions.push(action.payload.permission);
return {
...state,
[position]: {
...state[position],
entries: permissions
}
};
case DELETE_PERMISSION_SUCCESS:
const permissionPosition = action.payload.position;
const new_Permissions = deletePermissionFromState(
state[action.payload.position].entries,
action.payload.permission
);
return {
...state,
[permissionPosition]: {
...state[permissionPosition],
entries: new_Permissions
}
};
default:
return state;
}
}
// selectors
export function getAvailablePermissions(state: Object) {
if (state.permissions) {
return state.permissions.available;
}
}
export function getAvailableRepositoryRoles(state: Object) {
return available(state).repositoryRoles;
}
export function getAvailableRepositoryVerbs(state: Object) {
return available(state).repositoryVerbs;
}
function available(state: Object) {
if (state.permissions && state.permissions.available) {
return state.permissions.available;
}
return {};
}
export function getPermissionsOfRepo(
state: Object,
namespace: string,
repoName: string
) {
if (state.permissions && state.permissions[namespace + "/" + repoName]) {
return state.permissions[namespace + "/" + repoName].entries;
}
}
export function isFetchAvailablePermissionsPending(state: Object) {
return isPending(state, FETCH_AVAILABLE, "available");
}
export function isFetchPermissionsPending(
state: Object,
namespace: string,
repoName: string
) {
return isPending(state, FETCH_PERMISSIONS, namespace + "/" + repoName);
}
export function getFetchAvailablePermissionsFailure(state: Object) {
return getFailure(state, FETCH_AVAILABLE, "available");
}
export function getFetchPermissionsFailure(
state: Object,
namespace: string,
repoName: string
) {
return getFailure(state, FETCH_PERMISSIONS, namespace + "/" + repoName);
}
export function isModifyPermissionPending(
state: Object,
namespace: string,
repoName: string,
permission: Permission
) {
return isPending(
state,
MODIFY_PERMISSION,
createItemId(permission, namespace, repoName)
);
}
export function getModifyPermissionFailure(
state: Object,
namespace: string,
repoName: string,
permission: Permission
) {
return getFailure(
state,
MODIFY_PERMISSION,
createItemId(permission, namespace, repoName)
);
}
export function hasCreatePermission(
state: Object,
namespace: string,
repoName: string
) {
if (state.permissions && state.permissions[namespace + "/" + repoName])
return state.permissions[namespace + "/" + repoName].createPermission;
else return null;
}
export function isCreatePermissionPending(
state: Object,
namespace: string,
repoName: string
) {
return isPending(state, CREATE_PERMISSION, namespace + "/" + repoName);
}
export function getCreatePermissionFailure(
state: Object,
namespace: string,
repoName: string
) {
return getFailure(state, CREATE_PERMISSION, namespace + "/" + repoName);
}
export function isDeletePermissionPending(
state: Object,
namespace: string,
repoName: string,
permission: Permission
) {
return isPending(
state,
DELETE_PERMISSION,
createItemId(permission, namespace, repoName)
);
}
export function getDeletePermissionFailure(
state: Object,
namespace: string,
repoName: string,
permission: Permission
) {
return getFailure(
state,
DELETE_PERMISSION,
createItemId(permission, namespace, repoName)
);
}
export function getDeletePermissionsFailure(
state: Object,
namespace: string,
repoName: string
) {
const permissions =
state.permissions && state.permissions[namespace + "/" + repoName]
? state.permissions[namespace + "/" + repoName].entries
: null;
if (permissions == null) return undefined;
for (let i = 0; i < permissions.length; i++) {
if (
getDeletePermissionFailure(state, namespace, repoName, permissions[i])
) {
return getFailure(
state,
DELETE_PERMISSION,
createItemId(permissions[i], namespace, repoName)
);
}
}
return null;
}
export function getModifyPermissionsFailure(
state: Object,
namespace: string,
repoName: string
) {
const permissions =
state.permissions && state.permissions[namespace + "/" + repoName]
? state.permissions[namespace + "/" + repoName].entries
: null;
if (permissions == null) return undefined;
for (let i = 0; i < permissions.length; i++) {
if (
getModifyPermissionFailure(state, namespace, repoName, permissions[i])
) {
return getFailure(
state,
MODIFY_PERMISSION,
createItemId(permissions[i], namespace, repoName)
);
}
}
return null;
}
export function findVerbsForRole(
availableRepositoryRoles: RepositoryRole[],
roleName: string
) {
const matchingRole = availableRepositoryRoles.find(
role => roleName === role.name
);
if (matchingRole) {
return matchingRole.verbs;
} else {
return [];
}
}

View File

@@ -0,0 +1,783 @@
// @flow
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import fetchMock from "fetch-mock";
import reducer, {
CREATE_PERMISSION,
CREATE_PERMISSION_FAILURE,
CREATE_PERMISSION_PENDING,
CREATE_PERMISSION_SUCCESS,
createPermission,
createPermissionSuccess,
DELETE_PERMISSION,
DELETE_PERMISSION_FAILURE,
DELETE_PERMISSION_PENDING,
DELETE_PERMISSION_SUCCESS,
deletePermission,
deletePermissionSuccess,
FETCH_PERMISSIONS,
FETCH_PERMISSIONS_FAILURE,
FETCH_PERMISSIONS_PENDING,
FETCH_PERMISSIONS_SUCCESS,
fetchPermissions,
fetchPermissionsSuccess,
getCreatePermissionFailure,
getDeletePermissionFailure,
getDeletePermissionsFailure,
getFetchPermissionsFailure,
getModifyPermissionFailure,
getModifyPermissionsFailure,
getPermissionsOfRepo,
hasCreatePermission,
isCreatePermissionPending,
isDeletePermissionPending,
isFetchPermissionsPending,
isModifyPermissionPending,
MODIFY_PERMISSION,
MODIFY_PERMISSION_FAILURE,
MODIFY_PERMISSION_PENDING,
MODIFY_PERMISSION_SUCCESS,
modifyPermission,
modifyPermissionSuccess
} from "./permissions";
import type {Permission, PermissionCollection} from "@scm-manager/ui-types";
const hitchhiker_puzzle42Permission_user_eins: Permission = {
name: "user_eins",
type: "READ",
groupPermission: false,
_links: {
self: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
},
delete: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
},
update: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
}
},
verbs: []
};
const hitchhiker_puzzle42Permission_user_zwei: Permission = {
name: "user_zwei",
type: "WRITE",
groupPermission: true,
_links: {
self: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
},
delete: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
},
update: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
}
},
verbs: []
};
const hitchhiker_puzzle42Permissions: PermissionCollection = [
hitchhiker_puzzle42Permission_user_eins,
hitchhiker_puzzle42Permission_user_zwei
];
const hitchhiker_puzzle42RepoPermissions = {
_embedded: {
permissions: hitchhiker_puzzle42Permissions
},
_links: {
create: {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions"
}
}
};
describe("permission fetch", () => {
const REPOS_URL = "/api/v2/repositories";
const URL = "repositories";
const mockStore = configureMockStore([thunk]);
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should successfully fetch permissions to repo hitchhiker/puzzle42", () => {
fetchMock.getOnce(
REPOS_URL + "/hitchhiker/puzzle42/permissions",
hitchhiker_puzzle42RepoPermissions
);
const expectedActions = [
{
type: FETCH_PERMISSIONS_PENDING,
payload: {
namespace: "hitchhiker",
repoName: "puzzle42"
},
itemId: "hitchhiker/puzzle42"
},
{
type: FETCH_PERMISSIONS_SUCCESS,
payload: hitchhiker_puzzle42RepoPermissions,
itemId: "hitchhiker/puzzle42"
}
];
const store = mockStore({});
return store
.dispatch(
fetchPermissions(
URL + "/hitchhiker/puzzle42/permissions",
"hitchhiker",
"puzzle42"
)
)
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it("should dispatch FETCH_PERMISSIONS_FAILURE, it the request fails", () => {
fetchMock.getOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", {
status: 500
});
const store = mockStore({});
return store
.dispatch(
fetchPermissions(
URL + "/hitchhiker/puzzle42/permissions",
"hitchhiker",
"puzzle42"
)
)
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(FETCH_PERMISSIONS_PENDING);
expect(actions[1].type).toEqual(FETCH_PERMISSIONS_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should successfully modify user_eins permission", () => {
fetchMock.putOnce(
hitchhiker_puzzle42Permission_user_eins._links.update.href,
{
status: 204
}
);
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
const store = mockStore({});
return store
.dispatch(modifyPermission(editedPermission, "hitchhiker", "puzzle42"))
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(MODIFY_PERMISSION_PENDING);
expect(actions[1].type).toEqual(MODIFY_PERMISSION_SUCCESS);
});
});
it("should successfully modify user_eins permission and call the callback", () => {
fetchMock.putOnce(
hitchhiker_puzzle42Permission_user_eins._links.update.href,
{
status: 204
}
);
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
const store = mockStore({});
let called = false;
const callback = () => {
called = true;
};
return store
.dispatch(
modifyPermission(editedPermission, "hitchhiker", "puzzle42", callback)
)
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(MODIFY_PERMISSION_PENDING);
expect(actions[1].type).toEqual(MODIFY_PERMISSION_SUCCESS);
expect(called).toBe(true);
});
});
it("should fail modifying on HTTP 500", () => {
fetchMock.putOnce(
hitchhiker_puzzle42Permission_user_eins._links.update.href,
{
status: 500
}
);
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
const store = mockStore({});
return store
.dispatch(modifyPermission(editedPermission, "hitchhiker", "puzzle42"))
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(MODIFY_PERMISSION_PENDING);
expect(actions[1].type).toEqual(MODIFY_PERMISSION_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should add a permission successfully", () => {
// unmatched
fetchMock.postOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", {
status: 204,
headers: {
location: "repositories/hitchhiker/puzzle42/permissions/user_eins"
}
});
fetchMock.getOnce(
REPOS_URL + "/hitchhiker/puzzle42/permissions/user_eins",
hitchhiker_puzzle42Permission_user_eins
);
const store = mockStore({});
return store
.dispatch(
createPermission(
URL + "/hitchhiker/puzzle42/permissions",
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42"
)
)
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(CREATE_PERMISSION_PENDING);
expect(actions[1].type).toEqual(CREATE_PERMISSION_SUCCESS);
});
});
it("should fail adding a permission on HTTP 500", () => {
fetchMock.postOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", {
status: 500
});
const store = mockStore({});
return store
.dispatch(
createPermission(
URL + "/hitchhiker/puzzle42/permissions",
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42"
)
)
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(CREATE_PERMISSION_PENDING);
expect(actions[1].type).toEqual(CREATE_PERMISSION_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should call the callback after permission successfully created", () => {
// unmatched
fetchMock.postOnce(REPOS_URL + "/hitchhiker/puzzle42/permissions", {
status: 204,
headers: {
location: "repositories/hitchhiker/puzzle42/permissions/user_eins"
}
});
fetchMock.getOnce(
REPOS_URL + "/hitchhiker/puzzle42/permissions/user_eins",
hitchhiker_puzzle42Permission_user_eins
);
let callMe = "not yet";
const callback = () => {
callMe = "yeah";
};
const store = mockStore({});
return store
.dispatch(
createPermission(
URL + "/hitchhiker/puzzle42/permissions",
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42",
callback
)
)
.then(() => {
expect(callMe).toBe("yeah");
});
});
it("should delete successfully permission user_eins", () => {
fetchMock.deleteOnce(
hitchhiker_puzzle42Permission_user_eins._links.delete.href,
{
status: 204
}
);
const store = mockStore({});
return store
.dispatch(
deletePermission(
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42"
)
)
.then(() => {
const actions = store.getActions();
expect(actions.length).toBe(2);
expect(actions[0].type).toEqual(DELETE_PERMISSION_PENDING);
expect(actions[0].payload).toBe(
hitchhiker_puzzle42Permission_user_eins
);
expect(actions[1].type).toEqual(DELETE_PERMISSION_SUCCESS);
});
});
it("should call the callback, after successful delete", () => {
fetchMock.deleteOnce(
hitchhiker_puzzle42Permission_user_eins._links.delete.href,
{
status: 204
}
);
let called = false;
const callMe = () => {
called = true;
};
const store = mockStore({});
return store
.dispatch(
deletePermission(
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42",
callMe
)
)
.then(() => {
expect(called).toBeTruthy();
});
});
it("should fail to delete permission", () => {
fetchMock.deleteOnce(
hitchhiker_puzzle42Permission_user_eins._links.delete.href,
{
status: 500
}
);
const store = mockStore({});
return store
.dispatch(
deletePermission(
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42"
)
)
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(DELETE_PERMISSION_PENDING);
expect(actions[0].payload).toBe(
hitchhiker_puzzle42Permission_user_eins
);
expect(actions[1].type).toEqual(DELETE_PERMISSION_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
});
describe("permissions 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 permissions on FETCH_PERMISSION_SUCCESS", () => {
const newState = reducer(
{},
fetchPermissionsSuccess(
hitchhiker_puzzle42RepoPermissions,
"hitchhiker",
"puzzle42"
)
);
expect(newState["hitchhiker/puzzle42"].entries).toBe(
hitchhiker_puzzle42Permissions
);
});
it("should update permission", () => {
const oldState = {
"hitchhiker/puzzle42": {
entries: [hitchhiker_puzzle42Permission_user_eins]
}
};
let permissionEdited = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
let expectedState = {
"hitchhiker/puzzle42": {
entries: [permissionEdited]
}
};
const newState = reducer(
oldState,
modifyPermissionSuccess(permissionEdited, "hitchhiker", "puzzle42")
);
expect(newState["hitchhiker/puzzle42"]).toEqual(
expectedState["hitchhiker/puzzle42"]
);
});
it("should remove permission from state when delete succeeds", () => {
const state = {
"hitchhiker/puzzle42": {
entries: [
hitchhiker_puzzle42Permission_user_eins,
hitchhiker_puzzle42Permission_user_zwei
]
}
};
const expectedState = {
"hitchhiker/puzzle42": {
entries: [hitchhiker_puzzle42Permission_user_zwei]
}
};
const newState = reducer(
state,
deletePermissionSuccess(
hitchhiker_puzzle42Permission_user_eins,
"hitchhiker",
"puzzle42"
)
);
expect(newState["hitchhiker/puzzle42"]).toEqual(
expectedState["hitchhiker/puzzle42"]
);
});
it("should add permission", () => {
//changing state had to be removed because of errors
const oldState = {
"hitchhiker/puzzle42": {
entries: [hitchhiker_puzzle42Permission_user_eins]
}
};
let expectedState = {
"hitchhiker/puzzle42": {
entries: [
hitchhiker_puzzle42Permission_user_eins,
hitchhiker_puzzle42Permission_user_zwei
]
}
};
const newState = reducer(
oldState,
createPermissionSuccess(
hitchhiker_puzzle42Permission_user_zwei,
"hitchhiker",
"puzzle42"
)
);
expect(newState["hitchhiker/puzzle42"]).toEqual(
expectedState["hitchhiker/puzzle42"]
);
});
});
describe("permissions selectors", () => {
const error = new Error("something goes wrong");
it("should return the permissions of one repository", () => {
const state = {
permissions: {
"hitchhiker/puzzle42": {
entries: hitchhiker_puzzle42Permissions
}
}
};
const repoPermissions = getPermissionsOfRepo(
state,
"hitchhiker",
"puzzle42"
);
expect(repoPermissions).toEqual(hitchhiker_puzzle42Permissions);
});
it("should return true, when fetch permissions is pending", () => {
const state = {
pending: {
[FETCH_PERMISSIONS + "/hitchhiker/puzzle42"]: true
}
};
expect(isFetchPermissionsPending(state, "hitchhiker", "puzzle42")).toEqual(
true
);
});
it("should return false, when fetch permissions is not pending", () => {
expect(isFetchPermissionsPending({}, "hitchiker", "puzzle42")).toEqual(
false
);
});
it("should return error when fetch permissions did fail", () => {
const state = {
failure: {
[FETCH_PERMISSIONS + "/hitchhiker/puzzle42"]: error
}
};
expect(getFetchPermissionsFailure(state, "hitchhiker", "puzzle42")).toEqual(
error
);
});
it("should return undefined when fetch permissions did not fail", () => {
expect(getFetchPermissionsFailure({}, "hitchhiker", "puzzle42")).toBe(
undefined
);
});
it("should return true, when modify permission is pending", () => {
const state = {
pending: {
[MODIFY_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: true
}
};
expect(
isModifyPermissionPending(
state,
"hitchhiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toEqual(true);
});
it("should return false, when modify permission is not pending", () => {
expect(
isModifyPermissionPending(
{},
"hitchiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toEqual(false);
});
it("should return error when modify permission did fail", () => {
const state = {
failure: {
[MODIFY_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error
}
};
expect(
getModifyPermissionFailure(
state,
"hitchhiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toEqual(error);
});
it("should return undefined when modify permission did not fail", () => {
expect(
getModifyPermissionFailure(
{},
"hitchhiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toBe(undefined);
});
it("should return error when one of the modify permissions did fail", () => {
const state = {
permissions: {
"hitchhiker/puzzle42": { entries: hitchhiker_puzzle42Permissions }
},
failure: {
[MODIFY_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error
}
};
expect(
getModifyPermissionsFailure(state, "hitchhiker", "puzzle42")
).toEqual(error);
});
it("should return undefined when no modify permissions did not fail", () => {
expect(getModifyPermissionsFailure({}, "hitchhiker", "puzzle42")).toBe(
undefined
);
});
it("should return true, when createPermission is true", () => {
const state = {
permissions: {
"hitchhiker/puzzle42": {
createPermission: true
}
}
};
expect(hasCreatePermission(state, "hitchhiker", "puzzle42")).toBe(true);
});
it("should return false, when createPermission is false", () => {
const state = {
permissions: {
"hitchhiker/puzzle42": {
createPermission: false
}
}
};
expect(hasCreatePermission(state, "hitchhiker", "puzzle42")).toEqual(false);
});
it("should return true, when delete permission is pending", () => {
const state = {
pending: {
[DELETE_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: true
}
};
expect(
isDeletePermissionPending(
state,
"hitchhiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toEqual(true);
});
it("should return false, when delete permission is not pending", () => {
expect(
isDeletePermissionPending(
{},
"hitchiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toEqual(false);
});
it("should return error when delete permission did fail", () => {
const state = {
failure: {
[DELETE_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error
}
};
expect(
getDeletePermissionFailure(
state,
"hitchhiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toEqual(error);
});
it("should return undefined when delete permission did not fail", () => {
expect(
getDeletePermissionFailure(
{},
"hitchhiker",
"puzzle42",
hitchhiker_puzzle42Permission_user_eins
)
).toBe(undefined);
});
it("should return error when one of the delete permissions did fail", () => {
const state = {
permissions: {
"hitchhiker/puzzle42": { entries: hitchhiker_puzzle42Permissions }
},
failure: {
[DELETE_PERMISSION + "/hitchhiker/puzzle42/user_eins"]: error
}
};
expect(
getDeletePermissionsFailure(state, "hitchhiker", "puzzle42")
).toEqual(error);
});
it("should return undefined when no delete permissions did not fail", () => {
expect(getDeletePermissionsFailure({}, "hitchhiker", "puzzle42")).toBe(
undefined
);
});
it("should return true, when create permission is pending", () => {
const state = {
pending: {
[CREATE_PERMISSION + "/hitchhiker/puzzle42"]: true
}
};
expect(isCreatePermissionPending(state, "hitchhiker", "puzzle42")).toEqual(
true
);
});
it("should return false, when create permissions is not pending", () => {
expect(isCreatePermissionPending({}, "hitchiker", "puzzle42")).toEqual(
false
);
});
it("should return error when create permissions did fail", () => {
const state = {
failure: {
[CREATE_PERMISSION + "/hitchhiker/puzzle42"]: error
}
};
expect(getCreatePermissionFailure(state, "hitchhiker", "puzzle42")).toEqual(
error
);
});
it("should return undefined when create permissions did not fail", () => {
expect(getCreatePermissionFailure({}, "hitchhiker", "puzzle42")).toBe(
undefined
);
});
});