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

@@ -14,7 +14,7 @@
"eslint-fix": "eslint src --fix"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.26",
"@scm-manager/ui-bundler": "^0.0.28",
"create-index": "^2.3.0",
"enzyme": "^3.5.0",
"enzyme-adapter-react-16": "^1.3.1",
@@ -30,6 +30,7 @@
"@scm-manager/ui-types": "2.0.0-SNAPSHOT",
"classnames": "^2.2.6",
"moment": "^2.22.2",
"query-string": "5",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-diff-view": "^1.8.1",

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

View File

@@ -693,9 +693,9 @@
version "0.0.2"
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
"@scm-manager/ui-bundler@^0.0.26":
version "0.0.26"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66"
"@scm-manager/ui-bundler@^0.0.28":
version "0.0.28"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e"
dependencies:
"@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0"
@@ -6530,6 +6530,14 @@ qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
query-string@5:
version "5.1.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
dependencies:
decode-uri-component "^0.2.0"
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
querystring-es3@~0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -7631,6 +7639,10 @@ stream-throttle@^0.1.3:
commander "^2.2.0"
limiter "^1.0.5"
strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
string-length@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"