mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-07 14:05:44 +01:00
@@ -1,18 +1,26 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import {translate} from "react-i18next";
|
||||
import type {PagedCollection} from "@scm-manager/ui-types";
|
||||
import {Button} from "./buttons";
|
||||
import { translate } from "react-i18next";
|
||||
import type { PagedCollection } from "@scm-manager/ui-types";
|
||||
import { Button } from "./buttons";
|
||||
|
||||
type Props = {
|
||||
collection: PagedCollection,
|
||||
page: number,
|
||||
filter?: string,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class LinkPaginator extends React.Component<Props> {
|
||||
addFilterToLink(link: string) {
|
||||
const { filter } = this.props;
|
||||
if (filter) {
|
||||
return `${link}?q=${filter}`;
|
||||
}
|
||||
return link;
|
||||
}
|
||||
|
||||
renderFirstButton() {
|
||||
return (
|
||||
@@ -20,7 +28,7 @@ class LinkPaginator extends React.Component<Props> {
|
||||
className={"pagination-link"}
|
||||
label={"1"}
|
||||
disabled={false}
|
||||
link={"1"}
|
||||
link={this.addFilterToLink("1")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -34,7 +42,7 @@ class LinkPaginator extends React.Component<Props> {
|
||||
className={className}
|
||||
label={label ? label : previousPage.toString()}
|
||||
disabled={!this.hasLink("prev")}
|
||||
link={`${previousPage}`}
|
||||
link={this.addFilterToLink(`${previousPage}`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -52,7 +60,7 @@ class LinkPaginator extends React.Component<Props> {
|
||||
className={className}
|
||||
label={label ? label : nextPage.toString()}
|
||||
disabled={!this.hasLink("next")}
|
||||
link={`${nextPage}`}
|
||||
link={this.addFilterToLink(`${nextPage}`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -64,7 +72,7 @@ class LinkPaginator extends React.Component<Props> {
|
||||
className={"pagination-link"}
|
||||
label={`${collection.pageTotal}`}
|
||||
disabled={false}
|
||||
link={`${collection.pageTotal}`}
|
||||
link={this.addFilterToLink(`${collection.pageTotal}`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -115,18 +123,25 @@ class LinkPaginator extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<nav className="pagination is-centered" aria-label="pagination">
|
||||
{this.renderPreviousButton("pagination-previous", t("paginator.previous"))}
|
||||
<ul className="pagination-list">
|
||||
{this.pageLinks().map((link, index) => {
|
||||
return <li key={index}>{link}</li>;
|
||||
})}
|
||||
</ul>
|
||||
{this.renderNextButton("pagination-next", t("paginator.next"))}
|
||||
</nav>
|
||||
);
|
||||
const { collection, t } = this.props;
|
||||
|
||||
if(collection) {
|
||||
return (
|
||||
<nav className="pagination is-centered" aria-label="pagination">
|
||||
{this.renderPreviousButton(
|
||||
"pagination-previous",
|
||||
t("paginator.previous")
|
||||
)}
|
||||
<ul className="pagination-list">
|
||||
{this.pageLinks().map((link, index) => {
|
||||
return <li key={index}>{link}</li>;
|
||||
})}
|
||||
</ul>
|
||||
{this.renderNextButton("pagination-next", t("paginator.next"))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import type { History } from "history";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import injectSheet from "react-jss";
|
||||
import { FilterInput } from "./forms";
|
||||
import { Button, urls } from "./index";
|
||||
|
||||
type Props = {
|
||||
showCreateButton: boolean,
|
||||
link: string,
|
||||
label?: string,
|
||||
|
||||
// context props
|
||||
classes: Object,
|
||||
history: History,
|
||||
location: any
|
||||
};
|
||||
|
||||
const styles = {
|
||||
button: {
|
||||
float: "right",
|
||||
marginTop: "1.25rem",
|
||||
marginLeft: "1.25rem"
|
||||
}
|
||||
};
|
||||
|
||||
class OverviewPageActions extends React.Component<Props> {
|
||||
render() {
|
||||
const { history, location, link } = this.props;
|
||||
return (
|
||||
<>
|
||||
<FilterInput
|
||||
value={urls.getQueryStringFromLocation(location)}
|
||||
filter={filter => {
|
||||
history.push(`/${link}/?q=${filter}`);
|
||||
}}
|
||||
/>
|
||||
{this.renderCreateButton()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderCreateButton() {
|
||||
const { showCreateButton, classes, link, label } = this.props;
|
||||
if (showCreateButton) {
|
||||
return (
|
||||
<div className={classNames(classes.button, "input-button control")}>
|
||||
<Button label={label} link={`/${link}/create`} color="primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(withRouter(OverviewPageActions));
|
||||
@@ -0,0 +1,69 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import injectSheet from "react-jss";
|
||||
import classNames from "classnames";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
filter: string => void,
|
||||
value?: string,
|
||||
|
||||
// context props
|
||||
classes: Object,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = {
|
||||
value: string
|
||||
};
|
||||
|
||||
const styles = {
|
||||
inputField: {
|
||||
float: "right",
|
||||
marginTop: "1.25rem"
|
||||
},
|
||||
inputHeight: {
|
||||
height: "2.5rem"
|
||||
}
|
||||
};
|
||||
|
||||
class FilterInput extends React.Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { value: this.props.value ? this.props.value : "" };
|
||||
}
|
||||
|
||||
handleChange = event => {
|
||||
this.setState({ value: event.target.value });
|
||||
};
|
||||
|
||||
handleSubmit = event => {
|
||||
this.props.filter(this.state.value);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, t } = this.props;
|
||||
return (
|
||||
<form
|
||||
className={classNames(classes.inputField, "input-field")}
|
||||
onSubmit={this.handleSubmit}
|
||||
>
|
||||
<div className="control has-icons-left">
|
||||
<input
|
||||
className={classNames(classes.inputHeight, "input")}
|
||||
type="search"
|
||||
placeholder={t("filterEntries")}
|
||||
value={this.state.value}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<span className="icon is-small is-left">
|
||||
<i className="fas fa-search" />
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(translate("commons")(FilterInput));
|
||||
@@ -5,6 +5,7 @@ export { default as AutocompleteAddEntryToTableField } from "./AutocompleteAddEn
|
||||
export { default as MemberNameTable } from "./MemberNameTable.js";
|
||||
export { default as Checkbox } from "./Checkbox.js";
|
||||
export { default as Radio } from "./Radio.js";
|
||||
export { default as FilterInput } from "./FilterInput.js";
|
||||
export { default as InputField } from "./InputField.js";
|
||||
export { default as Select } from "./Select.js";
|
||||
export { default as Textarea } from "./Textarea.js";
|
||||
|
||||
@@ -22,12 +22,14 @@ export { default as ProtectedRoute } from "./ProtectedRoute.js";
|
||||
export { default as Help } from "./Help";
|
||||
export { default as HelpIcon } from "./HelpIcon";
|
||||
export { default as Tooltip } from "./Tooltip";
|
||||
// TODO do we need this? getPageFromMatch is already exported by urls
|
||||
export { getPageFromMatch } from "./urls";
|
||||
export { default as Autocomplete} from "./Autocomplete";
|
||||
export { default as BranchSelector } from "./BranchSelector";
|
||||
export { default as MarkdownView } from "./MarkdownView";
|
||||
export { default as SyntaxHighlighter } from "./SyntaxHighlighter";
|
||||
export { default as ErrorBoundary } from "./ErrorBoundary";
|
||||
export { default as OverviewPageActions } from "./OverviewPageActions.js";
|
||||
|
||||
export { apiClient } from "./apiclient.js";
|
||||
export * from "./errors";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//@flow
|
||||
import * as React from "react";
|
||||
import injectSheet from "react-jss";
|
||||
import classNames from "classnames";
|
||||
import Loading from "./../Loading";
|
||||
import ErrorNotification from "./../ErrorNotification";
|
||||
import Title from "./Title";
|
||||
import Subtitle from "./Subtitle";
|
||||
import injectSheet from "react-jss";
|
||||
import classNames from "classnames";
|
||||
import PageActions from "./PageActions";
|
||||
import ErrorBoundary from "../ErrorBoundary";
|
||||
|
||||
@@ -22,9 +22,9 @@ type Props = {
|
||||
};
|
||||
|
||||
const styles = {
|
||||
spacing: {
|
||||
marginTop: "1.25rem",
|
||||
textAlign: "right"
|
||||
actions: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end"
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,47 +36,45 @@ class Page extends React.Component<Props> {
|
||||
<div className="container">
|
||||
{this.renderPageHeader()}
|
||||
<ErrorBoundary>
|
||||
<ErrorNotification error={error}/>
|
||||
<ErrorNotification error={error} />
|
||||
{this.renderContent()}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
renderPageHeader() {
|
||||
const { title, subtitle, children, classes } = this.props;
|
||||
const { error, title, subtitle, children, classes } = this.props;
|
||||
|
||||
let pageActions = null;
|
||||
let pageActionsExists = false;
|
||||
React.Children.forEach(children, child => {
|
||||
if (child && child.type.name === PageActions.name) {
|
||||
pageActions = (
|
||||
<div className="column is-two-fifths">
|
||||
if (child && !error) {
|
||||
if (child.type.name === PageActions.name)
|
||||
pageActions = (
|
||||
<div
|
||||
className={classNames(
|
||||
classes.spacing,
|
||||
"is-mobile-create-button-spacing"
|
||||
classes.actions,
|
||||
"column is-three-fifths is-mobile-action-spacing"
|
||||
)}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
pageActionsExists = true;
|
||||
}
|
||||
});
|
||||
let underline = pageActionsExists ? (
|
||||
<hr className="header-with-actions"/>
|
||||
<hr className="header-with-actions" />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<Title title={title}/>
|
||||
<Subtitle subtitle={subtitle}/>
|
||||
<Title title={title} />
|
||||
<Subtitle subtitle={subtitle} />
|
||||
</div>
|
||||
{pageActions}
|
||||
</div>
|
||||
@@ -92,13 +90,15 @@ class Page extends React.Component<Props> {
|
||||
return null;
|
||||
}
|
||||
if (loading) {
|
||||
return <Loading/>;
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
let content = [];
|
||||
React.Children.forEach(children, child => {
|
||||
if (child && child.type.name !== PageActions.name) {
|
||||
content.push(child);
|
||||
if (child) {
|
||||
if (child.type.name !== PageActions.name) {
|
||||
content.push(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
return content;
|
||||
|
||||
@@ -7,12 +7,11 @@ import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
links: Links,
|
||||
links: Links
|
||||
};
|
||||
|
||||
class PrimaryNavigation extends React.Component<Props> {
|
||||
|
||||
createNavigationAppender = (navigationItems) => {
|
||||
createNavigationAppender = navigationItems => {
|
||||
const { t, links } = this.props;
|
||||
|
||||
return (to: string, match: string, label: string, linkName: string) => {
|
||||
@@ -24,8 +23,8 @@ class PrimaryNavigation extends React.Component<Props> {
|
||||
match={match}
|
||||
label={t(label)}
|
||||
key={linkName}
|
||||
/>)
|
||||
;
|
||||
/>
|
||||
);
|
||||
navigationItems.push(navigationItem);
|
||||
}
|
||||
};
|
||||
@@ -63,16 +62,26 @@ class PrimaryNavigation extends React.Component<Props> {
|
||||
<ExtensionPoint name="primary-navigation.first-menu" props={props} />
|
||||
);
|
||||
}
|
||||
append("/repos", "/(repo|repos)", "primary-navigation.repositories", "repositories");
|
||||
append("/users", "/(user|users)", "primary-navigation.users", "users");
|
||||
append("/groups", "/(group|groups)", "primary-navigation.groups", "groups");
|
||||
append(
|
||||
"/repos/",
|
||||
"/(repo|repos)",
|
||||
"primary-navigation.repositories",
|
||||
"repositories"
|
||||
);
|
||||
append("/users/", "/(user|users)", "primary-navigation.users", "users");
|
||||
append(
|
||||
"/groups/",
|
||||
"/(group|groups)",
|
||||
"primary-navigation.groups",
|
||||
"groups"
|
||||
);
|
||||
append("/config", "/config", "primary-navigation.config", "config");
|
||||
|
||||
navigationItems.push(
|
||||
<ExtensionPoint
|
||||
name="primary-navigation"
|
||||
renderAll={true}
|
||||
props={{links: this.props.links}}
|
||||
props={{ links: this.props.links }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -86,9 +95,7 @@ class PrimaryNavigation extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<nav className="tabs is-boxed">
|
||||
<ul>
|
||||
{navigationItems}
|
||||
</ul>
|
||||
<ul>{navigationItems}</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// @flow
|
||||
import queryString from "query-string";
|
||||
|
||||
export const contextPath = window.ctxPath || "";
|
||||
|
||||
export function withContextPath(path: string) {
|
||||
@@ -27,3 +29,7 @@ export function getPageFromMatch(match: any) {
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
export function getQueryStringFromLocation(location: any) {
|
||||
return location.search ? queryString.parse(location.search).q : undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import {concat, getPageFromMatch, withEndingSlash} from "./urls";
|
||||
import { concat, getPageFromMatch, getQueryStringFromLocation, withEndingSlash } from "./urls";
|
||||
|
||||
describe("tests for withEndingSlash", () => {
|
||||
|
||||
@@ -47,3 +47,28 @@ describe("tests for getPageFromMatch", () => {
|
||||
expect(getPageFromMatch(match)).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe("tests for getQueryStringFromLocation", () => {
|
||||
|
||||
function createLocation(search: string) {
|
||||
return {
|
||||
search
|
||||
};
|
||||
}
|
||||
|
||||
it("should return the query string", () => {
|
||||
const location = createLocation("?q=abc");
|
||||
expect(getQueryStringFromLocation(location)).toBe("abc");
|
||||
});
|
||||
|
||||
it("should return query string from multiple parameters", () => {
|
||||
const location = createLocation("?x=a&y=b&q=abc&z=c");
|
||||
expect(getQueryStringFromLocation(location)).toBe("abc");
|
||||
});
|
||||
|
||||
it("should return undefined if q is not available", () => {
|
||||
const location = createLocation("?x=a&y=b&z=c");
|
||||
expect(getQueryStringFromLocation(location)).toBeUndefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user