Merged in feature/git-config (pull request #103)

Repository type configuration + abstraction for global configuration
This commit is contained in:
Sebastian Sdorra
2018-11-07 13:46:22 +00:00
34 changed files with 813 additions and 1399 deletions

View File

@@ -3,16 +3,18 @@
"version": "0.0.1",
"description": "UI Components for SCM-Manager and its plugins",
"main": "src/index.js",
"files": ["src"],
"files": [
"src"
],
"repository": "https://bitbucket.org/sdorra/scm-manager",
"author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>",
"license" : "BSD-3-Clause",
"license": "BSD-3-Clause",
"scripts": {
"update-index": "create-index -r src",
"eslint-fix": "eslint src --fix"
},
"devDependencies": {
"@scm-manager/ui-bundler": "^0.0.19",
"@scm-manager/ui-bundler": "^0.0.21",
"create-index": "^2.3.0",
"enzyme": "^3.5.0",
"enzyme-adapter-react-16": "^1.3.1",
@@ -23,23 +25,32 @@
"react-router-enzyme-context": "^1.2.0"
},
"dependencies": {
"@scm-manager/ui-extensions": "^0.1.1",
"@scm-manager/ui-types": "0.0.1",
"classnames": "^2.2.6",
"moment": "^2.22.2",
"react": "^16.5.2",
"react-dom": "^16.5.2",
"react-i18next": "^7.11.0",
"react-jss": "^8.6.1",
"react-router-dom": "^4.3.1",
"@scm-manager/ui-types": "0.0.1"
"react-router-dom": "^4.3.1"
},
"browserify": {
"transform": [
["browserify-css"],
[
"browserify-css"
],
[
"babelify",
{
"plugins": ["@babel/plugin-proposal-class-properties"],
"presets": ["@babel/preset-env", "@babel/preset-flow", "@babel/preset-react"]
"plugins": [
"@babel/plugin-proposal-class-properties"
],
"presets": [
"@babel/preset-env",
"@babel/preset-flow",
"@babel/preset-react"
]
}
]
]

View File

@@ -0,0 +1,43 @@
// @flow
import React from "react";
import { binder } from "@scm-manager/ui-extensions";
import { NavLink } from "../navigation";
import { Route } from "react-router-dom";
import { translate } from "react-i18next";
class ConfigurationBinder {
i18nNamespace: string = "plugins";
bindGlobal(to: string, labelI18nKey: string, linkName: string, ConfigurationComponent: any) {
// create predicate based on the link name of the index resource
// if the linkname is not available, the navigation link and the route are not bound to the extension points
const configPredicate = (props: Object) => {
return props.links && props.links[linkName];
};
// create NavigationLink with translated label
const ConfigNavLink = translate(this.i18nNamespace)(({t}) => {
return <NavLink to={"/config" + to} label={t(labelI18nKey)} />;
});
// bind navigation link to extension point
binder.bind("config.navigation", ConfigNavLink, configPredicate);
// route for global configuration, passes the link from the index resource to component
const ConfigRoute = ({ url, links }) => {
const link = links[linkName].href;
return <Route path={url + to}
render={() => <ConfigurationComponent link={link}/>}
exact/>;
};
// bind config route to extension point
binder.bind("config.route", ConfigRoute, configPredicate);
}
}
export default new ConfigurationBinder();

View File

@@ -0,0 +1,162 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import type { Links } from "@scm-manager/ui-types";
import {
apiClient,
SubmitButton,
Loading,
ErrorNotification
} from "../";
type RenderProps = {
readOnly: boolean,
initialConfiguration: Configuration,
onConfigurationChange: (Configuration, boolean) => void
};
type Props = {
link: string,
render: (props: RenderProps) => any, // ???
// context props
t: (string) => string
};
type Configuration = {
_links: Links
} & Object;
type State = {
error?: Error,
fetching: boolean,
modifying: boolean,
contentType?: string,
configuration?: Configuration,
modifiedConfiguration?: Configuration,
valid: boolean
};
/**
* GlobalConfiguration uses the render prop pattern to encapsulate the logic for
* synchronizing the configuration with the backend.
*/
class GlobalConfiguration extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
fetching: true,
modifying: false,
valid: false
};
}
componentDidMount() {
const { link } = this.props;
apiClient.get(link)
.then(this.captureContentType)
.then(response => response.json())
.then(this.loadConfig)
.catch(this.handleError);
}
captureContentType = (response: Response) => {
const contentType = response.headers.get("Content-Type");
this.setState({
contentType
});
return response;
};
getContentType = (): string => {
const { contentType } = this.state;
return contentType ? contentType : "application/json";
};
handleError = (error: Error) => {
this.setState({
error,
fetching: false,
modifying: false
});
};
loadConfig = (configuration: Configuration) => {
this.setState({
configuration,
fetching: false,
error: undefined
});
};
getModificationUrl = (): ?string => {
const { configuration } = this.state;
if (configuration) {
const links = configuration._links;
if (links && links.update) {
return links.update.href;
}
}
};
isReadOnly = (): boolean => {
const modificationUrl = this.getModificationUrl();
return !modificationUrl;
};
configurationChanged = (configuration: Configuration, valid: boolean) => {
this.setState({
modifiedConfiguration: configuration,
valid
});
};
modifyConfiguration = (event: Event) => {
event.preventDefault();
this.setState({ modifying: true });
const {modifiedConfiguration} = this.state;
apiClient.put(this.getModificationUrl(), modifiedConfiguration, this.getContentType())
.then(() => this.setState({ modifying: false }))
.catch(this.handleError);
};
render() {
const { t } = this.props;
const { fetching, error, configuration, modifying, valid } = this.state;
if (error) {
return <ErrorNotification error={error}/>;
} else if (fetching || !configuration) {
return <Loading />;
} else {
const readOnly = this.isReadOnly();
const renderProps: RenderProps = {
readOnly,
initialConfiguration: configuration,
onConfigurationChange: this.configurationChanged
};
return (
<form onSubmit={this.modifyConfiguration}>
{ this.props.render(renderProps) }
<hr/>
<SubmitButton
label={t("config-form.submit")}
disabled={!valid || readOnly}
loading={modifying}
/>
</form>
);
}
}
}
export default translate("config")(GlobalConfiguration);

View File

@@ -0,0 +1,3 @@
// @flow
export { default as ConfigurationBinder } from "./ConfigurationBinder";
export { default as GlobalConfiguration } from "./GlobalConfiguration";

View File

@@ -4,15 +4,16 @@ import { Help } from "../index";
type Props = {
label?: string,
name?: string,
checked: boolean,
onChange?: boolean => void,
onChange?: (value: boolean, name?: string) => void,
disabled?: boolean,
helpText?: string
};
class Checkbox extends React.Component<Props> {
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
if (this.props.onChange) {
this.props.onChange(event.target.checked);
this.props.onChange(event.target.checked, this.props.name);
}
};

View File

@@ -5,11 +5,12 @@ import { LabelWithHelpIcon } from "../index";
type Props = {
label?: string,
name?: string,
placeholder?: string,
value?: string,
type?: string,
autofocus?: boolean,
onChange: string => void,
onChange: (value: string, name?: string) => void,
onReturnPressed?: () => void,
validationError: boolean,
errorMessage: string,
@@ -32,7 +33,7 @@ class InputField extends React.Component<Props> {
}
handleInput = (event: SyntheticInputEvent<HTMLInputElement>) => {
this.props.onChange(event.target.value);
this.props.onChange(event.target.value, this.props.name);
};
handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {

View File

@@ -9,10 +9,11 @@ export type SelectItem = {
};
type Props = {
name?: string,
label?: string,
options: SelectItem[],
value?: SelectItem,
onChange: string => void,
value?: string,
onChange: (value: string, name?: string) => void,
loading?: boolean,
helpText?: string
};
@@ -29,7 +30,7 @@ class Select extends React.Component<Props> {
}
handleInput = (event: SyntheticInputEvent<HTMLSelectElement>) => {
this.props.onChange(event.target.value);
this.props.onChange(event.target.value, this.props.name);
};
render() {
@@ -40,10 +41,10 @@ class Select extends React.Component<Props> {
return (
<div className="field">
<LabelWithHelpIcon label={label} helpText={helpText} />
<div className={classNames(
"control select",
loadingClass
)}>
<div className={classNames(
"control select",
loadingClass
)}>
<select
ref={input => {
this.field = input;

View File

@@ -24,6 +24,7 @@ export { getPageFromMatch } from "./urls";
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
export * from "./buttons";
export * from "./config";
export * from "./forms";
export * from "./layout";
export * from "./modals";

View File

@@ -641,9 +641,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.19":
version "0.0.19"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.19.tgz#646ab1fa1e5389fad614542215c60678fb9816ae"
"@scm-manager/ui-bundler@^0.0.21":
version "0.0.21"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.21.tgz#f8b5fa355415cc67b8aaf8744e1701a299dff647"
dependencies:
"@babel/core" "^7.0.0"
"@babel/plugin-proposal-class-properties" "^7.0.0"
@@ -681,6 +681,13 @@
vinyl-source-stream "^2.0.0"
watchify "^3.11.0"
"@scm-manager/ui-extensions@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.1.1.tgz#966e62d89981e92a14adf7e674e646e76de96d45"
dependencies:
react "^16.4.2"
react-dom "^16.4.2"
"@types/node@*":
version "10.12.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.0.tgz#ea6dcbddbc5b584c83f06c60e82736d8fbb0c235"
@@ -6229,6 +6236,15 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-dom@^16.4.2:
version "16.6.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.6.0.tgz#6375b8391e019a632a89a0988bce85f0cc87a92f"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.10.0"
react-dom@^16.5.2:
version "16.5.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
@@ -6299,6 +6315,15 @@ react-test-renderer@^16.0.0-0:
react-is "^16.5.2"
schedule "^0.5.0"
react@^16.4.2:
version "16.6.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.6.0.tgz#b34761cfaf3e30f5508bc732fb4736730b7da246"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.10.0"
react@^16.5.2:
version "16.5.2"
resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42"
@@ -6726,6 +6751,13 @@ schedule@^0.5.0:
dependencies:
object-assign "^4.1.1"
scheduler@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.10.0.tgz#7988de90fe7edccc774ea175a783e69c40c521e1"
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1:
version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"