diff --git a/scm-ui-components/packages/ui-components/package.json b/scm-ui-components/packages/ui-components/package.json index 1288448908..48cf089aba 100644 --- a/scm-ui-components/packages/ui-components/package.json +++ b/scm-ui-components/packages/ui-components/package.json @@ -3,10 +3,12 @@ "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 ", - "license" : "BSD-3-Clause", + "license": "BSD-3-Clause", "scripts": { "update-index": "create-index -r src", "eslint-fix": "eslint src --fix" @@ -23,23 +25,32 @@ "react-router-enzyme-context": "^1.2.0" }, "dependencies": { + "@scm-manager/ui-extensions": "^0.0.7", + "@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" + ] } ] ] diff --git a/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js b/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js new file mode 100644 index 0000000000..477eee5238 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/config/ConfigurationBinder.js @@ -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 ; + }); + + // 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 } + exact/>; + }; + + // bind config route to extension point + binder.bind("config.route", ConfigRoute, configPredicate); + } + +} + +export default new ConfigurationBinder(); diff --git a/scm-ui-components/packages/ui-components/src/config/GlobalConfiguration.js b/scm-ui-components/packages/ui-components/src/config/GlobalConfiguration.js new file mode 100644 index 0000000000..b2b7dca647 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/config/GlobalConfiguration.js @@ -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 { + + 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 ; + } else if (fetching || !configuration) { + return ; + } else { + const readOnly = this.isReadOnly(); + + const renderProps: RenderProps = { + readOnly, + initialConfiguration: configuration, + onConfigurationChange: this.configurationChanged + }; + + return ( +
+ { this.props.render(renderProps) } +
+ + + ); + } + } + +} + +export default translate("config")(GlobalConfiguration); diff --git a/scm-ui-components/packages/ui-components/src/config/index.js b/scm-ui-components/packages/ui-components/src/config/index.js new file mode 100644 index 0000000000..9596e9cda5 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/config/index.js @@ -0,0 +1,3 @@ +// @flow +export { default as ConfigurationBinder } from "./ConfigurationBinder"; +export { default as GlobalConfiguration } from "./GlobalConfiguration"; diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index 196c33c08f..2ebe57ec0c 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -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"; diff --git a/scm-ui-components/packages/ui-components/yarn.lock b/scm-ui-components/packages/ui-components/yarn.lock index 49f638e049..886ee3f0fd 100644 --- a/scm-ui-components/packages/ui-components/yarn.lock +++ b/scm-ui-components/packages/ui-components/yarn.lock @@ -681,6 +681,13 @@ vinyl-source-stream "^2.0.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@*": 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"