mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-14 01:15:44 +01:00
implemented ui for sources root
This commit is contained in:
27
scm-ui-components/packages/ui-types/src/Sources.js
Normal file
27
scm-ui-components/packages/ui-types/src/Sources.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import type { Collection, Links } from "./hal";
|
||||||
|
|
||||||
|
// TODO ?? check ?? links
|
||||||
|
export type SubRepository = {
|
||||||
|
repositoryUrl: string,
|
||||||
|
browserUrl: string,
|
||||||
|
revision: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export type File = {
|
||||||
|
name: string,
|
||||||
|
path: string,
|
||||||
|
directory: boolean,
|
||||||
|
description?: string,
|
||||||
|
length: number,
|
||||||
|
lastModified?: string,
|
||||||
|
subRepository?: SubRepository, // TODO
|
||||||
|
_links: Links
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SourcesCollection = Collection & {
|
||||||
|
_embedded: {
|
||||||
|
files: File[]
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -10,3 +10,5 @@ export type { Repository, RepositoryCollection, RepositoryGroup } from "./Reposi
|
|||||||
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
|
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
|
||||||
|
|
||||||
export type { Config } from "./Config";
|
export type { Config } from "./Config";
|
||||||
|
|
||||||
|
export type { SubRepository, File, SourcesCollection } from "./Sources";
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
"actions-label": "Actions",
|
"actions-label": "Actions",
|
||||||
"back-label": "Back",
|
"back-label": "Back",
|
||||||
"navigation-label": "Navigation",
|
"navigation-label": "Navigation",
|
||||||
"information": "Information"
|
"information": "Information",
|
||||||
|
"sources": "Sources"
|
||||||
},
|
},
|
||||||
"create": {
|
"create": {
|
||||||
"title": "Create Repository",
|
"title": "Create Repository",
|
||||||
@@ -42,5 +43,13 @@
|
|||||||
"submit": "Yes",
|
"submit": "Yes",
|
||||||
"cancel": "No"
|
"cancel": "No"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"file-tree": {
|
||||||
|
"name": "Name",
|
||||||
|
"length": "Length",
|
||||||
|
"lastModified": "Last modified",
|
||||||
|
"description": "Description"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { routerReducer, routerMiddleware } from "react-router-redux";
|
|||||||
import users from "./users/modules/users";
|
import users from "./users/modules/users";
|
||||||
import repos from "./repos/modules/repos";
|
import repos from "./repos/modules/repos";
|
||||||
import repositoryTypes from "./repos/modules/repositoryTypes";
|
import repositoryTypes from "./repos/modules/repositoryTypes";
|
||||||
|
import sources from "./repos/sources/modules/sources";
|
||||||
import groups from "./groups/modules/groups";
|
import groups from "./groups/modules/groups";
|
||||||
import auth from "./modules/auth";
|
import auth from "./modules/auth";
|
||||||
import pending from "./modules/pending";
|
import pending from "./modules/pending";
|
||||||
@@ -28,7 +29,8 @@ function createReduxStore(history: BrowserHistory) {
|
|||||||
repositoryTypes,
|
repositoryTypes,
|
||||||
groups,
|
groups,
|
||||||
auth,
|
auth,
|
||||||
config
|
config,
|
||||||
|
sources
|
||||||
});
|
});
|
||||||
|
|
||||||
return createStore(
|
return createStore(
|
||||||
|
|||||||
28
scm-ui/src/repos/components/RepositoryNavLink.js
Normal file
28
scm-ui/src/repos/components/RepositoryNavLink.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import type { Repository } from "@scm-manager/ui-types";
|
||||||
|
import { NavLink } from "@scm-manager/ui-components";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
repository: Repository,
|
||||||
|
to: string,
|
||||||
|
label: string,
|
||||||
|
linkName: string
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component renders only if the repository contains the link with the given name.
|
||||||
|
*/
|
||||||
|
class RepositoryNavLink extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { linkName, to, label, repository } = this.props;
|
||||||
|
|
||||||
|
if (!repository._links[linkName]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <NavLink to={to} label={label} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RepositoryNavLink;
|
||||||
49
scm-ui/src/repos/components/RepositoryNavLink.test.js
Normal file
49
scm-ui/src/repos/components/RepositoryNavLink.test.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { shallow, mount } from "enzyme";
|
||||||
|
import "../../tests/enzyme";
|
||||||
|
import "../../tests/i18n";
|
||||||
|
import ReactRouterEnzymeContext from "react-router-enzyme-context";
|
||||||
|
import RepositoryNavLink from "./RepositoryNavLink";
|
||||||
|
|
||||||
|
describe("RepositoryNavLink", () => {
|
||||||
|
const options = new ReactRouterEnzymeContext();
|
||||||
|
|
||||||
|
it("should render nothing, if the sources link is missing", () => {
|
||||||
|
const repository = {
|
||||||
|
_links: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navLink = shallow(
|
||||||
|
<RepositoryNavLink
|
||||||
|
repository={repository}
|
||||||
|
linkName="sources"
|
||||||
|
to="/sources"
|
||||||
|
label="Sources"
|
||||||
|
/>,
|
||||||
|
options.get()
|
||||||
|
);
|
||||||
|
expect(navLink.text()).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the navLink", () => {
|
||||||
|
const repository = {
|
||||||
|
_links: {
|
||||||
|
sources: {
|
||||||
|
href: "/sources"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navLink = mount(
|
||||||
|
<RepositoryNavLink
|
||||||
|
repository={repository}
|
||||||
|
linkName="sources"
|
||||||
|
to="/sources"
|
||||||
|
label="Sources"
|
||||||
|
/>,
|
||||||
|
options.get()
|
||||||
|
);
|
||||||
|
expect(navLink.text()).toBe("Sources");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -25,6 +25,8 @@ import Edit from "../containers/Edit";
|
|||||||
|
|
||||||
import type { History } from "history";
|
import type { History } from "history";
|
||||||
import EditNavLink from "../components/EditNavLink";
|
import EditNavLink from "../components/EditNavLink";
|
||||||
|
import Sources from "../sources/containers/Sources";
|
||||||
|
import RepositoryNavLink from "../components/RepositoryNavLink";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
namespace: string,
|
namespace: string,
|
||||||
@@ -101,12 +103,22 @@ class RepositoryRoot extends React.Component<Props> {
|
|||||||
path={`${url}/edit`}
|
path={`${url}/edit`}
|
||||||
component={() => <Edit repository={repository} />}
|
component={() => <Edit repository={repository} />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${url}/sources`}
|
||||||
|
component={() => <Sources repository={repository} />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="column">
|
<div className="column">
|
||||||
<Navigation>
|
<Navigation>
|
||||||
<Section label={t("repository-root.navigation-label")}>
|
<Section label={t("repository-root.navigation-label")}>
|
||||||
<NavLink to={url} label={t("repository-root.information")} />
|
<NavLink to={url} label={t("repository-root.information")} />
|
||||||
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
||||||
|
<RepositoryNavLink
|
||||||
|
repository={repository}
|
||||||
|
linkName="sources"
|
||||||
|
to={`${url}/sources`}
|
||||||
|
label={t("repository-root.sources")}
|
||||||
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
<Section label={t("repository-root.actions-label")}>
|
<Section label={t("repository-root.actions-label")}>
|
||||||
<DeleteNavAction repository={repository} delete={this.delete} />
|
<DeleteNavAction repository={repository} delete={this.delete} />
|
||||||
|
|||||||
20
scm-ui/src/repos/sources/components/FileIcon.js
Normal file
20
scm-ui/src/repos/sources/components/FileIcon.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import type { File } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
file: File
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileIcon extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { file } = this.props;
|
||||||
|
let icon = "file";
|
||||||
|
if (file.directory) {
|
||||||
|
icon = "folder";
|
||||||
|
}
|
||||||
|
return <i className={`fa fa-${icon}`} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileIcon;
|
||||||
27
scm-ui/src/repos/sources/components/FileSize.js
Normal file
27
scm-ui/src/repos/sources/components/FileSize.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
bytes: number
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileSize extends React.Component<Props> {
|
||||||
|
static format(bytes) {
|
||||||
|
if (!bytes) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const units = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
|
||||||
|
const size = (bytes / 1024 ** i).toFixed(2);
|
||||||
|
return `${size} ${units[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const formattedBytes = FileSize.format(this.props.bytes);
|
||||||
|
return <span>{formattedBytes}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileSize;
|
||||||
9
scm-ui/src/repos/sources/components/FileSize.test.js
Normal file
9
scm-ui/src/repos/sources/components/FileSize.test.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import FileSize from "./FileSize";
|
||||||
|
|
||||||
|
it("should format bytes", () => {
|
||||||
|
expect(FileSize.format(160)).toBe("160.00 B");
|
||||||
|
expect(FileSize.format(6304)).toBe("6.16 K");
|
||||||
|
expect(FileSize.format(28792588)).toBe("27.46 M");
|
||||||
|
expect(FileSize.format(1369510189)).toBe("1.28 G");
|
||||||
|
expect(FileSize.format(42949672960)).toBe("40.00 G");
|
||||||
|
});
|
||||||
48
scm-ui/src/repos/sources/components/FileTree.js
Normal file
48
scm-ui/src/repos/sources/components/FileTree.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import injectSheet from "react-jss";
|
||||||
|
import FileTreeLeaf from "./FileTreeLeaf";
|
||||||
|
import type { SourcesCollection } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
iconColumn: {
|
||||||
|
width: "16px"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
tree: SourcesCollection,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
classes: any,
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileTree extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { tree, classes, t } = this.props;
|
||||||
|
|
||||||
|
const files = tree._embedded.files;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="table table-hover table-sm is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className={classes.iconColumn} />
|
||||||
|
<th>{t("sources.file-tree.name")}</th>
|
||||||
|
<th>{t("sources.file-tree.length")}</th>
|
||||||
|
<th>{t("sources.file-tree.lastModified")}</th>
|
||||||
|
<th>{t("sources.file-tree.description")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{files.map(file => (
|
||||||
|
<FileTreeLeaf key={file.name} file={file} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default injectSheet(styles)(translate("repos")(FileTree));
|
||||||
49
scm-ui/src/repos/sources/components/FileTreeLeaf.js
Normal file
49
scm-ui/src/repos/sources/components/FileTreeLeaf.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import injectSheet from "react-jss";
|
||||||
|
import { DateFromNow } from "@scm-manager/ui-components";
|
||||||
|
import FileSize from "./FileSize";
|
||||||
|
import FileIcon from "./FileIcon";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import type { File } from "@scm-manager/ui-types";
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
iconColumn: {
|
||||||
|
width: "16px"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
file: File,
|
||||||
|
|
||||||
|
// context props
|
||||||
|
classes: any
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileTreeLeaf extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { file, classes } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td className={classes.iconColumn}>
|
||||||
|
<Link to="#todo">
|
||||||
|
<FileIcon file={file} />
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Link to="#todo">{file.name}</Link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<FileSize bytes={file.length} />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<DateFromNow date={file.lastModified} />
|
||||||
|
</td>
|
||||||
|
<td>{file.description}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectSheet(styles)(FileTreeLeaf);
|
||||||
72
scm-ui/src/repos/sources/containers/Sources.js
Normal file
72
scm-ui/src/repos/sources/containers/Sources.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import type { Repository, SourcesCollection } from "@scm-manager/ui-types";
|
||||||
|
import FileTree from "../components/FileTree";
|
||||||
|
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||||
|
import {
|
||||||
|
fetchSources,
|
||||||
|
getFetchSourcesFailure,
|
||||||
|
getSources,
|
||||||
|
isFetchSourcesPending
|
||||||
|
} from "../modules/sources";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
repository: Repository,
|
||||||
|
sources: SourcesCollection,
|
||||||
|
loading: boolean,
|
||||||
|
error: Error,
|
||||||
|
|
||||||
|
// dispatch props
|
||||||
|
fetchSources: (repository: Repository) => void
|
||||||
|
};
|
||||||
|
|
||||||
|
class Sources extends React.Component<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
const { fetchSources, repository } = this.props;
|
||||||
|
|
||||||
|
fetchSources(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { sources, loading, error } = this.props;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorNotification error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sources || loading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FileTree tree={sources} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state, ownProps) => {
|
||||||
|
const { repository } = ownProps;
|
||||||
|
const loading = isFetchSourcesPending(state, repository);
|
||||||
|
const error = getFetchSourcesFailure(state, repository);
|
||||||
|
const sources = getSources(state, repository);
|
||||||
|
|
||||||
|
console.log(sources);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
sources
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
fetchSources: (repository: Repository) => {
|
||||||
|
dispatch(fetchSources(repository));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(Sources);
|
||||||
106
scm-ui/src/repos/sources/modules/sources.js
Normal file
106
scm-ui/src/repos/sources/modules/sources.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import * as types from "../../../modules/types";
|
||||||
|
import type {
|
||||||
|
Repository,
|
||||||
|
SourcesCollection,
|
||||||
|
Action
|
||||||
|
} from "@scm-manager/ui-types";
|
||||||
|
import { apiClient } from "@scm-manager/ui-components";
|
||||||
|
import { isPending } from "../../../modules/pending";
|
||||||
|
import { getFailure } from "../../../modules/failure";
|
||||||
|
|
||||||
|
export const FETCH_SOURCES = "scm/repos/FETCH_SOURCES";
|
||||||
|
export const FETCH_SOURCES_PENDING = `${FETCH_SOURCES}_${types.PENDING_SUFFIX}`;
|
||||||
|
export const FETCH_SOURCES_SUCCESS = `${FETCH_SOURCES}_${types.SUCCESS_SUFFIX}`;
|
||||||
|
export const FETCH_SOURCES_FAILURE = `${FETCH_SOURCES}_${types.FAILURE_SUFFIX}`;
|
||||||
|
|
||||||
|
export function fetchSources(repository: Repository) {
|
||||||
|
return function(dispatch: any) {
|
||||||
|
dispatch(fetchSourcesPending(repository));
|
||||||
|
return apiClient
|
||||||
|
.get(repository._links.sources.href)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(sources => {
|
||||||
|
dispatch(fetchSourcesSuccess(repository, sources));
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
const error = new Error(`failed to fetch sources: ${err.message}`);
|
||||||
|
dispatch(fetchSourcesFailure(repository, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchSourcesPending(repository: Repository): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_SOURCES_PENDING,
|
||||||
|
itemId: createItemId(repository)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchSourcesSuccess(
|
||||||
|
repository: Repository,
|
||||||
|
sources: SourcesCollection
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
type: FETCH_SOURCES_SUCCESS,
|
||||||
|
payload: sources,
|
||||||
|
itemId: createItemId(repository)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchSourcesFailure(
|
||||||
|
repository: Repository,
|
||||||
|
error: Error
|
||||||
|
): Action {
|
||||||
|
return {
|
||||||
|
type: FETCH_SOURCES_FAILURE,
|
||||||
|
payload: error,
|
||||||
|
itemId: createItemId(repository)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createItemId(repository: Repository) {
|
||||||
|
return `${repository.namespace}/${repository.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reducer
|
||||||
|
|
||||||
|
export default function reducer(
|
||||||
|
state: any = {},
|
||||||
|
action: Action = { type: "UNKNOWN" }
|
||||||
|
): any {
|
||||||
|
if (action.type === FETCH_SOURCES_SUCCESS) {
|
||||||
|
return {
|
||||||
|
[action.itemId]: action.payload,
|
||||||
|
...state
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectors
|
||||||
|
|
||||||
|
export function getSources(
|
||||||
|
state: any,
|
||||||
|
repository: Repository
|
||||||
|
): ?SourcesCollection {
|
||||||
|
if (state.sources) {
|
||||||
|
return state.sources[createItemId(repository)];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFetchSourcesPending(
|
||||||
|
state: any,
|
||||||
|
repository: Repository
|
||||||
|
): boolean {
|
||||||
|
return isPending(state, FETCH_SOURCES, createItemId(repository));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFetchSourcesFailure(
|
||||||
|
state: any,
|
||||||
|
repository: Repository
|
||||||
|
): ?Error {
|
||||||
|
return getFailure(state, FETCH_SOURCES, createItemId(repository));
|
||||||
|
}
|
||||||
178
scm-ui/src/repos/sources/modules/sources.test.js
Normal file
178
scm-ui/src/repos/sources/modules/sources.test.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import type { Repository } from "@scm-manager/ui-types";
|
||||||
|
import configureMockStore from "redux-mock-store";
|
||||||
|
import thunk from "redux-thunk";
|
||||||
|
import fetchMock from "fetch-mock";
|
||||||
|
import {
|
||||||
|
FETCH_SOURCES,
|
||||||
|
FETCH_SOURCES_FAILURE,
|
||||||
|
FETCH_SOURCES_PENDING,
|
||||||
|
FETCH_SOURCES_SUCCESS,
|
||||||
|
fetchSources,
|
||||||
|
getFetchSourcesFailure,
|
||||||
|
isFetchSourcesPending,
|
||||||
|
default as reducer,
|
||||||
|
getSources
|
||||||
|
} from "./sources";
|
||||||
|
|
||||||
|
const sourcesUrl =
|
||||||
|
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources";
|
||||||
|
|
||||||
|
const repository: Repository = {
|
||||||
|
name: "core",
|
||||||
|
namespace: "scm",
|
||||||
|
type: "git",
|
||||||
|
_links: {
|
||||||
|
sources: {
|
||||||
|
href: sourcesUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const collection = {
|
||||||
|
revision: "76aae4bb4ceacf0e88938eb5b6832738b7d537b4",
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href:
|
||||||
|
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_embedded: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
name: "src",
|
||||||
|
path: "src",
|
||||||
|
directory: true,
|
||||||
|
description: null,
|
||||||
|
length: 176,
|
||||||
|
lastModified: null,
|
||||||
|
subRepository: null,
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href:
|
||||||
|
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/src"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "package.json",
|
||||||
|
path: "package.json",
|
||||||
|
directory: false,
|
||||||
|
description: "bump version",
|
||||||
|
length: 780,
|
||||||
|
lastModified: "2017-07-31T11:17:19Z",
|
||||||
|
subRepository: null,
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href:
|
||||||
|
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/content/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/package.json"
|
||||||
|
},
|
||||||
|
history: {
|
||||||
|
href:
|
||||||
|
"http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/history/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/package.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("sources fetch", () => {
|
||||||
|
const mockStore = configureMockStore([thunk]);
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fetchMock.reset();
|
||||||
|
fetchMock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fetch the sources of the repository", () => {
|
||||||
|
fetchMock.getOnce(sourcesUrl, collection);
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: FETCH_SOURCES_PENDING, itemId: "scm/core" },
|
||||||
|
{
|
||||||
|
type: FETCH_SOURCES_SUCCESS,
|
||||||
|
itemId: "scm/core",
|
||||||
|
payload: collection
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchSources(repository)).then(() => {
|
||||||
|
expect(store.getActions()).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should dispatch FETCH_SOURCES_FAILURE on server error", () => {
|
||||||
|
fetchMock.getOnce(sourcesUrl, {
|
||||||
|
status: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(fetchSources(repository)).then(() => {
|
||||||
|
const actions = store.getActions();
|
||||||
|
expect(actions[0].type).toBe(FETCH_SOURCES_PENDING);
|
||||||
|
expect(actions[1].type).toBe(FETCH_SOURCES_FAILURE);
|
||||||
|
expect(actions[1].itemId).toBe("scm/core");
|
||||||
|
expect(actions[1].payload).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("reducer tests", () => {
|
||||||
|
it("should return unmodified state on unknown action", () => {
|
||||||
|
const state = {};
|
||||||
|
expect(reducer(state)).toBe(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should store the collection", () => {
|
||||||
|
const expectedState = {
|
||||||
|
"scm/core": repository
|
||||||
|
};
|
||||||
|
expect(reducer({}, fetchSources(repository))).toEqual(expectedState);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("selector tests", () => {
|
||||||
|
it("should return null", () => {
|
||||||
|
expect(getSources({}, repository)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the source collection", () => {
|
||||||
|
const state = {
|
||||||
|
sources: {
|
||||||
|
"scm/core": collection
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getSources(state, repository)).toBe(collection);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true, when fetch sources is pending", () => {
|
||||||
|
const state = {
|
||||||
|
pending: {
|
||||||
|
[FETCH_SOURCES + "/scm/core"]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(isFetchSourcesPending(state, repository)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false, when fetch sources is not pending", () => {
|
||||||
|
expect(isFetchSourcesPending({}, repository)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = new Error("incredible error from hell");
|
||||||
|
|
||||||
|
it("should return error when fetch sources did fail", () => {
|
||||||
|
const state = {
|
||||||
|
failure: {
|
||||||
|
[FETCH_SOURCES + "/scm/core"]: error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expect(getFetchSourcesFailure(state, repository)).toEqual(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when fetch sources did not fail", () => {
|
||||||
|
expect(getFetchSourcesFailure({}, repository)).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user