Merged in feature/search (pull request #231)

feature/search
This commit is contained in:
Sebastian Sdorra
2019-04-24 11:51:57 +00:00
68 changed files with 872 additions and 2399 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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