mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-17 18:51:10 +01:00
added abstraction for global configurations, to simplify plugin development
This commit is contained in:
@@ -3,10 +3,12 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "UI Components for SCM-Manager and its plugins",
|
"description": "UI Components for SCM-Manager and its plugins",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"files": ["src"],
|
"files": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
"repository": "https://bitbucket.org/sdorra/scm-manager",
|
"repository": "https://bitbucket.org/sdorra/scm-manager",
|
||||||
"author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>",
|
"author": "Sebastian Sdorra <sebastian.sdorra@cloudogu.com>",
|
||||||
"license" : "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"update-index": "create-index -r src",
|
"update-index": "create-index -r src",
|
||||||
"eslint-fix": "eslint src --fix"
|
"eslint-fix": "eslint src --fix"
|
||||||
@@ -23,23 +25,32 @@
|
|||||||
"react-router-enzyme-context": "^1.2.0"
|
"react-router-enzyme-context": "^1.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@scm-manager/ui-extensions": "^0.0.7",
|
||||||
|
"@scm-manager/ui-types": "0.0.1",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"react": "^16.5.2",
|
"react": "^16.5.2",
|
||||||
"react-dom": "^16.5.2",
|
"react-dom": "^16.5.2",
|
||||||
"react-i18next": "^7.11.0",
|
"react-i18next": "^7.11.0",
|
||||||
"react-jss": "^8.6.1",
|
"react-jss": "^8.6.1",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1"
|
||||||
"@scm-manager/ui-types": "0.0.1"
|
|
||||||
},
|
},
|
||||||
"browserify": {
|
"browserify": {
|
||||||
"transform": [
|
"transform": [
|
||||||
["browserify-css"],
|
[
|
||||||
|
"browserify-css"
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"babelify",
|
"babelify",
|
||||||
{
|
{
|
||||||
"plugins": ["@babel/plugin-proposal-class-properties"],
|
"plugins": [
|
||||||
"presets": ["@babel/preset-env", "@babel/preset-flow", "@babel/preset-react"]
|
"@babel/plugin-proposal-class-properties"
|
||||||
|
],
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env",
|
||||||
|
"@babel/preset-flow",
|
||||||
|
"@babel/preset-react"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// @flow
|
||||||
|
export { default as ConfigurationBinder } from "./ConfigurationBinder";
|
||||||
|
export { default as GlobalConfiguration } from "./GlobalConfiguration";
|
||||||
@@ -24,6 +24,7 @@ export { getPageFromMatch } from "./urls";
|
|||||||
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
|
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
|
||||||
|
|
||||||
export * from "./buttons";
|
export * from "./buttons";
|
||||||
|
export * from "./config";
|
||||||
export * from "./forms";
|
export * from "./forms";
|
||||||
export * from "./layout";
|
export * from "./layout";
|
||||||
export * from "./modals";
|
export * from "./modals";
|
||||||
|
|||||||
@@ -681,6 +681,13 @@
|
|||||||
vinyl-source-stream "^2.0.0"
|
vinyl-source-stream "^2.0.0"
|
||||||
watchify "^3.11.0"
|
watchify "^3.11.0"
|
||||||
|
|
||||||
|
"@scm-manager/ui-extensions@^0.0.7":
|
||||||
|
version "0.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@scm-manager/ui-extensions/-/ui-extensions-0.0.7.tgz#a0a657a1410b78838ba0b36096ef631dca7fe27e"
|
||||||
|
dependencies:
|
||||||
|
react "^16.4.2"
|
||||||
|
react-dom "^16.4.2"
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "10.12.0"
|
version "10.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.0.tgz#ea6dcbddbc5b584c83f06c60e82736d8fbb0c235"
|
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"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
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:
|
react-dom@^16.5.2:
|
||||||
version "16.5.2"
|
version "16.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
|
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"
|
react-is "^16.5.2"
|
||||||
schedule "^0.5.0"
|
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:
|
react@^16.5.2:
|
||||||
version "16.5.2"
|
version "16.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42"
|
||||||
@@ -6726,6 +6751,13 @@ schedule@^0.5.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
object-assign "^4.1.1"
|
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:
|
"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"
|
version "5.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
|
||||||
|
|||||||
Reference in New Issue
Block a user